(function() { const canvas = document.getElementById("ascii-bg"); if (!canvas || !canvas.getContext) { return; } const context = canvas.getContext("2d", { alpha: true }); const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); const CELL_WIDTH = 13; const CELL_HEIGHT = 17; const ASPECT = CELL_WIDTH / CELL_HEIGHT; const FRAME_MS = 1000 / 24; // Ambient dither behind everything. const DUST = "ยท.:-=+*"; const DUST_BASE = 0.05; const DUST_WAVE = 0.16; // Lit torus, shaded the way donut.c does it: a luminance ramp from a dot to a // solid block, with a z-buffer so the near face hides the far one. const RAMP = ".,-~:;=!*#$@"; const R1 = 1; const R2 = 2; const K2 = 5; const THETA_STEPS = 120; const PHI_STEPS = 360; let columns = 0; let rows = 0; let dither = new Float32Array(0); let depth = new Float32Array(0); let shade = new Int8Array(0); const thetaCos = new Float32Array(THETA_STEPS); const thetaSin = new Float32Array(THETA_STEPS); const phiCos = new Float32Array(PHI_STEPS); const phiSin = new Float32Array(PHI_STEPS); for (let i = 0; i < THETA_STEPS; i += 1) { const theta = (i / THETA_STEPS) * Math.PI * 2; thetaCos[i] = Math.cos(theta); thetaSin[i] = Math.sin(theta); } for (let i = 0; i < PHI_STEPS; i += 1) { const phi = (i / PHI_STEPS) * Math.PI * 2; phiCos[i] = Math.cos(phi); phiSin[i] = Math.sin(phi); } let baseColor = "#8e97a8"; let accentColor = "#eb5234"; let dimColor = "#5d6472"; let time = 0; let angleA = 0.6; let angleB = 0.9; let lastFrame = 0; let rafId = 0; const readThemeColors = () => { const styles = getComputedStyle(document.body); baseColor = styles.getPropertyValue("--ascii-base").trim() || baseColor; accentColor = styles.getPropertyValue("--ascii-accent").trim() || accentColor; dimColor = styles.getPropertyValue("--ascii-dim").trim() || dimColor; }; const resize = () => { const ratio = Math.min(window.devicePixelRatio || 1, 2); const width = window.innerWidth; const height = window.innerHeight; canvas.width = Math.floor(width * ratio); canvas.height = Math.floor(height * ratio); canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; context.setTransform(ratio, 0, 0, ratio, 0, 0); context.font = `12px "JetBrains Mono", ui-monospace, monospace`; context.textBaseline = "top"; columns = Math.ceil(width / CELL_WIDTH); rows = Math.ceil(height / CELL_HEIGHT); const cells = columns * rows; dither = new Float32Array(cells); depth = new Float32Array(cells); shade = new Int8Array(cells); for (let row = 0; row < rows; row += 1) { for (let column = 0; column < columns; column += 1) { const noise = Math.sin(column * 127.1 + row * 311.7) * 43758.5453; dither[row * columns + column] = noise - Math.floor(noise); } } }; // Slow interfering swells that move brightness through the dust layer. const swellAt = (x, y, t) => { const radius = Math.sqrt(((x - columns / 2) * 0.55) ** 2 + (y - rows / 2) ** 2); return ( (Math.sin(x * 0.068 + t * 0.55) + Math.sin(y * 0.1 - t * 0.42) + Math.sin((x * 0.4 + y * 0.6) * 0.07 + t * 0.33) + Math.sin(radius * 0.1 - t * 0.6)) / 8 + 0.5 ); }; const projectTorus = () => { depth.fill(0); shade.fill(-1); const sinA = Math.sin(angleA); const cosA = Math.cos(angleA); const sinB = Math.sin(angleB); const cosB = Math.cos(angleB); const centerX = columns / 2; const centerY = rows / 2; // Size the torus against the shorter axis so it always fits on screen. const span = Math.min(rows, columns * ASPECT) * 0.6; const K1 = (span * K2) / (R1 + R2); for (let i = 0; i < THETA_STEPS; i += 1) { const ct = thetaCos[i]; const st = thetaSin[i]; const circleX = R2 + R1 * ct; const circleY = R1 * st; for (let j = 0; j < PHI_STEPS; j += 1) { const cp = phiCos[j]; const sp = phiSin[j]; const x = circleX * (cosB * cp + sinA * sinB * sp) - circleY * cosA * sinB; const y = circleX * (sinB * cp - sinA * cosB * sp) + circleY * cosA * cosB; const z = K2 + cosA * circleX * sp + circleY * sinA; const ooz = 1 / z; const screenX = Math.round(centerX + K1 * ooz * x); const screenY = Math.round(centerY - K1 * ooz * y * ASPECT); if (screenX < 0 || screenX >= columns || screenY < 0 || screenY >= rows) { continue; } const luminance = cp * ct * sinB - cosA * ct * sp - sinA * st + cosB * (cosA * st - ct * sinA * sp); if (luminance <= 0) { continue; } const index = screenY * columns + screenX; if (ooz > depth[index]) { depth[index] = ooz; shade[index] = Math.min(RAMP.length - 1, Math.floor(luminance * 8)); } } } }; const draw = () => { context.save(); context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, canvas.width, canvas.height); context.restore(); projectTorus(); let currentColor = null; const setColor = (color) => { if (color !== currentColor) { context.fillStyle = color; currentColor = color; } }; for (let row = 0; row < rows; row += 1) { for (let column = 0; column < columns; column += 1) { const index = row * columns + column; const lit = shade[index]; const x = column * CELL_WIDTH; const y = row * CELL_HEIGHT; if (lit >= 0) { const level = lit / (RAMP.length - 1); setColor(level > 0.72 ? accentColor : baseColor); context.globalAlpha = 0.22 + level * 0.62; context.fillText(RAMP[lit], x, y); continue; } const swell = swellAt(column, row, time); if (dither[index] > DUST_BASE + swell * DUST_WAVE) { continue; } setColor(dimColor); context.globalAlpha = 0.1 + swell * 0.4; context.fillText(DUST[Math.min(DUST.length - 1, Math.floor(swell * DUST.length))], x, y); } } context.globalAlpha = 1; }; const loop = (timestamp) => { rafId = window.requestAnimationFrame(loop); if (timestamp - lastFrame < FRAME_MS) { return; } lastFrame = timestamp; time += 0.016; angleA += 0.0115; angleB += 0.0062; draw(); }; const stop = () => { if (rafId) { window.cancelAnimationFrame(rafId); rafId = 0; } }; const start = () => { if (rafId || prefersReducedMotion.matches) { return; } lastFrame = 0; rafId = window.requestAnimationFrame(loop); }; const reset = () => { readThemeColors(); resize(); draw(); }; reset(); start(); let resizeTimer = 0; window.addEventListener("resize", () => { window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(reset, 150); }); document.addEventListener("visibilitychange", () => { if (document.hidden) { stop(); return; } start(); }); prefersReducedMotion.addEventListener("change", () => { stop(); start(); draw(); }); })();