const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const params = window.params = Object.assign({
cell: 5,
gps: 12,
density: 0.3,
pattern: 'soup',
hue: 145,
trails: true,
}, window.params);
const PATTERNS = {
gun: [
[0,4],[0,5],[1,4],[1,5],
[10,4],[10,5],[10,6],[11,3],[11,7],[12,2],[12,8],[13,2],[13,8],
[14,5],[15,3],[15,7],[16,4],[16,5],[16,6],[17,5],
[20,2],[20,3],[20,4],[21,2],[21,3],[21,4],[22,1],[22,5],
[24,0],[24,1],[24,5],[24,6],[34,2],[34,3],[35,2],[35,3],
],
pulsar: [
[2,0],[3,0],[4,0],[8,0],[9,0],[10,0],
[0,2],[5,2],[7,2],[12,2],[0,3],[5,3],[7,3],[12,3],[0,4],[5,4],[7,4],[12,4],
[2,5],[3,5],[4,5],[8,5],[9,5],[10,5],
[2,7],[3,7],[4,7],[8,7],[9,7],[10,7],
[0,8],[5,8],[7,8],[12,8],[0,9],[5,9],[7,9],[12,9],[0,10],[5,10],[7,10],[12,10],
[2,12],[3,12],[4,12],[8,12],[9,12],[10,12],
],
rpentomino: [[1,0],[2,0],[0,1],[1,1],[1,2]],
acorn: [[1,0],[3,1],[0,2],[1,2],[4,2],[5,2],[6,2]],
};
let cols = 0, rows = 0, grid = null, next = null, heat = null, image = null;
let generation = 0, population = 0;
const off = document.createElement('canvas');
const octx = off.getContext('2d');
function seed() {
grid.fill(0);
heat.fill(0);
generation = 0;
const cells = PATTERNS[params.pattern];
if (!cells) {
for (let i = 0; i < grid.length; i++) grid[i] = Math.random() < params.density ? 1 : 0;
return;
}
let maxX = 0, maxY = 0;
for (const c of cells) {
if (c[0] > maxX) maxX = c[0];
if (c[1] > maxY) maxY = c[1];
}
const gun = params.pattern === 'gun';
const ox = gun ? 2 : Math.floor((cols - maxX) / 2);
const oy = gun ? 2 : Math.floor((rows - maxY) / 2);
for (const c of cells) {
const x = ox + c[0], y = oy + c[1];
if (x >= 0 && x < cols && y >= 0 && y < rows) grid[y * cols + x] = 1;
}
}
function resize() {
const cell = Math.max(1, Math.round(params.cell));
canvas.width = Math.max(1, Math.floor(canvas.clientWidth));
canvas.height = Math.max(1, Math.floor(canvas.clientHeight));
cols = Math.max(1, Math.floor(canvas.width / cell));
rows = Math.max(1, Math.floor(canvas.height / cell));
off.width = cols;
off.height = rows;
grid = new Uint8Array(cols * rows);
next = new Uint8Array(cols * rows);
heat = new Uint8Array(cols * rows);
image = octx.createImageData(cols, rows);
seed();
}
resize();
new ResizeObserver(resize).observe(canvas);
let painting = false;
function paint(e) {
if (!painting) return;
const rect = canvas.getBoundingClientRect();
const cell = Math.max(1, Math.round(params.cell));
const cx = Math.floor((e.clientX - rect.left) / cell);
const cy = Math.floor((e.clientY - rect.top) / cell);
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const x = cx + dx, y = cy + dy;
if (x >= 0 && x < cols && y >= 0 && y < rows && Math.random() < 0.6) {
grid[y * cols + x] = 1;
heat[y * cols + x] = 255;
}
}
}
}
canvas.addEventListener('pointerdown', (e) => { painting = true; paint(e); });
canvas.addEventListener('pointermove', paint);
addEventListener('pointerup', () => { painting = false; });
function step() {
for (let y = 0; y < rows; y++) {
const up = ((y - 1 + rows) % rows) * cols;
const mid = y * cols;
const dn = ((y + 1) % rows) * cols;
for (let x = 0; x < cols; x++) {
const xl = (x - 1 + cols) % cols;
const xr = (x + 1) % cols;
const n = grid[up + xl] + grid[up + x] + grid[up + xr] +
grid[mid + xl] + grid[mid + xr] +
grid[dn + xl] + grid[dn + x] + grid[dn + xr];
next[mid + x] = (n === 3 || (n === 2 && grid[mid + x])) ? 1 : 0;
}
}
const swap = grid; grid = next; next = swap;
population = 0;
const fade = params.trails ? 14 : 255;
for (let i = 0; i < grid.length; i++) {
if (grid[i]) { heat[i] = 255; population++; }
else heat[i] = heat[i] > fade ? heat[i] - fade : 0;
}
generation++;
}
let ramp = null, rampHue = -1;
function buildRamp() {
const h = params.hue;
ramp = new Uint8Array(256 * 3);
for (let k = 0; k < 256; k++) {
const t = Math.pow(k / 255, 1.6);
const l = 0.12 + t * 0.5;
const s = 0.85;
const c = (1 - Math.abs(2 * l - 1)) * s;
const hp = (h % 360) / 60;
const x = c * (1 - Math.abs((hp % 2) - 1));
let r = 0, g = 0, b = 0;
if (hp < 1) { r = c; g = x; }
else if (hp < 2) { r = x; g = c; }
else if (hp < 3) { g = c; b = x; }
else if (hp < 4) { g = x; b = c; }
else if (hp < 5) { r = x; b = c; }
else { r = c; b = x; }
const m = l - c / 2;
ramp[k * 3] = Math.round((r + m) * 255 * t + 10 * (1 - t));
ramp[k * 3 + 1] = Math.round((g + m) * 255 * t + 7 * (1 - t));
ramp[k * 3 + 2] = Math.round((b + m) * 255 * t + 20 * (1 - t));
}
rampHue = h;
}
buildRamp();
function draw() {
if (params.hue !== rampHue) buildRamp();
const data = image.data;
for (let i = 0; i < heat.length; i++) {
const k = heat[i] * 3;
const o = i * 4;
data[o] = ramp[k];
data[o + 1] = ramp[k + 1];
data[o + 2] = ramp[k + 2];
data[o + 3] = 255;
}
octx.putImageData(image, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(off, 0, 0, cols, rows, 0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(6, 4, 14, 0.72)';
ctx.fillRect(0, 0, 250, 28);
ctx.fillStyle = '#e8e4ff';
ctx.font = '13px ui-monospace, SFMono-Regular, Menlo, monospace';
ctx.fillText('gen ' + generation + ' alive ' + population + ' ' + cols + 'x' + rows, 10, 19);
}
let last = 0, acc = 0;
function frame(now) {
const dt = last ? Math.min(0.25, (now - last) / 1000) : 0;
last = now;
acc += dt * params.gps;
let budget = 8;
while (acc >= 1 && budget-- > 0) { acc -= 1; step(); }
if (acc > 1) acc = 1;
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);