/** Small scheduler shared by the two canvas treatments. Visibility and business * state stay outside it; stopping an animation never stops a task. */ export function createParticleLoop({ draw, canRender, canAnimate, requestFrame = requestAnimationFrame, cancelFrame = cancelAnimationFrame, }: { draw: (elapsed: number, still: boolean) => void; canRender: () => boolean; canAnimate: () => boolean; requestFrame?: (callback: FrameRequestCallback) => number; cancelFrame?: (handle: number) => void; }) { let frame: number | null = null; let previous: number | null = null; let elapsed = 0; let disposed = false; function tick(now: number): void { frame = null; if (disposed || !canRender()) return; if (!canAnimate()) { draw(elapsed, true); return; } if (previous === null || now - previous >= 1000 / 30) { elapsed += previous === null ? 0 : Math.min(now - previous, 75); previous = now; draw(elapsed, false); } frame = requestFrame(tick); } function sync(): void { if (disposed) return; if (frame !== null) cancelFrame(frame); frame = null; previous = null; if (!canRender()) return; draw(elapsed, !canAnimate()); if (canAnimate()) frame = requestFrame(tick); } function dispose(): void { disposed = true; if (frame !== null) cancelFrame(frame); frame = null; } return { sync, dispose }; }