Generative Patterns
Tilings, L-systems, reaction-diffusion, and cellular automata — systematic approaches to infinite variety.
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