diff options
| -rw-r--r-- | src/app.js | 51 | ||||
| -rw-r--r-- | tools/feed-wheel-reverse-test.js | 32 |
2 files changed, 35 insertions, 48 deletions
@@ -6653,63 +6653,54 @@ if (INTENT_URI) { 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; }; + // A turn back against the coast catches the page at once and is a plain + // notch from rest — no flight the other way, since for a moment after a + // reversal (CALM) notches don't wind the speed up, so a couple of + // correcting notches travel a couple of notches. (A spell of quarter-size + // "hunting" notches ramping back up was tried here and felt like a + // stutter and a slow patch — Adam, 2026-10-16.) + const CALM = 700; // ms after a reversal without wind-up + let calmUntil = 0; // (lastDir/lastMoveAt: the tick's, above) 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; halt(); return; } + if (!notch) { smoothUntil = e.timeStamp + 400; stop(); 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; 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) { + // the way the page is going: its coast, or the coast that only just died + const going = v ? Math.sign(v) : now - lastMoveAt < 400 ? lastDir : 0; + if (going && dir !== going) { // 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); + // notch a small response now, then coast the rest of its travel. + stop(); + calmUntil = now + CALM; + const first = dir * Math.min(16, Math.abs(px * 3 / TAU) * 16); window.scrollBy({ top: first, behavior: 'instant' }); lastDir = dir; lastMoveAt = now; - coast(v + (travel - first) / HUNT_TAU, HUNT_TAU); + coast((px * 3 - first) / TAU, 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); + const gain = now < calmUntil ? 1 : 1 + Math.min(1.5, Math.abs(v) / 4); 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)) halt(); }); - window.addEventListener('touchstart', halt, { passive: true }); + window.addEventListener('keydown', (e) => { if (/^(Arrow|Page|Home|End| )/.test(e.key)) stop(); }); + window.addEventListener('touchstart', stop, { passive: true }); // ---- grab and flick let drag = null; document.addEventListener('pointerdown', (e) => { - if (raf) halt(); // a click catches a coasting page, like a finger does + if (raf) stop(); // 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: [] }; }); diff --git a/tools/feed-wheel-reverse-test.js b/tools/feed-wheel-reverse-test.js index 8a00b11..1f133ff 100644 --- a/tools/feed-wheel-reverse-test.js +++ b/tools/feed-wheel-reverse-test.js @@ -46,24 +46,20 @@ for (const dir of [1, -1]) { assert((f.y - reversed) * dir < 0, 'next frame continues in the new direction'); assert(Math.abs(f.y - reversed) < 20, 'new velocity is not applied to the old frame delay'); settle(f); - // The turn back is a hunt: a quarter of a notch's travel, not a flight the other way. + // The turn back is a plain notch from rest: its full travel, no flight the other way. const back = (before - f.y) * dir; - assert(back > 65 && back < 90, 'a reverse notch against a coast is a short step: ' + back); - // Each notch on grows the step back towards a full one; the sixth is a plain notch again. - const steps = []; - for (let i = 0; i < 5; i++) { f.advance(200); const y = f.y; f.wheel(-dir); settle(f); steps.push((y - f.y) * dir); } - assert(steps.every((s, i) => i === 0 || s > steps[i - 1] * 1.08), 'hunting steps grow notch by notch: ' + steps.map(Math.round)); - assert(steps[0] > 95 && steps[0] < 125 && steps[3] > 240 && steps[3] < 285 && steps[4] > 280 && steps[4] < 330, 'from about a third of a notch to a full one: ' + steps.map(Math.round)); - { f.advance(200); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 280 && s < 330, 'a few flicks on, the wheel flies again: ' + s); } - // A turn back while hunting starts the small steps over. - f.wheel(-dir); f.advance(17); f.frame(); f.advance(17); f.frame(); - { const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 65 && s < 90, 'a reversal while hunting is a short step again: ' + s); } - { f.advance(300); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 95 && s < 120, 'and the next grows from there: ' + s); } - // A pause ends the hunt: the next notch is the plain push from rest. - { f.advance(2200); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 280 && s < 330, 'after a pause a notch is a full push: ' + s); } - // A turn back against a coast that only just died hunts too. - { f.wheel(dir); settle(f); f.advance(200); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 65 && s < 90, 'reversing right after the coast dies is a short step: ' + s); } - // ...but a notch from rest the other way, well after, is a full one. + assert(back > 280 && back < 330, 'a reverse notch against a coast travels one notch: ' + back); + // For a moment after a reversal notches don't wind the speed up: a + // reversal and a quick second notch travel about two notches, not a flight. + let calm; + { f.wheel(-dir); f.advance(17); f.frame(); f.wheel(-dir); f.advance(120); f.frame(); // coasting again, the other way + const y = f.y; f.wheel(dir); f.advance(60); f.frame(); f.wheel(dir); settle(f); calm = (f.y - y) * dir; + assert(calm > 540 && calm < 700, 'a reversal and a quick second notch: about two notches of travel: ' + calm); } + // From rest, well after, the wind-up is back: the same two quick notches go further. + { f.advance(2500); const y = f.y; f.wheel(dir); f.advance(60); f.frame(); f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > calm + 40, 'two quick notches with the wind-up go further: ' + s + ' vs ' + calm); } + // From rest a notch either way is a full push. { f.advance(2500); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 280 && s < 330, 'from rest a notch either way is a full push: ' + s); } + // A turn back against a coast that only just died is a reversal too (caught, no wind-up). + { f.wheel(dir); settle(f, 40); f.advance(100); const y = f.y; f.wheel(-dir); f.advance(60); f.frame(); f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 520 && s < 700, 'reversing right after the coast dies: no wind-up: ' + s); } } -console.log('✓ immediate reversal both ways, stale frame timestamps, busy frame recovery, precision hunt after a reversal (short steps that grow, restart on another turn back, end on a pause)'); +console.log('✓ immediate reversal both ways, stale frame timestamps, busy frame recovery, a reversal is a plain notch with no wind-up for a moment'); |
