Live demo, running in your browser

ASCII fish

A green-phosphor terminal scene where a huge SIGNAL wordmark sits over drifting ASCII noise while schools of ASCII fish swim across and between the letters on sine paths, with wavy scanline distortion and a CRT overlay.

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>SIGNAL</title
    ><style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      html,
      body {
        height: 100%;
        background: #040806;
        overflow: hidden;
      }
      body {
        font-family: ui-monospace, "SF Mono", Menlo, Consolas, "Courier New", monospace;
        color: #d9e2d6;
      }
      .hero {
        position: relative;
        width: 100vw;
        height: 100vh;
        height: 100dvh;
        overflow: hidden;
        background: radial-gradient(120% 90% at 50% 45%, #07120b 0%, #040806 60%, #020403 100%);
      }
      canvas {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        display: block;
      }
      .crt {
        position: absolute;
        inset: 0;
        pointer-events: none;
        background: repeating-linear-gradient(
            to bottom,
            rgba(0, 0, 0, 0.22) 0 1px,
            transparent 1px 3px
          ),
          radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.65) 100%);
      }
      .bar {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        display: flex;
        justify-content: space-between;
        gap: 12px;
        padding: max(14px, 2.6vh) 4vw;
        font-size: clamp(10px, 1.1vw, 13px);
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: #7dff9e;
        text-shadow: 0 0 8px rgba(80, 255, 140, 0.5);
      }
      .bar .dot {
        display: inline-block;
        width: 0.7em;
        height: 0.7em;
        border-radius: 50%;
        background: #7dff9e;
        margin-right: 0.8em;
        box-shadow: 0 0 10px #7dff9e;
        animation: bl 1.6s steps(2) infinite;
      }
      @keyframes bl {
        50% {
          opacity: 0.15;
        }
      }
      .foot {
        position: absolute;
        left: 4vw;
        right: 4vw;
        bottom: max(18px, 5vh);
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        gap: 24px;
      }
      .foot p {
        max-width: 36ch;
        font-size: clamp(12px, 1.15vw, 15px);
        line-height: 1.55;
        color: #d3dccf;
        text-shadow: 0 1px 8px #000;
      }
      .cta {
        display: inline-block;
        white-space: nowrap;
        padding: 0.9em 1.3em;
        border: 1px solid #7dff9e;
        color: #7dff9e;
        text-decoration: none;
        font-size: clamp(12px, 1.1vw, 14px);
        letter-spacing: 0.08em;
        background: rgba(4, 14, 8, 0.7);
        box-shadow:
          0 0 18px rgba(80, 255, 140, 0.25),
          inset 0 0 12px rgba(80, 255, 140, 0.12);
      }
      .cta:hover {
        background: #7dff9e;
        color: #031008;
      }
      .caret {
        display: inline-block;
        width: 0.6em;
        height: 1em;
        background: currentColor;
        margin-left: 0.5em;
        vertical-align: -0.15em;
        animation: bl 1s steps(2) infinite;
      }
      @media (max-width: 700px) {
        .foot {
          flex-direction: column;
          align-items: flex-start;
          gap: 16px;
        }
      }
      .fb {
        display: none;
      }
      .nofx canvas,
      .nofx .crt {
        display: none;
      }
      .nofx .fb {
        display: flex;
        position: absolute;
        inset: 0;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 2vh;
        overflow: hidden;
      }
      .fb h1 {
        font:
          900 24vw/0.9 "Arial Black",
          "Helvetica Neue",
          Arial,
          sans-serif;
        letter-spacing: -0.04em;
        color: #ece8dc;
      }
      .fb pre {
        color: #7dff9e;
        font-size: clamp(14px, 2.4vw, 28px);
        line-height: 1.3;
        animation: dr 9s ease-in-out infinite alternate;
        text-shadow: 0 0 10px rgba(80, 255, 140, 0.6);
      }
      @keyframes dr {
        from {
          transform: translateX(-6vw);
        }
        to {
          transform: translateX(6vw);
        }
      }
    </style></head
  ><body
    ><div class="hero"
      ><canvas id="c" role="img" aria-label="SIGNAL"></canvas><div class="crt"></div
      ><div class="fb"
        ><h1>SIGNAL</h1
        ><pre>
&gt;&lt;(((°&gt;    &gt;&lt;(((((°&gt;
   &lt;°)))&gt;&lt;     &lt;°)))))&gt;&lt;</pre
        ></div
      ><div class="bar"
        ><span><i class="dot"></i>brinelab / school.exe</span
        ><span>v2.4 &#9646; live tide</span></div
      ><div class="foot"
        ><p
          >Listen to the whole stream. Brinelab turns noisy telemetry into a school that moves as
          one.</p
        ><a class="cta" href="#">$ start --trace<span class="caret"></span></a></div></div
    ><script>
      (function () {
        var P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
        try {
          var cv = document.getElementById("c");
          var ctx = cv.getContext("2d");
          if (!ctx) {
            document.body.classList.add("nofx");
            return;
          }
          var MONO = "ui-monospace,'SF Mono',Menlo,Consolas,'Courier New',monospace";
          var SANS = "'Arial Black','Helvetica Neue',Arial,sans-serif";
          var BS = String.fromCharCode(92);
          var MIR = { "/": BS, "(": ")", ")": "(", "<": ">", ">": "<", "{": "}", "}": "{" };
          MIR[BS] = "/";
          var NF = 4;
          var W = 0,
            H = 0,
            dpr = 1,
            letters = [],
            fish = [],
            bg = null,
            bgW = 0,
            cache = {};
          var CFG = {
            tiny: { C: 7, R: 1, m: 1.1 },
            small: { C: 13, R: 3, m: 1.1 },
            mid: { C: 22, R: 5, m: 1.2 },
            big: { C: 34, R: 7, m: 1.45 },
          };
          var RATE = { tiny: 7, small: 6, mid: 4.5, big: 3.2 };
          function clamp(v, a, b) {
            return Math.max(a, Math.min(b, v));
          }
          function rng(s) {
            return function () {
              s |= 0;
              s = (s + 0x6d2b79f5) | 0;
              var t = Math.imul(s ^ (s >>> 15), 1 | s);
              t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
              return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
            };
          }
          function genGlyphs(kind, f) {
            var c0 = CFG[kind],
              C = c0.C,
              R = c0.R,
              g = [];
            if (kind === "tiny") {
              var tails = ["><", ">{", ">-", ">{"];
              var s = tails[f] + "(((" + String.fromCharCode(176) + ">";
              var ds = [0.3, 0.35, 0.5, 0.65, 0.8, 1, 0.6];
              for (var i = 0; i < s.length; i++) g.push({ c: i, r: 0, ch: s[i], d: ds[i] });
              return g;
            }
            var wag = Math.sin((f / NF) * Math.PI * 2);
            var te = 0.24;
            var ramp = ".:-=+*#%@";
            var gc = Math.round((te + 0.78 * (1 - te)) * (C - 1));
            var ec = Math.round((te + 0.88 * (1 - te)) * (C - 1));
            var er = Math.max(0, Math.floor((R - 1) / 2) - (R >= 5 ? 1 : 0));
            for (var r = 0; r < R; r++) {
              var v = (r - (R - 1) / 2) / (R / 2);
              for (var c = 0; c < C; c++) {
                var u = c / (C - 1);
                var ch = null,
                  d = 0.5;
                if (u < te) {
                  var t = u / te;
                  var vv = v - wag * 0.35 * (1 - t);
                  var ht = 0.35 + 0.6 * (1 - t);
                  var gap = 0.3 * (1 - t);
                  var av = Math.abs(vv);
                  if (av <= ht && av >= gap) {
                    if (t < 0.6 && av > 0.12) {
                      ch = vv < 0 ? BS : "/";
                      d = 0.35 + 0.2 * t;
                    } else {
                      ch = "-";
                      d = 0.4;
                    }
                  }
                } else {
                  var s2 = (u - te) / (1 - te);
                  var h = Math.pow(Math.sin(Math.PI * Math.pow(s2, 1.3)), 0.6) * 0.92 + 0.08;
                  if (Math.abs(v) <= h) {
                    d =
                      0.95 -
                      (0.7 * Math.abs(v)) / Math.max(h, 0.1) +
                      v * 0.18 +
                      0.1 * Math.sin(c * 1.9 + r * 2.3 + f * 0.8);
                    d = clamp(d, 0, 1);
                    ch = ramp[Math.floor(d * (ramp.length - 1))];
                    if (c === gc && Math.abs(v) < h * 0.85) {
                      ch = ")";
                      d = 0.55;
                    }
                    if (c === ec && r === er) {
                      ch = String.fromCharCode(176);
                      d = 1;
                    }
                    if (c === C - 1 && Math.abs(v) < 0.01) {
                      ch = ">";
                      d = 0.6;
                    }
                  }
                }
                if (ch) g.push({ c: c, r: r, ch: ch, d: d });
              }
            }
            return g;
          }
          function getSprite(kind, dir, unit) {
            var key = kind + dir + "_" + unit + "_" + dpr;
            if (cache[key]) return cache[key];
            var c0 = CFG[kind],
              C = c0.C,
              R = c0.R,
              cell = unit * c0.m,
              cw = cell * 0.6,
              lh = cell * 1.05,
              pad = cell * 0.45;
            var fr = [],
              w = 0,
              h = 0;
            for (var f = 0; f < NF; f++) {
              var cnv = document.createElement("canvas");
              cnv.width = Math.ceil((C * cw + pad * 2) * dpr);
              cnv.height = Math.ceil((R * lh + pad * 2) * dpr);
              w = cnv.width / dpr;
              h = cnv.height / dpr;
              var x = cnv.getContext("2d");
              x.setTransform(dpr, 0, 0, dpr, 0, 0);
              x.font = "700 " + cell + "px " + MONO;
              x.textAlign = "center";
              x.textBaseline = "middle";
              x.lineJoin = "round";
              x.lineWidth = cell * 0.34;
              var g = genGlyphs(kind, f);
              for (var i = 0; i < g.length; i++) {
                var q = g[i];
                var cc = dir < 0 ? C - 1 - q.c : q.c;
                var ch = dir < 0 ? MIR[q.ch] || q.ch : q.ch;
                var px = pad + (cc + 0.5) * cw,
                  py = pad + (q.r + 0.5) * lh;
                x.strokeStyle = "rgba(2,6,4,.96)";
                x.strokeText(ch, px, py);
                x.fillStyle =
                  ch === String.fromCharCode(176)
                    ? "#f2fff5"
                    : "hsl(138,92%," + (30 + q.d * 50) + "%)";
                x.fillText(ch, px, py);
              }
              fr.push(cnv);
            }
            var o = { fr: fr, w: w, h: h };
            cache[key] = o;
            return o;
          }
          function layout() {
            var portrait = W / H < 0.85;
            var lines = portrait ? ["SIG", "NAL"] : ["SIGNAL"];
            var sy = portrait ? 1.25 : 1.45;
            var base = 100;
            ctx.font = "900 " + base + "px " + SANS;
            var lsr = -0.035;
            var widths = lines.map(function (L) {
              return L.split("").map(function (ch) {
                return ctx.measureText(ch).width;
              });
            });
            var lineW = widths.map(function (ws) {
              return (
                ws.reduce(function (a, b) {
                  return a + b;
                }, 0) +
                lsr * base * (ws.length - 1)
              );
            });
            var maxW = Math.max.apply(null, lineW);
            var size = (base * (W * (portrait ? 0.92 : 0.93))) / maxW;
            var capR = 0.72;
            size = Math.min(size, (H * 0.52) / (capR * sy * lines.length));
            var lineH = capR * size * sy;
            var gap = size * 0.07;
            var total = lines.length * lineH + (lines.length - 1) * gap;
            var cy = H * (portrait ? 0.4 : 0.47);
            var top = cy - total / 2;
            letters = [];
            var idx = 0;
            lines.forEach(function (L, k) {
              var lw = (lineW[k] * size) / base;
              var x = (W - lw) / 2;
              var y = top + (k + 1) * lineH + k * gap;
              for (var i = 0; i < L.length; i++) {
                letters.push({ ch: L[i], x: x, y: y, size: size, sy: sy, front: idx % 2 === 0 });
                x += (widths[k][i] * size) / base + lsr * size;
                idx++;
              }
            });
          }
          function buildBg() {
            var cell = W < 700 ? 13 : 16;
            var cwid = cell * 0.62;
            bgW = W + 60;
            bg = document.createElement("canvas");
            bg.width = Math.ceil(bgW * dpr);
            bg.height = Math.ceil(H * dpr);
            var x = bg.getContext("2d");
            x.setTransform(dpr, 0, 0, dpr, 0, 0);
            x.font = "600 " + (cell - 3) + "px " + MONO;
            x.textAlign = "center";
            x.textBaseline = "middle";
            var R = rng(3);
            var cols = Math.ceil(bgW / cwid),
              rows = Math.ceil(H / cell);
            var set = [".", " ", " ", String.fromCharCode(183), ":", "+", ","];
            for (var r = 0; r < rows; r++) {
              for (var c = 0; c < cols; c++) {
                var s = Math.sin(c * 0.12 + r * 0.55) + Math.sin(c * 0.05 - r * 0.3);
                var ch, a;
                if (s > 1.1) {
                  ch = R() < 0.5 ? "~" : "-";
                  a = 0.2;
                } else {
                  ch = set[Math.floor(R() * set.length)];
                  a = 0.07 + R() * 0.1;
                }
                if (ch === " ") continue;
                x.fillStyle = "rgba(70,255,140," + a + ")";
                x.fillText(ch, (c + 0.5) * cwid, (r + 0.5) * cell);
              }
            }
          }
          function buildFish() {
            var R = rng(11);
            var mob = W < 700;
            var unit = clamp(W / 110, 8, 14);
            var spd = clamp(W / 1440, 0.55, 1);
            var plan = [
              ["big", 1, 0.36, 1, 32],
              ["big", -1, 0.6, 1, 26],
              ["mid", 1, 0.5, 3, 44],
              ["mid", -1, 0.27, 2, 40],
              ["small", 1, 0.7, 4, 56],
              ["small", -1, 0.43, 4, 52],
              ["tiny", 1, 0.22, 6, 70],
              ["tiny", -1, 0.74, 6, 66],
              ["tiny", 1, 0.47, 4, 78],
            ];
            fish = [];
            plan.forEach(function (p) {
              var kind = p[0],
                dir = p[1],
                lane = p[2],
                n = mob ? Math.max(1, Math.round(p[3] * 0.6)) : p[3],
                base = p[4];
              if (mob && kind === "big" && dir < 0) n = 0;
              if (!n) return;
              var sp = getSprite(kind, dir, unit);
              var k = (Math.PI * 2) / (W * (0.5 + R() * 0.4));
              var amp = H * (mob ? 0.045 : 0.06) * (0.7 + R() * 0.6);
              var ws = 0.35 + R() * 0.4;
              var ph = R() * 6.28;
              var span = W + sp.w * 2 + 60;
              var start = R() * span;
              var gp = sp.w * (1.1 + R() * 0.8);
              for (var i = 0; i < n; i++) {
                fish.push({
                  sp: sp,
                  dir: dir,
                  y: H * (0.11 + (lane - 0.2) * 1.05) + (R() - 0.5) * H * 0.05,
                  x0: start + i * gp + R() * sp.w * 0.4,
                  speed: base * spd * (0.9 + R() * 0.2),
                  k: k,
                  amp: amp,
                  ws: ws,
                  ph: ph + R() * 0.6,
                  rate: RATE[kind],
                  span: span,
                  wamp: 0.6 + sp.h * 0.035,
                });
              }
            });
          }
          function build() {
            W = window.innerWidth;
            H = window.innerHeight;
            dpr = Math.min(window.devicePixelRatio || 1, P.maxDpr || 2);
            cv.width = Math.round(W * dpr);
            cv.height = Math.round(H * dpr);
            cache = {};
            layout();
            buildBg();
            buildFish();
          }
          function drawLetters(front) {
            ctx.textBaseline = "alphabetic";
            ctx.textAlign = "left";
            ctx.fillStyle = "#ece8dc";
            for (var i = 0; i < letters.length; i++) {
              var l = letters[i];
              if (l.front !== front) continue;
              ctx.save();
              if (front) {
                ctx.shadowColor = "rgba(0,0,0,.8)";
                ctx.shadowBlur = 22;
              }
              ctx.translate(l.x, l.y);
              ctx.scale(1, l.sy);
              ctx.font = "900 " + l.size + "px " + SANS;
              ctx.fillText(l.ch, 0, 0);
              ctx.restore();
            }
          }
          function drawBg(t) {
            var sh = 6;
            for (var y = 0; y < H; y += sh) {
              var hh = Math.min(sh, H - y);
              var off = Math.sin(y * 0.012 + t * 0.7) * 14 + Math.sin(y * 0.05 - t * 1.3) * 4;
              ctx.drawImage(
                bg,
                0,
                y * dpr,
                bg.width,
                Math.min(hh * dpr, bg.height - y * dpr),
                -30 + off,
                y,
                bgW,
                hh,
              );
            }
          }
          function drawFish(t) {
            for (var i = 0; i < fish.length; i++) {
              var f = fish[i],
                sp = f.sp;
              var x = (f.x0 + f.dir * f.speed * t) % f.span;
              if (x < 0) x += f.span;
              x -= sp.w + 30;
              var arg = x * f.k + t * f.ws + f.ph;
              var y = f.y + Math.sin(arg) * f.amp + Math.sin(t * 0.9 + f.ph * 2) * H * 0.006;
              var ang = Math.atan(Math.cos(arg) * f.amp * f.k) * 0.8;
              var fi = ((Math.floor(t * f.rate + f.ph * 3) % NF) + NF) % NF;
              var cn = sp.fr[fi];
              ctx.save();
              ctx.translate(x, y);
              ctx.rotate(ang);
              var sh = 3;
              for (var y0 = 0; y0 < sp.h; y0 += sh) {
                var hh = Math.min(sh, sp.h - y0);
                var sy0 = y0 * dpr;
                var sh0 = Math.min(hh * dpr, cn.height - sy0);
                if (sh0 <= 0) break;
                var off = Math.sin(y0 * 0.22 + t * f.rate * 1.2 + f.ph) * f.wamp;
                ctx.drawImage(cn, 0, sy0, cn.width, sh0, -sp.w / 2 + off, -sp.h / 2 + y0, sp.w, hh);
              }
              ctx.restore();
            }
          }
          function draw(t) {
            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
            ctx.clearRect(0, 0, W, H);
            ctx.fillStyle = "#040806";
            ctx.fillRect(0, 0, W, H);
            drawBg(t);
            drawLetters(false);
            drawFish(t);
            drawLetters(true);
          }
          var T = 0,
            last = 0,
            still = false;
          function loop(ts) {
            requestAnimationFrame(loop);
            try {
              if (P.paused) {
                last = ts;
                return;
              }
              if (P.reducedMotion) {
                if (!still) {
                  draw(6);
                  still = true;
                }
                last = ts;
                return;
              }
              var dt = last ? Math.min((ts - last) / 1000, 0.05) : 0;
              last = ts;
              T += dt;
              draw(T);
            } catch (e) {}
          }
          build();
          window.addEventListener("resize", function () {
            try {
              build();
              still = false;
              if (P.reducedMotion || P.paused) draw(T || 6);
            } catch (e) {}
          });
          draw(P.reducedMotion ? 6 : 0);
          requestAnimationFrame(loop);
        } catch (e) {
          document.body.classList.add("nofx");
        }
      })();
    </script></body
  ></html
>

Made by 6to9. Back to all motion patterns.