From 7dada9c77b6aa705338489c3720192a138a5d214 Mon Sep 17 00:00:00 2001 From: Open Pascal Date: Wed, 24 Jun 2026 20:02:25 +0000 Subject: [PATCH] fix(viewer): kick render on focus and visibility resume With frameloop="never" the rAF loop is the only render driver. Browsers throttle requestAnimationFrame when the tab is hidden/unfocused/occluded, which freezes the canvas (the viewer appears to 'turn off' between interactions on Linux Firefox/Chrome/Zen). Add visibilitychange/focus/pageshow listeners that force one synchronous advance() on resume, with matching cleanup. Lands #291 (mvanhorn) onto current main (resolves the comment-context conflict). Fixes #275. Closes #196. Co-authored-by: mvanhorn --- .../src/components/viewer/frame-limiter.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/viewer/src/components/viewer/frame-limiter.tsx b/packages/viewer/src/components/viewer/frame-limiter.tsx index 962dd8269..81765f79b 100644 --- a/packages/viewer/src/components/viewer/frame-limiter.tsx +++ b/packages/viewer/src/components/viewer/frame-limiter.tsx @@ -28,11 +28,33 @@ const FrameLimiter: React.FC = ({ fps = 50 }) => { set({ frameloop: 'never' }) // Kick off custom render loop 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) + // Restore initial setting return () => { if (raf) { cancelAnimationFrame(raf) } + document.removeEventListener('visibilitychange', onVisibilityChange) + window.removeEventListener('focus', kick) + window.removeEventListener('pageshow', kick) set({ frameloop: initFrameloop }) } }, [fps, advance, set, initFrameloop])