rastrillo / fiesta Public

Wake the bunting on a scroll's air, not only its speed

The wake gate divided a scroll event's distance by the time since the previous event, idle included, so one wheel turn after twenty quiet seconds read as 4 px/s and never woke the loop, though it had moved 100 px/s of air. Waking also when the air passes AIR_REST catches it; the speed gate stays for the events that carry little air.
Human

Prompt

Astra reviewed the branch: Ship after fixes (bunting-review.md). Fix all four without changing the feel. 1. (Important) bunting.js:517: a scroll after idle can fail to wake the cloth. Wake whenever the accumulated air exceeds AIR_REST, independent of the velocity gate. Add a drive: idle 20s, one 80px scroll with the row on screen, the cloth moves (red first).

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 wakes when the air exceeds AIR_REST; TestAScrollAfterIdleWakesTheBunting; its weight.
Agent notes
Red before the fix: 0.00% of the canvas moved (want 1%). Green: 2.17%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 29209480a5d12703098189e3271ae540980a2c95 parent a6ea97c
3 files changed, +34 −1
  • hack/test-weights.txt +1 −0
  • internal/www/landing_browser_test.go +29 −0
  • site/static/bunting.js +4 −1
diff --git a/hack/test-weights.txt b/hack/test-weights.txt
index b5c7d48..b012699 100644
--- a/hack/test-weights.txt
+++ b/hack/test-weights.txt
@@ -156,6 +156,7 @@ browser internal/gallery TestTheGallerySwitchLandsOnTheNextFrame 2
browser internal/gallery TestTheGallerysOwnControlsAreExactlyTheFloor 2
browser internal/gallery TestThePhoneIndexAndTheWayBack 2
browser internal/gallery TestTheWayBackFromAShellDemoIsATarget 2
+browser internal/www TestAScrollAfterIdleWakesTheBunting 29
browser internal/www TestTheLandingPassesAxe 16
browser internal/www TestAShakeMovesTheBuntingMoreThanAScroll 15
browser internal/www TestTheBuntingFollowsTheScrollBack 15
diff --git a/internal/www/landing_browser_test.go b/internal/www/landing_browser_test.go
index 811e96f..0e271e7 100644
--- a/internal/www/landing_browser_test.go
+++ b/internal/www/landing_browser_test.go
@@ -1362,6 +1362,35 @@ func TestTheBuntingNeverLeavesItsRow(t *testing.T) {
}
}
+// TestAScrollAfterIdleWakesTheBunting: a reader leaves the page for
+// twenty seconds, then turns the wheel once, 80 px, with the row on
+// screen. One scroll event that long after the last is a slow scroll
+// by its average speed, but the air it moves is real, so the cloth
+// must wake and lift.
+func TestAScrollAfterIdleWakesTheBunting(t *testing.T) {
+ rig, _ := siteRig(t)
+ ctx, cancel := context.WithTimeout(rig.Context(), 2*time.Minute)
+ defer cancel()
+ openStill(t, ctx, rig)
+ mustRun(t, ctx, chromedp.Evaluate(`scrollTo({top: 80, behavior: "instant"}); true`, nil))
+ mustRun(t, ctx, chromedp.Sleep(20*time.Second))
+ settle(t, ctx, "the cloth after twenty idle seconds", time.Second)
+ var peak float64
+ at(t, ctx, `(async () => {
+ const m = siteMotion;
+ m.rest();
+ scrollTo({top: 0, behavior: "instant"});
+ let peak = 0;
+ const t0 = performance.now();
+ while (performance.now() - t0 < 1000) { await m.frame(); peak = Math.max(peak, m.off()); }
+ return JSON.stringify(peak);
+})()`, &peak, awaitPromise)
+ t.Logf("one 80 px scroll after twenty idle seconds moved %.2f%% of the canvas within a second", peak)
+ if peak < 1 {
+ t.Errorf("one 80 px scroll after twenty idle seconds moved %.2f%% of the canvas; want at least 1%%: the cloth never woke", peak)
+ }
+}
+
// TestReducedMotionIgnoresScrolling: under reduced motion there is no
// canvas, and scrolling and shaking the page leave the still SVG
// exactly as it was.
diff --git a/site/static/bunting.js b/site/static/bunting.js
index 2dc2bb8..00cd1b1 100644
--- a/site/static/bunting.js
+++ b/site/static/bunting.js
@@ -514,7 +514,10 @@
sy = scrollY; st = now;
air = Math.max(-AIR_MAX, Math.min(AIR_MAX, airAt(now) + d / AIR_TAU)); airT = now;
scrollV = now - scrollT < 100 ? (scrollV + v) / 2 : v; scrollT = now;
- if (Math.abs(v) > 50) { lastInput = now; start(); }
+ // Wake on the air as well as the speed: v is averaged since the
+ // last event, so one wheel turn after a long idle reads as slow
+ // while the air it moved is not.
+ if (Math.abs(v) > 50 || Math.abs(air) > AIR_REST) { lastInput = now; start(); }
}, { passive: true });
new IntersectionObserver(function (es) { onScreen = es[0].isIntersecting; if (onScreen) start(); }).observe(row);