← sketches

cellular automata

2D cellular automata with configurable rules and fading trails.

p5.js

  import p5 from 'p5';

  interface Rule { name: string; S: [number, number]; B: [number, number]; density?: number }

  const rules: Rule[] = [
    { name: 'conway',      S: [2, 3], B: [3, 3] },
    { name: 'maze',        S: [1, 5], B: [3, 3] },
    { name: 'coral',       S: [4, 8], B: [3, 3], density: 15 },
    { name: 'anneal',      S: [3, 6], B: [4, 8], density: 50 },
    { name: 'day & night', S: [3, 8], B: [3, 6], density: 50 },
    { name: 'diamoeba',    S: [5, 8], B: [5, 8], density: 50 },
    { name: '34 life',     S: [3, 4], B: [3, 4] },
    { name: 'morley',      S: [2, 4], B: [3, 6], density: 40 },
    { name: 'seeds',       S: [0, 0], B: [2, 2], density: 5 },
    { name: 'stains',      S: [2, 3], B: [3, 6], density: 20 },
  ];

  let ruleIdx = 0;
  const label = document.getElementById('preset-label')!;

  function setSlider(id: string, val: number) {
    const input = document.getElementById(`ctrl-${id}`) as HTMLInputElement | null;
    if (input) {
      input.value = String(val);
      input.dispatchEvent(new Event('input'));
    }
  }

  function applyRule(idx: number) {
    ruleIdx = ((idx % rules.length) + rules.length) % rules.length;
    const r = rules[ruleIdx];
    label.textContent = r.name;
    setSlider('survlo', r.S[0]);
    setSlider('survhi', r.S[1]);
    setSlider('birthlo', r.B[0]);
    setSlider('birthhi', r.B[1]);
    if (r.density) setSlider('density', r.density);
  }

  document.getElementById('preset-prev')?.addEventListener('click', () => { applyRule(ruleIdx - 1); resetSim?.(); });
  document.getElementById('preset-next')?.addEventListener('click', () => { applyRule(ruleIdx + 1); resetSim?.(); });

  let resetSim: (() => void) | null = null;

  new p5((p: p5) => {
    let cols: number, rows: number;
    let grid: number[][];
    let trail: number[][];

    function makeGrid(fill: boolean) {
      return Array.from({ length: cols }, () =>
        Array.from({ length: rows }, () => (fill ? (p.random() > (1 - c.density / 100) ? 1 : 0) : 0))
      );
    }

    function makeTrailGrid() {
      return Array.from({ length: cols }, () => Array(rows).fill(0));
    }

    function countNeighbors(g: number[][], x: number, y: number) {
      let sum = 0;
      for (let i = -1; i <= 1; i++) {
        for (let j = -1; j <= 1; j++) {
          if (i === 0 && j === 0) continue;
          const cx = (x + i + cols) % cols;
          const cy = (y + j + rows) % rows;
          sum += g[cx][cy] > 0 ? 1 : 0;
        }
      }
      return sum;
    }

    function doReset() {
      cols = Math.floor(p.width / c.cell);
      rows = Math.floor(p.height / c.cell);
      grid = makeGrid(true);
      trail = makeTrailGrid();
      p.background(0);
    }
    resetSim = doReset;

    const c = controls({
      cell: { label: 'cell size', min: 3, max: 16, value: 6, onChange: doReset },
      fps: { label: 'frame rate', min: 5, max: 60, value: 15, onChange: (v) => p.frameRate(v) },
      trail: { label: 'trail length', min: 1, max: 30, value: 12 },
      density: { label: 'density', min: 10, max: 80, value: 30, format: (v) => v + '%' },
      survlo: { label: 'survive lo', min: 0, max: 8, value: 2 },
      survhi: { label: 'survive hi', min: 0, max: 8, value: 3 },
      birthlo: { label: 'birth lo', min: 0, max: 8, value: 3 },
      birthhi: { label: 'birth hi', min: 0, max: 8, value: 3 },
    }, {
      customRows: [{ html: '<div class="preset-row"><span>rule</span><span class="preset-nav"><button class="preset-btn" id="preset-prev">&larr;</button><span class="val" id="preset-label">conway</span><button class="preset-btn" id="preset-next">&rarr;</button></span></div>', index: 4 }],
      onRestart: doReset,
      onReset: () => { applyRule(0); doReset(); },
    });

    p.setup = () => {
      const el = document.getElementById('canvas')!.parentElement!;
      const cv = p.createCanvas(el.clientWidth, el.clientHeight);
      cv.parent(el);
      document.getElementById('canvas')?.remove();

      doReset();
      p.frameRate(15);
      p.background(0);
    };

    p.draw = () => {
      p.background(0, 25);

      for (let x = 0; x < cols; x++) {
        for (let y = 0; y < rows; y++) {
          if (grid[x][y] > 0) {
            trail[x][y] = c.trail;
            p.fill(255, 80, 40);
            p.noStroke();
            p.rect(x * c.cell, y * c.cell, c.cell, c.cell);
          } else if (trail[x][y] > 0) {
            trail[x][y]--;
            const t = trail[x][y] / c.trail;
            p.fill(60 * t, 80 * t, 120 * t);
            p.noStroke();
            p.rect(x * c.cell, y * c.cell, c.cell, c.cell);
          }
        }
      }

      const next = makeGrid(false);
      for (let x = 0; x < cols; x++) {
        for (let y = 0; y < rows; y++) {
          const n = countNeighbors(grid, x, y);
          if (grid[x][y] > 0) {
            next[x][y] = n >= c.survlo && n <= c.survhi ? 1 : 0;
          } else {
            next[x][y] = n >= c.birthlo && n <= c.birthhi ? 1 : 0;
          }
        }
      }
      grid = next;
    };
  });