cellular automata
2D cellular automata with configurable rules and fading trails.
p5.js
import p5 from 'p5';
interface Rule { name: string; S: [number, number]; B: [number, number]; density?: number }
const rules: Rule[] = [
{ name: 'conway', S: [2, 3], B: [3, 3] },
{ name: 'maze', S: [1, 5], B: [3, 3] },
{ name: 'coral', S: [4, 8], B: [3, 3], density: 15 },
{ name: 'anneal', S: [3, 6], B: [4, 8], density: 50 },
{ name: 'day & night', S: [3, 8], B: [3, 6], density: 50 },
{ name: 'diamoeba', S: [5, 8], B: [5, 8], density: 50 },
{ name: '34 life', S: [3, 4], B: [3, 4] },
{ name: 'morley', S: [2, 4], B: [3, 6], density: 40 },
{ name: 'seeds', S: [0, 0], B: [2, 2], density: 5 },
{ name: 'stains', S: [2, 3], B: [3, 6], density: 20 },
];
let ruleIdx = 0;
const label = document.getElementById('preset-label')!;
function setSlider(id: string, val: number) {
const input = document.getElementById(`ctrl-${id}`) as HTMLInputElement | null;
if (input) {
input.value = String(val);
input.dispatchEvent(new Event('input'));
}
}
function applyRule(idx: number) {
ruleIdx = ((idx % rules.length) + rules.length) % rules.length;
const r = rules[ruleIdx];
label.textContent = r.name;
setSlider('survlo', r.S[0]);
setSlider('survhi', r.S[1]);
setSlider('birthlo', r.B[0]);
setSlider('birthhi', r.B[1]);
if (r.density) setSlider('density', r.density);
}
document.getElementById('preset-prev')?.addEventListener('click', () => { applyRule(ruleIdx - 1); resetSim?.(); });
document.getElementById('preset-next')?.addEventListener('click', () => { applyRule(ruleIdx + 1); resetSim?.(); });
let resetSim: (() => void) | null = null;
new p5((p: p5) => {
let cols: number, rows: number;
let grid: number[][];
let trail: number[][];
function makeGrid(fill: boolean) {
return Array.from({ length: cols }, () =>
Array.from({ length: rows }, () => (fill ? (p.random() > (1 - c.density / 100) ? 1 : 0) : 0))
);
}
function makeTrailGrid() {
return Array.from({ length: cols }, () => Array(rows).fill(0));
}
function countNeighbors(g: number[][], x: number, y: number) {
let sum = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) continue;
const cx = (x + i + cols) % cols;
const cy = (y + j + rows) % rows;
sum += g[cx][cy] > 0 ? 1 : 0;
}
}
return sum;
}
function doReset() {
cols = Math.floor(p.width / c.cell);
rows = Math.floor(p.height / c.cell);
grid = makeGrid(true);
trail = makeTrailGrid();
p.background(0);
}
resetSim = doReset;
const c = controls({
cell: { label: 'cell size', min: 3, max: 16, value: 6, onChange: doReset },
fps: { label: 'frame rate', min: 5, max: 60, value: 15, onChange: (v) => p.frameRate(v) },
trail: { label: 'trail length', min: 1, max: 30, value: 12 },
density: { label: 'density', min: 10, max: 80, value: 30, format: (v) => v + '%' },
survlo: { label: 'survive lo', min: 0, max: 8, value: 2 },
survhi: { label: 'survive hi', min: 0, max: 8, value: 3 },
birthlo: { label: 'birth lo', min: 0, max: 8, value: 3 },
birthhi: { label: 'birth hi', min: 0, max: 8, value: 3 },
}, {
customRows: [{ html: '<div class="preset-row"><span>rule</span><span class="preset-nav"><button class="preset-btn" id="preset-prev">←</button><span class="val" id="preset-label">conway</span><button class="preset-btn" id="preset-next">→</button></span></div>', index: 4 }],
onRestart: doReset,
onReset: () => { applyRule(0); doReset(); },
});
p.setup = () => {
const el = document.getElementById('canvas')!.parentElement!;
const cv = p.createCanvas(el.clientWidth, el.clientHeight);
cv.parent(el);
document.getElementById('canvas')?.remove();
doReset();
p.frameRate(15);
p.background(0);
};
p.draw = () => {
p.background(0, 25);
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
if (grid[x][y] > 0) {
trail[x][y] = c.trail;
p.fill(255, 80, 40);
p.noStroke();
p.rect(x * c.cell, y * c.cell, c.cell, c.cell);
} else if (trail[x][y] > 0) {
trail[x][y]--;
const t = trail[x][y] / c.trail;
p.fill(60 * t, 80 * t, 120 * t);
p.noStroke();
p.rect(x * c.cell, y * c.cell, c.cell, c.cell);
}
}
}
const next = makeGrid(false);
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
const n = countNeighbors(grid, x, y);
if (grid[x][y] > 0) {
next[x][y] = n >= c.survlo && n <= c.survhi ? 1 : 0;
} else {
next[x][y] = n >= c.birthlo && n <= c.birthhi ? 1 : 0;
}
}
}
grid = next;
};
});