From 4775c46e8e84f65fb91cd7468f037abf8e8899e2 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 4 May 2026 18:58:09 -0700 Subject: [PATCH] fix(viewer): kick render on focus and visibility resume `frameloop="never"` makes FrameLimiter's requestAnimationFrame loop the only render driver. Browsers throttle rAF when the tab is hidden, the window is unfocused, or the system marks the tab as occluded; when that happens advance() stops firing and the canvas freezes on whatever was last in the swap chain (blank on first paint, or the previous frame on subsequent freezes). Listen to visibilitychange (visible), focus, and pageshow alongside the existing rAF loop. Each event calls advance() synchronously so the next visible frame matches the current scene state without waiting for rAF to resume. Fixes #275 Closes #196 --- .../src/components/viewer/frame-limiter.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/viewer/src/components/viewer/frame-limiter.tsx b/packages/viewer/src/components/viewer/frame-limiter.tsx index 9fa590800..eb62cb9ac 100644 --- a/packages/viewer/src/components/viewer/frame-limiter.tsx +++ b/packages/viewer/src/components/viewer/frame-limiter.tsx @@ -28,10 +28,31 @@ const FrameLimiter: React.FC = ({ fps = 50 }) => { set({ frameloop: 'never' }) raf = requestAnimationFrame(tick) + // Browsers throttle requestAnimationFrame when the tab is hidden, the + // window is unfocused, or the system marks the tab as occluded. With + // frameloop="never" rAF is the only render driver, so when it stalls the + // canvas freezes — Linux Firefox/Chrome and Zen show this as the viewer + // "turning off" between cursor interactions. Force one synchronous + // advance whenever the page resumes so the next visible frame matches the + // current scene state. + function kick() { + i += 1 / 1000 + advance(i) + } + function onVisibilityChange() { + if (document.visibilityState === 'visible') kick() + } + document.addEventListener('visibilitychange', onVisibilityChange) + window.addEventListener('focus', kick) + window.addEventListener('pageshow', kick) + return () => { if (raf) { cancelAnimationFrame(raf) } + document.removeEventListener('visibilitychange', onVisibilityChange) + window.removeEventListener('focus', kick) + window.removeEventListener('pageshow', kick) set({ frameloop: initFrameloop }) } }, [fps, advance, set, initFrameloop])