Merge nucleic/calm-willow-ibis-dozm into main

Mirrored from corpo 43dcea2.
This commit is contained in:
2026-08-11 19:44:40 -07:00
parent 42e78d0d66
commit 556aa9d20e
+34 -78
View File
@@ -1531,70 +1531,32 @@ const canonical = new URL('/', Astro.site);
return event.deltaY * multiplier; return event.deltaY * multiplier;
}; };
/* Trackpad momentum arrives as a long tail of wheel deltas, and one hard flick /* One flick, one product. A trackpad hands a hard flick over as a long tail of
carries enough of it to cross the whole stack — the products in the middle are wheel deltas, enough of them to cross the whole stack with the products in
then only a flicker on the way past, which is the one thing the beat pacing between only a flicker on the way past — and that tail is the system playing
above cannot fix, because by the time it has played a card the scroll is out momentum, not a hand still asking for anything. What tells the two apart
already somewhere else. So a step entered at fling speed is entered slowly. is that momentum only ever weakens: a finger on the pad can push as hard as it
The rule the whole thing reduces to: a gesture may cross any boundary it likes, a tail cannot. So a delta that arrives no weaker than the one before it
reaches at reading pace, and the moment it crosses one at fling speed, what is is the user, and is granted the product it has reached; a tail is granted the
left of that fling is held to a fifteenth of its weight. One flick, one product it has reached and nothing beyond it. Push again mid-flick and the
product; a reader who keeps pushing still gets through, just against the push counts, which is what lets a second flick take a second product instead
brakes. */ of dying against a brake — the hand is what the stack answers to, and its
const CATCH_STRENGTH = 0.95; speed is not otherwise measured. */
/* The stack answers to a shorter gesture than the page it sits in — three const TAIL_FALL = 0.97;
products across nine hundred pixels means a careful two-finger drag spends const TAIL_DRAG = 0.05;
itself inside one of them, which reads as a page that has not noticed. So a /* Long enough that a tail is still the gesture that threw it, short enough that
delta is worth a third again as much at reading pace, and that extra is given a deliberate second flick is a new one. */
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;
const GESTURE_GAP_MS = 140; 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 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; 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( document.addEventListener(
'wheel', 'wheel',
@@ -1605,29 +1567,23 @@ const canonical = new URL('/', Astro.site);
event.preventDefault(); event.preventDefault();
const now = performance.now(); const now = performance.now();
const gap = now - wheelAt;
const pixels = wheelPixels(event); const pixels = wheelPixels(event);
/* Per frame rather than per event, since a mouse notch and a trackpad const driven =
sample arrive at nothing like the same rate. The gap is held between a now - wheelAt > GESTURE_GAP_MS ||
quarter frame and four: below, two events sharing a millisecond would /* A reversal is a fresh intention whatever its weight. */
read as impossibly fast, and above, the first event of a gesture would pixels * lastPixels <= 0 ||
read as a crawl. */ Math.abs(pixels) >= Math.abs(lastPixels) * TAIL_FALL;
const frames = Math.max(4, Math.min(gap, 64)) / 16;
const instant = Math.abs(pixels) / frames;
if (gap > GESTURE_GAP_MS) {
wheelSpeed = instant;
gestureBeat = technologyAtScroll();
gestureCatch = 0;
} else {
wheelSpeed = wheelSpeed * 0.65 + instant * 0.35;
}
wheelAt = now; wheelAt = now;
lastPixels = pixels;
const fast = wheelFast(); const beat = technologyAtScroll();
const gain = 1 + (SLOW_GAIN - 1) * (1 - fast); 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 }, { capture: true, passive: false },
); );