From 556aa9d20e86d8837d2939d53050abc2d36638b9 Mon Sep 17 00:00:00 2001 From: Andrew Moore Date: Tue, 11 Aug 2026 19:44:40 -0700 Subject: [PATCH] Merge nucleic/calm-willow-ibis-dozm into main Mirrored from corpo 43dcea2. --- src/pages/index.astro | 112 +++++++++++++----------------------------- 1 file changed, 34 insertions(+), 78 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index 134ca36..bc50272 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1531,70 +1531,32 @@ const canonical = new URL('/', Astro.site); return event.deltaY * multiplier; }; - /* Trackpad momentum arrives as a long tail of wheel deltas, and one hard flick - carries enough of it to cross the whole stack — the products in the middle are - then only a flicker on the way past, which is the one thing the beat pacing - above cannot fix, because by the time it has played a card the scroll is - already somewhere else. So a step entered at fling speed is entered slowly. - The rule the whole thing reduces to: a gesture may cross any boundary it - reaches at reading pace, and the moment it crosses one at fling speed, what is - left of that fling is held to a fifteenth of its weight. One flick, one - product; a reader who keeps pushing still gets through, just against the - brakes. */ - const CATCH_STRENGTH = 0.95; - /* The stack answers to a shorter gesture than the page it sits in — three - products across nine hundred pixels means a careful two-finger drag spends - itself inside one of them, which reads as a page that has not noticed. So a - delta is worth a third again as much at reading pace, and that extra is given - back as the gesture speeds up, reaching nothing at fling speed. The one curve - answers both halves of the complaint: slow gestures cover more ground, fast - ones cover less, and it is the same number saying so. */ - const SLOW_GAIN = 1.35; - /* Below the first, a gesture is reading and is never touched; above the second - it is a fling. Both are per-frame, so they are speeds a hand can feel — about - 840px and 2040px a second — rather than counts of an event nobody controls. */ - const SLOW_FRAME_PX = 14; - const FAST_FRAME_PX = 34; + /* One flick, one product. A trackpad hands a hard flick over as a long tail of + wheel deltas, enough of them to cross the whole stack with the products in + between only a flicker on the way past — and that tail is the system playing + out momentum, not a hand still asking for anything. What tells the two apart + is that momentum only ever weakens: a finger on the pad can push as hard as it + likes, a tail cannot. So a delta that arrives no weaker than the one before it + is the user, and is granted the product it has reached; a tail is granted the + product it has reached and nothing beyond it. Push again mid-flick and the + push counts, which is what lets a second flick take a second product instead + of dying against a brake — the hand is what the stack answers to, and its + speed is not otherwise measured. */ + const TAIL_FALL = 0.97; + const TAIL_DRAG = 0.05; + /* Long enough that a tail is still the gesture that threw it, short enough that + a deliberate second flick is a new one. */ const GESTURE_GAP_MS = 140; + /* Three products across nine hundred pixels is a shorter run than the page this + sits in, so at page weight a careful two-finger drag spends itself inside one + of them, which reads as a page that has not noticed. */ + const WHEEL_GAIN = 1.3; let wheelAt = -Infinity; - let wheelSpeed = 0; + let lastPixels = 0; + /* The product this gesture is entitled to: re-granted on every delta the hand is + driving, frozen for as long as it is only momentum. */ let gestureBeat = -1; - /* How hard this gesture was going at the boundary it was caught at, held for the - rest of it: momentum decays, and a brake that eased off with it would let go - exactly when the tail is long enough to reach the next product. Nought while - the gesture is still free. */ - let gestureCatch = 0; - - /* Nought at reading pace, one at fling speed. Everything below reads the gesture - through this single number: how much a delta is worth, and how hard a boundary - pushes back once one has been crossed at speed. */ - const wheelFast = () => - Math.min( - 1, - Math.max(0, (wheelSpeed - SLOW_FRAME_PX) / (FAST_FRAME_PX - SLOW_FRAME_PX)), - ); - - const beatDrag = (fast: number) => { - const beat = technologyAtScroll(); - if (beat === gestureBeat) return 1; - - if (!gestureCatch) { - /* Reading pace: the gesture is entitled to the beat it has reached, and to - the next one it reaches the same way. Only speed is ever held back. */ - if (fast <= 0) { - gestureBeat = beat; - return 1; - } - gestureCatch = fast; - } - - /* Flat rather than a ramp that fades across the step. A ramp charges a fling - only the log of its weight — measured, it took barely a tenth off a hard - one — where a flat brake charges it in proportion, which is what makes a - flick spend itself inside one product instead of three. */ - return 1 - CATCH_STRENGTH * gestureCatch; - }; document.addEventListener( 'wheel', @@ -1605,29 +1567,23 @@ const canonical = new URL('/', Astro.site); event.preventDefault(); const now = performance.now(); - const gap = now - wheelAt; const pixels = wheelPixels(event); - /* Per frame rather than per event, since a mouse notch and a trackpad - sample arrive at nothing like the same rate. The gap is held between a - quarter frame and four: below, two events sharing a millisecond would - read as impossibly fast, and above, the first event of a gesture would - read as a crawl. */ - const frames = Math.max(4, Math.min(gap, 64)) / 16; - const instant = Math.abs(pixels) / frames; + const driven = + now - wheelAt > GESTURE_GAP_MS || + /* A reversal is a fresh intention whatever its weight. */ + pixels * lastPixels <= 0 || + Math.abs(pixels) >= Math.abs(lastPixels) * TAIL_FALL; - if (gap > GESTURE_GAP_MS) { - wheelSpeed = instant; - gestureBeat = technologyAtScroll(); - gestureCatch = 0; - } else { - wheelSpeed = wheelSpeed * 0.65 + instant * 0.35; - } wheelAt = now; + lastPixels = pixels; - const fast = wheelFast(); - const gain = 1 + (SLOW_GAIN - 1) * (1 - fast); + const beat = technologyAtScroll(); + if (driven) gestureBeat = beat; - scrollRoot.scrollBy({ top: pixels * gain * beatDrag(fast), behavior: 'auto' }); + scrollRoot.scrollBy({ + top: pixels * WHEEL_GAIN * (beat === gestureBeat ? 1 : TAIL_DRAG), + behavior: 'auto', + }); }, { capture: true, passive: false }, );