aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-13 18:24:57 +0000
committerAdam Soltys <[email protected]>2026-10-13 18:24:57 +0000
commit5550e929641b282a3de2c2cda66782d7c58689a1 (patch)
tree7b5bb4f0c2bc634cf38aabc73fdcdd9363296262
parentf3d03c642bef06b3afc8a3038136c5e0fc1e21f7 (diff)
downloadcoinosv3-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.js33
-rw-r--r--tools/pager-tap-slop-test.js10
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');