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.

\(\dfrac{\partial u}{\partial t}\) \(D_u \nabla^2 u\) \(u v^2\) \(F\,(1 - u)\)
\(\dfrac{\partial v}{\partial t}\) \(D_v \nabla^2 v\) \(u v^2\) \((F + k)\,v\)

∂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 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];
}
Why patterns and not mush. \(v\) diffuses slower than \(u\) (\(D_v = D_u/2\) by default). The catalyst stays concentrated where it is while its food arrives from far away — local activation, long-range inhibition. That asymmetry is Turing’s 1952 instability, and the whole zoo of spots, stripes, and worms lives on a knife’s edge in the \((F, k)\) plane.

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.

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;