BACK TO EXPERIMENTS
Direction: NEUTRAL
Move cursor continuously across screen to morph in 2D
Enable Gyroscope Tilt
Inclusions
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MorphSVGPlugin.min.js"></script>
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>