CSS
<style>
/* Main SVG Canvas Container */
.svg-container {
width: 85%;
max-width: 480px;
height: 65vh;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
}
svg {
width: 100%;
height: 100%;
overflow: visible;
filter: drop-shadow(0 15px 25px rgba(0, 0, 0, 0.08));
}
/* Hidden target container */
.hidden-targets {
display: none;
}
</style>
JS
<script>
window.addEventListener("DOMContentLoaded", () => {
// 1. Register MorphSVG Plugin directly
gsap.registerPlugin(MorphSVGPlugin);
// Helper to query paths by SVG ID
function getSvgPaths(id) {
const svg = document.getElementById(id);
return svg ? Array.from(svg.querySelectorAll("path")) : null;
}
const stageSvg = document.getElementById("stage-svg");
const stagePaths = Array.from(stageSvg.querySelectorAll("path"));
// 2. Map all 9 target direction path arrays by explicit SVG IDs
const targetMap = {
"neutral": stagePaths,
"up": getSvgPaths("svg-up"),
"right-up": getSvgPaths("svg-right-up"),
"right": getSvgPaths("svg-right"),
"right-down": getSvgPaths("svg-down-right"),
"down": getSvgPaths("svg-down"),
"left-down": getSvgPaths("svg-down-left"),
"left": getSvgPaths("svg-left"),
"left-up": getSvgPaths("svg-up-left")
};
// 3. NEW APPROACH: Pre-parse all 9 SVG states into raw coordinate arrays
const rawTargetMap = {};
Object.keys(targetMap).forEach(key => {
const paths = targetMap[key];
if (paths) {
rawTargetMap[key] = paths.map(p => MorphSVGPlugin.stringToRawPath(p.getAttribute("d")));
}
});
// Store active rawPath state for stage paths
let currentRawState = rawTargetMap["neutral"]
? rawTargetMap["neutral"].map(rp => JSON.parse(JSON.stringify(rp)))
: [];
// Pure 1:1 Direct Number Lerp function
function lerpRawPath(fromRaw, toRaw, t) {
if (!fromRaw || !toRaw) return fromRaw || toRaw;
const result = [];
const numSubpaths = Math.min(fromRaw.length, toRaw.length);
for (let s = 0; s < numSubpaths; s++) {
const subFrom = fromRaw[s];
const subTo = toRaw[s];
const subResult = [];
const numPts = Math.min(subFrom.length, subTo.length);
for (let p = 0; p < numPts; p++) {
const a = subFrom[p];
const b = subTo[p];
// Pure 1:1 coordinate lerp: A + (B - A) * t
subResult.push(a + (b - a) * t);
}
result.push(subResult);
}
return result;
}
const statusText = document.getElementById("status-text");
const indicator = document.getElementById("indicator");
const gyroBtn = document.getElementById("gyro-btn");
const hudInfo = document.getElementById("hud-info");
let currentDirection = "neutral";
// 4. Direction Morph Function using Direct Coordinate Interpolation
function updateDirection(dirKey) {
if (dirKey === currentDirection || !rawTargetMap[dirKey] || !stagePaths) return;
currentDirection = dirKey;
const targetRaw = rawTargetMap[dirKey];
// Snapshot current rawPath state as starting point
const snapshotRaw = currentRawState.map(rp => JSON.parse(JSON.stringify(rp)));
const tweenState = { t: 0 };
gsap.to(tweenState, {
t: 1,
duration: 0.7,
ease: "back.out(1.4)",
overwrite: "auto",
onUpdate: () => {
stagePaths.forEach((path, i) => {
const fromRaw = snapshotRaw[i];
const toRaw = targetRaw[i];
if (fromRaw && toRaw) {
const interpolated = lerpRawPath(fromRaw, toRaw, tweenState.t);
path.setAttribute("d", MorphSVGPlugin.rawPathToString(interpolated));
currentRawState[i] = interpolated;
}
});
}
});
// Update HUD UI
if (statusText) statusText.textContent = `Direction: ${dirKey.toUpperCase().replace("-", " ")}`;
if (indicator) {
indicator.style.transform = "scale(1.4)";
gsap.to(indicator, { scale: 1, duration: 0.4, ease: "back.out(2)" });
}
}
// 5. Map Mouse 2D Coordinates (X, Y) to 9 Directions
function handlePointerMove(e) {
const mouseX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
const mouseY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
const relX = (mouseX / window.innerWidth) * 2 - 1;
const relY = (mouseY / window.innerHeight) * 2 - 1;
const dist = Math.sqrt(relX * relX + relY * relY);
if (dist < 0.28) {
updateDirection("neutral");
return;
}
const angle = Math.atan2(relY, relX) * (180 / Math.PI);
if (angle >= -22.5 && angle < 22.5) updateDirection("right");
else if (angle >= 22.5 && angle < 67.5) updateDirection("right-down");
else if (angle >= 67.5 && angle < 112.5) updateDirection("down");
else if (angle >= 112.5 && angle < 157.5) updateDirection("left-down");
else if (angle >= 157.5 || angle < -157.5) updateDirection("left");
else if (angle >= -157.5 && angle < -112.5) updateDirection("left-up");
else if (angle >= -112.5 && angle < -67.5) updateDirection("up");
else if (angle >= -67.5 && angle < -22.5) updateDirection("right-up");
}
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("touchmove", handlePointerMove);
// 6. Mobile Gyroscope Tilt Handler
function handleDeviceOrientation(e) {
if (e.gamma === null || e.beta === null) return;
const relX = Math.max(-1, Math.min(1, e.gamma / 20));
const relY = Math.max(-1, Math.min(1, (e.beta - 40) / 20));
const dist = Math.sqrt(relX * relX + relY * relY);
if (dist < 0.28) {
updateDirection("neutral");
return;
}
const angle = Math.atan2(relY, relX) * (180 / Math.PI);
if (angle >= -22.5 && angle < 22.5) updateDirection("right");
else if (angle >= 22.5 && angle < 67.5) updateDirection("right-down");
else if (angle >= 67.5 && angle < 112.5) updateDirection("down");
else if (angle >= 112.5 && angle < 157.5) updateDirection("left-down");
else if (angle >= 157.5 || angle < -157.5) updateDirection("left");
else if (angle >= -157.5 && angle < -112.5) updateDirection("left-up");
else if (angle >= -112.5 && angle < -67.5) updateDirection("up");
else if (angle >= -67.5 && angle < -22.5) updateDirection("right-up");
}
function enableGyroscope() {
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission()
.then(permissionState => {
if (permissionState === 'granted') {
window.addEventListener('deviceorientation', handleDeviceOrientation);
if (gyroBtn) gyroBtn.style.display = 'none';
if (hudInfo) hudInfo.textContent = 'Tilt device or drag screen to look around';
}
})
.catch(console.error);
} else if ('DeviceOrientationEvent' in window) {
window.addEventListener('deviceorientation', handleDeviceOrientation);
if (hudInfo) hudInfo.textContent = 'Tilt device or drag screen to look around';
}
}
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
if (gyroBtn) {
gyroBtn.style.display = 'inline-block';
gyroBtn.addEventListener('click', enableGyroscope);
}
} else if ('DeviceOrientationEvent' in window && ('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
window.addEventListener('deviceorientation', handleDeviceOrientation);
}
});
</script>