Files
test01/app.js

550 lines
16 KiB
JavaScript
Raw Permalink Normal View History

2026-04-08 20:56:47 +08:00
(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);