About this technique →
game-of-life/sketch.js
// Conway's Game of Life — B3/S23 rule
//
// Two Uint8Array buffers double-buffer the grid. Each generation:
//   - A live cell with 2 or 3 live neighbors survives.
//   - A dead cell with exactly 3 live neighbors becomes alive.
//   - All other cells die or remain dead.
// Cells that just died are rendered in the "dying" color for one frame.

export const WARMUP = { framesBeforeReady: 60 };

export const PARAMS = {
  cellSize: { value: 4, options: ["1", "2", "4", "8"], label: "Cell Size (px)", folder: "Structure", rebuildOnChange: true },
  preset:   { value: "Random", options: ["Random", "Glider Gun", "R-Pentomino", "Acorn", "Pulsar"], label: "Initial Pattern", folder: "Structure", rebuildOnChange: true },
  density:  { value: 0.35, min: 0.05, max: 0.6, step: 0.01, label: "Initial Density", folder: "Structure", rebuildOnChange: true },
  speed:    { value: 1, options: ["1", "2", "4", "8"], label: "Steps/Frame", folder: "Behavior" },
  living:   { value: "#e8d8a8", type: "color", label: "Living", folder: "Appearance" },
  dying:    { value: "#5a3a2a", type: "color", label: "Dying", folder: "Appearance" },
  bg:       { value: "#0a0a0a", type: "color", label: "Background", folder: "Appearance" },
};

let state;

// Named Conway pattern coordinate tables (relative [col, row] offsets)
// Placed near grid center in sketchSetup.
const PRESET_PATTERNS = {
  "Glider Gun": [
    // Bill Gosper's Glider Gun (36×9 bounding box)
    [0,4],[0,5],[1,4],[1,5],[10,4],[10,5],[10,6],[11,3],[11,7],
    [12,2],[12,8],[13,2],[13,8],[14,5],[15,3],[15,7],[16,4],[16,5],[16,6],[17,5],
    [20,2],[20,3],[20,4],[21,2],[21,3],[21,4],[22,1],[22,5],
    [24,0],[24,1],[24,5],[24,6],[34,2],[34,3],[35,2],[35,3],
  ],
  "R-Pentomino": [
    [1,0],[2,0],[0,1],[1,1],[1,2],
  ],
  "Acorn": [
    [1,0],[3,1],[0,2],[1,2],[4,2],[5,2],[6,2],
  ],
  "Pulsar": [
    // Pulsar (period-3 oscillator) — 13×13 footprint
    [2,0],[3,0],[4,0],[8,0],[9,0],[10,0],
    [0,2],[5,2],[7,2],[12,2],[0,3],[5,3],[7,3],[12,3],[0,4],[5,4],[7,4],[12,4],
    [2,5],[3,5],[4,5],[8,5],[9,5],[10,5],
    [2,7],[3,7],[4,7],[8,7],[9,7],[10,7],
    [0,8],[5,8],[7,8],[12,8],[0,9],[5,9],[7,9],[12,9],[0,10],[5,10],[7,10],[12,10],
    [2,12],[3,12],[4,12],[8,12],[9,12],[10,12],
  ],
};

function hexToRgb(hex) {
  const v = parseInt(hex.replace('#', ''), 16);
  return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
}

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 current = new Uint8Array(n);
  const next = new Uint8Array(n);
  const dying = new Uint8Array(n); // 1 = just died this step

  const preset = params.preset;
  if (preset === "Random") {
    const density = params.density;
    for (let i = 0; i < n; i++) {
      current[i] = p.random() < density ? 1 : 0;
    }
  } else {
    const pattern = PRESET_PATTERNS[preset];
    if (pattern) {
      // Find bounding box of pattern
      const maxCol = Math.max(...pattern.map(([c]) => c));
      const maxRow = Math.max(...pattern.map(([, r]) => r));
      // Place near center
      const startCol = Math.floor((cols - maxCol) / 2);
      const startRow = Math.floor((rows - maxRow) / 2);
      for (const [dc, dr] of pattern) {
        const c = startCol + dc;
        const r = startRow + dr;
        if (c >= 0 && c < cols && r >= 0 && r < rows) {
          current[r * cols + c] = 1;
        }
      }
    }
  }

  p.pixelDensity(1);
  p.background(10);

  state = { cs, cols, rows, current, next, dying };
  return state;
}

export function sketchDraw(p, w, h, params) {
  const { cs, cols, rows, current, next, dying } = state;
  const speed = parseInt(params.speed) || 1;

  for (let s = 0; s < speed; s++) {
    // Clear dying buffer
    dying.fill(0);

    for (let row = 0; row < rows; row++) {
      for (let col = 0; col < cols; col++) {
        const idx = row * cols + col;
        let neighbors = 0;

        for (let dr = -1; dr <= 1; dr++) {
          for (let dc = -1; dc <= 1; dc++) {
            if (dr === 0 && dc === 0) continue;
            const r = (row + dr + rows) % rows;
            const c = (col + dc + cols) % cols;
            neighbors += current[r * cols + c];
          }
        }

        const alive = current[idx];
        if (alive) {
          next[idx] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
          if (next[idx] === 0) dying[idx] = 1;
        } else {
          next[idx] = neighbors === 3 ? 1 : 0;
        }
      }
    }

    // Swap buffers
    current.set(next);
  }

  // Render using loadPixels for performance
  const [lr, lg, lb] = hexToRgb(params.living);
  const [dr, dg, db] = hexToRgb(params.dying);
  const [br, bg2, bb] = hexToRgb(params.bg);

  p.loadPixels();
  const pd = p.pixels;

  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      const idx = row * cols + col;
      let r, g, b;
      if (current[idx]) {
        r = lr; g = lg; b = lb;
      } else if (dying[idx]) {
        r = dr; g = dg; b = db;
      } else {
        r = br; g = bg2; b = bb;
      }

      // Fill the cs×cs block of pixels
      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]     = r;
          pd[pi + 1] = g;
          pd[pi + 2] = b;
          pd[pi + 3] = 255;
        }
      }
    }
  }

  p.updatePixels();
}