The page you're reading has Conway's Game of Life running underneath it. Move your mouse around if you want to see it react; otherwise it's there in the corner of your eye, slowly evolving. Hold ctrl (or cmd) and drag to tilt the field in 3D, as if you were rotating a slab to look at it from a different angle. If you don't notice any of this, that's the design working.

The starting point was the kitchen table, playing Conway with my kids. Still fun, decades after I first saw it. The natural next move was to put it somewhere it could keep running on its own.

To be clear up front: what runs on this page is a fun visualization based on Conway's Game of Life rather than a strict implementation of it. The B3/S23 rule is in there between ticks, but background noise injects cells, birth events emit visible flashes, and the per-cell timing is randomised in ways canonical Conway wouldn't recognise. The point is the visualisation, not the simulation.

The first cut was loud. Live cells at full opacity, generations in lockstep, the whole field pulsing on every tick. It looked great as a demo and was unreadable as a backdrop. The rest of this post is how I closed the gap.

The brightness ceiling

Every cell is drawn from a precomputed offscreen sprite via drawImage, scaled per-cell by globalAlpha. The sprite is baked once at a configurable peak alpha (default 0.06) and its glow (a shadowBlur at a related alpha) is rasterised into the sprite at the same moment. Per-frame work in the inner loop is one alpha assignment and one drawImage per visible cell, roughly an order of magnitude cheaper than the original per-cell path-fill plus shadow.

Subliminal noise, mouse-paint trails, and the birth flashes (described below) each carry their own sprite at their own brightness ceiling. The live-cell alpha stays anchored to body-text readability; the other channels are free to be brighter or dimmer without dragging cells with them. Each visible channel is one extra drawImage on the cells where it's active, and most cells in any frame are dead, so the inner loop is mostly skip.

Tuning glow and cell alpha independently was the first thing I tried and the first thing I undid: the halo lingered brighter than the cells it was meant to soften. Baking them into one sprite forces the relationship to stay correct under any setting.

Why it doesn't pulse

Conway's tick is synchronous. Without intervention, every fade animation starts at the same instant, progresses at the same rate, and ends at the same instant, which reads as a clock. Three small decouplings break the rhythm:

tick fires at t = 0 rate jitter different speeds post-tick stagger different starts fade > tick next tick lands mid-fade
Three independent tricks, applied at once. Each one alone would still leave a faint rhythm; all three together and the field reads as continuous.

None of the three is doing the work alone. Drop any one and a faint rhythm comes back; all three together is what makes the field read as continuously alive rather than as a slow strobe.

Why it doesn't die

A sparse-seed field either thins out or settles into a few oscillators within a minute or two, so I added a steady background trickle of random events. Most of them are display-only: a brightness bump on the buffer that never touches the Conway grid. A small fraction get promoted to seeds: tiny alive clusters injected into the grid. Most seeds die on the next tick (B3/S23 is harsh on small structures) but the trickle is enough that there's always a new lineage starting somewhere.

Where the seeds land

The first version biased seed placement toward existing patterns on the intuition that seeds adjacent to live cells would actually grow rather than die alone. It worked and produced a static-looking field. Busy patches got more seeds and stayed put; dead patches stayed dead. The geography of the page didn't change much over time.

So I flipped the bias. Empty regions are now more likely to seed than full ones:

promoteProb = seedProb + seedBoost * (1 - localDensity)

Each event measures local life density in a small disk around the picked cell. The default gradient is gentle (about an 11x ratio between fully dead and fully alive neighbourhoods, where the first version was 120x in the opposite direction). New life sparks in the gaps, grows toward existing patterns, collides with them, and disturbs whatever had settled there.

Small change in code, large change in feel. The bias is subtle enough that nothing announces "a seed just landed in a quiet zone," but the earlier version settled within a screen-or-two of scroll and this one doesn't.

When new life appears

With the field this quiet, the random events were hard to see. A noise seed landing in dead space and surviving its first generation produces a Conway-born cluster that looks identical to any other. The pattern of seeded life was happening on the page; you just couldn't tell where. So I wanted births to be visible as events: a brief bright pop on top of the cell at the moment of birth, decaying back to the normal fade-in over a second or two.

The naive version was: every dead-to-alive flip writes a flash, drawn as an extra overlay layer scaled by an independent brightness alpha. Tried it, immediately collapsed into a heartbeat. Conway produces dozens to hundreds of births at every tick boundary; a uniform flash on all of them reads as a single bright pulse synchronised to the tick. The thing I was trying to surface (rare seed events) drowned in the routine production of natural births.

Two fixes, applied together.

Separate channels by source. Noise seeds and dropped patterns, mouse paint trails, and natural Conway births each get their own flash buffer, sprite, and brightness ceiling. The noise channel can pop bright to surface the random events; paint trails stay discreet so you can drag the mouse across the page without lighting it up like a billboard; Conway births default to a subtle pop (just enough to show where the simulation is producing new life on its own), and the slider lets you turn that up to debug or to 0 to hide it. One precomputed sprite per channel, one extra drawImage per cell when that channel is active.

Independent scheduling for the Conway channel. Each cell that becomes alive via the natural rule picks a random moment uniformly across 1.8x the tick interval to fire its flash, and the flash is gated both at decay-start and at render-time so it actually waits for that moment. Flashes from the previous tick are still decaying when the next tick's births start firing; on average, flashes from two consecutive ticks are in some phase of appearing at any given instant. The visible births read as scattered events rather than a clock.

The drop-pattern button at the bottom of the controls below is the cheapest way to see all of this working. Click it: a glider, a lightweight spaceship, an acorn, or a Gosper glider gun lands at a random spot on the page. The seed cells pop bright via the birth channel; the descendents that the Conway rule produces from them flash on the natural-birth channel (if you turn that slider up); the noise events keep firing in the background regardless.

Theme reactivity

The canvas reads its colour from getComputedStyle on every frame, picking up var(--fg) from the servo theme. Toggling light/dark via the toolbar recolours the field live: simulation state, per-cell fade rates, and noise events all keep running, only the rendering changes. Contrast against the background lands at roughly the same level in both modes, so there's no separate tuning pass for light.

Knobs you'd actually turn

About a dozen parameters drive the feel of the field, exposed as sliders below. Hover any of them for what it does. Everything tunes the simulation behind this page in real time; the defaults are what I'd ship.

alpha 0.06
tickMs 2500
speed 1.00x
flashRate 0.060
flashAlpha 0.30
noiseAlpha 0.40
birthAlpha 0.40
paintAlpha 0.20
normalBirthAlpha 0.10
seedBoost 0.005

Everything else has a default that's defensible without being tuned. The whole thing fits in a 400-line file plus a one-line script tag.

The lesson, if there is one: ambient effects fail the moment they ask for attention. The trick isn't building something subtle, it's building something normal and then patiently subtracting from it until it stops competing with the content. Conway started this design loud. Every change since has been making it quieter without making it dead.