Conway's Game of Life: How to Build the Most Famous Cellular Automaton With Code
In 1970, mathematician John Horton Conway invented a zero-player game that would become the most famous cellular automaton in history. The rules fit on an index card: cells on a grid are either alive or dead. A live cell with two or three live neighbours survives. A dead cell with exactly three live neighbours comes to life. Everything else dies. That is it — three rules, and from them emerges an entire universe of moving spaceships, self-replicating patterns, oscillators, logic gates, and even Turing-complete computers built entirely from blinking pixels.
Conway's Game of Life is not just a mathematical curiosity. It is a profound demonstration that complexity does not require complex rules. It launched the field of artificial life, influenced computer science (it can simulate any Turing machine), and remains one of the most visually captivating things you can build in code. If you have ever stared at a Life simulation and watched a glider sail across the screen, you have witnessed emergence — order arising spontaneously from chaos.
In this guide we build eight interactive simulations, starting from the classic Game of Life and ending with gallery-worthy generative art. Each example is self-contained, runs on a plain HTML Canvas with no external libraries, and stays under 50KB. Along the way you will learn about cellular automata theory, the zoo of Life patterns, HashLife acceleration, Life-like rule variants, and techniques for turning Life into art.
How Conway's Game of Life works
The Game of Life is played on an infinite two-dimensional grid of square cells. Each cell has exactly eight neighbours (the Moore neighbourhood). The simulation advances in discrete generations. At each generation, every cell simultaneously updates according to three rules:
- Survival: a live cell with 2 or 3 live neighbours stays alive
- Birth: a dead cell with exactly 3 live neighbours becomes alive
- Death: all other live cells die (underpopulation if <2 neighbours, overpopulation if >3)
These rules are sometimes written in the notation B3/S23 — birth on 3, survival on 2 or 3. Despite their simplicity, they produce a staggering taxonomy of patterns. Still lifes (blocks, beehives, loaves) never change. Oscillators (blinkers, toads, pulsars) cycle through states. Spaceships (gliders, lightweight spaceships) translate across the grid. And complex patterns like the Gosper glider gun produce an infinite stream of gliders, proving that Life populations can grow without bound — the very question Conway originally posed.
Example 1: Classic Game of Life
Our first example implements the standard Game of Life with a random initial state. Click the canvas to pause and resume. The grid wraps at the edges (toroidal topology) so patterns that exit one side reappear on the opposite side.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'pointer';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cellSize = 4;
var cols = Math.floor(c.width / cellSize);
var rows = Math.floor(c.height / cellSize);
var grid = [];
var running = true;
var gen = 0;
for (var i = 0; i < cols * rows; i++) {
grid[i] = Math.random() < 0.3 ? 1 : 0;
}
function step() {
var next = new Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
var nx = (x + dx + cols) % cols;
var ny = (y + dy + rows) % rows;
n += grid[ny * cols + nx];
}
}
var idx = y * cols + x;
if (grid[idx]) {
next[idx] = (n === 2 || n === 3) ? 1 : 0;
} else {
next[idx] = (n === 3) ? 1 : 0;
}
}
}
grid = next;
gen++;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
if (grid[y * cols + x]) {
var hue = (gen * 0.5 + x * 0.3 + y * 0.3) % 360;
ctx.fillStyle = 'hsl(' + hue + ',70%,60%)';
ctx.fillRect(x * cellSize, y * cellSize, cellSize - 1, cellSize - 1);
}
}
}
ctx.fillStyle = '#445';
ctx.font = '11px monospace';
ctx.fillText('Generation: ' + gen + ' | click to ' + (running ? 'pause' : 'resume'), 8, c.height - 8);
}
function animate() {
if (running) { step(); step(); }
draw();
requestAnimationFrame(animate);
}
c.addEventListener('click', function() { running = !running; });
animate();
Example 2: Pattern library
The Game of Life has a rich taxonomy of known patterns. This example lets you place classic patterns on the grid. Click buttons to select a pattern, then click the canvas to place it. Includes still lifes (block, beehive), oscillators (blinker, pulsar), and spaceships (glider, lightweight spaceship).
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'crosshair';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cs = 5, cols = Math.floor(c.width / cs), rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var running = true, gen = 0;
var patterns = {
glider: [[0,1,0],[0,0,1],[1,1,1]],
lwss: [[0,1,0,0,1],[1,0,0,0,0],[1,0,0,0,1],[1,1,1,1,0]],
block: [[1,1],[1,1]],
beehive: [[0,1,1,0],[1,0,0,1],[0,1,1,0]],
blinker: [[1,1,1]],
pulsar: (function() {
var p = [];
for (var i = 0; i < 13; i++) p[i] = [0,0,0,0,0,0,0,0,0,0,0,0,0];
var pts = [[0,2],[0,3],[0,4],[0,8],[0,9],[0,10],
[2,0],[3,0],[4,0],[2,5],[3,5],[4,5],
[2,7],[3,7],[4,7],[2,12],[3,12],[4,12],
[5,2],[5,3],[5,4],[5,8],[5,9],[5,10]];
for (var i = 0; i < pts.length; i++) {
p[pts[i][0]][pts[i][1]] = 1;
p[12 - pts[i][0]][pts[i][1]] = 1;
}
return p;
})(),
rpentomino: [[0,1,1],[1,1,0],[0,1,0]]
};
var selected = 'glider';
var wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;gap:4px;margin:4px 0;flex-wrap:wrap';
document.currentScript.parentNode.insertBefore(wrap, c.nextSibling);
var names = Object.keys(patterns);
for (var i = 0; i < names.length; i++) {
(function(name) {
var btn = document.createElement('button');
btn.textContent = name;
btn.style.cssText = 'padding:4px 10px;background:#1a1a2e;color:#8be;border:1px solid #334;border-radius:6px;cursor:pointer;font:11px monospace';
btn.addEventListener('click', function() { selected = name; });
wrap.appendChild(btn);
})(names[i]);
}
function place(cx, cy) {
var pat = patterns[selected];
for (var py = 0; py < pat.length; py++) {
for (var px = 0; px < pat[py].length; px++) {
if (pat[py][px]) {
var gx = (cx + px) % cols;
var gy = (cy + py) % rows;
grid[gy * cols + gx] = 1;
}
}
}
}
function step() {
var next = new Uint8Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
next[idx] = (grid[idx] ? (n === 2 || n === 3) : (n === 3)) ? 1 : 0;
}
}
grid = next;
gen++;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
if (grid[y * cols + x]) {
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(x * cs, y * cs, cs - 1, cs - 1);
}
}
}
ctx.fillStyle = '#445';
ctx.font = '11px monospace';
ctx.fillText('Gen ' + gen + ' | Pattern: ' + selected + ' | click canvas to place', 8, c.height - 8);
}
c.addEventListener('click', function(e) {
var r = c.getBoundingClientRect();
var gx = Math.floor((e.clientX - r.left) / cs);
var gy = Math.floor((e.clientY - r.top) / cs);
place(gx, gy);
});
place(Math.floor(cols / 2) - 6, Math.floor(rows / 2) - 6);
function animate() {
if (running) step();
draw();
requestAnimationFrame(animate);
}
animate();
Example 3: Spaceships and speed of light
In the Game of Life, information cannot travel faster than one cell per generation — this is the “speed of light” (denoted c). A glider moves at c/4 (one cell diagonally every 4 generations). This example tracks multiple spaceships of different speeds and visualizes their motion trails, showing how each pattern travels at its own characteristic velocity.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cs = 3, cols = Math.floor(c.width / cs), rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var trail = new Float32Array(cols * rows);
var gen = 0;
var ships = {
glider: { pat: [[0,1,0],[0,0,1],[1,1,1]], label: 'Glider c/4 diagonal', color: '#ff6b6b' },
lwss: { pat: [[0,1,0,0,1],[1,0,0,0,0],[1,0,0,0,1],[1,1,1,1,0]], label: 'LWSS c/2 orthogonal', color: '#4ecdc4' },
mwss: { pat: [[0,0,1,0,0,0],[1,0,0,0,1,0],[0,0,0,0,0,1],[1,0,0,0,0,1],[0,1,1,1,1,1]], label: 'MWSS c/2 orthogonal', color: '#ffd93d' },
hwss: { pat: [[0,0,1,1,0,0,0],[1,0,0,0,0,1,0],[0,0,0,0,0,0,1],[1,0,0,0,0,0,1],[0,1,1,1,1,1,1]], label: 'HWSS c/2 orthogonal', color: '#c084fc' }
};
function placePattern(pat, sx, sy) {
for (var y = 0; y < pat.length; y++)
for (var x = 0; x < pat[y].length; x++)
if (pat[y][x]) grid[((sy + y) % rows) * cols + ((sx + x) % cols)] = 1;
}
var labels = Object.keys(ships);
for (var i = 0; i < labels.length; i++) {
var ship = ships[labels[i]];
var sy = 20 + i * Math.floor(rows / 4);
placePattern(ship.pat, 10, sy);
}
function step() {
var next = new Uint8Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
next[idx] = (grid[idx] ? (n === 2 || n === 3) : (n === 3)) ? 1 : 0;
}
}
for (var j = 0; j < trail.length; j++) {
trail[j] *= 0.97;
if (grid[j]) trail[j] = 1;
}
grid = next;
gen++;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var idx = y * cols + x;
var t = trail[idx];
if (t > 0.01) {
var band = Math.floor(y / (rows / 4));
var colors = ['#ff6b6b','#4ecdc4','#ffd93d','#c084fc'];
var col = colors[Math.min(band, 3)];
ctx.globalAlpha = t * 0.6;
ctx.fillStyle = col;
ctx.fillRect(x * cs, y * cs, cs, cs);
}
if (grid[idx]) {
ctx.globalAlpha = 1;
var band2 = Math.floor(y / (rows / 4));
ctx.fillStyle = ['#ff6b6b','#4ecdc4','#ffd93d','#c084fc'][Math.min(band2, 3)];
ctx.fillRect(x * cs, y * cs, cs, cs);
}
}
}
ctx.globalAlpha = 1;
ctx.font = '11px monospace';
var shipNames = Object.keys(ships);
for (var i = 0; i < shipNames.length; i++) {
ctx.fillStyle = ships[shipNames[i]].color;
ctx.fillText(ships[shipNames[i]].label, 8, 16 + i * Math.floor(c.height / 4));
}
ctx.fillStyle = '#445';
ctx.fillText('Generation: ' + gen, 8, c.height - 8);
}
function animate() {
step();
draw();
requestAnimationFrame(animate);
}
animate();
Example 4: Gosper glider gun
In 1970 Bill Gosper discovered the first pattern that grows forever — the Gosper glider gun. It fires a new glider every 30 generations. This was a breakthrough: it proved that Life populations can grow without bound, answering Conway's original question and winning a $50 prize. This example shows the gun in action with a generation counter and glider count.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cs = 4, cols = Math.floor(c.width / cs), rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var gen = 0;
var gun = [
[24,0],[22,1],[24,1],[12,2],[13,2],[20,2],[21,2],[34,2],[35,2],
[11,3],[15,3],[20,3],[21,3],[34,3],[35,3],[0,4],[1,4],[10,4],[16,4],[20,4],[21,4],
[0,5],[1,5],[10,5],[14,5],[16,5],[17,5],[22,5],[24,5],
[10,6],[16,6],[24,6],[11,7],[15,7],[12,8],[13,8]
];
for (var i = 0; i < gun.length; i++) {
var gx = gun[i][0] + 2, gy = gun[i][1] + 2;
if (gx < cols && gy < rows) grid[gy * cols + gx] = 1;
}
function step() {
var next = new Uint8Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
next[idx] = (grid[idx] ? (n === 2 || n === 3) : (n === 3)) ? 1 : 0;
}
}
grid = next;
gen++;
}
function countLive() {
var n = 0;
for (var i = 0; i < grid.length; i++) n += grid[i];
return n;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
if (grid[y * cols + x]) {
var inGun = (x < 40 && y < 14);
ctx.fillStyle = inGun ? '#ff6b6b' : '#4ecdc4';
ctx.fillRect(x * cs, y * cs, cs - 1, cs - 1);
}
}
}
ctx.fillStyle = '#ccc';
ctx.font = '12px monospace';
ctx.fillText('Gosper Glider Gun', 8, c.height - 28);
ctx.fillStyle = '#888';
ctx.font = '11px monospace';
ctx.fillText('Gen: ' + gen + ' | Live cells: ' + countLive() + ' | Gliders fired: ~' + Math.floor(gen / 30), 8, c.height - 10);
}
function animate() {
step(); step();
draw();
requestAnimationFrame(animate);
}
animate();
Example 5: Life-like automata explorer
Conway's rules (B3/S23) are just one point in a vast space of “Life-like” cellular automata. Change the birth and survival conditions and you get entirely different universes. HighLife (B36/S23) has a self-replicating pattern called the Replicator. Day & Night (B3678/S34678) is symmetric between live and dead cells. Seeds (B2/S) produces chaotic expansion from any live cell. This example lets you switch between rulesets in real time.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cs = 3, cols = Math.floor(c.width / cs), rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var gen = 0;
var rulesets = [
{ name: 'Life B3/S23', birth: [3], survive: [2,3], color: '#4ecdc4' },
{ name: 'HighLife B36/S23', birth: [3,6], survive: [2,3], color: '#ff6b6b' },
{ name: 'Day&Night B3678/S34678', birth: [3,6,7,8], survive: [3,4,6,7,8], color: '#ffd93d' },
{ name: 'Seeds B2/S', birth: [2], survive: [], color: '#c084fc' },
{ name: 'Diamoeba B35678/S5678', birth: [3,5,6,7,8], survive: [5,6,7,8], color: '#f97316' },
{ name: 'Replicator B1357/S1357', birth: [1,3,5,7], survive: [1,3,5,7], color: '#22d3ee' }
];
var ruleIdx = 0;
var rule = rulesets[0];
function randomize() {
for (var i = 0; i < grid.length; i++) grid[i] = Math.random() < 0.25 ? 1 : 0;
gen = 0;
}
randomize();
var wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;gap:4px;margin:4px 0;flex-wrap:wrap';
document.currentScript.parentNode.insertBefore(wrap, c.nextSibling);
for (var i = 0; i < rulesets.length; i++) {
(function(idx) {
var btn = document.createElement('button');
btn.textContent = rulesets[idx].name;
btn.style.cssText = 'padding:3px 8px;background:#1a1a2e;color:' + rulesets[idx].color + ';border:1px solid #334;border-radius:6px;cursor:pointer;font:10px monospace';
btn.addEventListener('click', function() { ruleIdx = idx; rule = rulesets[idx]; randomize(); });
wrap.appendChild(btn);
})(i);
}
function step() {
var next = new Uint8Array(cols * rows);
var b = rule.birth, s = rule.survive;
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
if (grid[idx]) {
for (var k = 0; k < s.length; k++) { if (s[k] === n) { next[idx] = 1; break; } }
} else {
for (var k = 0; k < b.length; k++) { if (b[k] === n) { next[idx] = 1; break; } }
}
}
}
grid = next;
gen++;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
if (grid[y * cols + x]) {
ctx.fillStyle = rule.color;
ctx.fillRect(x * cs, y * cs, cs - 1, cs - 1);
}
}
}
ctx.fillStyle = '#888';
ctx.font = '11px monospace';
ctx.fillText(rule.name + ' | Gen ' + gen, 8, c.height - 8);
}
function animate() {
step(); step();
draw();
requestAnimationFrame(animate);
}
animate();
Example 6: Aging cells with heat map
Standard Life only tracks alive or dead. But if we also track how long each cell has been alive, we get a beautiful heat map where ancient cells glow hot white while newborns are cool blue. This reveals the hidden structure of Life — stable cores burn bright while chaotic frontiers flicker in cool tones. Click to scatter new live cells.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'crosshair';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cs = 4, cols = Math.floor(c.width / cs), rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var age = new Uint16Array(cols * rows);
var gen = 0;
for (var i = 0; i < grid.length; i++) {
grid[i] = Math.random() < 0.3 ? 1 : 0;
}
function step() {
var next = new Uint8Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
next[idx] = (grid[idx] ? (n === 2 || n === 3) : (n === 3)) ? 1 : 0;
if (next[idx]) {
age[idx] = grid[idx] ? Math.min(age[idx] + 1, 500) : 1;
} else {
age[idx] = 0;
}
}
}
grid = next;
gen++;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var a = age[y * cols + x];
if (a > 0) {
var t = Math.min(a / 120, 1);
var h = 240 - t * 240;
var l = 40 + t * 40;
ctx.fillStyle = 'hsl(' + h + ',90%,' + l + '%)';
ctx.fillRect(x * cs, y * cs, cs - 1, cs - 1);
}
}
}
ctx.fillStyle = '#667';
ctx.font = '11px monospace';
ctx.fillText('Gen ' + gen + ' | blue=young, red=old, white=ancient | click to add cells', 8, c.height - 8);
}
c.addEventListener('click', function(e) {
var r = c.getBoundingClientRect();
var gx = Math.floor((e.clientX - r.left) / cs);
var gy = Math.floor((e.clientY - r.top) / cs);
for (var dy = -3; dy <= 3; dy++) {
for (var dx = -3; dx <= 3; dx++) {
if (Math.random() < 0.5) {
var nx = (gx + dx + cols) % cols;
var ny = (gy + dy + rows) % rows;
grid[ny * cols + nx] = 1;
age[ny * cols + nx] = 1;
}
}
}
});
function animate() {
step();
draw();
requestAnimationFrame(animate);
}
animate();
Example 7: Interactive Life painter
This example turns you into the creator. Draw live cells by clicking and dragging on the canvas. Press Space to toggle the simulation. Press C to clear. Press R for a random fill. The grid is large enough for complex patterns and the simulation runs fast enough to see your creations evolve immediately.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
c.style.cursor = 'crosshair';
c.tabIndex = 1;
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var cs = 5, cols = Math.floor(c.width / cs), rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var running = false, gen = 0, painting = false;
function step() {
var next = new Uint8Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
next[idx] = (grid[idx] ? (n === 2 || n === 3) : (n === 3)) ? 1 : 0;
}
}
grid = next;
gen++;
}
function paint(e) {
var r = c.getBoundingClientRect();
var gx = Math.floor((e.clientX - r.left) / cs);
var gy = Math.floor((e.clientY - r.top) / cs);
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (Math.random() < 0.7) {
var nx = (gx + dx + cols) % cols;
var ny = (gy + dy + rows) % rows;
grid[ny * cols + nx] = 1;
}
}
}
}
c.addEventListener('mousedown', function(e) { painting = true; paint(e); });
c.addEventListener('mousemove', function(e) { if (painting) paint(e); });
c.addEventListener('mouseup', function() { painting = false; });
c.addEventListener('mouseleave', function() { painting = false; });
c.addEventListener('keydown', function(e) {
if (e.code === 'Space') { e.preventDefault(); running = !running; }
if (e.code === 'KeyC') { grid = new Uint8Array(cols * rows); gen = 0; }
if (e.code === 'KeyR') { for (var i = 0; i < grid.length; i++) grid[i] = Math.random() < 0.3 ? 1 : 0; gen = 0; }
});
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, c.width, c.height);
// Grid lines
ctx.strokeStyle = '#151528';
ctx.lineWidth = 0.5;
for (var x = 0; x <= c.width; x += cs) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, c.height); ctx.stroke(); }
for (var y = 0; y <= c.height; y += cs) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(c.width, y); ctx.stroke(); }
// Cells
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
if (grid[y * cols + x]) {
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(x * cs + 1, y * cs + 1, cs - 2, cs - 2);
}
}
}
ctx.fillStyle = '#667';
ctx.font = '11px monospace';
var status = running ? 'RUNNING' : 'PAUSED (draw!)';
ctx.fillText(status + ' | Gen ' + gen + ' | Space=play/pause C=clear R=random', 8, c.height - 8);
}
c.focus();
function animate() {
if (running) step();
draw();
requestAnimationFrame(animate);
}
animate();
Example 8: Generative Life art
Our final example transforms the Game of Life into a generative art piece. Multiple Life simulations run simultaneously on layered canvases with different color palettes, blend modes, and cell sizes. Dead cells leave persistent colour trails that fade slowly, building up complex organic textures over time. The result is an ever-evolving abstract painting driven entirely by Conway's three rules.
var c = document.createElement('canvas');
c.width = 580; c.height = 460;
c.style.background = '#0a0a1a';
c.style.borderRadius = '12px';
document.currentScript.parentNode.insertBefore(c, document.currentScript);
var ctx = c.getContext('2d');
var layers = [];
var palettes = [
['#ff6b6b','#ee5a6f','#cc3366'],
['#4ecdc4','#45b7aa','#2d9d92'],
['#ffd93d','#ffcd1f','#f0b000'],
['#c084fc','#a855f7','#7c3aed']
];
for (var L = 0; L < 4; L++) {
var cs = 4 + L * 2;
var cols = Math.floor(c.width / cs);
var rows = Math.floor(c.height / cs);
var grid = new Uint8Array(cols * rows);
var trail = new Float32Array(cols * rows);
for (var i = 0; i < grid.length; i++) grid[i] = Math.random() < 0.2 ? 1 : 0;
layers.push({ cs: cs, cols: cols, rows: rows, grid: grid, trail: trail, palette: palettes[L] });
}
var gen = 0;
function stepLayer(layer) {
var cols = layer.cols, rows = layer.rows, grid = layer.grid;
var next = new Uint8Array(cols * rows);
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var n = 0;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
n += grid[((y + dy + rows) % rows) * cols + ((x + dx + cols) % cols)];
}
}
var idx = y * cols + x;
next[idx] = (grid[idx] ? (n === 2 || n === 3) : (n === 3)) ? 1 : 0;
}
}
for (var j = 0; j < layer.trail.length; j++) {
layer.trail[j] *= 0.985;
if (grid[j] && !next[j]) layer.trail[j] = 1;
}
layer.grid = next;
}
function draw() {
ctx.fillStyle = 'rgba(10, 10, 26, 0.15)';
ctx.fillRect(0, 0, c.width, c.height);
for (var L = 0; L < layers.length; L++) {
var layer = layers[L];
var cs = layer.cs, cols = layer.cols, rows = layer.rows;
var pal = layer.palette;
ctx.globalAlpha = 0.4;
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
var idx = y * cols + x;
var t = layer.trail[idx];
if (t > 0.05) {
ctx.fillStyle = pal[2];
ctx.globalAlpha = t * 0.2;
ctx.fillRect(x * cs, y * cs, cs, cs);
}
if (layer.grid[idx]) {
ctx.globalAlpha = 0.7;
ctx.fillStyle = pal[Math.floor(Math.random() * pal.length)];
ctx.fillRect(x * cs, y * cs, cs - 1, cs - 1);
}
}
}
}
ctx.globalAlpha = 1;
ctx.fillStyle = '#556';
ctx.font = '11px monospace';
ctx.fillText('4 layered Life simulations | Gen ' + gen, 8, c.height - 8);
}
function animate() {
for (var L = 0; L < layers.length; L++) stepLayer(layers[L]);
gen++;
draw();
requestAnimationFrame(animate);
}
animate();
The computational universe of Life
Conway's Game of Life is not just a toy. It is a Turing-complete system — given enough space and time, it can compute anything a real computer can. This was proven by constructing logic gates (AND, OR, NOT) from glider streams, and then wiring those gates together into circuits. People have built entire processors, RAM, and even other cellular automata running inside Life.
The engineering effort behind these constructions is staggering. A glider gun produces a steady stream of gliders. Two crossing glider streams create an AND gate (gliders collide and annihilate unless both streams are present). A NOT gate uses a glider stream to erase an incoming signal. From these primitives, anything is possible — adders, counters, memory cells, and eventually full CPUs. The Game of Life is simultaneously the simplest and most complex programming language ever created.
Why Life still matters
More than fifty years after its invention, the Game of Life continues to produce new discoveries. In 2018, the first truly elementary knightship (a spaceship moving in an (2,1) direction) was found. New oscillators with exotic periods are still being catalogued. The LifeWiki contains thousands of documented patterns. And the broader field of cellular automata that Life inspired — from Wolfram's Rule 110 to Lenia's continuous-space systems — continues to reshape our understanding of computation, emergence, and artificial life.
For creative coders, Life offers something rare: a system where three lines of rules produce inexhaustible visual complexity. Every random initial state is a surprise. Every parameter tweak (grid size, colour mapping, trail decay, rule variant) opens a new aesthetic dimension. And the patterns themselves — gliders sailing through emptiness, pulsars beating like hearts, guns firing streams of information — have an unmistakable beauty that no amount of familiarity diminishes.
Where to go next
- Explore the broader world of cellular automata — from elementary 1D rules to exotic 2D systems like Brian's Brain, Wireworld, and Lenia
- Use procedural generation techniques to seed Life simulations with structured initial conditions instead of random noise
- Apply mathematical art techniques to Life — use Fourier analysis to detect oscillator periods, or map the state space of small Life patterns exhaustively
- Combine Life with particle systems — let particles follow paths defined by glider trajectories, or use cell birth events as particle emitters
- Layer Life simulations over Perlin noise fields to create organic terrain where Life patterns act as evolving weather systems
- On Lumitree, Life gardens grow from visitor seeds — each seed initializes a unique Life simulation that evolves into a self-sustaining micro-ecosystem, proving that three simple rules can grow infinite worlds