← Back to ZHABOTINSKY Index

Lab Notebook · How it was made

A clock
with no hands

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.

01 · The concept

One job: prove the reaction is alive

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.

02 · Palette

Four colours, one chemical story

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.

Reagent dark
#160A12
The unlit medium at rest, deep in reduction. Everything is written on this.
Reduced red
#D63A3A
Ferroin — the recovering wake trailing every front.
Oxidation blue
#3A7BD8
Ferriin — the excited leading edge. The accent, used only for marks and display.
Rim highlight
#E8D0C0
The bright crest at the steepest change — and the body-text ink.
03 · Type

Display shouts, instrument whispers

Spiral
Unica One · display

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.

0.75 · 0.01
IBM Plex Mono · everything else

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.

04 · Techniques

Signature: a Barkley excitable medium on the GPU

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.

05 · Iteration log

Three passes

Pass 1
Craft — spacing, type, contrast, copy
  • Set a deliberate type scale: hero at clamp(3.1rem,12.5vw,9.2rem), section titles at a single display step, mono body pinned to ≤52ch so lines stay comfortable.
  • Pulled all small text off the blue accent — blue tested at ~4.3:1 on reagent dark, under the floor — and moved body/labels to the warm rim ink, which clears it comfortably.
  • Unified radii (6–8px), one hairline rule token, and a single ease curve across reveals and hovers. Rewrote hero and legend copy to name real chemistry (ferroin, ferriin, refractory) instead of filler.
Pass 2
Depth — enrich the signature, add a second read
  • Correctness first: the initial field of four scattered broken discs kept dying — the fronts propagated to the boundary and the medium relaxed back to rest. Replaced it with the canonical Barkley cross initial condition (a u-step crossed by a v-step), which pins a rotating spiral in view that genuinely never settles.
  • Added the live oscilloscope: a real off-centre probe read off the GPU each frame, driving a measured oscillation period in the HUD and scope caption — the second-read detail that proves the field is computed, not filmed.
  • Enriched the display shader with a rim-highlight term on the steepest gradient, so wavefronts carry a thin bright crest; pointer-down seeds a real broken wave, so a touch grows a new spiral rather than a splash.
Pass 3
Hardening — mobile, reduced-motion, performance
  • Caught a silent GPU bug under headless verification: the one-pixel probe target was built without width/height, so the render pass set a NaN viewport and 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.
  • Strengthened the hero scrim once the field filled edge-to-edge: the lede sat over a busy region, so a taller bottom gradient now holds body copy well above 4.5:1. Simulation grid drops to 260px with fewer substeps on small screens.
  • Reduced-motion path runs the reaction forward ~260 steps then paints one settled spiral frame — never blank, never moving. DPR capped at 2, rAF paused on document.hidden and off-screen via IntersectionObserver, sim reallocated only on a real aspect change.