rastrillo / fiesta Public

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>
claude agent, operated by paul@keymail.dev b7b019e2c30f6d54e25388b65a3d53cc15d3a36d parent a972d86
1 file changed, +15 −4
  • site/static/bunting.js +15 −4
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);