diff options
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 60 |
1 files changed, 43 insertions, 17 deletions
@@ -6589,7 +6589,8 @@ if (INTENT_URI) { } return true; }; - let v = 0, acc = 0, raf = 0, last = 0; + let v = 0, acc = 0, raf = 0, last = 0, tau = TAU; + let lastDir = 0, lastMoveAt = 0; // which way the coast last moved the page, and when const stop = () => { v = 0; acc = 0; if (raf) cancelAnimationFrame(raf); raf = 0; }; const tick = (now) => { const dt = Math.max(0, Math.min(40, now - last)); last = Math.max(last, now); // a frame stamp can predate the push @@ -6599,50 +6600,75 @@ if (INTENT_URI) { const y = window.scrollY; window.scrollBy({ top: step, behavior: 'instant' }); acc -= step; + lastDir = Math.sign(step); lastMoveAt = now; if (window.scrollY === y) return stop(); // an end of the page } - v *= Math.exp(-dt / TAU); + v *= Math.exp(-dt / tau); if (Math.abs(v) < 0.02) return stop(); raf = requestAnimationFrame(tick); }; - const coast = (nv) => { - v = Math.max(-VMAX, Math.min(VMAX, nv)); + const coast = (nv, t = TAU) => { + v = Math.max(-VMAX, Math.min(VMAX, nv)); tau = t; if (!raf && v) { last = performance.now(); raf = requestAnimationFrame(tick); } }; // ---- the wheel + // A turn back against the coast is a reader who flew past the post they + // wanted and is hunting for it: the page is caught at once, and that notch + // and the next few are short, quick steps (a quarter of a notch's travel, + // settling fast) rather than a fresh flight the other way. Each notch on + // in the same direction grows the step back towards a full one, so a few + // flicks have the wheel flying again; a turn back while hunting starts the + // small steps over, and a pause ends the hunt — the next notch is the + // plain push from rest it has always been. + const HUNT = [0.25, 0.35, 0.5, 0.7, 0.85]; // of a notch's travel, notch by notch + const HUNT_TAU = 110; // ms: a hunting step settles quickly + const HUNT_IDLE = 2000; // ms without a notch, and the hunt is over + let hunt = -1, huntDir = 0, huntAt = 0; // the next HUNT entry while hunting + const halt = () => { stop(); hunt = -1; }; let smoothUntil = 0; // a trackpad's events were just seen: hands off window.addEventListener('wheel', (e) => { if (e.ctrlKey || e.shiftKey || e.defaultPrevented) return; const notch = e.deltaMode === 1 || (e.wheelDeltaY && e.wheelDeltaY % 120 === 0 && !e.deltaX); - if (!notch) { smoothUntil = e.timeStamp + 400; stop(); return; } + if (!notch) { smoothUntil = e.timeStamp + 400; halt(); return; } if (e.timeStamp < smoothUntil || matchMedia('(pointer: coarse)').matches || !inScope(e.target)) return; e.preventDefault(); const px = e.deltaMode === 1 ? e.deltaY * 40 : e.deltaY; - // A reversal catches the old coast immediately. Discard its fractional - // step and frame clock too: after a busy frame, applying the NEW velocity - // to the OLD elapsed time made the first reverse step jump. Give the - // first notch a small response now, then coast the remaining distance. - if (v && px && Math.sign(v) !== Math.sign(px)) { - stop(); - const first = Math.sign(px) * Math.min(16, Math.abs(px * 3 / TAU) * 16); + const dir = Math.sign(px); + if (!dir) return; + const now = e.timeStamp; + if (hunt >= 0 && now - huntAt > HUNT_IDLE) hunt = -1; + // the way the page is going: its coast, the coast that just died, or the hunt + const going = v ? Math.sign(v) : now - lastMoveAt < 400 ? lastDir : hunt >= 0 ? huntDir : 0; + if (going && dir !== going) { stop(); hunt = 0; huntDir = dir; } + else if (hunt >= 0 && ++hunt >= HUNT.length) hunt = -1; + if (hunt >= 0) { + // A reversal catches the old coast immediately. Discard its fractional + // step and frame clock too: after a busy frame, applying the NEW velocity + // to the OLD elapsed time made the first reverse step jump. Give the + // notch a small response now, then a quick coast over the rest of its + // (short) travel; quick flicks still add up. + huntAt = now; + const travel = px * 3 * HUNT[hunt]; + const first = dir * Math.min(16, Math.abs(travel / HUNT_TAU) * 16); window.scrollBy({ top: first, behavior: 'instant' }); - coast((px * 3 - first) / TAU); + lastDir = dir; lastMoveAt = now; + coast(v + (travel - first) / HUNT_TAU, HUNT_TAU); } else { // A notch into a page already moving pushes harder: the faster it // goes, the more each turn adds (up to 2.5x), so keeping the wheel // turning winds the speed up instead of settling at a stroll. From // rest a notch is the same push as ever. const gain = 1 + Math.min(1.5, Math.abs(v) / 4); - coast(v + (px * 3 * gain) / TAU); + coast(v + (px * 3 * gain) / TAU, TAU); } }, { passive: false }); // anything else that moves the page takes over from a coast - window.addEventListener('keydown', (e) => { if (/^(Arrow|Page|Home|End| )/.test(e.key)) stop(); }); - window.addEventListener('touchstart', stop, { passive: true }); + window.addEventListener('keydown', (e) => { if (/^(Arrow|Page|Home|End| )/.test(e.key)) halt(); }); + window.addEventListener('touchstart', halt, { passive: true }); // ---- grab and flick let drag = null; document.addEventListener('pointerdown', (e) => { - if (raf) stop(); // a click catches a coasting page, like a finger does + if (raf) halt(); // a click catches a coasting page, like a finger does if (e.pointerType !== 'mouse' || e.button !== 0 || !inScope(e.target)) return; drag = { x: e.clientX, y: e.clientY, ly: e.clientY, on: false, pts: [] }; }); |
