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;
};
/* 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 },
);