Lab Notebook · How it was made
ZHABOTINSKY is a fictional exhibit for a real thing: the Belousov–Zhabotinsky reaction, the beaker that oxidises blue, reduces red, and refuses to reach equilibrium. The page runs the reaction live on the GPU as an excitable medium, so the spirals you see are computed, not looped — and every touch seeds a genuine new core.
The single job of the page is to make a visitor believe the loop is not a video. Everything serves that: a real two-variable simulation on the canvas, a live oscilloscope reading concentration off the running field, and a seed-on-touch interaction that only makes sense if the medium is truly excitable. The audience is anyone who has seen a BZ reaction in a petri dish and wondered how a liquid can keep time.
The palette is argued straight from the chemistry: the two oxidation states of the ferroin indicator, the dark reagent they live in, and a highlight for the wavefront. Bright blue is reserved for lines, marks and large display only — body text takes the warm rim ink, which clears 4.5:1 on the reagent dark.
A tall, condensed all-caps face with a slightly industrial pinch — it reads like a hand-lettered reagent label. Used only for headings and the wordmark, always uppercase, tight leading.
The whole instrument voice — labels, body, HUD, legend — is one monospace. Short measures and tabular figures make the page read like a lab readout, which is the point.
The moment of bravery: the hero wordmark set enormous in Unica One with a single blue word — own time — carrying the whole idea, while every supporting line stays in disciplined mono.
The field is a real reaction–diffusion system, not the Gray–Scott growth model — it is the Barkley model of an excitable medium, the textbook system for spiral waves. Two chemicals live in one RGBA16F texture: u (fast excitation) in red, v (slow recovery) in green. Each frame a fragment shader ping-pongs between two framebuffers, integrating ∂u/∂t = ∇²u + (1/ε)·u(1−u)(u − (v+b)/a) and ∂v/∂t = u − v with a nine-point Laplacian, twelve substeps a frame. A cell fires only after its recovery variable relaxes, so fronts travel outward and never reflect — that refractory lag is the whole trick.
Spirals need a broken wavefront to curl around. The seed pass stamps a small disc where u=1 but sets the recovery variable high on only one half of the disc — an excited front that is already refractory on one side, so it pinwheels into a rotating spiral. That is exactly what happens when you touch the dish: a new core, computed from the same rule. A separate one-pixel probe pass renders the centre concentration into an RGBA8 target and readPixels pulls it back each frame; that byte drives the oscilloscope and a real rising-edge measurement of the oscillation period. No libraries — raw WebGL2 with a WebGL1 half-float fallback and a static CSS spiral if no context is available.
clamp(3.1rem,12.5vw,9.2rem), section titles at a single display step, mono body pinned to ≤52ch so lines stay comfortable.readPixels always returned zero — the clock read a flat line forever. Fixed the target, and the measured period now locks on after the spiral reaches the probe.document.hidden and off-screen via IntersectionObserver, sim reallocated only on a real aspect change.