CGL background works!
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
:publishing-function org-html-publish-to-html
|
:publishing-function org-html-publish-to-html
|
||||||
:html-head-include-default-style nil
|
:html-head-include-default-style nil
|
||||||
:html-head-include-scripts 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-preamble ,site-nav
|
||||||
:html-postamble nil
|
:html-postamble nil
|
||||||
:with-author nil
|
:with-author nil
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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 ───────────────────────────────────────────── */
|
/* ── Reset ───────────────────────────────────────────── */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
|
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
|
||||||
|
|||||||
Reference in New Issue
Block a user