Chemistry · Cellular automata
📅 March 2026 ⏱ ≈ 11 min read 🎯 Intermediate

Reaction-diffusion: how chemistry paints leopard spots

Anishchenko turbulence, zebra stripes, spiral waves — all arise from simple reaction and diffusion equations. The Gray-Scott model generates endless pattern diversity from two chemical components and just four parameters.

1. Reaction, diffusion and self-organisation

У 1952 році Алан Тюрінг опублікував статтю «Хімічна основа морфогенезу», де показав, що дифузна нестабільність може виникати навіть у системі, яка однорідна та стабільна без дифузії. Це здається парадоксом: дифузія зазвичай вирівнює концентрації — але якщо два реагенти дифундують з різними швидкостями, може виникнути просторовий паттерн.

Загальний механізм: активатор (A) стимулює своє власне виробництво та виробництво інгібітора (B); інгібітор пригнічує активатор; при цьому інгібітор дифундує швидше. Результат — локальне збудження активатора, оточене зоною придушення: плями, смуги, спіралі.

Приклади у природі: пігментація шкіри риб та ссавців, розподіл волосяних фолікулів, паттерни вигибання пальців ембріона, дендрити кристалів, серцева аритмія.

2. General reaction-diffusion equations

Загальна форма для двох компонент — це система двох PDE (рівнянь у частинних похідних):

∂A/∂t = Dₐ · ∇²A + f(A, B)
∂B/∂t = D_b · ∇²B + g(A, B)

∇² — Laplacian (spatial diffusion)
Dₐ, D_b — diffusion coefficients for A and B
f, g — reaction functions (specific to each model)

Різні вибори f і g дають різні моделі: Бріссель (Prigogine), Гіерера-Майнгардта, Фіцгью-Нагумо (збудливі нейрони), Ленгтон (цифровий антропоморфізм) — і нашу найпростішу, найзручнішу для симуляції: модель Грея-Скотта.

3. The Gray-Scott model

Грей та Скотт (1984, 1985) описали реакцію у відкритому реакторі, де речовина A подається ззовні і реагує з B за схемою: A + 2B → 3B, B → P (P — неактивний продукт).

∂A/∂t = Dₐ · ∇²A − A·B² + F·(1 − A)
∂B/∂t = D_b · ∇²B + A·B² − (F + k)·B

F — feed rate: rate of supply of substance A from outside
k — kill rate: rate of removal of B
Dₐ — typically 0.2100, D_b = 0.1050 (ratio 2:1)

Розберемо доданки:

Початкові умови: A = 1, B = 0 скрізь, крім кількох квадратів-«сід» у центрі, де A = 0.5, B = 0.25 + невеликий шум. Шум запускає симетрію-зламаний ріст паттернів.

4. Parameters F and k — pattern zoo

Невеликі зміни у F та k кардинально змінюють морфологію паттернів. Нижче — класифікація основних режимів:

Spots
F=0.035, k=0.065
Isolated islands of B, resembling leopard spots or spotted fish.
Stripes
F=0.060, k=0.062
Labyrinthine stripes, resembling a zebra or zebrafish.
Mitosis
F=0.028, k=0.062
Spots self-replicate — division resembling cell mitosis.
Mandala
F=0.025, k=0.060
Concentric ring waves propagating from the centre.
Labyrinth
F=0.040, k=0.060
Connected complex labyrinthine structures without clear isolation.
Extinction
F=0.014, k=0.054
B quickly dies; A fills the field uniformly — no live patterns.
F k Pattern type Nature analogue
0.029 0.057 Worms (хробаки) Seahorse skin
0.035 0.065 Spots (плями) Leopard, jaguar
0.046 0.065 Holes (дірки) Inverted spots
0.060 0.062 Stripes (смуги) Zebra, tiger eel
0.014 0.047 Moving spots Moving protozoa

5. FTCS discretisation

FTCS (Forward Time, Central Space) — найпростіша явна схема різниць. Лапласіан апроксимується п'ятиточковим шаблоном (4-зв'язність):

∇²A[i,j] ≈ (A[i+1,j] + A[i−1,j] + A[i,j+1] + A[i,j−1] − 4·A[i,j]) / h²

h = 1.0 (spatial step), dt = 1.0 (time step)

A_new[i,j] = A[i,j] + dt · (Dₐ·∇²A − A·B² + F·(1−A))
B_new[i,j] = B[i,j] + dt · (D_b·∇²B + A·B² − (F+k)·B)

Умова стійкості Куранта: dt · D / h² ≤ 0.5. При Dₐ=0.21, h=1, dt=1 — 0.21·1/1 = 0.21 < 0.5. Але для більш стійкого результату часто роблять кілька кроків FTCS за один "відображений" кадр.

Граничні умови: найпростіші — wrap-around (тороїдальна область). Без цього країв полотна паттерни відрізняються. Реалізується через % W у JS.

6. JavaScript Canvas implementation

const W = 256, H = 256;
const Da = 0.21, Db = 0.105;
const F  = 0.035, k  = 0.065;  // ← spots
const dt = 1.0;

// Grid initialisation
let A = new Float32Array(W * H).fill(1);
let B = new Float32Array(W * H).fill(0);
let nA = new Float32Array(W * H);
let nB = new Float32Array(W * H);

// Initial excitation
for (let seed = 0; seed < 5; seed++) {
  const cx = Math.floor(Math.random()*W);
  const cy = Math.floor(Math.random()*H);
  for (let dy = -5; dy <= 5; dy++) {
    for (let dx = -5; dx <= 5; dx++) {
      const x = (cx+dx+W) % W, y = (cy+dy+H) % H;
      A[y*W+x] = 0.5;
      B[y*W+x] = 0.25 + Math.random()*0.1;
    }
  }
}

function step() {
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      const i = y*W + x;

      // Laplacian (wrap-around)
      const la = A[((y-1+H)%H)*W+x] + A[((y+1)%H)*W+x]
               + A[y*W+(x-1+W)%W] + A[y*W+(x+1)%W] - 4*A[i];
      const lb = B[((y-1+H)%H)*W+x] + B[((y+1)%H)*W+x]
               + B[y*W+(x-1+W)%W] + B[y*W+(x+1)%W] - 4*B[i];

      const ab2 = A[i] * B[i] * B[i];
      nA[i] = A[i] + dt*(Da*la - ab2 + F*(1-A[i]));
      nB[i] = B[i] + dt*(Db*lb + ab2 - (F+k)*B[i]);

      // Clamp [0,1]
      if (nA[i] < 0) nA[i] = 0;
      if (nB[i] < 0) nB[i] = 0;
    }
  }
  [A, nA] = [nA, A]; [B, nB] = [nB, B]; // swap buffers
}

function draw(ctx, imageData) {
  const d = imageData.data;
  for (let i = 0, p = 0; i < W*H; i++, p+=4) {
    const v = Math.floor(B[i] * 255);
    d[p]=v; d[p+1]=v>>1; d[p+2]=255-v; d[p+3]=255;
  }
  ctx.putImageData(imageData, 0, 0);
}

// Loop: 8 steps per frame for visible progress
function loop() {
  for (let s = 0; s < 8; s++) step();
  draw(ctx, imageData);
  requestAnimationFrame(loop);
}

При 256×256 і 8 кроків за кадр отримуємо ~350 000 оновлень клітин за кадр. На сучасному браузері це виконується за <8 мс (менше ніж бюджет 16 мс для 60 FPS).

7. WebGL2: GPU simulation

Для роздільної здатності 512×512+ CPU-підхід повільний. WebGL2 дозволяє виконати кроки FTCS у фрагментному шейдері паралельно для всіх пікселів:

// Fragment shader (GLSL ES 3.0)
#version 300 es
precision highp float;
uniform sampler2D u_state;   // rgba: r=A, g=B
uniform vec2      u_res;
out vec4 fragColor;

void main() {
  vec2 uv = gl_FragCoord.xy / u_res;
  vec2 texel = 1.0 / u_res;

  vec2 cur = texture(u_state, uv).rg;
  vec2 top = texture(u_state, uv + vec2(0, texel.y)).rg;
  vec2 bot = texture(u_state, uv - vec2(0, texel.y)).rg;
  vec2 lft = texture(u_state, uv - vec2(texel.x, 0)).rg;
  vec2 rgt = texture(u_state, uv + vec2(texel.x, 0)).rg;

  vec2 lap = top + bot + lft + rgt - 4.0 * cur;

  float F = 0.035, k = 0.065;
  float Da = 0.21, Db = 0.105;
  float ab2 = cur.r * cur.g * cur.g;

  float newA = cur.r + Da*lap.r - ab2 + F*(1.0 - cur.r);
  float newB = cur.g + Db*lap.g + ab2 - (F+k)*cur.g;

  fragColor = vec4(clamp(newA, 0.0, 1.0),
                   clamp(newB, 0.0, 1.0),
                   0.0, 1.0);
}

Техніка ping-pong буферів: два framebuffer-и (FBO) чергуються — один є вхідним texture, інший — вихідним render target. Це дозволяє робити 30–50 кроків за кадр при 512×512 без будь-якого гальмування.

8. Connection to Turing and biology

Тюрінг (1952) довів: якщо інгібітор дифундує швидше за активатора, однорідний стан стає нестійким і виникає просторовий паттерн. Gray-Scott реалізує саме такий механізм: Dₐ > D_b і A грає роль «активатора» (само-каталітично утворює B).

Реальні приклади «тюрінгових паттернів»:

2012, Science: науковці з Гельсінкі продемонстрували, що позиція вусів (вібрис) у мишей визначається саме тюрінговим паттерном. Це перший прямий доказ у ссавця.

9. Extensions

▶ Live Demo

⚗️ Reaction-diffusion (coming soon)

A real-time Gray-Scott simulation with adjustable F and k parameters is coming to the Chemistry section.

Chemistry section →

🔗 Related Simulations

🎮Game of Life 🏖️Sand ❄️Snowflake 🔲Cellular Automata