// Simple Conway's Game of Life - frontend only const canvas = document.getElementById('lifeCanvas'); const ctx = canvas.getContext('2d'); const startBtn = document.getElementById('startBtn'); const stepBtn = document.getElementById('stepBtn'); const randomBtn = document.getElementById('randomBtn'); const clearBtn = document.getElementById('clearBtn'); const speedRange = document.getElementById('speedRange'); const cellSizeRange = document.getElementById('cellSizeRange'); const generationEl = document.getElementById('generation'); const presetSelect = document.getElementById('presetSelect'); const wrapCheckbox = document.getElementById('wrapCheckbox'); const saveBtn = document.getElementById('saveBtn'); const loadBtn = document.getElementById('loadBtn'); const baseFrequencyRange = document.getElementById('baseFrequencyRange'); const volumeRange = document.getElementById('volumeRange'); const musicModeSelect = document.getElementById('musicModeSelect'); const { startRecording, stopRecording, recordingInputNode } = window.recording; let cellSize = parseInt(cellSizeRange.value, 10); let cols, rows; let grid; let running = false; let timer = null; let generation = 0; let isMouseDown = false; let drawMode = 'toggle'; // 'on' 'off' 'toggle' let wrap = false; let offsetX = 0; let offsetY = 0; let centerCellX = 0; // Track the logical center cell for stable zooming let centerCellY = 0; let currentCell = null; let previousCell = null; function resizeCanvas() { // fit the canvas to its displayed size const rect = canvas.getBoundingClientRect(); canvas.width = Math.floor(rect.width); canvas.height = Math.floor(rect.height); cols = Math.floor(canvas.width / cellSize); rows = Math.floor(canvas.height / cellSize); // Calculate offsets to center the grid const gridWidth = cols * cellSize; const gridHeight = rows * cellSize; offsetX = (canvas.width - gridWidth) / 2; offsetY = (canvas.height - gridHeight) / 2; } function makeGrid(empty = true) { const g = new Array(rows); for (let y = 0; y < rows; y++) { g[y] = new Array(cols).fill(0); } if (!empty) { for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) g[y][x] = Math.random() < 0.2 ? 1 : 0; } return g; } function drawGrid() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#062033'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#7ee0ff'; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { if (grid[y][x]) { ctx.fillRect(offsetX + x * cellSize, offsetY + y * cellSize, cellSize - 1, cellSize - 1); } } } } function countNeighbors(x, y) { let count = 0; for (let oy = -1; oy <= 1; oy++) { for (let ox = -1; ox <= 1; ox++) { if (ox === 0 && oy === 0) continue; let nx = x + ox; let ny = y + oy; if (wrap) { nx = ((nx % cols) + cols) % cols; ny = ((ny % rows) + rows) % rows; count += grid[ny][nx]; } else { if (nx >= 0 && nx < cols && ny >= 0 && ny < rows) { count += grid[ny][nx]; } } } } return count; } function step() { const next = makeGrid(true); for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { let count = countNeighbors(x, y); if (grid[y][x]) { next[y][x] = (count === 2 || count === 3) ? 1 : 0; } else { next[y][x] = (count === 3) ? 1 : 0; } } } grid = next; generation++; generationEl.textContent = 'Generation: ' + generation; lifeMusic.generateFromGrid(grid, cols, rows); drawGrid(); } function runInterval() { if (!running) return; // If a timer is already scheduled, don't clear/reset it here — let the pending tick happen, // the loop reads the current speed each time so new speed will take effect on the next interval if (timer) return; const loop = () => { if (!running) return; step(); const stepsPerSecond = parseFloat(speedRange.value, 10); const ms = Math.max(10, Math.round(1000 / stepsPerSecond)); //const v = parseInt(speedRange.value, 10); //const ms = Math.max(10, Math.round(1550 - v)); timer = setTimeout(loop, ms); }; const stepsPerSecond = parseFloat(speedRange.value, 10); const ms = Math.max(10, Math.round(1000 / stepsPerSecond)); //const v = parseInt(speedRange.value, 10); //const ms = Math.max(10, Math.round(1550 - v)); timer = setTimeout(loop, ms); } async function start() { setSynthesisMode(musicModeSelect.value); if (!running) { running = true; startBtn.textContent = 'Stop'; runInterval(); if (!lifeMusic.initialized) { await lifeMusic.init(); } // Required by browser autoplay policy //await lifeMusic.audioContext.resume(); lifeMusic.play(); lifeMusic.generateFromGrid(grid, cols, rows); } else { running = false; startBtn.textContent = 'Start'; if (timer) clearInterval(timer); timer = null; lifeMusic.stop(); lifeMusic.generateFromGrid(grid, cols, rows); } // GUI updateRunState(); } function clearGrid() { grid = makeGrid(true); generation = 0; generationEl.textContent = 'Generation: 0'; lifeMusic.generateFromGrid(grid, cols, rows); drawGrid(); } function randomize() { grid = makeGrid(false); generation = 0; generationEl.textContent = 'Generation: 0'; drawGrid(); } function canvasToCell(clientX, clientY) { const rect = canvas.getBoundingClientRect(); const cx = clientX - rect.left; const cy = clientY - rect.top; const cellX = Math.floor((cx - offsetX) / cellSize); const cellY = Math.floor((cy - offsetY) / cellSize); return { cellX, cellY }; } function cellChanged(cellX, cellY) { if (currentCell === null || currentCell.x !== cellX || currentCell.y !== cellY) { previousCell = currentCell; currentCell = { x: cellX, y: cellY }; return true; } return false; } canvas.addEventListener('mousedown', (e) => { isMouseDown = true; const { cellX, cellY } = canvasToCell(e.clientX, e.clientY); if (cellX < 0 || cellX >= cols || cellY < 0 || cellY >= rows) return; // Only toggle on left click (button 0), right click (button 2) will erase on drag if (e.button === 0) { grid[cellY][cellX] = grid[cellY][cellX] ? 0 : 1; drawGrid(); } else if (e.button === 2) { grid[cellY][cellX] = 0; drawGrid(); } }); canvas.addEventListener('mousemove', (e) => { const { cellX, cellY } = canvasToCell(e.clientX, e.clientY); if (cellX < 0 || cellX >= cols || cellY < 0 || cellY >= rows) return; let isDifferentCell = cellChanged(cellX, cellY); if (!isMouseDown || !isDifferentCell) return; // paint with left button, erase with right button; prevent default on right-button drag if (e.buttons === 1) { grid[cellY][cellX] = 1; } else if (e.buttons === 2) { grid[cellY][cellX] = 0; } drawGrid(); }); canvas.addEventListener('contextmenu', (e) => { e.preventDefault(); }); canvas.addEventListener('wheel', (e) => { e.preventDefault(); const minSize = parseInt(cellSizeRange.min, 10); const maxSize = parseInt(cellSizeRange.max, 10); // Calculate new cell size based on scroll direction const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Scroll down = zoom out, scroll up = zoom in const newCellSize = Math.max(minSize, Math.min(maxSize, Math.round(cellSize * zoomFactor))); if (newCellSize === cellSize) return; // No change cellSize = newCellSize; cellSizeRange.value = cellSize; cellSizeRange.dispatchEvent(new Event('input')); // Trigger the input event to handle zoom }, { passive: false }); window.addEventListener('mouseup', () => { isMouseDown = false; }); startBtn.addEventListener('click', start); stepBtn.addEventListener('click', () => { step(); }); clearBtn.addEventListener('click', clearGrid); randomBtn.addEventListener('click', randomize); speedRange.addEventListener('input', () => { if (running) runInterval(); }); cellSizeRange.addEventListener('input', () => { const oldCols = cols; const oldRows = rows; const oldGrid = grid; cellSize = parseInt(cellSizeRange.value, 10); resizeCanvas(); // This recalculates cols, rows, and offsets based on new cellSize // Create new grid with potentially different dimensions const newGrid = makeGrid(true); // Calculate offset to keep the same logical center point // Use floor consistently to prevent drift const offsetX = Math.floor((cols - oldCols) / 2); const offsetY = Math.floor((rows - oldRows) / 2); // Copy old grid cells to new grid, centered for (let y = 0; y < oldRows; y++) { for (let x = 0; x < oldCols; x++) { const newX = x + offsetX; const newY = y + offsetY; // Only copy if the new position is within bounds if (newX >= 0 && newX < cols && newY >= 0 && newY < rows) { newGrid[newY][newX] = oldGrid[y][x]; } } } grid = newGrid; drawGrid(); }); wrapCheckbox.addEventListener('change', (e) => { wrap = e.target.checked; }); /*const musicPlayBtn = document.getElementById('musicPlayBtn'); const musicStopBtn = document.getElementById('musicStopBtn'); musicPlayBtn.addEventListener('click', () => { lifeMusic.play(); lifeMusic.generateFromGrid(grid, cols, rows); }); musicStopBtn.addEventListener('click', () => { lifeMusic.stop(); lifeMusic.generateFromGrid(grid, cols, rows); });*/ volumeRange.addEventListener('input', (e) => { const minGain = 0.001; // avoid log(0) const maxGain = 1; // maximum master gain const position = e.target.value / 100; // 0..1 // logarithmic mapping //const gain = minGain * Math.pow(maxGain / minGain, position); const gain = position; lifeMusic.masterGain.gain.setValueAtTime(gain, lifeMusic.audioContext.currentTime); }); musicModeSelect.addEventListener('change', async (e) => { setSynthesisMode(e.target.value); //await start(); }); presetSelect.addEventListener('change', (e) => { const v = e.target.value; if (!v) return; applyPreset(v); e.target.value = ''; }); saveBtn.addEventListener('click', () => { try { const payload = { cols, rows, grid, cellSize, generation }; localStorage.setItem('life-save', JSON.stringify(payload)); alert('Saved to local storage.'); } catch (err) { alert('Save failed: ' + err); } }); //window.addEventListener("DOMContentLoaded", () => { // window.recording.init(); // no context yet //}); loadBtn.addEventListener('click', () => { try { const data = localStorage.getItem('life-save'); if (!data) { alert('No saved pattern in local storage.'); return; } const obj = JSON.parse(data); // if dimensions differ, create new grid and center loaded pattern if (obj.cols === cols && obj.rows === rows) { grid = obj.grid; } else { const newGrid = makeGrid(true); const offY = Math.floor((rows - obj.rows) / 2); const offX = Math.floor((cols - obj.cols) / 2); for (let y = 0; y < Math.min(rows, obj.rows); y++) for (let x = 0; x < Math.min(cols, obj.cols); x++) newGrid[y + Math.max(0, offY)][x + Math.max(0, offX)] = obj.grid[y][x]; grid = newGrid; } generation = obj.generation || 0; generationEl.textContent = 'Generation: ' + generation; drawGrid(); } catch (err) { alert('Load failed: ' + err); } }); baseFrequencyRange.addEventListener('input', (e) => { const newBaseFrequency = parseInt(e.target.value, 10); lifeMusic.setBaseFrequency(newBaseFrequency); }); function applyPreset(name) { clearGrid(); const midY = Math.floor(rows / 2); const midX = Math.floor(cols / 2); if (name === 'glider') { const p = [[0, 1, 0], [0, 0, 1], [1, 1, 1]]; // glider for (let y = 0; y < p.length; y++) for (let x = 0; x < p[y].length; x++) grid[midY + y][midX + x] = p[y][x]; } else if (name === 'lwss') { const p = [ [0, 1, 1, 1, 1], [1, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 0, 0, 1, 0] ]; for (let y = 0; y < p.length; y++) for (let x = 0; x < p[y].length; x++) grid[midY + y][midX + x] = p[y][x]; } else if (name === 'gosper') { // Gosper glider gun pattern hard-coded relative points const pts = [ [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] ]; for (const [dx, dy] of pts) { const x = midX + dx - 10; const y = midY + dy - 5; if (y >= 0 && y < rows && x >= 0 && x < cols) grid[y][x] = 1; } } drawGrid(); } function init() { resizeCanvas(); grid = makeGrid(true); centerCellX = cols / 2; // Track the logical center centerCellY = rows / 2; generation = 0; generationEl.textContent = 'Generation: 0'; drawGrid(); } window.addEventListener('resize', () => { // preserve approximate pattern when resizing by copying to a new grid const oldCols = cols, oldRows = rows, old = grid; resizeCanvas(); const newGrid = makeGrid(true); for (let y = 0; y < Math.min(oldRows, rows); y++) for (let x = 0; x < Math.min(oldCols, cols); x++) newGrid[y][x] = old[y][x]; grid = newGrid; drawGrid(); }); wrap = wrapCheckbox.checked; init();