p5.js Intermediate Article 4

Generative Patterns

Tilings, L-systems, reaction-diffusion, and cellular automata — systematic approaches to infinite variety.

⏱ 25 min read generative L-system cellular automata reaction-diffusion tiling

What makes a pattern “generative”?

A generative pattern emerges from rules applied repeatedly. The rules are simple; the output is complex. The power is in the rule-space: small changes to parameters produce vastly different results.

Truchet tiles

A Truchet tile is a square tile with an asymmetric design that creates patterns when rotated. The classic version has a quarter-circle arc from one edge mid-point to an adjacent one:

function setup() {
  createCanvas(600, 600);
  noLoop();
}

function draw() {
  background(240);
  let s = 40;

  for (let x = 0; x < width; x += s) {
    for (let y = 0; y < height; y += s) {
      drawTruchet(x, y, s, floor(random(2)));
    }
  }
}

function drawTruchet(x, y, s, variant) {
  push();
  translate(x + s / 2, y + s / 2);
  noFill();
  stroke(30);
  strokeWeight(2);

  if (variant === 0) {
    arc(-s / 2, -s / 2, s, s, 0, HALF_PI);
    arc( s / 2,  s / 2, s, s, PI, PI + HALF_PI);
  } else {
    arc( s / 2, -s / 2, s, s, HALF_PI, PI);
    arc(-s / 2,  s / 2, s, s, PI + HALF_PI, TWO_PI);
  }

  pop();
}

L-systems

A Lindenmayer system (L-system) is a string-rewriting system. Start with an axiom, apply production rules repeatedly to get a new string, then interpret each character as a drawing command:

let axiom   = 'F';
let rules   = { 'F': 'FF+[+F-F-F]-[-F+F+F]' };
let sentence;
let len;

function setup() {
  createCanvas(600, 700);
  noLoop();
  sentence = axiom;
  for (let i = 0; i < 5; i++) sentence = generate(sentence);
  len = 4;
}

function generate(s) {
  let next = '';
  for (let ch of s) next += rules[ch] || ch;
  return next;
}

function draw() {
  background(20);
  translate(width / 2, height);
  rotate(-HALF_PI);
  stroke(100, 200, 100, 180);
  strokeWeight(0.8);
  noFill();

  let stack = [];

  for (let ch of sentence) {
    if (ch === 'F') {
      line(0, 0, len, 0);
      translate(len, 0);
    } else if (ch === '+') {
      rotate(radians(25));
    } else if (ch === '-') {
      rotate(radians(-25));
    } else if (ch === '[') {
      stack.push({ x: 0, y: 0, angle: 0 });
      push();
    } else if (ch === ']') {
      pop();
      stack.pop();
    }
  }
}

Change the axiom, rules, or angle for completely different plants.

Conway’s Game of Life

A cellular automaton where each cell is alive or dead. Rules: live cell with 2-3 neighbours survives; dead cell with exactly 3 neighbours is born:

let cols, rows, grid, next;
const CELL = 10;

function setup() {
  createCanvas(600, 500);
  cols = floor(width  / CELL);
  rows = floor(height / CELL);
  grid = makeGrid(() => random() > 0.7 ? 1 : 0);
  frameRate(12);
}

function makeGrid(init) {
  return Array.from({ length: cols }, (_, c) =>
    Array.from({ length: rows }, (_, r) => init(c, r))
  );
}

function draw() {
  background(20);
  next = makeGrid(() => 0);

  for (let c = 0; c < cols; c++) {
    for (let r = 0; r < rows; r++) {
      let n = countNeighbours(c, r);
      let alive = grid[c][r];

      if (alive && (n === 2 || n === 3)) next[c][r] = 1;
      else if (!alive && n === 3)        next[c][r] = 1;

      fill(alive ? color(100, 220, 140) : color(20));
      noStroke();
      rect(c * CELL + 1, r * CELL + 1, CELL - 2, CELL - 2, 2);
    }
  }

  grid = next;
}

function countNeighbours(c, r) {
  let count = 0;
  for (let dc = -1; dc <= 1; dc++) {
    for (let dr = -1; dr <= 1; dr++) {
      if (dc === 0 && dr === 0) continue;
      let nc = (c + dc + cols) % cols;
      let nr = (r + dr + rows) % rows;
      count += grid[nc][nr];
    }
  }
  return count;
}

Reaction-diffusion (Gray-Scott)

Two chemical species A and B react and diffuse across a grid, producing complex organic-looking patterns:

let gridA, gridB, nextA, nextB;
const W = 200, H = 150;
const DA = 1.0, DB = 0.5, f = 0.055, k = 0.062;

function setup() {
  createCanvas(W * 3, H * 3);
  pixelDensity(1);

  gridA = makeGrid2(W, H, 1);
  gridB = makeGrid2(W, H, 0);

  // Seed a small region
  for (let i = 90; i < 110; i++) {
    for (let j = 65; j < 85; j++) {
      gridA[i][j] = 0.5 + random(-0.01, 0.01);
      gridB[i][j] = 0.25 + random(-0.01, 0.01);
    }
  }

  frameRate(30);
}

function makeGrid2(w, h, val) {
  return Array.from({ length: w }, () => new Float32Array(h).fill(val));
}

function draw() {
  nextA = makeGrid2(W, H, 0);
  nextB = makeGrid2(W, H, 0);

  for (let x = 1; x < W - 1; x++) {
    for (let y = 1; y < H - 1; y++) {
      let a = gridA[x][y], b = gridB[x][y];
      let lapA = gridA[x+1][y] + gridA[x-1][y] + gridA[x][y+1] + gridA[x][y-1] - 4 * a;
      let lapB = gridB[x+1][y] + gridB[x-1][y] + gridB[x][y+1] + gridB[x][y-1] - 4 * b;

      nextA[x][y] = a + DA * lapA - a * b * b + f * (1 - a);
      nextB[x][y] = b + DB * lapB + a * b * b - (k + f) * b;
      nextA[x][y] = constrain(nextA[x][y], 0, 1);
      nextB[x][y] = constrain(nextB[x][y], 0, 1);
    }
  }

  gridA = nextA; gridB = nextB;

  loadPixels();
  for (let x = 0; x < W; x++) {
    for (let y = 0; y < H; y++) {
      let v   = floor((gridA[x][y] - gridB[x][y]) * 255);
      v = constrain(v, 0, 255);
      let idx = (x * 3 + y * 3 * width * 3) * 4;
      for (let dx = 0; dx < 3; dx++) for (let dy = 0; dy < 3; dy++) {
        let i = ((x * 3 + dx) + (y * 3 + dy) * width) * 4;
        pixels[i] = v; pixels[i+1] = v; pixels[i+2] = v; pixels[i+3] = 255;
      }
    }
  }
  updatePixels();
}

Experiment with different f and k values — the Gray-Scott parameter map shows the range of patterns available.


Key takeaways

  • Truchet tiles create complex patterns from two randomly-chosen orientations of a simple tile
  • L-systems rewrite strings according to rules and interpret the result as drawing commands
  • Cellular automata (Game of Life) compute next state from neighbours — simple rules, complex emergent behaviour
  • Reaction-diffusion (Gray-Scott) simulates chemical reactions to produce organic textures
  • All of these benefit from seeds (noiseSeed, randomSeed) for reproducibility