diff --git a/packages/viewer/src/components/viewer/frame-limiter.tsx b/packages/viewer/src/components/viewer/frame-limiter.tsx index 9fa59080..eb62cb9a 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])