diff --git a/src/lib/background/SceneBackground.svelte b/src/lib/background/SceneBackground.svelte index 20af222..3faac2c 100644 --- a/src/lib/background/SceneBackground.svelte +++ b/src/lib/background/SceneBackground.svelte @@ -5,14 +5,29 @@ let { reading = false }: { reading?: boolean } = $props(); let frame: HTMLDivElement; let canvas: HTMLCanvasElement; + let ready = $state(false); + let checked = $state(false); + let revealDelay = $state(4000); onMount(() => { - const controller = new BackgroundController(canvas, frame); + const revealAt = performance.now() + 4000; + const controller = new BackgroundController(canvas, frame, () => { + revealDelay = Math.max(0, revealAt - performance.now()); + checked = true; + }); + ready = true; return () => controller.destroy(); }); -
+
@@ -49,6 +64,21 @@ } @media (prefers-reduced-motion: no-preference) { + .ready.checked canvas { + animation: background-reveal 900ms ease var(--background-reveal-delay) both; + } + + @keyframes background-reveal { + from { + opacity: 0; + visibility: hidden; + } + to { + opacity: 1; + visibility: visible; + } + } + .scene-background { transition: width 650ms cubic-bezier(0.22, 1, 0.36, 1), @@ -95,6 +125,11 @@ cursor: default; touch-action: auto; } + + .ready canvas { + opacity: 1; + visibility: visible; + } } canvas { @@ -103,5 +138,12 @@ height: 100%; touch-action: none; cursor: default; + opacity: 0; + visibility: hidden; + } + + .ready.checked canvas { + opacity: 1; + visibility: visible; } diff --git a/src/lib/background/controller.ts b/src/lib/background/controller.ts index 712ce27..6011b49 100644 --- a/src/lib/background/controller.ts +++ b/src/lib/background/controller.ts @@ -7,8 +7,10 @@ const clamp = (value: number, min: number, max: number) => Math.max(min, Math.mi const PROTECTED_CONTENT = 'a, button, h1, h2, h3, h4, h5, h6, p, pre, blockquote, table, img'; -const FRAME_CPU_BUDGET_MS = 1000 / 60; -const PERFORMANCE_SAMPLE_FRAMES = 8; +const FRAME_CPU_BUDGET_MS = 50; +const PERFORMANCE_WARMUP_MS = 1000; +const PERFORMANCE_WINDOW_MS = 2000; +const PERFORMANCE_MIN_FRAMES = 8; export class BackgroundController { private readonly fluid = new FluidField(); @@ -18,6 +20,8 @@ export class BackgroundController { private performanceFallback = false; private sampledFrames = 0; private sampledCpuTime = 0; + private sampleWindowStart = 0; + private initialCheckComplete = false; private get still(): boolean { return this.softwareFallback || this.performanceFallback || this.reducedMotion.matches; @@ -47,7 +51,8 @@ export class BackgroundController { constructor( private readonly canvas: HTMLCanvasElement, - private readonly frame: HTMLElement + private readonly frame: HTMLElement, + private readonly onPerformanceReady: () => void ) { const result = createRenderer(canvas); this.renderer = result.renderer; @@ -97,6 +102,7 @@ export class BackgroundController { const height = Math.max(1, Math.round(bounds.height)); const dpr = Math.min(window.devicePixelRatio || 1, 2); if (width === this.cssWidth && height === this.cssHeight && dpr === this.dpr) return; + this.resetPerformanceSampling(); this.cssWidth = width; this.cssHeight = height; this.dpr = dpr; @@ -202,12 +208,23 @@ export class BackgroundController { this.draw(); }; + private resetPerformanceSampling(): void { + this.sampledFrames = 0; + this.sampledCpuTime = 0; + this.sampleWindowStart = performance.now() + PERFORMANCE_WARMUP_MS; + } + + private finishInitialCheck(): void { + if (this.initialCheckComplete) return; + this.initialCheckComplete = true; + this.onPerformanceReady(); + } + private syncPlayback = () => { cancelAnimationFrame(this.animation); this.animation = 0; this.lastTime = 0; - this.sampledFrames = 0; - this.sampledCpuTime = 0; + this.resetPerformanceSampling(); if (this.disposed) return; if (this.still) { this.endDrag(); @@ -218,6 +235,7 @@ export class BackgroundController { this.scene.renderPreset(); this.draw(); } + this.finishInitialCheck(); return; } this.staticComposition = false; @@ -242,18 +260,26 @@ export class BackgroundController { this.scene.update(elapsed, this.time); this.fluid.step(elapsed, this.time, this.scene); this.draw(); - this.sampledCpuTime += performance.now() - started; - this.sampledFrames++; - if (this.sampledFrames === PERFORMANCE_SAMPLE_FRAMES) { - if (this.sampledCpuTime > FRAME_CPU_BUDGET_MS * PERFORMANCE_SAMPLE_FRAMES) { - this.performanceFallback = true; - this.canvas.style.pointerEvents = 'none'; - this.canvas.style.touchAction = 'auto'; - this.syncPlayback(); - return; + const cpuTime = performance.now() - started; + if (started >= this.sampleWindowStart) { + this.sampledCpuTime += cpuTime; + this.sampledFrames++; + if ( + started - this.sampleWindowStart >= PERFORMANCE_WINDOW_MS && + this.sampledFrames >= PERFORMANCE_MIN_FRAMES + ) { + if (this.sampledCpuTime / this.sampledFrames > FRAME_CPU_BUDGET_MS) { + this.performanceFallback = true; + this.canvas.style.pointerEvents = 'none'; + this.canvas.style.touchAction = 'auto'; + this.syncPlayback(); + return; + } + this.sampledFrames = 0; + this.sampledCpuTime = 0; + this.sampleWindowStart = started; + this.finishInitialCheck(); } - this.sampledFrames = 0; - this.sampledCpuTime = 0; } this.animation = requestAnimationFrame(this.tick); };