7
Conway's Game of Life
Drag to paint live cells
idle
133 lines · vanilla
view source
const GW = 160, GH = 120, AGEMAX = 50, BTN = 44, PAD = 10, RATE = 14;
let grid, next, imgData, img, buf, bctx, gen, running, acc;
let lutR, lutG, lutB;
let W = 0, H = 0;
const GUN = [
[0, 4], [0, 5], [1, 4], [1, 5],
[10, 4], [10, 5], [10, 6], [11, 3], [11, 7], [12, 2], [12, 8], [13, 2], [13, 8],
[14, 5], [15, 3], [15, 7], [16, 4], [16, 5], [16, 6], [17, 5],
[20, 2], [20, 3], [20, 4], [21, 2], [21, 3], [21, 4], [22, 1], [22, 5],
[24, 0], [24, 1], [24, 5], [24, 6],
[34, 2], [34, 3], [35, 2], [35, 3],
];
function seedGun() {
grid.fill(0);
for (const [x, y] of GUN) grid[(y + 6) * GW + (x + 4)] = 1;
for (let y = 6; y < GH - 6; y++)
for (let x = (GW * 0.55) | 0; x < GW - 6; x++)
if (Math.random() < 0.12) grid[y * GW + x] = 1 + ((Math.random() * 20) | 0);
gen = 0;
}
function init({ ctx, width, height }) {
W = width; H = height;
grid = new Uint8Array(GW * GH);
next = new Uint8Array(GW * GH);
imgData = ctx.createImageData(GW, GH);
img = imgData.data;
for (let i = 0; i < img.length; i += 4) img[i + 3] = 255;
buf = new OffscreenCanvas(GW, GH);
bctx = buf.getContext("2d");
lutR = new Uint8Array(AGEMAX + 1); lutG = new Uint8Array(AGEMAX + 1); lutB = new Uint8Array(AGEMAX + 1);
for (let a = 1; a <= AGEMAX; a++) {
const t = (a - 1) / (AGEMAX - 1);
lutR[a] = 120 + (15 - 120) * t;
lutG[a] = 255 + (60 - 255) * t;
lutB[a] = 255 + (170 - 255) * t;
}
running = true; acc = 0;
seedGun();
step(); step(); // pre-roll so frame 1 has age variation
}
function step() {
for (let y = 0; y < GH; y++) {
const ym = ((y - 1 + GH) % GH) * GW, y0 = y * GW, yp = ((y + 1) % GH) * GW;
for (let x = 0; x < GW; x++) {
const xm = (x - 1 + GW) % GW, xp = (x + 1) % GW;
const n =
(grid[ym + xm] ? 1 : 0) + (grid[ym + x] ? 1 : 0) + (grid[ym + xp] ? 1 : 0) +
(grid[y0 + xm] ? 1 : 0) + (grid[y0 + xp] ? 1 : 0) +
(grid[yp + xm] ? 1 : 0) + (grid[yp + x] ? 1 : 0) + (grid[yp + xp] ? 1 : 0);
const a = grid[y0 + x];
next[y0 + x] = a ? (n === 2 || n === 3 ? (a < AGEMAX ? a + 1 : a) : 0) : (n === 3 ? 1 : 0);
}
}
const t = grid; grid = next; next = t;
gen++;
}
function inRect(x, y, rx, ry, rw, rh) { return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh; }
function btnRects() {
const y = H - PAD - BTN;
const b3 = W - PAD - BTN, b2 = b3 - BTN - 8, b1 = b2 - BTN - 8;
return { b1, b2, b3, y };
}
function drawBtnBox(ctx, x, y, hot) {
ctx.fillStyle = hot ? "rgba(120,200,255,0.85)" : "rgba(0,0,0,0.65)";
ctx.fillRect(x, y, BTN, BTN);
ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, y + 0.5, BTN - 1, BTN - 1);
}
function tick({ ctx, dt, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
const { b1, b2, b3, y: by } = btnRects();
for (const c of input.consumeClicks()) {
if (inRect(c.x, c.y, b1, by, BTN, BTN)) running = !running;
else if (inRect(c.x, c.y, b2, by, BTN, BTN)) {
grid.fill(0);
for (let i = 0; i < grid.length; i++) if (Math.random() < 0.18) grid[i] = 1;
gen = 0;
} else if (inRect(c.x, c.y, b3, by, BTN, BTN)) seedGun();
}
// paint with press + drag (skip the button zone)
if (input.mouseDown && !inRect(input.mouseX, input.mouseY, b1 - 8, by - 8, W - b1 + 8, BTN + 16)) {
const cx = (input.mouseX / W * GW) | 0, cy = (input.mouseY / H * GH) | 0;
if (cx >= 0 && cx < GW && cy >= 0 && cy < GH) {
grid[cy * GW + cx] = 1;
if (cx > 0) grid[cy * GW + cx - 1] = 1;
if (cx < GW - 1) grid[cy * GW + cx + 1] = 1;
if (cy > 0) grid[(cy - 1) * GW + cx] = 1;
if (cy < GH - 1) grid[(cy + 1) * GW + cx] = 1;
}
}
if (running) {
acc += dt * RATE;
let n = 0;
while (acc >= 1 && n < 4) { step(); acc -= 1; n++; }
if (acc > 4) acc = 0;
}
let pop = 0;
for (let i = 0; i < grid.length; i++) {
const a = grid[i], o = i << 2;
if (a) {
pop++;
img[o] = lutR[a]; img[o + 1] = lutG[a]; img[o + 2] = lutB[a];
} else {
img[o] = 8; img[o + 1] = 10; img[o + 2] = 18;
}
}
bctx.putImageData(imgData, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(buf, 0, 0, W, H);
// HUD
ctx.fillStyle = "rgba(0,0,0,0.55)"; ctx.fillRect(PAD, PAD, 196, 46);
ctx.fillStyle = "#fff"; ctx.font = "13px monospace";
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
ctx.fillText("gen " + gen + (running ? "" : " paused"), PAD + 10, PAD + 19);
ctx.fillText("pop " + pop, PAD + 10, PAD + 37);
const hot1 = input.mouseDown && inRect(input.mouseX, input.mouseY, b1, by, BTN, BTN);
const hot2 = input.mouseDown && inRect(input.mouseX, input.mouseY, b2, by, BTN, BTN);
const hot3 = input.mouseDown && inRect(input.mouseX, input.mouseY, b3, by, BTN, BTN);
drawBtnBox(ctx, b1, by, hot1);
drawBtnBox(ctx, b2, by, hot2);
drawBtnBox(ctx, b3, by, hot3);
ctx.fillStyle = "#fff";
if (running) { // pause bars
ctx.fillRect(b1 + 14, by + 12, 6, 20); ctx.fillRect(b1 + 25, by + 12, 6, 20);
} else { // play triangle
ctx.beginPath(); ctx.moveTo(b1 + 15, by + 11); ctx.lineTo(b1 + 33, by + 22); ctx.lineTo(b1 + 15, by + 33);
ctx.closePath(); ctx.fill();
}
ctx.font = "bold 11px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText("RND", b2 + BTN / 2, by + BTN / 2);
ctx.fillText("GUN", b3 + BTN / 2, by + BTN / 2);
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.font = "11px monospace";
ctx.fillText("drag to paint cells", PAD, H - 16);
}
Comments (0)
Log in to comment.