CGL background works!

This commit is contained in:
2026-05-07 23:49:38 +03:00
parent d183adaf0a
commit 8190aa15bf
3 changed files with 218 additions and 1 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
:publishing-function org-html-publish-to-html
:html-head-include-default-style nil
:html-head-include-scripts nil
:html-head "<link rel=\"stylesheet\" href=\"/style.css\">"
:html-head "<link rel=\"stylesheet\" href=\"/style.css\"><script src=\"/animation.js\" defer></script>"
:html-preamble ,site-nav
:html-postamble nil
:with-author nil
+205
View File
@@ -0,0 +1,205 @@
// Conway's Game of Life background animation
// 100×100 toroidal grid, 1cm cells with 0.1cm gaps, tiled to fill the viewport.
// State persists in a cookie until midnight; cells fade in/out on state change.
const COOKIE_STATE = 'cgol_state';
const ROWS = 100;
const COLS = 100;
const CELL_CM = 1.0; // cell size in cm
const PAD_CM = 0.1; // gap between cells in cm
const FADE_SPEED = 4.0; // opacity units/sec → full transition in 0.25s
const STEP_MS = 1000; // game step interval in ms
const CELL_ALPHA = 0.03; // max cell opacity (keeps it a subtle background)
let canvas, ctx;
let cmPx = 38; // pixels per cm, measured at setup
let cellColor = '#2f4f4f';
let board = null; // Uint8Array ROWS*COLS — 0=dead, 1=alive
let opacity = null; // Float32Array ROWS*COLS — current fade value [0,1]
let animFrame = null;
let lastStepTime = 0;
let lastFrameTime = 0;
// ── Board helpers ──────────────────────────────────────────────────────────
function idx(r, c) { return r * COLS + c; }
function randomBoard() {
const b = new Uint8Array(ROWS * COLS);
for (let i = 0; i < b.length; i++) b[i] = Math.random() < 0.3 ? 1 : 0;
return b;
}
function stepConway(b) {
const next = new Uint8Array(ROWS * COLS);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
let n = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
n += b[idx((r + dr + ROWS) % ROWS, (c + dc + COLS) % COLS)];
}
}
const alive = b[idx(r, c)];
next[idx(r, c)] = alive
? (n === 2 || n === 3 ? 1 : 0)
: (n === 3 ? 1 : 0);
}
}
return next;
}
// ── Serialisation (bit-packed → base64url, ~1667 chars for 100×100) ────────
function serializeBoard(b) {
const bytes = new Uint8Array(Math.ceil(ROWS * COLS / 8));
for (let i = 0; i < ROWS * COLS; i++) {
if (b[i]) bytes[i >> 3] |= 1 << (i & 7);
}
let s = '';
for (const byte of bytes) s += String.fromCharCode(byte);
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_');
}
function deserializeBoard(b64) {
const s = atob(b64.replace(/-/g, '+').replace(/_/g, '/'));
const b = new Uint8Array(ROWS * COLS);
for (let i = 0; i < ROWS * COLS; i++) {
b[i] = (s.charCodeAt(i >> 3) >> (i & 7)) & 1;
}
return b;
}
// ── Cookie (expires at midnight tonight) ──────────────────────────────────
function saveToCookie() {
if (!board) return;
const midnight = new Date();
midnight.setHours(24, 0, 0, 0);
const data = encodeURIComponent(serializeBoard(board));
document.cookie =
`${COOKIE_STATE}=${data};expires=${midnight.toUTCString()};path=/;SameSite=Lax`;
}
function loadFromCookie() {
const match = document.cookie.match(
new RegExp('(?:^|; )' + COOKIE_STATE + '=([^;]*)')
);
if (!match) return null;
try { return deserializeBoard(decodeURIComponent(match[1])); }
catch { return null; }
}
// ── Setup ──────────────────────────────────────────────────────────────────
function measureCmPx() {
const el = document.createElement('div');
el.style.cssText = 'position:absolute;width:1cm;height:0;visibility:hidden';
document.body.appendChild(el);
const px = el.offsetWidth || 38;
document.body.removeChild(el);
return px;
}
function setup() {
cmPx = measureCmPx();
cellColor = getComputedStyle(document.documentElement)
.getPropertyValue('--fg').trim() || '#2f4f4f';
canvas = document.createElement('canvas');
canvas.id = 'bg-animation';
document.body.prepend(canvas);
ctx = canvas.getContext('2d');
resize();
window.addEventListener('resize', resize);
board = loadFromCookie() || randomBoard();
opacity = new Float32Array(ROWS * COLS);
// Initialise opacity to match loaded state (no fade-in on first load)
for (let i = 0; i < ROWS * COLS; i++) opacity[i] = board[i];
lastStepTime = performance.now();
lastFrameTime = performance.now();
animFrame = requestAnimationFrame(frame);
}
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// ── Animation loop ─────────────────────────────────────────────────────────
function frame(ts) {
const dt = Math.min((ts - lastFrameTime) / 1000, 0.1); // cap delta at 100ms
lastFrameTime = ts;
if (ts - lastStepTime >= STEP_MS) {
board = stepConway(board);
lastStepTime = ts;
}
// Nudge each cell's opacity toward its target (0 or 1)
const fade = FADE_SPEED * dt;
for (let i = 0; i < ROWS * COLS; i++) {
if (board[i]) {
if (opacity[i] < 1) opacity[i] = Math.min(1, opacity[i] + fade);
} else {
if (opacity[i] > 0) opacity[i] = Math.max(0, opacity[i] - fade);
}
}
draw();
animFrame = requestAnimationFrame(frame);
}
// ── Draw ───────────────────────────────────────────────────────────────────
function draw() {
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
const cellPx = cmPx * CELL_CM;
const stepPx = cmPx * (CELL_CM + PAD_CM);
const gridW = COLS * stepPx;
const gridH = ROWS * stepPx;
ctx.fillStyle = cellColor;
// Tile grid to fill canvas; start one full grid before origin to cover edges.
for (let tileY = -gridH; tileY < h; tileY += gridH) {
for (let tileX = -gridW; tileX < w; tileX += gridW) {
for (let r = 0; r < ROWS; r++) {
const y = tileY + r * stepPx;
if (y + cellPx < 0 || y > h) continue;
for (let c = 0; c < COLS; c++) {
const op = opacity[idx(r, c)];
if (op < 0.005) continue;
const x = tileX + c * stepPx;
if (x + cellPx < 0 || x > w) continue;
ctx.globalAlpha = op * CELL_ALPHA;
ctx.fillRect(x, y, cellPx, cellPx);
}
}
}
}
ctx.globalAlpha = 1;
}
// ── Teardown ───────────────────────────────────────────────────────────────
function teardown() {
saveToCookie();
cancelAnimationFrame(animFrame);
window.removeEventListener('resize', resize);
if (canvas) canvas.remove();
canvas = ctx = board = opacity = null;
}
// ── Init ───────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', setup);
window.addEventListener('pagehide', teardown);
+12
View File
@@ -22,6 +22,18 @@
}
}
/* ── Background animation canvas ────────────────────── */
#bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
will-change: transform;
}
/* ── Reset ───────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; -webkit-text-size-adjust: 100%; }