初始化项目并上传代码
This commit is contained in:
549
app.js
Normal file
549
app.js
Normal file
@@ -0,0 +1,549 @@
|
||||
(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);
|
||||
Reference in New Issue
Block a user