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>
><(((°> ><(((((°>
<°)))>< <°)))))><</pre
></div
><div class="bar"
><span><i class="dot"></i>brinelab / school.exe</span
><span>v2.4 ▮ 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.