Live demo, running in your browser

Glyph dust

The word FORM is built from hundreds of tiny monospace glyphs that drift in, sweep into place left to right, flash with a bright wave while locked, then scatter again on a 9-second loop with a live phase readout.

Code: MIT license.

<!doctype html>
<html lang="en">
  <head
    ><script>
      /* 6to9 pattern harness */ (function () {
        var parentPaused = false,
          hidden = document.hidden,
          p = (window.__pattern = { paused: false, reducedMotion: false, maxDpr: 2 });
        function sync() {
          p.paused = parentPaused || hidden;
        }
        sync();
        try {
          p.reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
        } catch (e) {}
        document.addEventListener("visibilitychange", function () {
          hidden = document.hidden;
          sync();
        });
        addEventListener("message", function (e) {
          var d = e.data || {};
          if (d.pattern === "pause") {
            parentPaused = true;
            sync();
          }
          if (d.pattern === "play") {
            parentPaused = false;
            sync();
          }
        });
      })();
    </script>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>FORM — glyph dust</title>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      html,
      body {
        width: 100%;
        height: 100%;
        background: #050807;
        overflow: hidden;
      }
      body {
        font-family: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
        color: #d4ffec;
      }
      .hero {
        position: relative;
        width: 100vw;
        height: 100vh;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        background:
          radial-gradient(ellipse 60% 50% at 50% 48%, rgba(70, 200, 150, 0.12), transparent 70%),
          linear-gradient(rgba(150, 255, 210, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
          linear-gradient(90deg, rgba(150, 255, 210, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
          #050807;
      }
      .scan {
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        height: 1px;
        background: linear-gradient(90deg, transparent, rgba(160, 255, 215, 0.35), transparent);
        animation: scan 6s linear infinite;
        z-index: 0;
        pointer-events: none;
      }
      .still .scan {
        animation: none;
        top: 30%;
      }
      @keyframes scan {
        from {
          transform: translateY(0);
        }
        to {
          transform: translateY(100vh);
        }
      }
      .tag {
        position: absolute;
        top: 22px;
        left: 24px;
        right: 24px;
        display: flex;
        justify-content: space-between;
        font-size: 11px;
        letter-spacing: 0.18em;
        text-transform: uppercase;
        color: rgba(190, 255, 225, 0.55);
        z-index: 3;
      }
      .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
      }
      .stage {
        position: relative;
        flex: none;
        z-index: 1;
      }
      .g {
        position: absolute;
        display: block;
        text-align: center;
        white-space: nowrap;
        font-weight: 700;
        color: #d4ffec;
        will-change: transform, opacity;
        pointer-events: none;
      }
      .copy {
        position: relative;
        z-index: 3;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 20px;
        margin-top: clamp(28px, 6vh, 56px);
        padding: 0 24px;
        text-align: center;
      }
      .readout {
        font-size: 11px;
        letter-spacing: 0.22em;
        text-transform: uppercase;
        color: rgba(190, 255, 225, 0.6);
        min-height: 14px;
      }
      .sub {
        max-width: 460px;
        font-size: clamp(14px, 1.3vw, 17px);
        line-height: 1.55;
        color: #b5e8d0;
      }
      .btn {
        font-family: inherit;
        font-size: 13px;
        letter-spacing: 0.16em;
        text-transform: uppercase;
        padding: 15px 30px;
        color: #04100b;
        background: #bfffe0;
        border: 1px solid #bfffe0;
        border-radius: 2px;
        cursor: pointer;
        transition:
          background 0.25s,
          color 0.25s;
      }
      .btn:hover {
        background: transparent;
        color: #bfffe0;
      }
      .fallback {
        display: none;
        font-family: "Arial Black", "Helvetica Neue", Arial, sans-serif;
        font-weight: 900;
        font-size: clamp(80px, 24vw, 300px);
        line-height: 1;
        color: #d4ffec;
        letter-spacing: -0.02em;
      }
      .nofx .fallback {
        display: block;
      }
      .nofx .stage {
        display: none;
      }
    </style>
  </head>
  <body>
    <main class="hero" id="hero">
      <div class="scan"></div>
      <div class="tag"><span>form/engine</span><span>v0.4 · generative</span></div>
      <h1 class="sr-only">FORM</h1>
      <div class="fallback" aria-hidden="true">FORM</div>
      <div class="stage" id="stage" aria-hidden="true"></div>
      <div class="copy">
        <div class="readout" id="readout">-- glyphs · --</div>
        <p class="sub"
          >Hundreds of tiny particles find their place, hold for a beat, then scatter. Precision, on
          a loop.</p
        >
        <button class="btn" type="button">Start building</button>
      </div>
    </main>
    <script>
      (function () {
        var P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
        var hero = document.getElementById("hero");
        var stage = document.getElementById("stage");
        var readout = document.getElementById("readout");
        var CYCLE = 9000;
        var CHARS = "01234567890·:;+*=/<>[]{}()#%&abcdefxyz_-~^|!?";
        var FONT = "'Arial Black','Helvetica Neue',Arial,sans-serif";
        var DUST = "#3f9a7c";
        var LOCK = "#d4ffec";
        var ACCENT = "#8dffd0";
        var anims = [];
        var count = 0;
        var clock = CYCLE * 0.08;
        var lastNow = performance.now();
        var lastText = "";
        var resizeTimer = 0;

        if (P.reducedMotion) {
          hero.classList.add("still");
        }

        function rand(a, b) {
          return a + Math.random() * (b - a);
        }

        function tf(x, y, rot, sc) {
          return (
            "translate(-50%,-50%) translate(" +
            x.toFixed(1) +
            "px," +
            y.toFixed(1) +
            "px) rotate(" +
            rot.toFixed(1) +
            "deg) scale(" +
            sc +
            ")"
          );
        }

        // Measure the word and decide the size of the stage.
        function layout() {
          var vw = window.innerWidth;
          var vh = window.innerHeight;
          var stacked = vw / vh < 0.8;
          var lines = stacked ? ["FO", "RM"] : ["FORM"];
          var c = document.createElement("canvas");
          var ctx = c.getContext("2d");
          ctx.font = "900 100px " + FONT;
          var wm = 0;
          lines.forEach(function (l) {
            wm = Math.max(wm, ctx.measureText(l).width);
          });
          var asc = ctx.measureText("FORM").actualBoundingBoxAscent || 72;
          var gap = 1.16;
          var hUnits = asc * (1 + (lines.length - 1) * gap);
          var maxW = Math.min(vw * 0.88, 1180);
          var k = Math.min(maxW / wm, (vh * (stacked ? 0.34 : 0.4)) / hUnits);
          return {
            vw: vw,
            vh: vh,
            lines: lines,
            wm: wm,
            asc: asc,
            gap: gap,
            k: k,
            W: Math.ceil(wm * k),
            H: Math.ceil(hUnits * k),
          };
        }

        // Sample the letter shapes from an offscreen canvas into target points.
        function samplePoints(L) {
          var c = document.createElement("canvas");
          c.width = L.W;
          c.height = L.H;
          var ctx = c.getContext("2d", { willReadFrequently: true });
          ctx.fillStyle = "#fff";
          ctx.font = "900 " + 100 * L.k + "px " + FONT;
          ctx.textAlign = "center";
          ctx.textBaseline = "alphabetic";
          L.lines.forEach(function (line, i) {
            ctx.fillText(line, L.W / 2, L.asc * L.k * (1 + i * L.gap));
          });
          var data = ctx.getImageData(0, 0, L.W, L.H).data;
          var step = Math.max(6.5, L.W / 70);
          var pts = [];
          var rows = Math.floor(L.H / step);
          var cols = Math.floor(L.W / step);
          for (var r = 0; r < rows; r++) {
            var off = (r % 2) * step * 0.5;
            for (var q = 0; q < cols; q++) {
              var x = (q + 0.5) * step + off;
              var y = (r + 0.5) * step;
              var ix = Math.min(L.W - 1, Math.floor(x));
              var iy = Math.min(L.H - 1, Math.floor(y));
              if (data[(iy * L.W + ix) * 4 + 3] > 110) {
                pts.push([x, y]);
              }
            }
          }
          return { pts: pts, step: step };
        }

        // Every animation is kept paused; the rAF loop scrubs currentTime from one shared clock.
        function applyClock() {
          var t = clock % CYCLE;
          for (var i = 0; i < anims.length; i++) {
            anims[i].currentTime = t;
          }
        }

        function build() {
          anims.forEach(function (a) {
            a.cancel();
          });
          anims = [];
          stage.textContent = "";
          var L = layout();
          stage.style.width = L.W + "px";
          stage.style.height = L.H + "px";
          var S = samplePoints(L);
          var rect = stage.getBoundingClientRect();
          var frag = document.createDocumentFragment();
          var fontSize = Math.max(8, S.step * 1.05);
          count = S.pts.length;
          S.pts.forEach(function (p) {
            var tx = p[0];
            var ty = p[1];
            var el = document.createElement("span");
            el.className = "g";
            el.textContent = CHARS.charAt(Math.floor(Math.random() * CHARS.length));
            el.style.left = tx.toFixed(1) + "px";
            el.style.top = ty.toFixed(1) + "px";
            el.style.width = S.step + "px";
            el.style.lineHeight = S.step + "px";
            el.style.fontSize = fontSize + "px";
            var lockColor = Math.random() < 0.18 ? ACCENT : LOCK;
            var lockT = tf(0, 0, 0, 1);
            if (P.reducedMotion) {
              el.style.transform = lockT;
              el.style.color = lockColor;
              frag.appendChild(el);
              return;
            }
            // Scatter positions are relative to the glyph's own target.
            var bx = rand(0, L.vw) - (rect.left + tx);
            var by = rand(0, L.vh) - (rect.top + ty);
            var ax = bx + rand(-60, 60);
            var ay = by + rand(-60, 60);
            var a2x = ax + rand(-70, 70);
            var a2y = ay + rand(-70, 70);
            var startRot = rand(-90, 90);
            var startT = tf(ax, ay, startRot, 0.7);
            // Assemble sweeps left to right with noise, dissolve radiates from the centre.
            var s = (tx / L.W) * 0.6 + Math.random() * 0.4;
            var dx = (tx / L.W - 0.5) * 2;
            var dy = (ty / L.H - 0.5) * 2;
            var rr = Math.min(1, Math.sqrt(dx * dx + dy * dy) / 1.4) * 0.6 + Math.random() * 0.4;
            var a0 = 0.03 + s * 0.13;
            var a1 = a0 + 0.2;
            var d0 = 0.64 + rr * 0.06;
            var d1 = d0 + 0.18;
            // A bright wave sweeps across the locked word so the hold is never frozen.
            var w = a1 + (d0 - a1) * (0.15 + 0.7 * (tx / L.W));
            var wHalf = (d0 - a1) * 0.12;
            var frames = [
              { offset: 0, transform: startT, opacity: 0.35, color: DUST, easing: "linear" },
              {
                offset: a0,
                transform: tf(a2x, a2y, rand(-180, 180), 0.8),
                opacity: 0.55,
                color: DUST,
                easing: "cubic-bezier(.16,1,.3,1)",
              },
              { offset: a1, transform: lockT, opacity: 1, color: lockColor, easing: "linear" },
              {
                offset: w - wHalf,
                transform: lockT,
                opacity: 1,
                color: lockColor,
                easing: "ease-in-out",
              },
              {
                offset: w,
                transform: tf(0, 0, 0, 1.18),
                opacity: 1,
                color: "#ffffff",
                easing: "ease-in-out",
              },
              {
                offset: w + wHalf,
                transform: lockT,
                opacity: 1,
                color: lockColor,
                easing: "linear",
              },
              {
                offset: d0,
                transform: lockT,
                opacity: 1,
                color: lockColor,
                easing: "cubic-bezier(.6,0,.9,.5)",
              },
              {
                offset: d1,
                transform: tf(bx, by, rand(-180, 180), 0.7),
                opacity: 0.4,
                color: DUST,
                easing: "linear",
              },
              { offset: 1, transform: startT, opacity: 0.35, color: DUST },
            ];
            var anim = el.animate(frames, { duration: CYCLE, iterations: Infinity, fill: "both" });
            anim.pause();
            anims.push(anim);
            frag.appendChild(el);
          });
          stage.appendChild(frag);
          applyClock();
          lastText = "";
          if (P.reducedMotion) {
            readout.textContent = count + " glyphs · locked";
          }
        }

        function phaseName(p) {
          if (p < 0.03) return "drifting";
          if (p < 0.36) return "assembling";
          if (p < 0.64) return "locked";
          if (p < 0.86) return "dissolving";
          return "drifting";
        }

        function tick(now) {
          requestAnimationFrame(tick);
          var dt = Math.min(100, now - lastNow);
          lastNow = now;
          if (P.reducedMotion || !anims.length || P.paused) {
            return;
          }
          clock += dt;
          applyClock();
          var p = (clock % CYCLE) / CYCLE;
          var pct = Math.floor(p * 100);
          var text = count + " glyphs · " + phaseName(p) + " · " + (pct < 10 ? "0" : "") + pct;
          if (text !== lastText) {
            lastText = text;
            readout.textContent = text;
          }
        }

        function start() {
          try {
            if (!stage.animate && !P.reducedMotion) {
              throw new Error("no WAAPI");
            }
            build();
          } catch (e) {
            hero.classList.add("nofx");
            return;
          }
          window.addEventListener("resize", function () {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function () {
              try {
                build();
              } catch (e) {
                hero.classList.add("nofx");
              }
            }, 160);
          });
          requestAnimationFrame(tick);
        }

        start();
      })();
    </script>
  </body>
</html>

Made by 6to9. Back to all motion patterns.