← Mike Higgins Home
Custom Code Experiment •

Electric Hot

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>Electric Hot</title>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Raleway:wght@900&display=swap');

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      background: #000;
      overflow: hidden;
      width: 100vw;
      height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    #stage {
      position: relative;
      width: 100vw;
      height: 100px;
      filter: url(#mt) blur(0.4px);
    }

    .mt {
      position: absolute;
      width: 100%;
      text-align: center;
      font-family: 'Raleway', sans-serif;
      font-size: 80pt;
      font-weight: 900;
      color: #fff;
      will-change: opacity, filter;
      transform: translateZ(0);
      letter-spacing: -0.02em;
      text-rendering: geometricPrecision;
    }

    @media (max-width: 720px) {
      .mt { font-size: 13vw; }
      #stage { height: 18vw; }
    }

    svg {
      position: absolute;
      width: 0;
      height: 0;
      visibility: hidden;
      pointer-events: none;
    }
  </style>
</head>
<body>

  <div id="stage">
    <span id="ma" class="mt"></span>
    <span id="mb" class="mt"></span>
  </div>

  <svg aria-hidden="true">
    <defs>
      <filter id="mt" x="-20%" y="-40%" width="140%" height="180%">
        <feColorMatrix type="matrix"
          values="1 0 0 0 0
                  0 1 0 0 0
                  0 0 1 0 0
                  0 0 0 255 -128"/>
      </filter>
    </defs>
  </svg>

  <script>
    (function () {
      const elA = document.getElementById('ma');
      const elB = document.getElementById('mb');

      const words = [
        "It",
        "is",
        "the",
        "body",
        "of",
        "nothing",
        "and",
        "electric",
        "hot."
      ];

      const HOLD       = 1.1;
      const TRANSITION = 1.3;

      let idx  = 0;
      let t    = 0;
      let last = null;

      function smoothstep(t) {
        t = Math.max(0, Math.min(1, t));
        return t * t * (3 - 2 * t);
      }

      function applyProgress(el, progress, entering) {
        const p = entering ? progress : 1 - progress;
        const s = smoothstep(p);
        const blurVal = 12 * (1 - s);
        el.style.opacity = s;
        el.style.filter  = blurVal < 0.05 ? 'none' : `blur(${blurVal.toFixed(2)}px)`;
      }

      function show(el, word) {
        el.textContent  = word;
        el.style.opacity = 1;
        el.style.filter  = 'none';
      }

      function hide(el) {
        el.style.opacity = 0;
        el.style.filter  = 'blur(12px)';
      }

      let phase    = 'hold';
      let current  = elA;
      let incoming = elB;

      show(current, words[0]);
      hide(incoming);
      incoming.textContent = words[1];

      function tick(ts) {
        requestAnimationFrame(tick);
        if (!last) { last = ts; return; }
        const dt = Math.min((ts - last) / 1000, 0.064);
        last = ts;
        t += dt;

        if (phase === 'hold') {
          if (t >= HOLD) {
            t -= HOLD;
            phase = 'transition';
            idx = (idx + 1) % words.length;
            incoming.textContent = words[idx];
            hide(incoming);
          }
        } else {
          const progress = Math.min(t / TRANSITION, 1);
          applyProgress(current,  progress, false);
          applyProgress(incoming, progress, true);
          if (progress >= 1) {
            const tmp = current;
            current  = incoming;
            incoming = tmp;
            show(current, words[idx]);
            hide(incoming);
            t -= TRANSITION;
            phase = 'hold';
          }
        }
      }

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