const canvas = document.getElementById('lifeCanvas'); const ctx = canvas.getContext('2d'); const cols = 80; const rows = 60; const cellSize = canvas.width / cols; let grid = makeGrid(); let running = false; let generation = 0; let births = 0; let deaths = 0; let timer = null; let speed = 8; const themeTokens = { sprout: ['oklch(58% 0.16 145)', 'oklch(72% 0.14 170)'], nebula: ['oklch(60% 0.18 285)', 'oklch(70% 0.14 235)'], sunny: ['oklch(70% 0.15 65)', 'oklch(74% 0.12 35)'] }; function makeGrid() { return Array.from({ length: rows }, () => Array(cols).fill(0)); } function randomize(chance = 0.28) { grid = grid.map(row => row.map(() => (Math.random() < chance ? 1 : 0))); generation = 0; births = 0; deaths = 0; updateLesson('A fresh universe appeared. Press Play and watch which clusters survive the first few generations.'); updateStats(); draw(); } function sparkles() { grid = makeGrid(); const centerX = Math.floor(cols / 2); const centerY = Math.floor(rows / 2); const points = [[0,0], [1,0], [-1,0], [0,1], [0,-1], [2,1], [-2,-1], [4,0], [4,1], [4,-1], [-4,0], [-4,1], [-4,-1], [7,2], [8,2], [7,3], [8,3]]; points.forEach(([x, y]) => { grid[centerY + y][centerX + x] = 1; }); generation = 0; births = 0; deaths = 0; updateLesson('Sparkles loaded. Step slowly and look for parts that blink while other parts float away.'); updateStats(); draw(); } function neighbors(x, y) { let total = 0; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const nx = (x + dx + cols) % cols; const ny = (y + dy + rows) % rows; total += grid[ny][nx]; } } return total; } function step() { const next = makeGrid(); births = 0; deaths = 0; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const n = neighbors(x, y); const alive = grid[y][x] === 1; const nextAlive = alive ? (n === 2 || n === 3) : n === 3; next[y][x] = nextAlive ? 1 : 0; if (!alive && nextAlive) births++; if (alive && !nextAlive) deaths++; } } grid = next; generation++; updateStats(); draw(); } function draw() { const styles = getComputedStyle(document.documentElement); const cellA = styles.getPropertyValue('--cell-a').trim(); const cellB = styles.getPropertyValue('--cell-b').trim(); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = styles.getPropertyValue('--surface').trim(); ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = styles.getPropertyValue('--grid').trim(); ctx.lineWidth = 0.7; for (let x = 0; x <= cols; x++) { ctx.beginPath(); ctx.moveTo(x * cellSize, 0); ctx.lineTo(x * cellSize, canvas.height); ctx.stroke(); } for (let y = 0; y <= rows; y++) { ctx.beginPath(); ctx.moveTo(0, y * cellSize); ctx.lineTo(canvas.width, y * cellSize); ctx.stroke(); } const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); gradient.addColorStop(0, cellA); gradient.addColorStop(1, cellB); ctx.fillStyle = gradient; ctx.shadowColor = cellA; ctx.shadowBlur = 8; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { if (grid[y][x]) { const inset = 1.4; ctx.beginPath(); ctx.roundRect(x * cellSize + inset, y * cellSize + inset, cellSize - inset * 2, cellSize - inset * 2, 3); ctx.fill(); } } } ctx.shadowBlur = 0; } function population() { return grid.reduce((sum, row) => sum + row.reduce((rowSum, cell) => rowSum + cell, 0), 0); } function updateStats() { document.getElementById('generation').textContent = generation; document.getElementById('population').textContent = population(); document.getElementById('births').textContent = births; document.getElementById('deaths').textContent = deaths; } function updateLesson(text) { document.getElementById('lessonText').textContent = text; } function play() { running = true; document.getElementById('playBtn').textContent = 'Pause'; document.getElementById('runStatus').textContent = 'Running - universe is alive'; loop(); } function pause() { running = false; document.getElementById('playBtn').textContent = 'Play'; document.getElementById('runStatus').textContent = 'Paused - ready to experiment'; if (timer) window.clearTimeout(timer); } function loop() { if (!running) return; step(); timer = window.setTimeout(() => requestAnimationFrame(loop), 620 - speed * 45); } function setTheme(name) { const [a, b] = themeTokens[name]; document.documentElement.style.setProperty('--cell-a', a); document.documentElement.style.setProperty('--cell-b', b); document.documentElement.style.setProperty('--accent', a); document.querySelectorAll('.theme').forEach(button => { button.setAttribute('aria-pressed', String(button.dataset.theme === name)); }); updateLesson(name === 'sprout' ? 'Sprout mode keeps the science-lab look: calm, clear, and easy to read.' : name === 'nebula' ? 'Nebula mode feels more arcade-like. It is great for spotting moving patterns.' : 'Sunny mode is warm and playful for younger scientists presenting to family.'); draw(); } canvas.addEventListener('pointerdown', event => { pause(); const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = Math.floor((event.clientX - rect.left) * scaleX / cellSize); const y = Math.floor((event.clientY - rect.top) * scaleY / cellSize); if (x >= 0 && x < cols && y >= 0 && y < rows) { grid[y][x] = grid[y][x] ? 0 : 1; births = 0; deaths = 0; updateLesson('You drew on the board. Press Step once to test your prediction before running it fast.'); updateStats(); draw(); } }); document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play()); document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); }); document.getElementById('clearBtn').addEventListener('click', () => { pause(); grid = makeGrid(); generation = 0; births = 0; deaths = 0; updateLesson('The board is clear. Draw a creature, then test if it survives.'); updateStats(); draw(); }); document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28)); document.getElementById('sparkBtn').addEventListener('click', sparkles); document.getElementById('speed').addEventListener('input', event => { speed = Number(event.target.value); document.getElementById('speedLabel').textContent = speed; }); document.querySelectorAll('.theme').forEach(button => button.addEventListener('click', () => setTheme(button.dataset.theme))); if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function roundRect(x, y, width, height, radius) { this.moveTo(x + radius, y); this.arcTo(x + width, y, x + width, y + height, radius); this.arcTo(x + width, y + height, x, y + height, radius); this.arcTo(x, y + height, x, y, radius); this.arcTo(x, y, x + width, y, radius); return this; }; } randomize(0.18);