A modified Game of Life on a 600×600 grid with a blue / black / white colouring rule — a plaid of hard rails that dissolves into organic noise. Built with TypeScript and thi.ng/umbrella: a declarative reactive shell (atom + streams + rdom) around a tight typed-array kernel, because that's where the speed actually lives.
▶︎ Run it live — drag to draw · press r for a new rule · Download PNG to keep a frame
🔊 Audio POC (experimental) — the same automaton, sonified: press a for sound, size the region to set pitch. Rough proof of concept — see Audio POC below.
This started life around 2016 as a Processing sketch — sketch_10, one in a
series of quick cellular-automaton experiments. The rule is Conway's Game of Life
"slightly modified ;)": alongside the usual survival check, each cell copies its
top-left neighbour, and a living cell is tinted blue when the cell to its left
is also alive. The result is neither the usual Life soup nor a clean fractal.
This repo is that sketch revamped as modern web tech — the same rule, bit for bit,
now running in the browser on a typed-array core, with interactive rule controls,
freehand drawing, and still-image export. The original lives on as a reference:
sketch_10_ca_experiment_optimized.pde.
Still frames exported straight from the app with Download PNG — different rules from the Randomize pool, caught at different generations.
yarn install
yarn dev # http://localhost:5173
yarn build # normal chunked dist/
yarn build:single # one self-contained dist/index.html (no external requests)
yarn test # verifies the engine is bit-exact vs the original sketch| Action | Input |
|---|---|
| Run / pause | space or the Run button |
| Step one frame | n or Step ▶ — advance a single generation (only while paused; disabled during run) |
| Randomize rule | r or Randomize — random rule, keeps the current buffer so it keeps evolving under the new algorithm |
| Reset | x or Reset — restore the original rule + defaults, then reseed |
| Seed | s or Seed — lay down the first-frame seed pattern, keeping the current rule |
| Clear stage | c or Clear — empty the grid completely (draw your own) |
| Download frame | Download PNG — saves the current canvas as ca-experiment-gen<N>-<hash>.png |
| Draw / erase cell | drag (or click) on the canvas |
| Pencil size | slider, 1–48 cells (square brush, strokes interpolated) |
| Draw / erase mode | Draw / Erase segmented toggle |
| Seed spacing | slider, 2–300 (distProbability) |
| Horizontal seed | checkbox (stripesB) |
| Speed | slider, 0.1–8× — generations per frame (< 1 = slow motion) |
| Auto re-seed | checkbox — re-seed patches that have dissolved into noise (experimental, off by default) |
| Noise threshold | slider, 0.50–1.00 — the patch entropy at which auto re-seed kicks in |
The rule readout shows the active algorithm as <reference-direction arrow> <survival-threshold> (e.g. ↖ 6, the original). Randomize draws from a curated pool of 8 rules — the four diagonal reference directions (↖ ↗ ↙ ↘) at survival 6–7 — and never repeats the current rule; Reset returns to ↖ 6.
Left alone, some rules eventually chew their weave into featureless pixel noise.
The Auto re-seed toggle fights that: every 30 generations the grid is split
into 75×75-cell patches and each is scored by the Shannon entropy of its 2×2
block patterns, normalised to 0..1 (1 = all 16 patterns equally likely). Lattices
reuse a handful of patterns and score low; noise approaches 1. Patches at or
above the Noise threshold get re-seeded in place, so the pattern regenerates
locally instead of decaying. The entropy readout shows the highest patch score
from the last scan — useful for finding the threshold by eye, though it only
moves while the toggle is on (the scan doesn't run otherwise).
Faithful to the source sketch,
including its quirky mixed edge behaviour: left/top neighbours clamp,
right/bottom neighbours wrap. The rules, seeding (populate), and the
left-neighbour blue colouring are reproduced exactly — yarn test diffs grid
state and pixel output against a naive reference port of the .pde across
several configs and many generations. The original's hard-coded top-left
reference and sum < 6 survival are the defaults of the now-tunable rule, so
the default behaviour is bit-identical.
- Flat
Uint8Arraygrids, double-buffered. Noint[][], no per-generation allocation — the next state is written into a second buffer and the two references are swapped (O(1)). - Precomputed neighbour index maps. The clamp/wrap edge rules are baked into
four small
Int32Arrays, so the inner loop is branch-free at the borders. - Fused update + render. At speed = 1 a single pass advances the generation
and writes pixels (
step()); extra sub-steps use the draw-freeprocess(). - Direct framebuffer. Pixels go straight into a
@thi.ng/pixelABGR8888buffer whoseUint32Arraymaps 1:1 onto canvasImageData, so the blit is oneputImageData.
Measured in V8: ~4 ms per fused generation at 360k cells (~240 gen/s capacity), i.e. comfortably above 60 fps with headroom for multi-step speeds.
@thi.ng/atom— config state container@thi.ng/rstream— RAF loop, atom-derived streams, DOM events@thi.ng/rstream-gestures— pointer drawing@thi.ng/rdom— declarative, reactively-bound control UI@thi.ng/transducers—map/dedupefor derived streams@thi.ng/equiv— value equality for thosededupes (reference equality re-fires them)@thi.ng/pixel—ABGR8888framebuffer + canvas blit
src/
ca.ts typed-array engine (kernel, double buffer, framebuffer) ← the fast core
entropy.ts patch entropy scan — noise detection behind auto re-seed
state.ts reactive atom + readout streams
app.ts loop, gestures, keyboard, reactions, declarative rdom UI
main.ts entry
style.css instrument HUD
test/
verify.ts bit-exact correctness check vs the original sketch
bench.ts kernel benchmark
Proof of concept, not a finished feature. This lives on the
poc-audio-synthesisbranch and is deployed separately at /poc/. It is a rough, work-in-progress sketch — expect sharp edges, loud transients, and design decisions still in flux. The main app above is unaffected.
The POC sonifies the automaton: a rectangular region of the grid is raster-scanned into a wavetable (alive → +, dead → −) that a read phasor loops, so you literally hear the pattern as a waveform. Region size sets the pitch; the CA's evolution morphs the timbre. Two coupled instances share one master tempo, so the image advances on the beat.
Turn your speakers down before pressing a.
- Press
a(or the Audio button) to start sound — a browser gesture is required to open the audio context, so it only starts on that keypress/click. - Use the Region size (pitch) presets (16 / 32 / 64 / 128) to pick a pitch; smaller region = shorter wavetable = higher note.
- Run (
space) to hear the sound morph as the CA evolves; pause to freeze it into a steady drone of the current frame.
| Control | What it does |
|---|---|
a / Audio button |
Toggle sound on/off (starts/stops the audio context) |
| Volume | Output level, in dB |
| Tempo (BPM) | Master tempo — drives how fast the CA steps |
| Visual subdivision | Steps per beat = the live morph rate of the timbre |
| Region size (pitch) | Preset squares 16–128 cells; Alt-drag moves the region, Shift-drag boxes it to any W×H |
| Width / Diffusion | Stereo width and allpass decorrelation of the two channels |
| Wave strip | Live view of the current wavetable / output waveform |
- Running = live mirror. The visual CA drives and the region is mirrored into the audio engine every generation, so the sound morphs at the tempo grid's rate.
- Paused = frozen drone. No new frames arrive, so the last wavetable just loops — a steady drone of the frozen picture.
- Step (
n, while paused) = re-seed the drone. Each step advances the frozen frame once and pushes it, so the drone jumps to the new frame — scrub it by hand.
Rule (r randomize) and tempo are always live, so they are audible without a
re-capture. Design rationale for all of this lives in the branch's
docs/adr/0001–0005.
Both the source code and the images (the screenshot and the exported frames) are released under CC BY-NC-SA 4.0 (Attribution — NonCommercial — ShareAlike): use, remix, and share them — credit the source, don't sell them, and keep derivatives under the same terms.









