Step 1 · Equations
Two chemicals and a Laplacian
Course 3 used the Laplacian to relax pressure toward balance. Here the same operator does the opposite: coupled to a reaction, diffusion stops smoothing things out and starts growing patterns.
The screen shows one of two invisible chemicals. Everywhere starts full of feedstock (drawn dark — you never see it directly). A second chemical, the catalyst (drawn bright), does one thing: it converts feedstock into more of itself. So catalyst spreads into the dark regions, eating as it grows.
If that were all, it would just fill the screen. Two things hold it back: catalyst is slowly removed (kill rate \(k\)), and fresh feedstock constantly drips back in (feed rate \(F\)). The pattern is the standoff between growth and removal — and because the catalyst spreads slower than the feedstock refills, that standoff never settles into flat gray. It keeps carving edges. That's the whole trick.
Why the patterns move — and split
Here's the part that trips everyone up: the catalyst kills itself as it advances. Watch a bright front spread. At its leading edge it's pushing into fresh feedstock, so it grows. But right behind it the feedstock is already used up — nothing left to eat — and there the kill term wins, so the trailing edge dies off. The shape isn't sliding across the grid; it's being rebuilt at the front and starved at the back. That's why patterns appear to travel.
Same reason a blob doesn't just swell forever: once it's wide enough, its own middle runs out of feedstock and hollows out, so a solid dot becomes a ring — and the ring pinches into two. That's the “mitosis” look, cells dividing. Growth only survives at the boundary between fed and starved, which is exactly why you see edges and fronts instead of filled shapes.
Every term is one of three jobs
Both chemicals follow the same skeleton — each term does one of three jobs. The middle one is what ties them together:
| Job | \(u\) — feedstock | \(v\) — catalyst |
|---|---|---|
| Spread to neighbors the \(\nabla^2\) Laplacian | spreads out | spreads out, slower |
| Reaction | − gets eaten | + gets made |
| Supply / removal | + fresh drips in feed \(F\) | − washed out kill \(F + k\) |
Read the reaction row across: it's the same quantity, minus for feedstock and plus for catalyst. Every scrap of feedstock the reaction destroys reappears as catalyst — that shared term is why it's two coupled equations, not two unrelated ones. They're joined right there.
The precise version
Same three jobs, written as math — the Gray–Scott system, the exact update the playground and the T-QT firmware run each step. Hover any term for what it does:
Hover or focus a highlighted term. The readout explains what that piece does to the field.
∂u/∂t — how fast feedstock changes
- Du∇²u feedstock spreads out to its neighbors
- − uv² the reaction eats it — only happens where catalyst is present
- + F(1−u) fresh feedstock drips in everywhere, at feed rate F
∂v/∂t — how fast catalyst changes
- Dv∇²v catalyst spreads out too, but slower
- + uv² the reaction makes it — the v² is “you need catalyst to make catalyst” (v × v), so it grows only next to itself
- − (F+k)v catalyst is removed at kill rate F+k
Hover any term above for a longer note.
Discrete Laplacian on the grid
On a grid, \(\nabla^2 u\) at a cell is “how far below the neighborhood average am I” — a weighted sum of the 8 neighbors minus the cell itself. This lab (and the firmware it exports to) uses the 9-point stencil: adjacent cells weigh 0.2, diagonals 0.05, center −1. Edges wrap around, so the domain is a torus.
// identical kernel in the browser and in the LilyGO T-QT firmware float Laplacian(float[] f, int x, int y) { return 0.2f * (L + R + T + B) + 0.05f * (TL + TR + BL + BR) - f[x, y]; }
Step 2 · Playground
The (F, k) plane, live on a 128×128 torus
Step 1 fixed the two update rules; this canvas just runs them — 128×128, the exact resolution of the LilyGO T-QT’s screen, one sim cell per hardware pixel. Presets jump to known regimes of the \((F, k)\) plane; then nudge k a tick at a time and watch a species flip into another. Draw on the canvas to inject catalyst.
bright = catalyst · dark = feedstock (invisible)
Presets
Parameters
Palette
Save
Saved presets live in your browser and are collected in Step 3 for export to the T-QT.
All dark: the catalyst went extinct (\(k\) too high) — Reseed. All bright: it saturated the dish (\(F\) too high). The interesting universes sit on the boundary between those two fates.
Step 3 · Presets for Lily
From browser dish to pocket dish
The playground runs the same kernel, same grid, same parameters as the LilyGO T-QT Pro firmware — so a preset saved here is a complete description of what the hardware will render. This step is the bridge: collect the presets you saved in Step 2, then export them in a form the firmware build can consume. On the device, the two physical buttons cycle presets and reseed the dish.
No saved presets yet — find something alive in Step 2 and hit Save preset.
How the export is used
presets.json drops into the firmware project (~/Lily)
and is baked in at build time. Copy C header produces the same data as a
lily_presets.h you can paste directly into a chat with your build assistant, or into the
firmware source. Either way each entry is just four numbers and a name:
typedef struct { const char *name; float F; // feed rate float k; // kill rate float dvdu; // Dv / Du diffusion ratio uint8_t palette; } GsPreset;