(function (global) { 'use strict'; const PATTERNS = { glider: { name: 'Glider', cells: [ [0, 1], [1, 2], [2, 0], [2, 1], [2, 2], ], }, pulsar: { name: 'Pulsar', cells: [ [0, 2], [0, 3], [0, 4], [0, 8], [0, 9], [0, 10], [2, 0], [2, 5], [2, 7], [2, 12], [3, 0], [3, 5], [3, 7], [3, 12], [4, 0], [4, 5], [4, 7], [4, 12], [5, 2], [5, 3], [5, 4], [5, 8], [5, 9], [5, 10], [7, 2], [7, 3], [7, 4], [7, 8], [7, 9], [7, 10], [8, 0], [8, 5], [8, 7], [8, 12], [9, 0], [9, 5], [9, 7], [9, 12], [10, 0], [10, 5], [10, 7], [10, 12], [12, 2], [12, 3], [12, 4], [12, 8], [12, 9], [12, 10], ], }, gosperGliderGun: { name: 'Gosper Glider Gun', cells: [ [0, 24], [1, 22], [1, 24], [2, 12], [2, 13], [2, 20], [2, 21], [2, 34], [2, 35], [3, 11], [3, 15], [3, 20], [3, 21], [3, 34], [3, 35], [4, 0], [4, 1], [4, 10], [4, 16], [4, 20], [4, 21], [5, 0], [5, 1], [5, 10], [5, 14], [5, 16], [5, 17], [5, 22], [5, 24], [6, 10], [6, 16], [6, 24], [7, 11], [7, 15], [8, 12], [8, 13], ], }, }; function createEmptyGrid(rows, cols) { return Array.from({ length: rows }, function () { return Array(cols).fill(0); }); } function cloneGrid(grid) { return grid.map(function (row) { return row.slice(); }); } function countLiveNeighbors(grid, row, col) { let total = 0; for (let rowOffset = -1; rowOffset <= 1; rowOffset += 1) { for (let colOffset = -1; colOffset <= 1; colOffset += 1) { if (rowOffset === 0 && colOffset === 0) { continue; } const nextRow = row + rowOffset; const nextCol = col + colOffset; if ( nextRow >= 0 && nextRow < grid.length && nextCol >= 0 && nextCol < grid[0].length ) { total += grid[nextRow][nextCol]; } } } return total; } function stepGrid(grid) { return grid.map(function (cells, rowIndex) { return cells.map(function (cell, colIndex) { const neighbors = countLiveNeighbors(grid, rowIndex, colIndex); if (cell === 1) { return neighbors === 2 || neighbors === 3 ? 1 : 0; } return neighbors === 3 ? 1 : 0; }); }); } function stampPattern(grid, pattern, offsetRow, offsetCol) { pattern.cells.forEach(function (cell) { const row = cell[0] + offsetRow; const col = cell[1] + offsetCol; if (row >= 0 && row < grid.length && col >= 0 && col < grid[0].length) { grid[row][col] = 1; } }); return grid; } function countLiveCells(grid) { return grid.reduce(function (total, row) { return total + row.reduce(function (rowTotal, cell) { return rowTotal + cell; }, 0); }, 0); } function centerPattern(grid, pattern) { const lastRow = pattern.cells.reduce(function (maxRow, cell) { return Math.max(maxRow, cell[0]); }, 0); const lastCol = pattern.cells.reduce(function (maxCol, cell) { return Math.max(maxCol, cell[1]); }, 0); const offsetRow = Math.max(0, Math.floor((grid.length - (lastRow + 1)) / 2)); const offsetCol = Math.max(0, Math.floor((grid[0].length - (lastCol + 1)) / 2)); return stampPattern(grid, pattern, offsetRow, offsetCol); } function createInitialState(options) { const rows = options.rows; const cols = options.cols; const defaultPattern = options.defaultPattern || 'pulsar'; const grid = createEmptyGrid(rows, cols); centerPattern(grid, PATTERNS[defaultPattern]); return { cols: cols, defaultPattern: defaultPattern, generation: 0, grid: grid, liveCount: countLiveCells(grid), rows: rows, running: false, selectedPattern: defaultPattern, speed: 1, }; } function randomizeGrid(grid, probability) { const chance = typeof probability === 'number' ? probability : 0.28; return grid.map(function (row) { return row.map(function () { return Math.random() < chance ? 1 : 0; }); }); } function toggleCell(grid, row, col, forcedValue) { const next = cloneGrid(grid); if (row < 0 || row >= next.length || col < 0 || col >= next[0].length) { return next; } next[row][col] = typeof forcedValue === 'number' ? forcedValue : next[row][col] ? 0 : 1; return next; } function applyPreset(state, patternName) { const grid = createEmptyGrid(state.rows, state.cols); const nextPattern = PATTERNS[patternName] ? patternName : state.defaultPattern; centerPattern(grid, PATTERNS[nextPattern]); return { cols: state.cols, defaultPattern: state.defaultPattern, generation: 0, grid: grid, liveCount: countLiveCells(grid), rows: state.rows, running: false, selectedPattern: nextPattern, speed: state.speed, }; } function advanceState(state) { const nextGrid = stepGrid(state.grid); return Object.assign({}, state, { generation: state.generation + 1, grid: nextGrid, liveCount: countLiveCells(nextGrid), }); } function setSpeed(state, speed) { return Object.assign({}, state, { speed: Math.max(1, Math.min(6, speed)), }); } function getSpeedLabel(speed) { if (speed <= 1) { return 'Drift'; } if (speed <= 3) { return 'Pulse'; } if (speed <= 5) { return 'Hyper'; } return 'Lightstorm'; } function getSpeedDelay(speed) { const delays = { 1: 520, 2: 340, 3: 220, 4: 140, 5: 90, 6: 60, }; return delays[Math.max(1, Math.min(6, speed))]; } function getPatternLabel(patternName) { return PATTERNS[patternName] ? PATTERNS[patternName].name : 'Custom'; } function createEmptyLike(state) { return createEmptyGrid(state.rows, state.cols); } function replaceGrid(state, grid, selectedPattern) { return Object.assign({}, state, { generation: 0, grid: grid, liveCount: countLiveCells(grid), running: false, selectedPattern: selectedPattern || 'custom', }); } function getCanvasPoint(event, canvas, state) { const rect = canvas.getBoundingClientRect(); const col = Math.floor(((event.clientX - rect.left) / rect.width) * state.cols); const row = Math.floor(((event.clientY - rect.top) / rect.height) * state.rows); if (row < 0 || row >= state.rows || col < 0 || col >= state.cols) { return null; } return { row: row, col: col }; } function resizeCanvas(canvas) { const ratio = global.devicePixelRatio || 1; const bounds = canvas.getBoundingClientRect(); const width = Math.max(1, Math.floor(bounds.width * ratio)); const height = Math.max(1, Math.floor(bounds.height * ratio)); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } } function drawSimulation(canvas, state) { const context = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const cellWidth = width / state.cols; const cellHeight = height / state.rows; const inset = Math.max(1.5, Math.min(cellWidth, cellHeight) * 0.12); context.clearRect(0, 0, width, height); const background = context.createLinearGradient(0, 0, width, height); background.addColorStop(0, '#081321'); background.addColorStop(1, '#040a13'); context.fillStyle = background; context.fillRect(0, 0, width, height); const bloom = context.createRadialGradient(width * 0.18, height * 0.16, 0, width * 0.18, height * 0.16, width * 0.55); bloom.addColorStop(0, 'rgba(115, 240, 221, 0.15)'); bloom.addColorStop(1, 'rgba(115, 240, 221, 0)'); context.fillStyle = bloom; context.fillRect(0, 0, width, height); context.beginPath(); context.strokeStyle = 'rgba(126, 192, 212, 0.1)'; context.lineWidth = 1; for (let row = 0; row <= state.rows; row += 1) { const y = Math.round(row * cellHeight) + 0.5; context.moveTo(0, y); context.lineTo(width, y); } for (let col = 0; col <= state.cols; col += 1) { const x = Math.round(col * cellWidth) + 0.5; context.moveTo(x, 0); context.lineTo(x, height); } context.stroke(); for (let rowIndex = 0; rowIndex < state.rows; rowIndex += 1) { for (let colIndex = 0; colIndex < state.cols; colIndex += 1) { if (!state.grid[rowIndex][colIndex]) { continue; } const x = colIndex * cellWidth + inset; const y = rowIndex * cellHeight + inset; const drawWidth = Math.max(2, cellWidth - inset * 2); const drawHeight = Math.max(2, cellHeight - inset * 2); const fill = context.createLinearGradient(x, y, x + drawWidth, y + drawHeight); fill.addColorStop(0, 'rgba(115, 240, 221, 0.98)'); fill.addColorStop(1, 'rgba(190, 252, 125, 0.94)'); context.fillStyle = fill; context.shadowColor = 'rgba(115, 240, 221, 0.4)'; context.shadowBlur = Math.max(8, Math.min(cellWidth, cellHeight) * 0.66); context.fillRect(x, y, drawWidth, drawHeight); } } context.shadowBlur = 0; } function renderState(state, refs) { resizeCanvas(refs.canvas); drawSimulation(refs.canvas, state); refs.statusText.textContent = state.running ? 'Running' : 'Paused'; refs.statusText.classList.toggle('running', state.running); refs.speedLabel.textContent = getSpeedLabel(state.speed); refs.speedReadout.textContent = state.speed + ' / 6'; refs.generationValue.textContent = String(state.generation); refs.liveValue.textContent = String(state.liveCount); refs.presetValue.textContent = getPatternLabel(state.selectedPattern); refs.heroGeneration.textContent = String(state.generation); refs.heroLive.textContent = String(state.liveCount); refs.heroPattern.textContent = getPatternLabel(state.selectedPattern); refs.canvasHint.textContent = state.running ? '演化进行中,拖拽会自动暂停' : '点击或拖拽即可绘制细胞'; refs.playToggle.textContent = state.running ? '暂停演化' : '开始演化'; refs.stepButton.disabled = state.running; refs.speedSlider.value = String(state.speed); refs.presetButtons.forEach(function (button) { button.classList.toggle('active', button.dataset.pattern === state.selectedPattern); }); } function initDemo() { const canvas = document.getElementById('life-canvas'); if (!canvas) { return; } const refs = { canvas: canvas, canvasHint: document.getElementById('canvas-hint'), clearButton: document.getElementById('clear-button'), generationValue: document.getElementById('generation-value'), heroGeneration: document.getElementById('hero-generation'), heroLive: document.getElementById('hero-live'), heroPattern: document.getElementById('hero-pattern'), liveValue: document.getElementById('live-value'), loadDefaultHero: document.getElementById('load-default-hero'), playToggle: document.getElementById('play-toggle'), presetButtons: Array.from(document.querySelectorAll('[data-pattern]')), presetValue: document.getElementById('preset-value'), randomButton: document.getElementById('random-button'), resetButton: document.getElementById('reset-button'), speedLabel: document.getElementById('speed-label'), speedReadout: document.getElementById('speed-readout'), speedSlider: document.getElementById('speed-slider'), statusText: document.getElementById('status-text'), stepButton: document.getElementById('step-button'), }; const baseSize = global.innerWidth <= 720 ? 34 : 44; let state = createInitialState({ rows: baseSize, cols: baseSize, defaultPattern: 'pulsar', }); let lastTick = 0; let drawing = false; let drawValue = 1; let lastCellKey = ''; function setState(nextState) { state = nextState; renderState(state, refs); } function applyCanvasPoint(event) { const point = getCanvasPoint(event, refs.canvas, state); if (!point) { return; } const cellKey = point.row + ':' + point.col; if (cellKey === lastCellKey) { return; } lastCellKey = cellKey; const nextGrid = toggleCell(state.grid, point.row, point.col, drawValue); setState(replaceGrid(state, nextGrid, 'custom')); } refs.playToggle.addEventListener('click', function () { setState(Object.assign({}, state, { running: !state.running, })); }); refs.stepButton.addEventListener('click', function () { if (!state.running) { setState(advanceState(state)); } }); refs.clearButton.addEventListener('click', function () { setState(replaceGrid(state, createEmptyLike(state), 'custom')); }); refs.randomButton.addEventListener('click', function () { setState(replaceGrid(state, randomizeGrid(createEmptyLike(state), 0.22), 'custom')); }); refs.resetButton.addEventListener('click', function () { const patternName = PATTERNS[state.selectedPattern] ? state.selectedPattern : state.defaultPattern; setState(applyPreset(state, patternName)); }); refs.loadDefaultHero.addEventListener('click', function () { setState(applyPreset(state, state.defaultPattern)); document.getElementById('lab').scrollIntoView({ behavior: 'smooth', block: 'start' }); }); refs.speedSlider.addEventListener('input', function (event) { const speed = Number(event.target.value); setState(setSpeed(state, speed)); }); refs.presetButtons.forEach(function (button) { button.addEventListener('click', function () { setState(applyPreset(state, button.dataset.pattern)); }); }); refs.canvas.addEventListener('pointerdown', function (event) { const point = getCanvasPoint(event, refs.canvas, state); if (!point) { return; } drawing = true; lastCellKey = ''; drawValue = state.grid[point.row][point.col] ? 0 : 1; refs.canvas.setPointerCapture(event.pointerId); applyCanvasPoint(event); }); refs.canvas.addEventListener('pointermove', function (event) { if (!drawing) { return; } applyCanvasPoint(event); }); function stopDrawing(event) { if (drawing && event && refs.canvas.hasPointerCapture(event.pointerId)) { refs.canvas.releasePointerCapture(event.pointerId); } drawing = false; lastCellKey = ''; } refs.canvas.addEventListener('pointerup', stopDrawing); refs.canvas.addEventListener('pointerleave', stopDrawing); global.addEventListener('resize', function () { renderState(state, refs); }); function animate(timestamp) { if (state.running && timestamp - lastTick >= getSpeedDelay(state.speed)) { lastTick = timestamp; setState(advanceState(state)); } global.requestAnimationFrame(animate); } renderState(state, refs); global.requestAnimationFrame(animate); } const api = { advanceState: advanceState, applyPreset: applyPreset, PATTERNS: PATTERNS, cloneGrid: cloneGrid, countLiveNeighbors: countLiveNeighbors, countLiveCells: countLiveCells, createInitialState: createInitialState, createEmptyGrid: createEmptyGrid, getSpeedDelay: getSpeedDelay, getSpeedLabel: getSpeedLabel, randomizeGrid: randomizeGrid, setSpeed: setSpeed, stampPattern: stampPattern, stepGrid: stepGrid, toggleCell: toggleCell, }; if (typeof module !== 'undefined' && module.exports) { module.exports = api; } global.LifeDemo = api; if (typeof document !== 'undefined') { document.addEventListener('DOMContentLoaded', initDemo); } })(typeof window !== 'undefined' ? window : globalThis);