← Mike Higgins Home
Custom Code Experiment •

JUNO: Trailing Text

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>JUNO - Trailing Text Animation</title>
  <link href="https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" rel="stylesheet">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }

    .stage {
      position: relative;
      width: 100%;
      height: 100%;
    }

    .layer {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: 'Great Vibes', cursive;
      font-size: 28vw;
      font-weight: 500;
      letter-spacing: 0.02em;
      line-height: 1;
      -webkit-text-stroke: .3vw #111;
      text-stroke: .3vw #111;
      pointer-events: none;
      will-change: transform;
      transform-origin: center center;
    }

    .layer:nth-child(1) { color: #222A26; z-index: 1; }
    .layer:nth-child(2) { color: #0AFF81; z-index: 2; }
    .layer:nth-child(3) { color: #51ECE9; z-index: 3; }
    .layer:nth-child(4) { color: #F31688; z-index: 4; }
    .layer:nth-child(5) { color: #ECBD51; z-index: 5; }
  </style>
</head>
<body>
  <div class="stage">
    <div class="layer">JUNO</div>
    <div class="layer">JUNO</div>
    <div class="layer">JUNO</div>
    <div class="layer">JUNO</div>
    <div class="layer">JUNO</div>
  </div>

  <script>
    const layers = Array.from(document.querySelectorAll('.layer'));
    const front  = layers[4];
    const trails = layers.slice(0, 4);

    const corners = [
      { rot:  14, ty:  6 },
      { rot: -14, ty:  6 },
      { rot: -14, ty: -6 },
      { rot:  14, ty: -6 },
    ];

    const DURATION = 7000;
    const LEG      = DURATION / 4;

    const lagMs = [110, 230, 370, 540];

    const depthScale = [1.25, 1.6, 2.05, 2.6];

    function easeOvershoot(t) {
      const c4 = (2 * Math.PI) / 3;
      if (t === 0) return 0;
      if (t === 1) return 1;
      return Math.pow(2, -8 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
    }

    function easeInOut(t) {
      return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    }

    function getTransform(elapsed) {
      const t_loop = ((elapsed % DURATION) + DURATION) % DURATION;
      const leg    = Math.floor(t_loop / LEG);
      const t      = easeInOut((t_loop % LEG) / LEG);
      const from   = corners[leg % 4];
      const to     = corners[(leg + 1) % 4];
      return {
        rot: from.rot + (to.rot - from.rot) * t,
        ty:  from.ty  + (to.ty  - from.ty)  * t,
      };
    }

    const springs = trails.map(() => ({ rot: 0, ty: 0, vRot: 0, vTy: 0 }));
    const stiffness = 0.12;
    const damping   = 0.62;

    const history = [];
    const MAX_LAG = lagMs[lagMs.length - 1] + 300;

    let startTime  = null;
    let lastTime   = null;

    function tick(now) {
      if (!startTime) startTime = now;
      if (!lastTime)  lastTime  = now;
      const elapsed = now - startTime;
      const dt      = Math.min(now - lastTime, 32);
      lastTime = now;

      const current = getTransform(elapsed);

      history.push({ time: elapsed, ...current });
      while (history.length > 1 && history[0].time < elapsed - MAX_LAG) {
        history.shift();
      }

      front.style.transform =
        `rotate(${current.rot}deg) translateY(${current.ty}vh)`;

      trails.forEach((el, i) => {
        const targetTime = elapsed - lagMs[i];
        let entry = history[0];
        for (const h of history) {
          if (h.time <= targetTime) entry = h;
          else break;
        }

        const s = depthScale[i];
        const targetRot = entry.rot * s;
        const targetTy  = entry.ty  * s;

        const sp = springs[i];

        const fRot = (targetRot - sp.rot) * stiffness;
        const fTy  = (targetTy  - sp.ty)  * stiffness;

        sp.vRot = sp.vRot * damping + fRot;
        sp.vTy  = sp.vTy  * damping + fTy;

        sp.rot += sp.vRot;
        sp.ty  += sp.vTy;

        el.style.transform =
          `rotate(${sp.rot}deg) translateY(${sp.ty}vh)`;
      });

      requestAnimationFrame(tick);
    }

    requestAnimationFrame(tick);
  </script>
</body>
</html>