Cybernetic Glyph Grid
A Canvas 2D grid of hex pairs and glitch glyphs that pulse, flicker and re-randomize on a deterministic timer.
<CyberneticGlyphGrid isAnimating={true} tickMs={100} updateRatio={0.12} />
man cybernetic-glyph-grid
A line ref opens it in the source below
Name
cybernetic-glyph-grid — a Canvas 2D grid of hex pairs that glitch and settle, drawn from a glyph atlas.
How it works
Text drawn once. A glowing
fillText(shadowBlur) is the costly part, so each glyph is drawn once, per value, tone, font size and pixel ratio, on its own small canvas; every frame after that is onedrawImageper cell. The xray prints the atlas as it grows.A scattered rewrite. Every 100 ms, 12 % of the cells are rewritten, picked at
(tick × 11 + i × 37 + 17) mod n, each with a new hex pair from its ownlcgseed.Glitch, then settle. When
(seed >>> 1) & 0x1f ≤ 2(3 rewrites in 32) the cell shows one of 37 block tokens (▚▞,◢◣…) for 92 ms, jittering sideways, then lands on the hex pair it drew in advance.Fixed accents, a travelling pulse. One cell in 16 is pink for good (
((0x2f6a91c3 + i × 13) >>> 2) & 0xf == 0); every cell's alpha breathes on a 2.9 s sine shifted by(i × 17 mod 240) / 70s.
Cost
Capped. At most 30 draws a second,
devicePixelRatioheld at 1.25; the atlas is emptied only when the pixel ratio or the font size changes, or once the fonts have loaded.Idle when still. Paused, or under
prefers-reduced-motion: one frame, then norequestAnimationFrame. The xray and Step draw by hand.
See also
- glitch-signal-map — the same generator, drawn as blocks.
- computer-3d — this grid is zone 3 of its screen.
import { memo, useEffect, useEffectEvent, useRef, useState } from "react";
import * as styles from "./cybernetic-glyph-grid.css";
export interface CyberneticGlyphGridProps {
isAnimating: boolean;
className?: string;
/** Milliseconds between two rewrites of the grid. */
tickMs?: number;
/** Share of the cells rewritten each tick (0–1). */
updateRatio?: number;
/** The Lab's xray: cell boxes, the cells this tick rewrote, the glitching ones. */
xray?: boolean;
/** Changing it runs one tick by hand (while paused). */
stepToken?: number;
/** After each tick. */
onTick?: (stats: GlyphGridStats) => void;
/** Whenever the glyph atlas gains a bitmap. */
onAtlas?: (glyphs: readonly GlyphAtlasEntry[]) => void;
}
export interface GlyphGridStats {
tick: number;
cells: number;
cols: number;
rows: number;
rewritten: number;
/** Rewrites this tick that drew a glitch token. */
glitched: number;
/** Cells showing a glitch token right now. */
glitching: number;
atlas: number;
}
/** One cached bitmap: its key (`value:tone:size:dpr`) and the canvas drawn once for it. */
export interface GlyphAtlasEntry {
key: string;
canvas: HTMLCanvasElement;
width: number;
height: number;
}
const HEX = "0123456789ABCDEF";
const GLYPH_GLITCH_TOKENS = [
" ",
" .",
". ",
"░░",
"▒▒",
"▓▓",
"██",
"█░",
"░█",
"▚▞",
"▞▚",
"▖▗",
"▘▝",
"▙▟",
"▛▜",
"◢◣",
"◤◥",
"◧◨",
"◩◪",
"▔▁",
"▁▔",
"▏▕",
"▕▏",
"╳╳",
"╱╲",
"╲╱",
"┇┇",
"┊┊",
"╍╍",
"⟦⟧",
"⟨⟩",
"◌◌",
"◍◍",
"◇◆",
"◆◇",
"◎◉",
"◉◎",
];
const BASE_SEED = 0x2f6a91c3;
const LCG_A = 1664525;
const LCG_C = 1013904223;
const CELL_BASE_W = 16;
const CELL_BASE_H = 13;
const GAP_PX = 2;
const MAX_DPR = 1.25;
const TICK_MS = 100;
// Repaints capped at 30fps: cells mutate every TICK_MS and the pulse period
// is 2.9s, so redrawing on every rAF frame only burns CPU for no visible gain.
const FRAME_MS = 1000 / 30;
const UPDATE_RATIO = 0.12;
const PULSE_PERIOD_S = 2.9;
const PULSE_RADIANS_PER_SECOND = (Math.PI * 2) / PULSE_PERIOD_S;
const GLITCH_DURATION_MS = 92;
const GLITCH_BASE_ALPHA = 0.58;
const FONT_FAMILY = "Doto, system-ui, sans-serif";
export const CYBERNETIC_GLYPH_GRID_DEFAULTS = {
tickMs: TICK_MS,
updateRatio: UPDATE_RATIO,
glitchMs: GLITCH_DURATION_MS,
seed: BASE_SEED,
} as const;
const lcg = (seed: number): number => (seed * LCG_A + LCG_C) >>> 0;
const hexPair = (seed: number): [string, number] => {
const next = lcg(seed);
const value = `${HEX[next & 0x0f]}${HEX[(next >>> 4) & 0x0f]}`;
return [value, lcg(next)];
};
const glitchToken = (seed: number): [string, number] => {
const next = lcg(seed);
return [GLYPH_GLITCH_TOKENS[next % GLYPH_GLITCH_TOKENS.length] ?? "░░", lcg(next)];
};
interface CellState {
value: string;
seed: number;
accent: boolean;
glitchUntil: number;
settleValue: string;
settleSeed: number;
pulseDelay: number;
}
interface GridLayout {
cols: number;
rows: number;
cellW: number;
cellH: number;
}
type GlyphTone = "normal" | "accent" | "glitch";
interface GlyphBitmap {
canvas: HTMLCanvasElement;
width: number;
height: number;
}
const TONE_STYLES: Record<
GlyphTone,
{
color: string;
glow: string;
shadowBlur: number;
}
> = {
normal: {
color: "oklch(0.82 0.15 130)",
glow: "oklch(0.8858 0.182 95.69 / 0.12)",
shadowBlur: 7,
},
accent: {
color: "oklch(0.71 0.17 336)",
glow: "oklch(0.8858 0.182 95.69 / 0.18)",
shadowBlur: 10,
},
glitch: {
color: "oklch(0.77 0.21 105)",
glow: "oklch(0.5548 0.2575 312.98 / 0.2)",
shadowBlur: 12,
},
};
const buildCells = (count: number): CellState[] => {
const cells: CellState[] = [];
let seed = BASE_SEED;
for (let index = 0; index < count; index += 1) {
const [value, nextSeed] = hexPair(seed);
const cellSeed = nextSeed ^ (index * 2654435761);
cells.push({
value,
seed: cellSeed,
accent: (((BASE_SEED + index * 13) >>> 2) & 0x0f) === 0,
glitchUntil: 0,
settleValue: value,
settleSeed: cellSeed,
pulseDelay: ((index * 17) % 240) / 70,
});
seed = nextSeed;
}
return cells;
};
const computeLayout = (w: number, h: number): GridLayout => {
const cols = Math.max(8, Math.floor((w - GAP_PX) / (CELL_BASE_W + GAP_PX)));
const rows = Math.max(6, Math.floor((h - GAP_PX) / (CELL_BASE_H + GAP_PX)));
const cellW = (w - GAP_PX * (cols + 1)) / cols;
const cellH = (h - GAP_PX * (rows + 1)) / rows;
return { cols, rows, cellW, cellH };
};
const makeGlyphKey = (value: string, tone: GlyphTone, fontSize: number, dpr: number) =>
`${value}:${tone}:${Math.round(fontSize * 100)}:${Math.round(dpr * 100)}`;
const createGlyphBitmap = (
value: string,
tone: GlyphTone,
fontSize: number,
dpr: number,
): GlyphBitmap => {
const toneStyle = TONE_STYLES[tone];
const measureCanvas = document.createElement("canvas");
const measureCtx = measureCanvas.getContext("2d");
const font = `700 ${fontSize}px ${FONT_FAMILY}`;
let textWidth = fontSize * 1.6;
if (measureCtx) {
measureCtx.font = font;
textWidth = measureCtx.measureText(value).width;
}
const padding = Math.ceil(toneStyle.shadowBlur + 5);
const cssWidth = Math.ceil(textWidth + padding * 2);
const cssHeight = Math.ceil(fontSize * 1.65 + padding * 2);
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.ceil(cssWidth * dpr));
canvas.height = Math.max(1, Math.ceil(cssHeight * dpr));
const ctx = canvas.getContext("2d", {
alpha: true,
desynchronized: true,
});
if (ctx) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.font = font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = toneStyle.color;
ctx.shadowColor = toneStyle.glow;
ctx.shadowBlur = toneStyle.shadowBlur * dpr;
ctx.fillText(value, cssWidth / 2, cssHeight / 2);
}
return {
canvas,
width: cssWidth,
height: cssHeight,
};
};
export const CyberneticGlyphGrid = memo(
({
isAnimating,
className,
tickMs = TICK_MS,
updateRatio = UPDATE_RATIO,
xray = false,
stepToken = 0,
onTick,
onAtlas,
}: CyberneticGlyphGridProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const cellsRef = useRef<CellState[]>([]);
const glyphAtlasRef = useRef<Map<string, GlyphBitmap>>(new Map());
const layoutRef = useRef<GridLayout>({
cols: 12,
rows: 8,
cellW: CELL_BASE_W,
cellH: CELL_BASE_H,
});
const dprRef = useRef(1);
const canvasWRef = useRef(0);
const canvasHRef = useRef(0);
const fontSizeRef = useRef(0);
const rafRef = useRef<number | null>(null);
const animStartRef = useRef(0);
const lastTickRef = useRef(0);
const tickStepRef = useRef(0);
const mountedRef = useRef(false);
// What the last tick rewrote, for the xray; whether the atlas grew since it was reported.
const touchedRef = useRef<number[]>([]);
const glitchedRef = useRef(0);
const atlasGrewRef = useRef(false);
const xrayRef = useRef(xray);
const [reducedMotion, setReducedMotion] = useState(false);
const getGlyph = (
value: string,
tone: GlyphTone,
fontSize: number,
dpr: number,
): GlyphBitmap => {
const key = makeGlyphKey(value, tone, fontSize, dpr);
const cached = glyphAtlasRef.current.get(key);
if (cached) return cached;
const bitmap = createGlyphBitmap(value, tone, fontSize, dpr);
glyphAtlasRef.current.set(key, bitmap);
atlasGrewRef.current = true;
return bitmap;
};
// Effect events: called from the resize observer, font loading and the frame
// loop, they read the latest props without restarting those effects.
const ensureCells = useEffectEvent((count: number) => {
if (cellsRef.current.length === count) return;
cellsRef.current = buildCells(count);
});
const updateCells = useEffectEvent((now: number) => {
tickStepRef.current += 1;
const cells = cellsRef.current;
const count = cells.length;
if (count === 0) return;
const batch = Math.max(2, Math.floor(count * updateRatio));
const step = tickStepRef.current;
const touched: number[] = [];
let glitched = 0;
for (let i = 0; i < batch; i += 1) {
const ci = (step * 11 + i * 37 + 17) % count;
const cell = cells[ci];
if (!cell) continue;
touched.push(ci);
const shouldGlitch = ((cell.seed >>> 1) & 0x1f) <= 2;
if (shouldGlitch) glitched += 1;
const [nextValue, nextSeed] = shouldGlitch ? glitchToken(cell.seed) : hexPair(cell.seed);
cell.value = nextValue;
cell.seed = nextSeed;
if (shouldGlitch) {
const settleSeed = lcg(nextSeed);
const [settleValue, stableSeed] = hexPair(settleSeed);
cell.glitchUntil = now + GLITCH_DURATION_MS;
cell.settleValue = settleValue;
cell.settleSeed = stableSeed;
} else {
cell.glitchUntil = 0;
}
}
touchedRef.current = touched;
glitchedRef.current = glitched;
});
// The readout's numbers, and the atlas when it grew. Only while someone listens.
const report = useEffectEvent((now: number) => {
if (onTick) {
const { cols, rows } = layoutRef.current;
const cells = cellsRef.current;
onTick({
tick: tickStepRef.current,
cells: cells.length,
cols,
rows,
rewritten: touchedRef.current.length,
glitched: glitchedRef.current,
glitching: cells.filter((cell) => cell.glitchUntil > now).length,
atlas: glyphAtlasRef.current.size,
});
}
if (onAtlas && atlasGrewRef.current) {
atlasGrewRef.current = false;
onAtlas(
[...glyphAtlasRef.current].map(([key, glyph]) => ({
key,
canvas: glyph.canvas,
width: glyph.width,
height: glyph.height,
})),
);
}
});
const tick = useEffectEvent((now: number) => {
updateCells(now);
report(now);
});
const tickIsDue = useEffectEvent((now: number) => now - lastTickRef.current >= tickMs);
const draw = useEffectEvent((now: number) => {
const ctx = ctxRef.current;
if (!ctx) return;
const dpr = dprRef.current;
const { cols, rows, cellW, cellH } = layoutRef.current;
const cells = cellsRef.current;
const w = canvasWRef.current;
const h = canvasHRef.current;
if (w <= 0 || h <= 0 || cells.length === 0) return;
const running = isAnimating && !reducedMotion;
const elapsed = running ? (now - animStartRef.current) / 1000 : 0;
const fontSize = fontSizeRef.current;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
ctx.globalCompositeOperation = "source-over";
const bg = ctx.createLinearGradient(0, 0, 0, h);
bg.addColorStop(0, "oklch(0.13 0.03 210 / 0.92)");
bg.addColorStop(1, "oklch(0.095 0.02 225 / 0.96)");
ctx.fillStyle = bg;
ctx.fillRect(0, 0, w, h);
for (let row = 0; row < rows; row += 1) {
for (let col = 0; col < cols; col += 1) {
const idx = row * cols + col;
const cell = cells[idx];
if (!cell) continue;
if (cell.glitchUntil > 0 && now >= cell.glitchUntil) {
cell.value = cell.settleValue;
cell.seed = cell.settleSeed;
cell.glitchUntil = 0;
}
const isGlitch = cell.glitchUntil > 0;
const tone: GlyphTone = isGlitch ? "glitch" : cell.accent ? "accent" : "normal";
let alpha: number;
let offsetX = 0;
if (isGlitch) {
const phase = (now * 0.0053 + idx * 0.731) % 1;
alpha = GLITCH_BASE_ALPHA + 0.37 * Math.abs(Math.sin(phase * Math.PI));
offsetX = Math.sin(phase * 7.7) * 0.6;
} else {
alpha = running
? 0.54 +
0.32 *
(Math.sin((elapsed + cell.pulseDelay) * PULSE_RADIANS_PER_SECOND - Math.PI / 2) *
0.5 +
0.5)
: 0.62;
}
const cx = GAP_PX + col * (cellW + GAP_PX) + cellW / 2;
const cy = GAP_PX + row * (cellH + GAP_PX) + cellH / 2;
const glyph = getGlyph(cell.value, tone, fontSize, dpr);
ctx.globalAlpha = alpha;
ctx.drawImage(
glyph.canvas,
cx - glyph.width / 2 + offsetX,
cy - glyph.height / 2,
glyph.width,
glyph.height,
);
}
}
ctx.globalAlpha = 1;
if (xrayRef.current) {
const box = (index: number) => {
const col = index % cols;
const row = Math.floor(index / cols);
return [GAP_PX + col * (cellW + GAP_PX), GAP_PX + row * (cellH + GAP_PX)] as const;
};
ctx.lineWidth = 1;
ctx.strokeStyle = "oklch(0.82 0.15 130 / 0.16)";
for (let index = 0; index < cols * rows; index += 1) {
const [x, y] = box(index);
ctx.strokeRect(x + 0.5, y + 0.5, cellW - 1, cellH - 1);
}
// Glitching now: violet. Rewritten by the last tick: gold, on top.
ctx.strokeStyle = "oklch(0.62 0.25 313)";
cells.forEach((cell, index) => {
if (cell.glitchUntil <= now || index >= cols * rows) return;
const [x, y] = box(index);
ctx.strokeRect(x + 0.5, y + 0.5, cellW - 1, cellH - 1);
});
ctx.strokeStyle = "oklch(0.8858 0.182 95.69)";
ctx.lineWidth = 1.5;
for (const index of touchedRef.current) {
if (index >= cols * rows) continue;
const [x, y] = box(index);
ctx.strokeRect(x + 0.75, y + 0.75, cellW - 1.5, cellH - 1.5);
}
}
});
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => {
setReducedMotion(mq.matches);
};
update();
mq.addEventListener("change", update);
return () => {
mq.removeEventListener("change", update);
};
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
ctxRef.current = canvas.getContext("2d", {
alpha: false,
desynchronized: true,
});
if (ctxRef.current) {
ctxRef.current.imageSmoothingEnabled = false;
}
return () => {
ctxRef.current = null;
};
}, []);
useEffect(() => {
const el = rootRef.current;
const canvas = canvasRef.current;
if (!el || !canvas) return;
const resize = () => {
const rect = el.getBoundingClientRect();
const w = Math.floor(rect.width);
const h = Math.floor(rect.height);
if (w <= 0 || h <= 0) return;
const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
const layout = computeLayout(w, h);
const fontSize = Math.max(7, layout.cellH * 0.72);
const count = layout.cols * layout.rows;
const dprChanged = dprRef.current !== dpr;
const fontChanged = Math.abs(fontSizeRef.current - fontSize) > 0.25;
dprRef.current = dpr;
canvasWRef.current = w;
canvasHRef.current = h;
layoutRef.current = layout;
fontSizeRef.current = fontSize;
canvas.width = Math.max(1, Math.ceil(w * dpr));
canvas.height = Math.max(1, Math.ceil(h * dpr));
if (dprChanged || fontChanged) {
glyphAtlasRef.current.clear();
}
ensureCells(count);
draw(performance.now());
};
resize();
const ro = new ResizeObserver(resize);
ro.observe(el);
return () => {
ro.disconnect();
};
}, []);
useEffect(() => {
let cancelled = false;
document.fonts?.ready.then(() => {
if (cancelled) return;
glyphAtlasRef.current.clear();
draw(performance.now());
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
if (!isAnimating || reducedMotion) {
draw(performance.now());
return;
}
mountedRef.current = true;
animStartRef.current = performance.now();
lastTickRef.current = animStartRef.current;
tickStepRef.current = 0;
let lastDraw = 0;
const loop = (now: number) => {
if (!mountedRef.current) return;
if (!document.hidden && tickIsDue(now)) {
lastTickRef.current = now;
tick(now);
}
if (!document.hidden && now - lastDraw >= FRAME_MS) {
lastDraw = now;
draw(now);
}
rafRef.current = requestAnimationFrame(loop);
};
rafRef.current = requestAnimationFrame(loop);
return () => {
mountedRef.current = false;
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
}, [isAnimating, reducedMotion]);
// Paused or under reduced motion, nothing redraws on its own: the xray and a step do. A
// readout opening gets the grid and the whole atlas as they are, not after the next tick.
useEffect(() => {
xrayRef.current = xray;
const now = performance.now();
draw(now);
if (!xray) return;
atlasGrewRef.current = true;
report(now);
}, [xray]);
useEffect(() => {
if (stepToken === 0) return;
const now = performance.now();
tick(now);
draw(now);
}, [stepToken]);
const rootClassName = className ? `${styles.rootStyles} ${className}` : styles.rootStyles;
return (
<div ref={rootRef} className={rootClassName} aria-hidden="true">
<canvas ref={canvasRef} className={styles.canvasStyles} />
<div className={styles.noiseOverlayStyles} />
</div>
);
},
);