Lab · Playground
Life with colour trails
Conway's Game of Life where live cells are coloured by age and dead ones leave a fading ghost, so you can see where the action has been.
John Conway’s Game of Life (1970) is a grid of cells, each alive or dead, and two rules applied to every cell at once, generation after generation:
-
a live cell with two or three live neighbours survives; otherwise it dies;
-
a dead cell with exactly three live neighbours comes alive.
Plain Life shows only the current generation, which makes it hard to follow. This version keeps two extra numbers per cell: its age (how many generations it has been alive — new cells are hot yellow, old stable blocks cool to blue) and a trail that fades after it dies. Gliders leave comet tails; oscillators glow in place; the trails show the ground the chaos has covered.
Click or drag to draw live cells. Press space (after clicking the canvas) to pause, r for a new random soup, c to clear.
// Things to change: CELL, DENSITY, FADE, and the rule (BIRTH / SURVIVE).
const CELL = 6; // pixels per cell
const DENSITY = 0.22; // share of cells alive in a random soup
const FADE = 0.94; // how much of a trail is left after each generation
const BIRTH = [3]; // B3/S23 is Conway's Life
const SURVIVE = [2, 3];
const FPS = 15;
let cols, rows, alive, age, trail, next;
let paused = false;
function setup() {
createCanvas(min(windowWidth, 640), 320);
cols = floor(width / CELL);
rows = floor(height / CELL);
alive = new Uint8Array(cols * rows);
next = new Uint8Array(cols * rows);
age = new Uint16Array(cols * rows);
trail = new Float32Array(cols * rows);
colorMode(HSB, 360, 100, 100);
noStroke();
frameRate(FPS);
soup();
}
function soup() {
for (let i = 0; i < alive.length; i++) { alive[i] = random() < DENSITY ? 1 : 0; age[i] = 0; trail[i] = 0; }
}
function stepLife() {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let n = 0;
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (dx || dy) n += alive[((y + dy + rows) % rows) * cols + (x + dx + cols) % cols];
}
const i = y * cols + x;
next[i] = alive[i] ? (SURVIVE.includes(n) ? 1 : 0) : (BIRTH.includes(n) ? 1 : 0);
}
}
for (let i = 0; i < alive.length; i++) {
if (next[i]) { age[i] = alive[i] ? age[i] + 1 : 0; trail[i] = 1; }
else trail[i] *= FADE;
}
[alive, next] = [next, alive];
}
function draw() {
if (!paused) stepLife();
background(240, 30, 6);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const i = y * cols + x;
if (alive[i]) {
// Young cells are yellow, old ones cool through green to blue.
fill(55 + min(age[i], 40) * 4.5, 85, 100);
} else if (trail[i] > 0.03) {
fill(285, 70, 70 * trail[i]);
} else continue;
rect(x * CELL, y * CELL, CELL - 1, CELL - 1);
}
}
}
function paint() {
const x = floor(mouseX / CELL), y = floor(mouseY / CELL);
if (x < 0 || y < 0 || x >= cols || y >= rows) return;
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (random() < 0.6) { const j = ((y + dy + rows) % rows) * cols + (x + dx + cols) % cols; alive[j] = 1; age[j] = 0; }
}
}
function mousePressed() { paint(); }
function mouseDragged() { paint(); }
function keyPressed() {
if (key === ' ') paused = !paused;
if (key === 'r') soup();
if (key === 'c') { alive.fill(0); trail.fill(0); }
}
Things to change
-
The rule.
BIRTH = [3, 6]withSURVIVE = [2, 3]is HighLife, which has a replicator;BIRTH = [3, 6, 7, 8],SURVIVE = [3, 4, 6, 7, 8]is Day & Night, which is symmetric between live and dead;BIRTH = [3],SURVIVE = [1, 2, 3, 4, 5]is Maze. -
FADE—0.99keeps a long memory of everywhere life has been;0.6is a short flicker. -
DENSITY— around0.3is a good soup; very low or very high densities die out quickly. -
CELL = 3gives four times as many cells (and a lower frame rate on a phone). -
FPS— raise it to60to watch long-running soups settle into still lifes and blinkers.