About this technique →
gray-scott-cpu/sketch.js
// 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();
}