This commit is contained in:
midou committed 2026-11-06 01:26:19 +00:00
1 parent 3c4eed6b3a
commit 4132289815
2 files changed
+86 -18

No files matched your search

+44 -2
View File
@@ -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();
});
</script>
<div class="scene-background bg-gray-300 dark:bg-black" class:reading bind:this={frame}>
<div
class="scene-background bg-gray-300 dark:bg-black"
class:reading
class:ready
class:checked
style:--background-reveal-delay={`${revealDelay}ms`}
bind:this={frame}
>
<canvas bind:this={canvas} aria-hidden="true"></canvas>
</div>
@@ -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;
}
</style>
+42 -16
View File
@@ -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);
};