diff --git a/src/pages/index.astro b/src/pages/index.astro index a60c9b9..c9296d0 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -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