Smooth fractal background animation
This commit is contained in:
parent
8ce7b655bc
commit
74471a21d2
9 changed files with 49 additions and 21 deletions
|
|
@ -113,7 +113,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
.back-link a:hover { color: rgba(255, 255, 255, 0.7); }
|
.back-link a:hover { color: rgba(255, 255, 255, 0.7); }
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
.back-link a:hover { color: rgba(255, 255, 255, 0.7); }
|
.back-link a:hover { color: rgba(255, 255, 255, 0.7); }
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
.back-link a:hover { color: rgba(255, 255, 255, 0.7); }
|
.back-link a:hover { color: rgba(255, 255, 255, 0.7); }
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -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 DIST_URL = '/routes/root/dist.png'; // monolithic source (Canvas2D fallback)
|
||||||
var DARK_HEX = '#060a10'; // backdrop / unloaded tone
|
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) {
|
function initFractal(canvasId) {
|
||||||
console.log('[fractal-gl] initFractal:', canvasId);
|
console.log('[fractal-gl] initFractal:', canvasId);
|
||||||
|
|
||||||
|
|
@ -190,7 +205,7 @@ var FIELD_FRAG_GLSL = [
|
||||||
'void main() {',
|
'void main() {',
|
||||||
' vec2 uv = gl_FragCoord.xy / u_resolution;',
|
' vec2 uv = gl_FragCoord.xy / u_resolution;',
|
||||||
' float aspect = u_resolution.x / u_resolution.y;',
|
' 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 maxH = 1.0 / max(aspect, 1.0);',
|
||||||
' float zoomFrac = 0.65 + 0.15 * sin(t * 0.4);',
|
' float zoomFrac = 0.65 + 0.15 * sin(t * 0.4);',
|
||||||
' float halfH = maxH * zoomFrac * 0.5;',
|
' 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 v = texture2D(u_dist, vec2(sx, fv)).r * 255.0;',
|
||||||
' float ditherMix = clamp((v - 180.0) / 60.0, 0.0, 1.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;',
|
' float noise = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;',
|
||||||
' v += noise * 1.2 * ditherMix;',
|
' v += noise * 0.45 * ditherMix;',
|
||||||
' float offset = u_time * 10.8;',
|
' float offset = u_time * 4.2;',
|
||||||
' vec3 color = calmBand(v - offset);',
|
' vec3 color = calmBand(v - offset);',
|
||||||
' gl_FragColor = vec4(mix(DARK, color, m), 1.0);',
|
' gl_FragColor = vec4(mix(DARK, color, m), 1.0);',
|
||||||
'}'
|
'}'
|
||||||
|
|
@ -341,8 +356,16 @@ function initWebGL(canvas, gl) {
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
|
|
||||||
var startTime = performance.now();
|
var startTime = performance.now();
|
||||||
function frame() {
|
var reducedMotion = _prefersReducedMotion();
|
||||||
var t = (performance.now() - startTime) * 0.001;
|
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.
|
// Ease the displayed extent toward the loaded extent for a smooth bloom.
|
||||||
// On a repeat visit (cached field) snap instead of replaying the intro.
|
// On a repeat visit (cached field) snap instead of replaying the intro.
|
||||||
var ease = revealed ? 1.0 : 0.12;
|
var ease = revealed ? 1.0 : 0.12;
|
||||||
|
|
@ -353,7 +376,6 @@ function initWebGL(canvas, gl) {
|
||||||
gl.uniform1f(uV0, dispV0);
|
gl.uniform1f(uV0, dispV0);
|
||||||
gl.uniform1f(uV1, dispV1);
|
gl.uniform1f(uV1, dispV1);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||||
requestAnimationFrame(frame);
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
console.log('[fractal-gl] WebGL loop started');
|
console.log('[fractal-gl] WebGL loop started');
|
||||||
|
|
@ -409,6 +431,9 @@ function initCanvas2D(canvas) {
|
||||||
var out32 = new Uint32Array(imgOut.data.buffer);
|
var out32 = new Uint32Array(imgOut.data.buffer);
|
||||||
var lastW = canvas.width, lastH = canvas.height;
|
var lastW = canvas.width, lastH = canvas.height;
|
||||||
var startTime = performance.now();
|
var startTime = performance.now();
|
||||||
|
var reducedMotion = _prefersReducedMotion();
|
||||||
|
var targetFrameMs = 1000 / CANVAS_TARGET_FPS;
|
||||||
|
var lastFrameTime = 0;
|
||||||
var faded = false;
|
var faded = false;
|
||||||
|
|
||||||
// 4x4 Bayer matrix, recentered to ±0.6 source units. Only applied where
|
// 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]);
|
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;
|
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;
|
var W = canvas.width, H = canvas.height;
|
||||||
if (W !== lastW || H !== lastH) {
|
if (W !== lastW || H !== lastH) {
|
||||||
imgOut = ctx2d.createImageData(W, H);
|
imgOut = ctx2d.createImageData(W, H);
|
||||||
out32 = new Uint32Array(imgOut.data.buffer);
|
out32 = new Uint32Array(imgOut.data.buffer);
|
||||||
lastW = W; lastH = H;
|
lastW = W; lastH = H;
|
||||||
}
|
}
|
||||||
var elapsed = (performance.now() - startTime) * 0.001;
|
var elapsed = reducedMotion ? 0 : (now - startTime) * 0.001;
|
||||||
var t = elapsed * 0.12;
|
var t = elapsed * MOTION_SPEED;
|
||||||
var aspect = W / H;
|
var aspect = W / H;
|
||||||
var maxH = 1.0 / Math.max(aspect, 1.0);
|
var maxH = 1.0 / Math.max(aspect, 1.0);
|
||||||
var zoomFrac = 0.65 + 0.15 * Math.sin(t * 0.4);
|
var zoomFrac = 0.65 + 0.15 * Math.sin(t * 0.4);
|
||||||
|
|
@ -437,8 +466,8 @@ function initCanvas2D(canvas) {
|
||||||
var y0 = (cy - halfH) * SH;
|
var y0 = (cy - halfH) * SH;
|
||||||
var dxs = (2 * halfW) / W * SW;
|
var dxs = (2 * halfW) / W * SW;
|
||||||
var dys = (2 * halfH) / H * SH;
|
var dys = (2 * halfH) / H * SH;
|
||||||
// offset in 1024-entry space (4x granularity) — same cycle period (~24 s) as before
|
// offset in 1024-entry space (4x granularity), deliberately slow to avoid shimmer.
|
||||||
var offsetHD = (elapsed * 10.8 * 4) | 0;
|
var offsetHD = (elapsed * COLOR_CYCLE_SPEED * 4) | 0;
|
||||||
|
|
||||||
var SW1 = SW - 1, SH1 = SH - 1;
|
var SW1 = SW - 1, SH1 = SH - 1;
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
|
|
@ -470,7 +499,7 @@ function initCanvas2D(canvas) {
|
||||||
// Selective dither: only the outer smooth region (v 180→240 ramps in).
|
// Selective dither: only the outer smooth region (v 180→240 ramps in).
|
||||||
if (v > 180) {
|
if (v > 180) {
|
||||||
var mix = v >= 240 ? 1 : (v - 180) / 60;
|
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.
|
// Multiply by 4 to index into 1024-entry palette, then wrap.
|
||||||
out32[idx++] = palette32[(((v * 4) | 0) - offsetHD) & 1023];
|
out32[idx++] = palette32[(((v * 4) | 0) - offsetHD) & 1023];
|
||||||
|
|
@ -481,7 +510,6 @@ function initCanvas2D(canvas) {
|
||||||
}
|
}
|
||||||
ctx2d.putImageData(imgOut, 0, 0);
|
ctx2d.putImageData(imgOut, 0, 0);
|
||||||
if (!faded) { faded = true; canvas.style.opacity = '1'; }
|
if (!faded) { faded = true; canvas.style.opacity = '1'; }
|
||||||
requestAnimationFrame(frame);
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
console.log('[fractal-gl] Canvas2D loop started; internal',
|
console.log('[fractal-gl] Canvas2D loop started; internal',
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
<link rel="preload" href="/routes/root/dist-strips.bin?v=1" as="fetch" crossorigin />
|
||||||
<script defer src="/routes/root/fractal-gl.js?v=12"></script>
|
<script defer src="/routes/root/fractal-gl.js?v=13"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="fractal-canvas"></canvas>
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue