From 74471a21d28e9b050931777eb2d0726950f5dc1c Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Fri, 24 Jul 2026 13:06:42 +0400 Subject: [PATCH] Smooth fractal background animation --- routes/about/index.html | 2 +- routes/contact/index.html | 2 +- routes/contact/message/index.html | 2 +- .../airfrans-neural-cfd-surrogate/index.html | 2 +- routes/projects/cstat-yoke/index.html | 2 +- routes/projects/index.html | 2 +- routes/projects/swactor/index.html | 2 +- routes/root/fractal-gl.js | 54 ++++++++++++++----- routes/root/index.html | 2 +- 9 files changed, 49 insertions(+), 21 deletions(-) diff --git a/routes/about/index.html b/routes/about/index.html index 374268a..a163872 100644 --- a/routes/about/index.html +++ b/routes/about/index.html @@ -113,7 +113,7 @@ } - + diff --git a/routes/contact/index.html b/routes/contact/index.html index 077a289..25e217a 100644 --- a/routes/contact/index.html +++ b/routes/contact/index.html @@ -118,7 +118,7 @@ } - + diff --git a/routes/contact/message/index.html b/routes/contact/message/index.html index a53c832..5bd4241 100644 --- a/routes/contact/message/index.html +++ b/routes/contact/message/index.html @@ -280,7 +280,7 @@ } - + diff --git a/routes/projects/airfrans-neural-cfd-surrogate/index.html b/routes/projects/airfrans-neural-cfd-surrogate/index.html index bcc02d0..2b3d6b2 100644 --- a/routes/projects/airfrans-neural-cfd-surrogate/index.html +++ b/routes/projects/airfrans-neural-cfd-surrogate/index.html @@ -25,7 +25,7 @@ .back-link a:hover { color: rgba(255, 255, 255, 0.7); } - + diff --git a/routes/projects/cstat-yoke/index.html b/routes/projects/cstat-yoke/index.html index 3acb77f..16cc457 100644 --- a/routes/projects/cstat-yoke/index.html +++ b/routes/projects/cstat-yoke/index.html @@ -19,7 +19,7 @@ .back-link a:hover { color: rgba(255, 255, 255, 0.7); } - + diff --git a/routes/projects/index.html b/routes/projects/index.html index 60494a1..6b6a328 100644 --- a/routes/projects/index.html +++ b/routes/projects/index.html @@ -120,7 +120,7 @@ } - + diff --git a/routes/projects/swactor/index.html b/routes/projects/swactor/index.html index dd0233e..01c208f 100644 --- a/routes/projects/swactor/index.html +++ b/routes/projects/swactor/index.html @@ -51,7 +51,7 @@ .back-link a:hover { color: rgba(255, 255, 255, 0.7); } - + diff --git a/routes/root/fractal-gl.js b/routes/root/fractal-gl.js index 7024c26..27133ae 100644 --- a/routes/root/fractal-gl.js +++ b/routes/root/fractal-gl.js @@ -16,6 +16,21 @@ var FIELD_URL = '/routes/root/dist-strips.bin?v=1'; // streamed WebP bands (bump var DIST_URL = '/routes/root/dist.png'; // monolithic source (Canvas2D fallback) var DARK_HEX = '#060a10'; // backdrop / unloaded tone +// The old palette cycle advanced quickly enough to look shimmery on some displays. +// Keep the camera drift slow, cap redraws below display refresh, and pause real work +// while the tab is hidden. The field still streams at full speed; only presentation +// frames are rate-limited. +var MOTION_SPEED = 0.045; +var COLOR_CYCLE_SPEED = 4.2; +var WEBGL_TARGET_FPS = 30; +var CANVAS_TARGET_FPS = 24; +var DITHER_STRENGTH = 0.45; + +function _prefersReducedMotion() { + return window.matchMedia && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; +} + function initFractal(canvasId) { console.log('[fractal-gl] initFractal:', canvasId); @@ -190,7 +205,7 @@ var FIELD_FRAG_GLSL = [ 'void main() {', ' vec2 uv = gl_FragCoord.xy / u_resolution;', ' float aspect = u_resolution.x / u_resolution.y;', - ' float t = u_time * 0.12;', + ' float t = u_time * 0.045;', ' float maxH = 1.0 / max(aspect, 1.0);', ' float zoomFrac = 0.65 + 0.15 * sin(t * 0.4);', ' float halfH = maxH * zoomFrac * 0.5;', @@ -210,8 +225,8 @@ var FIELD_FRAG_GLSL = [ ' float v = texture2D(u_dist, vec2(sx, fv)).r * 255.0;', ' float ditherMix = clamp((v - 180.0) / 60.0, 0.0, 1.0);', ' float noise = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;', - ' v += noise * 1.2 * ditherMix;', - ' float offset = u_time * 10.8;', + ' v += noise * 0.45 * ditherMix;', + ' float offset = u_time * 4.2;', ' vec3 color = calmBand(v - offset);', ' gl_FragColor = vec4(mix(DARK, color, m), 1.0);', '}' @@ -341,8 +356,16 @@ function initWebGL(canvas, gl) { window.addEventListener('resize', resize); var startTime = performance.now(); - function frame() { - var t = (performance.now() - startTime) * 0.001; + var reducedMotion = _prefersReducedMotion(); + var targetFrameMs = 1000 / WEBGL_TARGET_FPS; + var lastFrameTime = 0; + function frame(now) { + requestAnimationFrame(frame); + if (document.hidden) return; + if (now - lastFrameTime < targetFrameMs) return; + lastFrameTime = now; + + var t = reducedMotion ? 0 : (now - startTime) * 0.001; // Ease the displayed extent toward the loaded extent for a smooth bloom. // On a repeat visit (cached field) snap instead of replaying the intro. var ease = revealed ? 1.0 : 0.12; @@ -353,7 +376,6 @@ function initWebGL(canvas, gl) { gl.uniform1f(uV0, dispV0); gl.uniform1f(uV1, dispV1); gl.drawArrays(gl.TRIANGLES, 0, 3); - requestAnimationFrame(frame); } requestAnimationFrame(frame); console.log('[fractal-gl] WebGL loop started'); @@ -409,6 +431,9 @@ function initCanvas2D(canvas) { var out32 = new Uint32Array(imgOut.data.buffer); var lastW = canvas.width, lastH = canvas.height; var startTime = performance.now(); + var reducedMotion = _prefersReducedMotion(); + var targetFrameMs = 1000 / CANVAS_TARGET_FPS; + var lastFrameTime = 0; var faded = false; // 4x4 Bayer matrix, recentered to ±0.6 source units. Only applied where @@ -416,15 +441,19 @@ function initCanvas2D(canvas) { var BAYER = new Float32Array([0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5]); for (var bi = 0; bi < 16; bi++) BAYER[bi] = (BAYER[bi] - 7.5) * 0.6 / 7.5; - function frame() { + function frame(now) { + requestAnimationFrame(frame); + if (document.hidden) return; + if (now - lastFrameTime < targetFrameMs) return; + lastFrameTime = now; var W = canvas.width, H = canvas.height; if (W !== lastW || H !== lastH) { imgOut = ctx2d.createImageData(W, H); out32 = new Uint32Array(imgOut.data.buffer); lastW = W; lastH = H; } - var elapsed = (performance.now() - startTime) * 0.001; - var t = elapsed * 0.12; + var elapsed = reducedMotion ? 0 : (now - startTime) * 0.001; + var t = elapsed * MOTION_SPEED; var aspect = W / H; var maxH = 1.0 / Math.max(aspect, 1.0); var zoomFrac = 0.65 + 0.15 * Math.sin(t * 0.4); @@ -437,8 +466,8 @@ function initCanvas2D(canvas) { var y0 = (cy - halfH) * SH; var dxs = (2 * halfW) / W * SW; var dys = (2 * halfH) / H * SH; - // offset in 1024-entry space (4x granularity) — same cycle period (~24 s) as before - var offsetHD = (elapsed * 10.8 * 4) | 0; + // offset in 1024-entry space (4x granularity), deliberately slow to avoid shimmer. + var offsetHD = (elapsed * COLOR_CYCLE_SPEED * 4) | 0; var SW1 = SW - 1, SH1 = SH - 1; var idx = 0; @@ -470,7 +499,7 @@ function initCanvas2D(canvas) { // Selective dither: only the outer smooth region (v 180→240 ramps in). if (v > 180) { var mix = v >= 240 ? 1 : (v - 180) / 60; - v += BAYER[(y & 3) * 4 + (x & 3)] * 1.6 * mix; + v += BAYER[(y & 3) * 4 + (x & 3)] * DITHER_STRENGTH * mix; } // Multiply by 4 to index into 1024-entry palette, then wrap. out32[idx++] = palette32[(((v * 4) | 0) - offsetHD) & 1023]; @@ -481,7 +510,6 @@ function initCanvas2D(canvas) { } ctx2d.putImageData(imgOut, 0, 0); if (!faded) { faded = true; canvas.style.opacity = '1'; } - requestAnimationFrame(frame); } requestAnimationFrame(frame); console.log('[fractal-gl] Canvas2D loop started; internal', diff --git a/routes/root/index.html b/routes/root/index.html index 9bb90bd..90c0d3b 100755 --- a/routes/root/index.html +++ b/routes/root/index.html @@ -101,7 +101,7 @@ } - +