You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

434 lines
14 KiB

// 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 v = parseInt(speedRange.value, 10);
const ms = Math.max(10, Math.round(1550 - v));
timer = setTimeout(loop, ms);
};
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();

Powered by TurnKey Linux.