Stop the bunting's loop when the tab hides, so it comes back fresh
A hidden tab's queued frame can be suspended rather than run, which left running set; the return's start() then returned early and the loop resumed the old follower and jig, a stale shake. Hiding (visibilitychange or pagehide) now cancels the queued frame and clears running, so the way back is a fresh start(), which resets the follower and jig.
Human
Prompt
Astra reviewed the branch: Ship after fixes (bunting-review.md). Fix all four without changing the feel. 3. bunting.js:481: on hide/pagehide cancel the queued frame and clear running; on resume reset the follower and jig, so a long-hidden tab doesn't resume a stale shake.
From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.
Summary
bunting.js: pause() on hide and pagehide; start() on show and pageshow.
Agent notes
No drive: headless Chrome does not hide the page. Checked by hand with a throwaway drive that faked document.hidden: running went false on hide, true on return, and the loop slept 4 s later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+15
−4
site/static/bunting.js+15 −4
Intent
diff --git a/site/static/bunting.js b/site/static/bunting.js| index 8a3efe3..23790a8 100644 |
| --- a/site/static/bunting.js |
| +++ b/site/static/bunting.js |
| @@ -437,7 +437,7 @@ |
| // live is whether the canvas is on the page: false under reduced |
| // motion. running is whether the frame loop is going. |
| - var live = false, running = false, onScreen = true, acc = 0, lastT = 0, drawn = false; |
| + var live = false, running = false, onScreen = true, acc = 0, lastT = 0, drawn = false, raf = 0; |
| function frame(now) { |
| if (!running || !live) return; |
| if (!onScreen || document.hidden) { running = false; return; } |
| @@ -452,7 +452,7 @@ |
| Math.abs(frameV) < 1 && Math.abs(scrollY - frameY) < 0.5 && motion < 1e-4 && !(puffs.length && now / 1000 - puffs[puffs.length - 1].t < 0.6); |
| if (n && (still || now - lastInput < CALM || (drawn = !drawn))) draw(); |
| if (still) { running = false; return; } |
| - requestAnimationFrame(frame); |
| + raf = requestAnimationFrame(frame); |
| } |
| // record widens the probe's extremes to this frame's cloth: every |
| // point's place, and the worst stretch of any link over its length. |
| @@ -483,7 +483,16 @@ |
| // The frame picks up the page where it is, moving as it was, so |
| // waking mid-scroll is not a jolt. |
| frameY = scrollY; frameV = lastT - scrollT < 100 ? scrollV : 0; jig = 0; |
| - requestAnimationFrame(frame); |
| + raf = requestAnimationFrame(frame); |
| + } |
| + // A hidden tab's queued frame may be suspended rather than run, which |
| + // would leave running set, so start() on the way back would return |
| + // early and the loop would pick up the follower and jig where they |
| + // were, a stale shake. Cancelling it and clearing running on the way |
| + // out makes the way back a fresh start(). |
| + function pause() { |
| + cancelAnimationFrame(raf); |
| + running = false; |
| } |
| var lx = null, ly = 0, lt = 0; |
| @@ -525,7 +534,9 @@ |
| }, { passive: true }); |
| new IntersectionObserver(function (es) { onScreen = es[0].isIntersecting; if (onScreen) start(); }).observe(row); |
| - document.addEventListener("visibilitychange", start); |
| + document.addEventListener("visibilitychange", function () { if (document.hidden) pause(); else start(); }); |
| + addEventListener("pagehide", pause); |
| + addEventListener("pageshow", start); |
| var rz; |
| addEventListener("resize", function () { |
| clearTimeout(rz); |