Files
Conways_Game_of_life/script.js
T
2026-06-06 23:40:47 -04:00

210 lines
7.1 KiB
JavaScript

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);