// Gray-Scott Reaction-Diffusion (CPU)
//
// Two chemicals U and V react and diffuse on a 2D grid:
// dU/dt = Du * ∇²U - U*V² + F*(1-U)
// dV/dt = Dv * ∇²V + U*V² - (F+k)*V
//
// U initialized to 1.0 everywhere. V seeded with small random noise spots.
// Run multiple steps per frame for visible motion.
// Render by mapping (U - V) to a hue-based color gradient.
//
// The 3×3 Laplacian kernel:
// [0.05 0.20 0.05]
// [0.20 -1.00 0.20]
// [0.05 0.20 0.05]
export const WARMUP = { framesBeforeReady: 120 };
export const PARAMS = {
preset: { value: "Coral", options: ["Coral", "Mitosis", "Stripes", "Spots", "Custom"], label: "Pattern Preset", folder: "Structure" },
feed: { value: 0.030, min: 0, max: 0.1, step: 0.0001, label: "Feed Rate (F)", folder: "Dynamics" },
kill: { value: 0.057, min: 0.04, max: 0.075, step: 0.0001, label: "Kill Rate (k)", folder: "Dynamics" },
Du: { value: 1.0, min: 0.5, max: 2, step: 0.01, label: "Du", folder: "Dynamics" },
Dv: { value: 0.5, min: 0.1, max: 1, step: 0.01, label: "Dv", folder: "Dynamics" },
steps: { value: 8, min: 1, max: 20, step: 1, label: "Steps/Frame", folder: "Performance" },
cellSize: { value: 2, options: ["1", "2", "3", "4"], label: "Cell Size (px)", folder: "Performance", rebuildOnChange: true },
density: { value: 0.005, min: 0.001, max: 0.05, step: 0.001, label: "Initial V Density", folder: "Structure", rebuildOnChange: true },
hue: { value: 200, min: 0, max: 360, step: 1, label: "Hue", folder: "Appearance" },
};
const PRESETS = {
Coral: { feed: 0.030, kill: 0.057 },
Mitosis: { feed: 0.055, kill: 0.062 },
Stripes: { feed: 0.039, kill: 0.058 },
Spots: { feed: 0.025, kill: 0.060 },
};
let state;
let lastPreset = null;
export function sketchSetup(p, w, h, params) {
const cs = parseInt(params.cellSize);
const cols = Math.floor(w / cs);
const rows = Math.floor(h / cs);
const n = cols * rows;
const U = new Float32Array(n);
const V = new Float32Array(n);
const nU = new Float32Array(n);
const nV = new Float32Array(n);
// Initialize U=1 everywhere, seed V with random spots
U.fill(1.0);
const density = params.density;
for (let i = 0; i < n; i++) {
if (p.random() < density) {
V[i] = 1.0;
// small square seed
const row = Math.floor(i / cols);
const col = i % cols;
for (let dr = -2; dr <= 2; dr++) {
for (let dc = -2; dc <= 2; dc++) {
const r = row + dr;
const c = col + dc;
if (r >= 0 && r < rows && c >= 0 && c < cols) {
V[r * cols + c] = 1.0;
U[r * cols + c] = 0.0;
}
}
}
}
}
p.pixelDensity(1);
p.noStroke();
p.background(0);
lastPreset = params.preset;
state = { cs, cols, rows, n, U, V, nU, nV };
return state;
}
function applyPreset(preset, params) {
if (preset === "Custom") return;
const p = PRESETS[preset];
if (!p) return;
params.feed = p.feed;
params.kill = p.kill;
}
export function sketchDraw(p, w, h, params) {
const { cs, cols, rows, n, U, V, nU, nV } = state;
// When preset changes (and it's not Custom), update feed/kill
if (params.preset !== lastPreset) {
lastPreset = params.preset;
applyPreset(params.preset, params);
}
const feed = params.feed;
const kill = params.kill;
const Du = params.Du * 0.2; // scale down for dt=1 stability
const Dv = params.Dv * 0.1;
const steps = Math.round(params.steps);
for (let s = 0; s < steps; s++) {
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const i = row * cols + col;
// Wrap-around neighbor indices
const rU = (row - 1 + rows) % rows;
const rD = (row + 1) % rows;
const cL = (col - 1 + cols) % cols;
const cR = (col + 1) % cols;
const iUL = rU * cols + cL, iUC = rU * cols + col, iUR = rU * cols + cR;
const iML = row * cols + cL, iMR = row * cols + cR;
const iDL = rD * cols + cL, iDC = rD * cols + col, iDR = rD * cols + cR;
// 3×3 Laplacian (weighted)
const lapU =
0.05 * U[iUL] + 0.20 * U[iUC] + 0.05 * U[iUR] +
0.20 * U[iML] - 1.00 * U[i] + 0.20 * U[iMR] +
0.05 * U[iDL] + 0.20 * U[iDC] + 0.05 * U[iDR];
const lapV =
0.05 * V[iUL] + 0.20 * V[iUC] + 0.05 * V[iUR] +
0.20 * V[iML] - 1.00 * V[i] + 0.20 * V[iMR] +
0.05 * V[iDL] + 0.20 * V[iDC] + 0.05 * V[iDR];
const u = U[i];
const v = V[i];
const uvv = u * v * v;
nU[i] = u + Du * lapU - uvv + feed * (1.0 - u);
nV[i] = v + Dv * lapV + uvv - (feed + kill) * v;
// Clamp to [0,1]
if (nU[i] < 0) nU[i] = 0; else if (nU[i] > 1) nU[i] = 1;
if (nV[i] < 0) nV[i] = 0; else if (nV[i] > 1) nV[i] = 1;
}
}
// Swap buffers
U.set(nU);
V.set(nV);
}
// Render via loadPixels
p.loadPixels();
const pd = p.pixels;
const hue = params.hue;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const i = row * cols + col;
// Map concentration difference to color
// c=0 (all U, no V) → background dark; c=1 (lots of V) → bright
const c = Math.max(0, Math.min(1, U[i] - V[i]));
// HSL-like palette: vary hue offset from c, full saturation
const t = 1.0 - c; // bright where V is high
const r2 = (hue / 360.0 + t * 0.35) % 1.0;
const rr = Math.floor(255 * Math.abs(Math.sin(Math.PI * (r2 + 0.0))));
const gg = Math.floor(255 * Math.abs(Math.sin(Math.PI * (r2 + 0.33))));
const bb = Math.floor(255 * Math.abs(Math.sin(Math.PI * (r2 + 0.67))));
const bright = t * t * 255;
// Blend palette color with brightness
const pr = Math.floor(rr * 0.4 + bright * 0.6);
const pg = Math.floor(gg * 0.4 + bright * 0.6);
const pb = Math.floor(bb * 0.4 + bright * 0.6);
// Fill cs×cs pixel block
for (let py = 0; py < cs; py++) {
for (let px = 0; px < cs; px++) {
const pi = ((row * cs + py) * w + (col * cs + px)) * 4;
pd[pi] = pr;
pd[pi + 1] = pg;
pd[pi + 2] = pb;
pd[pi + 3] = 255;
}
}
}
}
p.updatePixels();
}