Last active
June 11, 2022 17:17
-
-
Save vladris/bda4ca4d5e815ecacec3b35ed1fc39a7 to your computer and use it in GitHub Desktop.
Game of Life visualization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Game of Life cell matrix wrapped in an object | |
type Matrix = { | |
m: boolean[][] | |
wrap: boolean; | |
}; | |
// Makes matrix of desired width x height plus an initial configuration of "on" cells and wrap-around flag | |
function makeMatrix(width: number, height: number, initialConfig: [number, number][], wrap: boolean) { | |
let m = new Array<boolean[]>(height); | |
// If not wrap-around, add some padding | |
if (!wrap) { | |
height += 4; | |
width += 4; | |
} | |
for (let i = 0; i < height; i++) { | |
m[i] = new Array<boolean>(width); | |
for (let j = 0; j < width; j++) { | |
m[i][j] = false; | |
} | |
} | |
for (let coord in initialConfig) { | |
m[initialConfig[coord][0]][initialConfig[coord][1]] = true; | |
} | |
return { m: m, wrap: wrap }; | |
} | |
// Counts "on" neighbors without wrap-around | |
function neighborsNoWrap(matrix: Matrix, i: number, j: number) { | |
let m = matrix.m; | |
let result = 0; | |
if (i > 0) { | |
if (j > 0) { | |
result += +m[i - 1][j - 1]; | |
} | |
result += +m[i - 1][j] | |
if (j < m[0].length - 1) { | |
result += +m[i - 1][j + 1]; | |
} | |
} | |
if (j > 0) { | |
result += +m[i][j - 1]; | |
} | |
if (j < m[0].length - 1) { | |
result += +m[i][j + 1]; | |
} | |
if (i < m.length - 1) { | |
if (j > 0) { | |
result += +m[i + 1][j - 1]; | |
} | |
result += +m[i + 1][j] | |
if (j < m[0].length - 1) { | |
result += +m[i + 1][j + 1]; | |
} | |
} | |
return result; | |
} | |
// Counts "on" neighbors with wrap-around | |
function neighborsWrap(matrix: Matrix, i: number, j: number) { | |
const m = matrix.m; | |
// Wraparound | |
const fst_i = i != 0 ? i - 1 : m.length - 1; | |
const fst_j = j != 0 ? j - 1 : m[0].length - 1; | |
const lst_i = i + 1 != m.length ? i + 1 : 0; | |
const lst_j = j + 1 != m[0].length ? j + 1 : 0; | |
return +m[fst_i][fst_j] + +m[fst_i][j] + +m[fst_i][lst_j] + | |
+m[i][fst_j] + +m[i][lst_j] + | |
+m[lst_i][fst_j] + +m[lst_i][j] + +m[lst_i][lst_j]; | |
} | |
// Computes next state and update matrix | |
function step(matrix: Matrix) { | |
let m2 = new Array<Array<boolean>>(matrix.m.length); | |
const neighbors = matrix.wrap ? neighborsWrap : neighborsNoWrap; | |
for (let i = 0; i < matrix.m.length; i++) { | |
m2[i] = new Array<boolean>(matrix.m[0].length); | |
for (let j = 0; j < matrix.m[0].length; j++) { | |
const n = neighbors(matrix, i, j); | |
if (n == 3) { | |
m2[i][j] = true; | |
} | |
else if (n == 2 && matrix.m[i][j]) { | |
m2[i][j] = true; | |
} | |
else { | |
m2[i][j] = false; | |
} | |
} | |
} | |
matrix.m = m2; | |
} | |
// Parameters for drawing: a 2D context, width, height, and cell size in pixels | |
type DrawParams = { | |
ctx: CanvasRenderingContext2D, | |
width: number, | |
height: number, | |
cellSize: number, | |
} | |
// Draws the cell grid | |
function drawGrid(params: DrawParams) { | |
params.ctx.clearRect(0, 0, params.width * params.cellSize, params.height * params.cellSize); | |
params.ctx.strokeStyle = "lightGray" | |
params.ctx.fillStyle = "black"; | |
params.ctx.rect(0, 0, params.width * params.cellSize, params.height * params.cellSize); | |
params.ctx.stroke(); | |
for (let i = 0; i < params.height; i++) { | |
params.ctx.moveTo(0, i * params.cellSize); | |
params.ctx.lineTo(params.width * params.cellSize, i * params.cellSize); | |
params.ctx.stroke(); | |
} | |
for (let j = 0; j < params.width; j++) { | |
params.ctx.moveTo(j * params.cellSize, 0); | |
params.ctx.lineTo(j * params.cellSize, params.height * params.cellSize); | |
params.ctx.stroke(); | |
} | |
} | |
// Draws matrix state | |
function drawState(params: DrawParams, matrix: Matrix) { | |
for (let i = 0; i < params.height; i++) { | |
for (let j = 0; j < params.width; j++) { | |
if (matrix.m[i][j]) { | |
params.ctx.fillRect(j * params.cellSize, i * params.cellSize, params.cellSize, params.cellSize); | |
} | |
} | |
} | |
} | |
// Draws grid and state | |
function draw(params: DrawParams, matrix: Matrix) { | |
drawGrid(params); | |
drawState(params, matrix); | |
} | |
// Keeps track of current running interval & play/stop button | |
let running: { intervalId: number, button: HTMLButtonElement } = { intervalId: 0, button: undefined }; | |
// Starts animation | |
function start(params: DrawParams, matrix: Matrix) { | |
return setInterval(() => { | |
draw(params, matrix); | |
step(matrix); | |
}, 200); | |
} | |
// Stops currently running animation | |
function stopRunning() { | |
if (running.intervalId === 0) { | |
return; | |
} | |
// Stop interval and update button to say "Play" | |
clearInterval(running.intervalId); | |
running.button.textContent = "Play"; | |
// Nothing running anymore | |
running = { intervalId: 0, button: undefined }; | |
} | |
// Animates cellular automata in target div | |
function animate(width: number, height: number, initialConfig: [number, number][], targetDiv: string, wrap: boolean = true) { | |
let m = makeMatrix(width, height, initialConfig, wrap); | |
const cellSize = 10; | |
const div = document.getElementById(targetDiv) as HTMLDivElement; | |
// Create play button | |
const button = document.createElement("button"); | |
button.style.display = "block"; | |
button.textContent = "Play"; | |
button.onclick = () => { | |
// If stopped (button reads "Play") stop currently running animation and start this one | |
if (button.textContent === "Play") { | |
stopRunning(); | |
running = { intervalId: start({ ctx, width, height, cellSize }, m), button: button }; | |
button.textContent = "Stop"; | |
} | |
// Else this is the running animation, so just stop it | |
else { | |
stopRunning(); | |
} | |
}; | |
// Create canvas to render in | |
const canvas = document.createElement("canvas"); | |
const ctx = canvas.getContext("2d"); | |
canvas.width = width * cellSize; | |
canvas.height = height * cellSize; | |
// Append new elements to div | |
div.appendChild(button); | |
div.appendChild(canvas); | |
// Draw initial state | |
draw({ctx, width, height, cellSize}, m); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment