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 },
);
/* One swipe, one product — the finger's version of the rule above, and it has
to be taken over rather than damped: the momentum a touch scroller hands back
after the finger has gone is the whole of the problem, since it carries a
flick clean across the stack and there is nothing left holding the glass to
ask it not to. So the swipe is given the product it starts on and the one
either side of it, tracks the finger exactly between them, and past them
takes the finger's weight and returns a fifth of it, the way a scroller
resists at its end. Lifting off settles on the product on the stage. A tap is
untouched — nothing is prevented until the finger has actually moved — and so
is a second finger, which is the reader zooming and not scrolling at all. */
const TOUCH_STRETCH = 0.2;
/* And never further than this, however long the swipe goes on. A resistance
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;
/* One swipe, one product, and nothing else about the swipe is read. A finger
asking for the next product asks the same thing whether it travels an inch or
the length of the screen, so the stack does not follow it: it steps once, the
moment the gesture is unmistakably a swipe, and ignores the rest of it. That
is also why there is no resistance anywhere below — nothing is tracking the
finger, so there is nothing for the reader to push against and nothing to
spring back from. It replaces the native scroller rather than damping it,
because the momentum a touch scroller hands back after the finger has gone
carries a flick clean across the stack and there is nothing left holding the
glass to ask it not to. A tap never reaches the threshold; a second finger is
a reader zooming and not scrolling at all. */
const SWIPE_MIN_PX = 24;
/* 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 touchBeat = 0;
let touchGive = 0;
/* 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;
let touchX = 0;
let touchTaken = false;
document.addEventListener(
'touchstart',
(event) => {
touchY = event.touches.length === 1 ? event.touches[0]?.clientY ?? null : null;
if (touchY === null) return;
measureBeats();
touchMoved = false;
touchBeat = technologyAtScroll();
touchTop = scrollRoot.scrollTop;
touchLow = beatRest(touchBeat - 1);
touchHigh = beatRest(touchBeat + 1);
touchGive = scrollRoot.clientHeight * TOUCH_STRETCH_MAX;
touchX = event.touches[0]?.clientX ?? 0;
touchTaken = false;
},
{ capture: true, passive: true },
);
@@ -1611,45 +1593,38 @@ const canonical = new URL('/', Astro.site);
document.addEventListener(
'touchmove',
(event) => {
const nextTouchY = event.touches[0]?.clientY;
if (touchY === null || nextTouchY === undefined) return;
const finger = event.touches[0];
if (touchY === null || !finger) return;
event.preventDefault();
touchMoved = true;
touchTop += touchY - nextTouchY;
touchY = nextTouchY;
if (touchTaken) return;
const bound = Math.min(touchHigh, Math.max(touchLow, touchTop));
const past = touchTop - bound;
/* Starts out giving back a fifth and closes on the cap: the same curve a
scroller bounces with, so the swipe meets a spring rather than a wall. */
const give =
Math.sign(past) *
touchGive *
(1 - 1 / (1 + (TOUCH_STRETCH * Math.abs(past)) / touchGive));
const travelled = touchY - finger.clientY;
/* More across than down is a reader swiping at the browser rather than at
the stack, and the stack is not what should answer. */
if (
Math.abs(travelled) < SWIPE_MIN_PX ||
Math.abs(travelled) < Math.abs(touchX - finger.clientX)
) {
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 },
);
/* The stage is asked where it is rather than told, so a swipe that gave up
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;
const endTouch = () => {
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('touchcancel', settleTouch, { capture: true, passive: true });
document.addEventListener('touchend', endTouch, { 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