diff options
| author | Adam Soltys <[email protected]> | 2026-10-13 18:24:57 +0000 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-13 18:24:57 +0000 |
| commit | 5550e929641b282a3de2c2cda66782d7c58689a1 (patch) | |
| tree | 7b5bb4f0c2bc634cf38aabc73fdcdd9363296262 | |
| parent | f3d03c642bef06b3afc8a3038136c5e0fc1e21f7 (diff) | |
| download | coinosv3-5550e929641b282a3de2c2cda66782d7c58689a1.tar.gz coinosv3-5550e929641b282a3de2c2cda66782d7c58689a1.zip | |
Pager: a snappy glide to the next slide, not the browser's slow smooth scroll
Letting go of a swipe (or an arrow key) runs the pager's own eased-out
glide, at most 220ms and quicker after a fast flick; a finger catching it
mid-glide takes it from there. A swipe now lands in ~240ms (was ~425ms).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
| -rw-r--r-- | src/features/messages.js | 33 | ||||
| -rw-r--r-- | tools/pager-tap-slop-test.js | 10 |
2 files changed, 36 insertions, 7 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 3cb94fd..a7dc54c 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12433,6 +12433,26 @@ export function messagesFeature(ctx) { const cur = pager.cur && pager.cur.querySelector('video.mp-media'); if (cur && cur.paused) cur.play().catch(() => {}); } + // The pager's own glide from slide to slide: the browser's smooth scroll + // dawdles (half a second and more on Android). Quick and eased out, faster + // still after a fast flick; snapping stays off until it lands. + function pagerGlide(box, axis, to, vel = 0) { + const y = axis === 'y', from = y ? box.scrollTop : box.scrollLeft, dist = to - from; + cancelAnimationFrame(box._glide); + box.style.scrollSnapType = 'none'; + const done = () => { box._glide = 0; box.style.scrollSnapType = ''; }; + if (Math.abs(dist) < 1) { done(); return; } + const ms = Math.max(90, Math.min(220, Math.abs(dist) / Math.max(2.5, Math.abs(vel) * 1.5))); + const t0 = performance.now(); + const step = (now) => { + const k = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - k, 3); + if (y) box.scrollTop = from + dist * e; else box.scrollLeft = from + dist * e; + if (k < 1) box._glide = requestAnimationFrame(step); else done(); + }; + box._glide = requestAnimationFrame(step); + } + const pagerHold = (box) => { if (box && box._glide) { cancelAnimationFrame(box._glide); box._glide = 0; } }; + function buildPager(want) { const video = want.kind === 'videos'; const scroller = h('div', { class: 'mp-scroll' }); @@ -12470,6 +12490,7 @@ export function messagesFeature(ctx) { d.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y'; d.box = d.axis === 'y' ? pager.scroller : d.slide && d.slide.parentElement; if (!d.box) { touch = null; return; } + pagerHold(d.box); // caught mid-glide: from where it is d.start = d.axis === 'y' ? d.box.scrollTop : d.box.scrollLeft; d.box.style.scrollSnapType = 'none'; // following the finger: no snapping under it d.track = []; @@ -12480,14 +12501,12 @@ export function messagesFeature(ctx) { if (d.track.length > 5) d.track.shift(); }, { passive: false }); // let go of a drag: on to the next (or the one before), or back - function settle(d, step) { + function settle(d, step, vel = 0) { const box = d.box, y = d.axis === 'y'; const size = (y ? box.clientHeight : box.clientWidth) || 1; const last = Math.max(0, Math.round(((y ? box.scrollHeight : box.scrollWidth) - size) / size)); const to = Math.min(last, Math.max(0, Math.round(d.start / size) + step)); - box.scrollTo({ [y ? 'top' : 'left']: to * size, behavior: 'smooth' }); - clearTimeout(box._snapBack); - box._snapBack = setTimeout(() => { box.style.scrollSnapType = ''; }, 450); + pagerGlide(box, d.axis, to * size, vel); } el.addEventListener('touchend', (e) => { const d = touch; @@ -12500,7 +12519,7 @@ export function messagesFeature(ctx) { const a = d.track[0], b = d.track[d.track.length - 1]; const vel = a && b && b.t > a.t ? (b.v - a.v) / (b.t - a.t) : 0; const go = Math.abs(delta) > size * SWIPE_FRAC || (Math.abs(delta) >= FLICK_MIN && Math.abs(vel) > FLICK && Math.sign(vel) === Math.sign(delta)); - settle(d, go ? (delta < 0 ? 1 : -1) : 0); + settle(d, go ? (delta < 0 ? 1 : -1) : 0, go ? vel : 0); return; } if (Date.now() - d.at > 500) return; // a press, not a tap @@ -12715,13 +12734,13 @@ export function messagesFeature(ctx) { e.preventDefault(); const from = pager.onTail || !here ? pager.tail : here.closest('.mp-group'); const to = from && (v > 0 ? from.nextElementSibling : from.previousElementSibling); - if (to) pager.scroller.scrollTo({ top: to.offsetTop, behavior: 'smooth' }); + if (to) pagerGlide(pager.scroller, 'y', to.offsetTop); return; } if (hz) { e.preventDefault(); const to = !pager.onTail && here && (hz > 0 ? here.nextElementSibling : here.previousElementSibling); - if (to) here.parentElement.scrollTo({ left: to.offsetLeft, behavior: 'smooth' }); + if (to) pagerGlide(here.parentElement, 'x', to.offsetLeft); return; } if (e.key === ' ' && pager.cur) { diff --git a/tools/pager-tap-slop-test.js b/tools/pager-tap-slop-test.js index c8d3609..b0bd339 100644 --- a/tools/pager-tap-slop-test.js +++ b/tools/pager-tap-slop-test.js @@ -123,6 +123,16 @@ try { await drag(195, 500, 0, -80, 2); s = await st(); check('a quick 80px flick up: the next post', s.on === was, JSON.stringify(s)); + + console.log('\n[snappy: let go, and it lands at once]'); + await drag(195, 500, 0, 300, 8); + await page.touchscreen.touchStart(195, 650); + for (let i = 1; i <= 8; i++) { await page.touchscreen.touchMove(195, 650 - 40 * i); await sleep(16); } + // from the finger lifting to the next slide resting square on the screen + await page.evaluate(() => { window.__landed = new Promise((res) => { const t0 = performance.now(); const look = () => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 1 && Math.abs(r.left) < 1; }); if (s && !s.getAttribute('data-k').endsWith('/tall.webm')) res(Math.round(performance.now() - t0)); else if (performance.now() - t0 > 2000) res(-1); else requestAnimationFrame(look); }; requestAnimationFrame(look); }); }); + await page.touchscreen.touchEnd(); + const ms = await page.evaluate(() => window.__landed); + check('a swipe lands on the next post within 300ms', ms > 0 && ms <= 300, ms + 'ms'); check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\nā
a little movement is still a tap' : '\nā failed'); |
