feat: draw gol on canvas
This commit is contained in:
parent
3b3e5b9173
commit
070bfd8d62
|
@ -22,30 +22,11 @@ const { title } = Astro.props;
|
||||||
</html>
|
</html>
|
||||||
<style is:global>
|
<style is:global>
|
||||||
:root {
|
:root {
|
||||||
--accent: 136, 58, 234;
|
--h1-desktop: 3.815rem;
|
||||||
--accent-light: 224, 204, 250;
|
font-size: 18px;
|
||||||
--accent-dark: 49, 10, 101;
|
|
||||||
--accent-gradient: linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgb(var(--accent)),
|
|
||||||
rgb(var(--accent-light)) 30%,
|
|
||||||
white 60%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
html {
|
body {
|
||||||
font-family: system-ui, sans-serif;
|
display: flex;
|
||||||
background: #13151a;
|
justify-content: center;
|
||||||
background-size: 224px;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
font-family:
|
|
||||||
Menlo,
|
|
||||||
Monaco,
|
|
||||||
Lucida Console,
|
|
||||||
Liberation Mono,
|
|
||||||
DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono,
|
|
||||||
Courier New,
|
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,12 +1,26 @@
|
||||||
---
|
---
|
||||||
import { Title } from '../components/header';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import '../styles/gol.css';
|
import '../styles/gol.css';
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="title-and-gol">
|
<Layout title="log101">
|
||||||
<Title />
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="main-title">log101</h1>
|
||||||
<div id="board" class="board"></div>
|
<div id="board" class="board"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="startButton">Start Game</button>
|
<button id="startButton">Start Game</button>
|
||||||
<script src="../scripts/gol.js"></script>
|
<button id="changeWidthButton">Change Width</button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConwaySimulator from '../scripts/conway-simulator';
|
||||||
|
|
||||||
|
let gol = new ConwaySimulator(10, 46, 10, 250);
|
||||||
|
document.getElementById('board')?.append(gol.canvas);
|
||||||
|
gol.start();
|
||||||
|
const startButton = document.getElementById('startButton');
|
||||||
|
startButton!.onclick = () => gol.stop();
|
||||||
|
</script>
|
||||||
|
|
485
src/scripts/conway-simulator.js
Normal file
485
src/scripts/conway-simulator.js
Normal file
|
@ -0,0 +1,485 @@
|
||||||
|
import { heavyWeightSpaceshipCell } from './utils'
|
||||||
|
|
||||||
|
/*
|
||||||
|
A wrapper for an HTML <canvas> based visualizatiuon of Conway's Game of Life.
|
||||||
|
*/
|
||||||
|
export default class ConwaySimulator {
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a new simulation. A simulation is comprised of a
|
||||||
|
2D data grid (rows-by-cols) of ConwayPixels, a canvas element,
|
||||||
|
and a canvas context.
|
||||||
|
*/
|
||||||
|
constructor(rows, cols, pixelSize, interRoundDelay) {
|
||||||
|
this.rows = rows;
|
||||||
|
this.cols = cols;
|
||||||
|
this.pixelSize = pixelSize;
|
||||||
|
this.interRoundDelay = interRoundDelay;
|
||||||
|
this.mouseIsDown = false;
|
||||||
|
this.paused = false;
|
||||||
|
this.intervalId = null;
|
||||||
|
|
||||||
|
// Make the grid
|
||||||
|
this.grid = [];
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
this.grid.push([]);
|
||||||
|
for (let j = 0; j < cols; j++) {
|
||||||
|
let alive = heavyWeightSpaceshipCell(j, i)
|
||||||
|
this.grid[i].push(new ConwayPixel(alive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform each pixel who it's neighbors are (performance optimization)
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
this.grid[i][j].neighbors = this.getNeighbors(i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the canvas
|
||||||
|
let width = this.pixelSize * this.cols
|
||||||
|
let height = this.pixelSize * this.rows
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.canvas.width = width;
|
||||||
|
this.canvas.height = height;
|
||||||
|
this.canvasCtx = this.canvas.getContext('2d', { alpha: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Starts the simulation via setInterval if it's not running
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
if (this.intervalId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.advanceRound();
|
||||||
|
this.repaint();
|
||||||
|
}, this.interRoundDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
If the simulation is running, stop it using clearInterval
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Return the neighbors of a particular grid location
|
||||||
|
*/
|
||||||
|
getNeighbors(row, col) {
|
||||||
|
let neighbors = [];
|
||||||
|
for (let i = row - 1; i <= row + 1; i++) {
|
||||||
|
for (let j = col - 1; j <= col + 1; j++) {
|
||||||
|
if (i === row && j === col) continue;
|
||||||
|
if (this.grid[i] && this.grid[i][j]) {
|
||||||
|
neighbors.push(this.grid[i][j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return neighbors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Update the grid according to the rules for each SimEntity
|
||||||
|
*/
|
||||||
|
advanceRound() {
|
||||||
|
if (this.mouseIsDown) return;
|
||||||
|
|
||||||
|
// First prepare each pixel (give it a next state)
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
this.grid[i][j].prepareUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then actually advance them, once all the new states are computed
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
this.grid[i][j].update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Optimized repaint that only updates pixels that have changed, and paints
|
||||||
|
in batches by color. Using force will repaint all pixels regardless of their
|
||||||
|
state/previousState/nextState, which is slower.
|
||||||
|
*/
|
||||||
|
repaint(force = false) {
|
||||||
|
if (this.mouseIsDown && !force) return;
|
||||||
|
|
||||||
|
// Canvas optimization -- it's faster to paint by color than placement.
|
||||||
|
let byColor = {};
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
let pixel = this.grid[i][j];
|
||||||
|
|
||||||
|
if (!force && !pixel.forceRepaint && pixel.alive === pixel.previousState) {
|
||||||
|
continue; // No need to repaint if the pixel didn't change
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = pixel.alive ? pixel.lifeStyle : pixel.deathStyle;
|
||||||
|
if (byColor[color] === undefined) {
|
||||||
|
byColor[color] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
byColor[color].push([i, j]);
|
||||||
|
pixel.forceRepaint = false; // Once a pixel is painted, reset it's forced state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let color in byColor) {
|
||||||
|
this.canvasCtx.fillStyle = color;
|
||||||
|
for (let [row, col] of byColor[color]) {
|
||||||
|
this.canvasCtx.fillRect(
|
||||||
|
col * this.pixelSize,
|
||||||
|
row * this.pixelSize,
|
||||||
|
this.pixelSize,
|
||||||
|
this.pixelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Paint an individual pixel. This is not used by repaint because of a batch
|
||||||
|
optimziation. painting an individual pixel does take place when click events
|
||||||
|
happen.
|
||||||
|
*/
|
||||||
|
paintPixel(row, col) {
|
||||||
|
this.grid[row][col].setPaintStyles(this.canvasCtx);
|
||||||
|
this.canvasCtx.fillRect(
|
||||||
|
col * this.pixelSize,
|
||||||
|
row * this.pixelSize,
|
||||||
|
this.pixelSize,
|
||||||
|
this.pixelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============
|
||||||
|
Visualizatiuon Modifiers
|
||||||
|
================ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Give each entity in the grid an alive style such that when all pixels are alive
|
||||||
|
the grid would be a rainbow gradient.
|
||||||
|
*/
|
||||||
|
setRainbowScheme() {
|
||||||
|
let rows = this.grid.length;
|
||||||
|
let cols = this.grid[0].length;
|
||||||
|
let diagonalLength = Math.sqrt((this.rows * this.rows) + (this.cols * this.cols));
|
||||||
|
let hueIncrement = 360 / diagonalLength;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
let h = Math.floor(Math.sqrt((i * i) + (j * j)) * hueIncrement);
|
||||||
|
let px = this.grid[i][j];
|
||||||
|
px.lifeStyle = `hsl(${h}, 100%, 60%)`;
|
||||||
|
px.deathStyle = `#000000`;
|
||||||
|
px.forceRepaint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Give each entity in the specified area of the grid an alive style
|
||||||
|
such that when all pixels are alive the area would be a rainbow gradient.
|
||||||
|
*/
|
||||||
|
setRainbowSchemeWithin(startRow, stopRow, startCol, stopCol) {
|
||||||
|
let rows = stopRow - startRow;
|
||||||
|
let cols = stopCol - startCol;
|
||||||
|
let diagonalLength = Math.sqrt((rows * rows) + (cols * cols));
|
||||||
|
let hueIncrement = 360 / diagonalLength;
|
||||||
|
|
||||||
|
for (let i = startRow; i < stopRow; i++) {
|
||||||
|
for (let j = startCol; j < stopCol; j++) {
|
||||||
|
let h = Math.floor(Math.sqrt((i * i) + (j * j)) * hueIncrement);
|
||||||
|
let px = this.grid[i][j];
|
||||||
|
px.lifeStyle = `hsl(${h}, 100%, 60%)`;
|
||||||
|
px.deathStyle = `#000000`;
|
||||||
|
px.forceRepaint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
set colors to the provided parameters
|
||||||
|
*/
|
||||||
|
setPixelColors(lifeStyle, deathStyle) {
|
||||||
|
this.grid.forEach((row) => {
|
||||||
|
row.forEach((entity) => {
|
||||||
|
entity.lifeStyle = lifeStyle;
|
||||||
|
entity.deathStyle = deathStyle;
|
||||||
|
entity.forceRepaint = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Give the board random semi-complementary colors.
|
||||||
|
*/
|
||||||
|
setRandomPixelColors() {
|
||||||
|
let baseHue = randomInteger(1, 360);
|
||||||
|
let complementaryHue = (baseHue + randomInteger(90, 270) % 360);
|
||||||
|
this.setPixelColors(`hsl(${baseHue}, 100%, 60%)`, `hsl(${complementaryHue}, 100%, 60%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Given a bounding box, apply the currently selected rules to ONLY the
|
||||||
|
pixels within the provided box.
|
||||||
|
*/
|
||||||
|
applyColorsWithin(rowStart, rowStop, colStart, colStop, lifeStyle, deathStyle) {
|
||||||
|
for (let i = rowStart; i < rowStop; i++) {
|
||||||
|
for (let j = colStart; j < colStop; j++) {
|
||||||
|
let pixel = this.grid[i][j];
|
||||||
|
pixel.lifeStyle = lifeStyle;
|
||||||
|
pixel.deathStyle = deathStyle;
|
||||||
|
pixel.forceRepaint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Give a sopecific area of the board random semi-complementary colors.
|
||||||
|
*/
|
||||||
|
applyRandomColorsWithin(rowStart, rowStop, colStart, colStop) {
|
||||||
|
let baseHue = randomInteger(1, 360);
|
||||||
|
let complementaryHue = (baseHue + randomInteger(90, 270) % 360);
|
||||||
|
this.applyColorsWithin(rowStart, rowStop, colStart, `hsl(${baseHue}, 100%, 60%)`, `hsl(${complementaryHue}, 100%, 60%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set all the pixels to alive=false
|
||||||
|
*/
|
||||||
|
resetLife(chanceOfLife) {
|
||||||
|
this.grid.forEach((row) => {
|
||||||
|
row.forEach((pixel) => {
|
||||||
|
pixel.previousState = pixel.alive;
|
||||||
|
pixel.alive = Math.random() < chanceOfLife;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Given a bounding box, apply the currently selected rules to ONLY the
|
||||||
|
pixels within the provided box.
|
||||||
|
*/
|
||||||
|
resetLifeWithin(rowStart, rowStop, colStart, colStop, chanceOfLife = .1) {
|
||||||
|
for (let i = rowStart; i < rowStop; i++) {
|
||||||
|
for (let j = colStart; j < colStop; j++) {
|
||||||
|
let pixel = this.grid[i][j];
|
||||||
|
if (pixel) {
|
||||||
|
pixel.previousState = pixel.alive;
|
||||||
|
pixel.alive = Math.random() < chanceOfLife;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Update the rules for all the pixels
|
||||||
|
*/
|
||||||
|
setRules(underpopulation, overpopulation, reproductionMin, reproductionMax) {
|
||||||
|
this.grid.forEach((row) => {
|
||||||
|
row.forEach((pixel) => {
|
||||||
|
pixel.underpopulation = underpopulation;
|
||||||
|
pixel.overpopulation = overpopulation;
|
||||||
|
pixel.reproductionMin = reproductionMin;
|
||||||
|
pixel.reproductionMax = reproductionMax;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Swap life and death styles across the center of the grid.
|
||||||
|
*/
|
||||||
|
setYinYangMode() {
|
||||||
|
for (let i = 0; i < this.rows; i++) {
|
||||||
|
for (let j = 0; j < this.cols / 2; j++) {
|
||||||
|
let t = this.grid[i][j].lifeStyle;
|
||||||
|
this.grid[i][j].lifeStyle = this.grid[i][j].deathStyle;
|
||||||
|
this.grid[i][j].deathStyle = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.repaint(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Given a bounding box, apply the currently selected rules to ONLY the
|
||||||
|
pixels within the provided box.
|
||||||
|
*/
|
||||||
|
setRulesWithin(rowStart, rowStop, colStart, colStop, underpopulation, overpopulation, reproductionMin, reproductionMax) {
|
||||||
|
for (let i = rowStart; i < rowStop; i++) {
|
||||||
|
for (let j = colStart; j < colStop; j++) {
|
||||||
|
let pixel = this.grid[i][j];
|
||||||
|
pixel.underpopulation = underpopulation;
|
||||||
|
pixel.overpopulation = overpopulation;
|
||||||
|
pixel.reproductionMin = reproductionMin;
|
||||||
|
pixel.reproductionMax = reproductionMax;
|
||||||
|
pixel.forceRepaint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The grid has click, and click-and-drag functionality. Entities define their
|
||||||
|
own behavior when clicked, and this function ensures the proper entity is
|
||||||
|
updated when it is clicked (or dragged-over)
|
||||||
|
*/
|
||||||
|
registerMouseListeners() {
|
||||||
|
bindMultipleEventListener(this.canvas, ['mousemove', 'touchmove'], (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.mouseIsDown) {
|
||||||
|
let x, y;
|
||||||
|
if (e.touches) {
|
||||||
|
let rect = e.target.getBoundingClientRect();
|
||||||
|
x = Math.floor((e.touches[0].pageX - rect.left) / this.pixelSize);
|
||||||
|
y = Math.floor((e.touches[0].pageY - rect.top) / this.pixelSize);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x = Math.floor(e.offsetX / this.pixelSize);
|
||||||
|
y = Math.floor(e.offsetY / this.pixelSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.grid[y][x].handleClick();
|
||||||
|
this.paintPixel(y, x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture mouse state for click and drag features
|
||||||
|
bindMultipleEventListener(this.canvas, ['mousedown', 'touchstart'], (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let rect = e.target.getBoundingClientRect();
|
||||||
|
let x, y;
|
||||||
|
if (e.touches) {
|
||||||
|
let rect = e.target.getBoundingClientRect();
|
||||||
|
x = Math.floor((e.touches[0].pageX - rect.left) / this.pixelSize);
|
||||||
|
y = Math.floor((e.touches[0].pageY - rect.top) / this.pixelSize);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x = Math.floor(e.offsetX / this.pixelSize);
|
||||||
|
y = Math.floor(e.offsetY / this.pixelSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.grid[y][x].handleClick();
|
||||||
|
this.paintPixel(y, x);
|
||||||
|
this.mouseIsDown = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
bindMultipleEventListener(this.canvas, ['mouseup', 'touchend'], (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mouseIsDown = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
A single pixel within a greater ConwaySimulator. Each ConwayPixel has it's own rules for evolution,
|
||||||
|
and for performance reason's maintains a list of it's neighbors inside of it's simulator.
|
||||||
|
|
||||||
|
This class is intended as an internal class, and is not exported. Manipulation of individual
|
||||||
|
ConwayPixels outside of the ConwaySimulator class is not advised.
|
||||||
|
*/
|
||||||
|
class ConwayPixel {
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constuct a default ConwayPixel, which follows the original Game of Life rules.
|
||||||
|
*/
|
||||||
|
constructor(alive) {
|
||||||
|
this.alive = alive;
|
||||||
|
this.lifeStyle = '#000000';
|
||||||
|
this.deathStyle = '#FFFFFF';
|
||||||
|
this.underpopulation = 2;
|
||||||
|
this.overpopulation = 3;
|
||||||
|
this.reproductionMin = 3;
|
||||||
|
this.reproductionMax = 3;
|
||||||
|
|
||||||
|
// Experimental improvement...
|
||||||
|
this.neighbors = [];
|
||||||
|
this.nextState = null;
|
||||||
|
this.previousState = null;
|
||||||
|
this.forceRepaint = true;
|
||||||
|
|
||||||
|
// Reproduction min cannot be more than reproduction max
|
||||||
|
if (this.reproductionMax < this.reproductionMin) {
|
||||||
|
this.reproductionMin = this.reproductionMax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
In order to process whole rounds at a time, update returns
|
||||||
|
a replacement entity, it does not edit the entity in place.
|
||||||
|
|
||||||
|
Following the rules for conway's game of life:
|
||||||
|
Any live cell with fewer than two live neighbors dies,
|
||||||
|
as if caused by underpopulation.
|
||||||
|
|
||||||
|
Any live cell with two or three live neighbors lives on
|
||||||
|
to the next generation.
|
||||||
|
|
||||||
|
Any live cell with more than three live neighbors dies,
|
||||||
|
as if by overpopulation.
|
||||||
|
|
||||||
|
Any dead cell with exactly three live neighbors becomes
|
||||||
|
a live cell, as if by reproduction.
|
||||||
|
|
||||||
|
*/
|
||||||
|
prepareUpdate() {
|
||||||
|
let sum = 0;
|
||||||
|
let nextState = this.alive;
|
||||||
|
|
||||||
|
for (let n of this.neighbors) {
|
||||||
|
if (n.alive && n !== this) sum++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextState && sum < this.underpopulation) {
|
||||||
|
nextState = false;
|
||||||
|
}
|
||||||
|
else if (nextState && sum > this.overpopulation) {
|
||||||
|
nextState = false;
|
||||||
|
}
|
||||||
|
else if (!nextState && sum >= this.reproductionMin && sum <= this.reproductionMax) {
|
||||||
|
nextState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextState = nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Advance this pixel to it's nextState.
|
||||||
|
*/
|
||||||
|
update() {
|
||||||
|
this.previousState = this.alive;
|
||||||
|
this.alive = this.nextState;
|
||||||
|
this.nextState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The calling context infers that a click HAS occured, this is not a mouse;
|
||||||
|
this is not an event listener.
|
||||||
|
*/
|
||||||
|
handleClick() {
|
||||||
|
this.alive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Provided with a canvas context, paint ourselves!
|
||||||
|
*/
|
||||||
|
setPaintStyles(canvasCtx) {
|
||||||
|
canvasCtx.fillStyle = this.alive ? this.lifeStyle : this.deathStyle;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
const boardElement = document.getElementById('board');
|
const boardElement = document.getElementById('board');
|
||||||
const boardSize = 10;
|
let boardWidth = 100; // Width of the board
|
||||||
const grid = [];
|
const boardHeight = 7; // Height of the board
|
||||||
let intervalId;
|
const currentGrid = []; // Current state of the grid
|
||||||
|
const nextGrid = []; // Next state of the grid
|
||||||
|
let intervalId; // ID for the setInterval
|
||||||
|
|
||||||
const cells = ['30', '20', '51', '01', '62', '63', '03', '64', '54', '44', '34', '24', '14']
|
const cells = ['30', '20', '51', '01', '62', '63', '03', '64', '54', '44', '34', '24', '14']
|
||||||
|
|
||||||
|
@ -11,49 +13,44 @@ const heavyWeightSpaceshipCell = (x, y) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeBoard() {
|
function initializeBoard() {
|
||||||
for (let y = 0; y < boardSize; y++) {
|
for (let y = 0; y < boardHeight; y++) {
|
||||||
let row = [];
|
currentGrid[y] = [];
|
||||||
for (let x = 0; x < boardSize; x++) {
|
nextGrid[y] = [];
|
||||||
|
for (let x = 0; x < boardWidth; x++) {
|
||||||
let cell = document.createElement('div');
|
let cell = document.createElement('div');
|
||||||
cell.classList.add('cell');
|
cell.classList.add('cell');
|
||||||
|
boardElement.appendChild(cell);
|
||||||
if (heavyWeightSpaceshipCell(x, y)) {
|
if (heavyWeightSpaceshipCell(x, y)) {
|
||||||
cell.classList.add('alive');
|
cell.classList.add('alive')
|
||||||
boardElement.appendChild(cell);
|
currentGrid[y][x] = 1;
|
||||||
row.push(1)
|
nextGrid[y][x] = 0;
|
||||||
} else {
|
} else {
|
||||||
boardElement.appendChild(cell);
|
currentGrid[y][x] = 0;
|
||||||
row.push(0);
|
nextGrid[y][x] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
grid.push(row);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeNextGeneration() {
|
function computeNextGeneration() {
|
||||||
let changes = [];
|
for (let y = 0; y < boardHeight; y++) {
|
||||||
|
for (let x = 0; x < boardWidth; x++) {
|
||||||
grid.forEach((row, y) => {
|
const aliveNeighbors = countAliveNeighbors(y, x);
|
||||||
row.forEach((cell, x) => {
|
const cell = currentGrid[y][x];
|
||||||
let aliveNeighbors = countAliveNeighbors(y, x);
|
nextGrid[y][x] = (cell === 1 && (aliveNeighbors === 2 || aliveNeighbors === 3)) || (cell === 0 && aliveNeighbors === 3) ? 1 : 0;
|
||||||
if (cell === 1 && (aliveNeighbors < 2 || aliveNeighbors > 3)) {
|
}
|
||||||
changes.push({ y, x, state: 0 });
|
|
||||||
} else if (cell === 0 && aliveNeighbors === 3) {
|
|
||||||
changes.push({ y, x, state: 1 });
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
changes.forEach(change => {
|
// Apply changes and minimize DOM updates
|
||||||
grid[change.y][change.x] = change.state;
|
for (let y = 0; y < boardHeight; y++) {
|
||||||
const cellElement = boardElement.children[change.y * boardSize + change.x];
|
for (let x = 0; x < boardWidth; x++) {
|
||||||
cellElement.animate([
|
if (currentGrid[y][x] !== nextGrid[y][x]) {
|
||||||
{ backgroundColor: change.state ? 'black' : 'white' }
|
const cellElement = boardElement.children[y * boardWidth + x];
|
||||||
], {
|
cellElement.classList.toggle('alive', nextGrid[y][x] === 1);
|
||||||
duration: 50,
|
}
|
||||||
fill: 'forwards'
|
currentGrid[y][x] = nextGrid[y][x];
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function countAliveNeighbors(y, x) {
|
function countAliveNeighbors(y, x) {
|
||||||
|
@ -61,23 +58,43 @@ function countAliveNeighbors(y, x) {
|
||||||
for (let yOffset = -1; yOffset <= 1; yOffset++) {
|
for (let yOffset = -1; yOffset <= 1; yOffset++) {
|
||||||
for (let xOffset = -1; xOffset <= 1; xOffset++) {
|
for (let xOffset = -1; xOffset <= 1; xOffset++) {
|
||||||
if (yOffset === 0 && xOffset === 0) continue;
|
if (yOffset === 0 && xOffset === 0) continue;
|
||||||
// Wrap around the edges
|
const newY = (y + yOffset + boardHeight) % boardHeight;
|
||||||
let newY = (y + yOffset + boardSize) % boardSize;
|
const newX = (x + xOffset + boardWidth) % boardWidth;
|
||||||
let newX = (x + xOffset + boardSize) % boardSize;
|
count += currentGrid[newY][newX];
|
||||||
count += grid[newY][newX];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGame() {
|
function startGame() {
|
||||||
console.log('starting')
|
|
||||||
if (intervalId) clearInterval(intervalId);
|
if (intervalId) clearInterval(intervalId);
|
||||||
intervalId = setInterval(computeNextGeneration, 500);
|
intervalId = setInterval(computeNextGeneration, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeWidth() {
|
||||||
|
if (intervalId) clearInterval(intervalId); // stop the game
|
||||||
|
|
||||||
|
document.querySelectorAll(".cell").forEach(el => el.remove()); // remove the cells
|
||||||
|
|
||||||
|
var r = document.querySelector(':root')
|
||||||
|
boardWidth += 50;
|
||||||
|
boardWidth %= 200;
|
||||||
|
|
||||||
|
|
||||||
|
r.style.setProperty('--board-width', boardWidth)
|
||||||
|
initializeBoard()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startButton = document.getElementById('startButton')
|
const startButton = document.getElementById('startButton')
|
||||||
|
|
||||||
startButton.onclick = startGame
|
startButton.onclick = startGame
|
||||||
|
|
||||||
|
const changeWidthButton = document.getElementById('changeWidthButton')
|
||||||
|
|
||||||
|
changeWidthButton.onclick = changeWidth
|
||||||
|
|
||||||
|
window.onresize = () => {
|
||||||
|
console.log(document.getElementById('board').clientWidth)
|
||||||
|
}
|
||||||
|
|
||||||
initializeBoard();
|
initializeBoard();
|
||||||
|
|
6
src/scripts/utils.js
Normal file
6
src/scripts/utils.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const cells = ['30', '20', '51', '01', '62', '63', '03', '64', '54', '44', '34', '24', '14']
|
||||||
|
|
||||||
|
export const heavyWeightSpaceshipCell = (x, y) => {
|
||||||
|
const coor = `${x}${y}`
|
||||||
|
return cells.includes(coor)
|
||||||
|
}
|
|
@ -1,20 +1,14 @@
|
||||||
.board {
|
.container {
|
||||||
display: grid;
|
width: 710;
|
||||||
grid-template-columns: repeat(10, 5px);
|
|
||||||
row-gap: 1px;
|
|
||||||
column-gap: 1px;
|
|
||||||
}
|
|
||||||
.cell {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
.alive {
|
|
||||||
background-color: black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#title-and-gol {
|
.main-title {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: var(--h1-desktop);
|
||||||
|
max-width: 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user