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

Mirrored from corpo 5e27961.
This commit is contained in:
2026-08-11 20:53:14 -07:00
parent e59987f03b
commit 05a54f4bc6
+40 -65
View File
@@ -1560,50 +1560,32 @@ const canonical = new URL('/', Astro.site);
{ capture: true, passive: false }, { capture: true, passive: false },
); );
/* One swipe, one product — the finger's version of the rule above, and it has /* One swipe, one product, and nothing else about the swipe is read. A finger
to be taken over rather than damped: the momentum a touch scroller hands back asking for the next product asks the same thing whether it travels an inch or
after the finger has gone is the whole of the problem, since it carries a the length of the screen, so the stack does not follow it: it steps once, the
flick clean across the stack and there is nothing left holding the glass to moment the gesture is unmistakably a swipe, and ignores the rest of it. That
ask it not to. So the swipe is given the product it starts on and the one is also why there is no resistance anywhere below — nothing is tracking the
either side of it, tracks the finger exactly between them, and past them finger, so there is nothing for the reader to push against and nothing to
takes the finger's weight and returns a fifth of it, the way a scroller spring back from. It replaces the native scroller rather than damping it,
resists at its end. Lifting off settles on the product on the stage. A tap is because the momentum a touch scroller hands back after the finger has gone
untouched — nothing is prevented until the finger has actually moved — and so carries a flick clean across the stack and there is nothing left holding the
is a second finger, which is the reader zooming and not scrolling at all. */ glass to ask it not to. A tap never reaches the threshold; a second finger is
const TOUCH_STRETCH = 0.2; a reader zooming and not scrolling at all. */
/* And never further than this, however long the swipe goes on. A resistance const SWIPE_MIN_PX = 24;
that only ever divides still arrives at the next product eventually, and the
reader sees it take the stage and get taken off again on the way back — the
one thing the whole rule is here to prevent. A fifteenth of the screen is far
enough to read as give and much nearer than the next card's edge. */
const TOUCH_STRETCH_MAX = 0.06;
/* Where the finger went down — the swipe is measured from there rather than
from the last move, so a slow drag accumulates to the same threshold a flick
crosses at once. */
let touchY: number | null = null; let touchY: number | null = null;
let touchBeat = 0; let touchX = 0;
let touchGive = 0; let touchTaken = false;
/* A tap is a finger that never moved, and settling under one would scroll the
page out from under the link being followed. */
let touchMoved = false;
/* Where the swipe has asked to be, before any of the resistance below: reading
it back off the element instead would compound the resistance against itself,
and the finger would meet a wall rather than a spring. */
let touchTop = 0;
let touchLow = 0;
let touchHigh = 0;
document.addEventListener( document.addEventListener(
'touchstart', 'touchstart',
(event) => { (event) => {
touchY = event.touches.length === 1 ? event.touches[0]?.clientY ?? null : null; touchY = event.touches.length === 1 ? event.touches[0]?.clientY ?? null : null;
if (touchY === null) return; touchX = event.touches[0]?.clientX ?? 0;
touchTaken = false;
measureBeats();
touchMoved = false;
touchBeat = technologyAtScroll();
touchTop = scrollRoot.scrollTop;
touchLow = beatRest(touchBeat - 1);
touchHigh = beatRest(touchBeat + 1);
touchGive = scrollRoot.clientHeight * TOUCH_STRETCH_MAX;
}, },
{ capture: true, passive: true }, { capture: true, passive: true },
); );
@@ -1611,45 +1593,38 @@ const canonical = new URL('/', Astro.site);
document.addEventListener( document.addEventListener(
'touchmove', 'touchmove',
(event) => { (event) => {
const nextTouchY = event.touches[0]?.clientY; const finger = event.touches[0];
if (touchY === null || nextTouchY === undefined) return; if (touchY === null || !finger) return;
event.preventDefault(); event.preventDefault();
touchMoved = true; if (touchTaken) return;
touchTop += touchY - nextTouchY;
touchY = nextTouchY;
const bound = Math.min(touchHigh, Math.max(touchLow, touchTop)); const travelled = touchY - finger.clientY;
const past = touchTop - bound; /* More across than down is a reader swiping at the browser rather than at
/* Starts out giving back a fifth and closes on the cap: the same curve a the stack, and the stack is not what should answer. */
scroller bounces with, so the swipe meets a spring rather than a wall. */ if (
const give = Math.abs(travelled) < SWIPE_MIN_PX ||
Math.sign(past) * Math.abs(travelled) < Math.abs(touchX - finger.clientX)
touchGive * ) {
(1 - 1 / (1 + (TOUCH_STRETCH * Math.abs(past)) / touchGive)); return;
}
scrollRoot.scrollTop = bound + give; touchTaken = true;
measureBeats();
scrollRoot.scrollTo({
top: beatRest(technologyAtScroll() + Math.sign(travelled)),
behavior: reducedMotion.matches ? 'auto' : 'smooth',
});
}, },
{ capture: true, passive: false }, { capture: true, passive: false },
); );
/* The stage is asked where it is rather than told, so a swipe that gave up const endTouch = () => {
halfway settles back where it came from — but never further than the one
product the swipe was granted, whatever the stretch above let through. */
const settleTouch = () => {
if (touchY === null) return;
touchY = null; touchY = null;
if (!touchMoved) return;
const beat = Math.min(touchBeat + 1, Math.max(touchBeat - 1, technologyAtScroll()));
scrollRoot.scrollTo({
top: beatRest(beat),
behavior: reducedMotion.matches ? 'auto' : 'smooth',
});
}; };
document.addEventListener('touchend', settleTouch, { capture: true, passive: true }); document.addEventListener('touchend', endTouch, { capture: true, passive: true });
document.addEventListener('touchcancel', settleTouch, { capture: true, passive: true }); document.addEventListener('touchcancel', endTouch, { capture: true, passive: true });
} }
/* Where the band below reaches: the story hands over to a card when that card's /* Where the band below reaches: the story hands over to a card when that card's