About this technique →
wolfram-1d/sketch.js
// Wolfram Elementary 1D Cellular Automaton
//
// Each cell's next state depends on its own state and its two neighbors
// (3 cells = 8 possible combinations = 256 possible rules, numbered 0-255).
// The canvas shows a spacetime diagram: each row is one generation.
// When the canvas fills, the content scrolls up and new rows appear at the bottom.
//
// Famous rules: 30 (chaotic), 90 (Sierpinski triangle), 110 (Turing-complete)

export const WARMUP = { framesBeforeReady: 0 };

export const PARAMS = {
  rule:     { value: 30, min: 0, max: 255, step: 1, label: "Wolfram Rule (0-255)", folder: "Structure", rebuildOnChange: true },
  cellSize: { value: 3, options: ["1", "2", "3", "4", "6"], label: "Cell Size (px)", folder: "Structure", rebuildOnChange: true },
  init:     { value: "Single", options: ["Single", "Random", "Gradient"], label: "Initial Row", folder: "Structure", rebuildOnChange: true },
  living:   { value: "#e8d8a8", type: "color", label: "Living", folder: "Appearance" },
  bg:       { value: "#0a0a0a", type: "color", label: "Background", folder: "Appearance" },
};

let state;

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

function makeInitRow(cols, mode) {
  const row = new Uint8Array(cols);
  if (mode === "Single") {
    row[Math.floor(cols / 2)] = 1;
  } else if (mode === "Random") {
    for (let i = 0; i < cols; i++) row[i] = Math.random() < 0.5 ? 1 : 0;
  } else {
    // Gradient: density increases from edges to center
    const mid = cols / 2;
    for (let i = 0; i < cols; i++) {
      const dist = Math.abs(i - mid) / mid; // 0 at center, 1 at edge
      const density = 1 - dist;
      row[i] = Math.random() < density ? 1 : 0;
    }
  }
  return row;
}

export function sketchSetup(p, w, h, params) {
  const cs = parseInt(params.cellSize);
  const rule = Math.floor(params.rule);
  const cols = Math.floor(w / cs);
  const rowsOnScreen = Math.floor(h / cs);

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

  const current = makeInitRow(cols, params.init);
  const next = new Uint8Array(cols);

  // Track which canvas row we're drawing into (in cell units)
  state = { cs, cols, rowsOnScreen, current, next, rule, drawRow: 0, full: false };
  return state;
}

function stepRow(current, next, cols, rule) {
  for (let col = 0; col < cols; col++) {
    const l = current[(col - 1 + cols) % cols];
    const c = current[col];
    const r = current[(col + 1) % cols];
    const idx = (l << 2) | (c << 1) | r;
    next[col] = (rule >> idx) & 1;
  }
  current.set(next);
}

export function sketchDraw(p, w, h, params) {
  const { cs, cols, rowsOnScreen, current, next } = state;
  const rule = Math.floor(params.rule);

  // Compute one generation per frame
  stepRow(current, next, cols, rule);

  const [lr, lg, lb] = hexToRgb(params.living);
  const [br, bg2, bb] = hexToRgb(params.bg);

  if (state.drawRow >= rowsOnScreen) {
    // Scroll the canvas up by one cell row
    p.copy(p, 0, cs, w, h - cs, 0, 0, w, h - cs);
    state.drawRow = rowsOnScreen - 1;
  }

  // Draw the new row at the bottom of the current content
  p.loadPixels();
  const pd = p.pixels;
  const rowY = state.drawRow * cs;

  for (let col = 0; col < cols; col++) {
    const alive = current[col];
    const r = alive ? lr : br;
    const g = alive ? lg : bg2;
    const b = alive ? lb : bb;

    for (let py = 0; py < cs; py++) {
      for (let px = 0; px < cs; px++) {
        const pi = ((rowY + py) * w + (col * cs + px)) * 4;
        pd[pi]     = r;
        pd[pi + 1] = g;
        pd[pi + 2] = b;
        pd[pi + 3] = 255;
      }
    }
  }

  p.updatePixels();
  state.drawRow++;
}