From 8190aa15bf0f585c60593f390eb9a563dc7590e7 Mon Sep 17 00:00:00 2001 From: Aner Zakobar Date: Thu, 7 May 2026 23:49:38 +0300 Subject: [PATCH] CGL background works! --- build.el | 2 +- static/animation.js | 205 ++++++++++++++++++++++++++++++++++++++++++++ static/style.css | 12 +++ 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 static/animation.js diff --git a/build.el b/build.el index 53dd994..7c6414c 100644 --- a/build.el +++ b/build.el @@ -24,7 +24,7 @@ :publishing-function org-html-publish-to-html :html-head-include-default-style nil :html-head-include-scripts nil - :html-head "" + :html-head "" :html-preamble ,site-nav :html-postamble nil :with-author nil diff --git a/static/animation.js b/static/animation.js new file mode 100644 index 0000000..d1b877c --- /dev/null +++ b/static/animation.js @@ -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); diff --git a/static/style.css b/static/style.css index 576c636..151b2e5 100644 --- a/static/style.css +++ b/static/style.css @@ -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%; }