aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-13 18:32:23 +0000
committerAdam Soltys <[email protected]>2026-10-13 18:32:23 +0000
commit35feb53c50cf5662aca40e9af5a0547e36297631 (patch)
tree4dd6056d8c4c81f07cbbe282b3a74c518a496a8f
parent5550e929641b282a3de2c2cda66782d7c58689a1 (diff)
downloadcoinosv3-35feb53c50cf5662aca40e9af5a0547e36297631.tar.gz
coinosv3-35feb53c50cf5662aca40e9af5a0547e36297631.zip
Pager glide: land on the slide itself, no overshoot and snap-back; quicker
The glide aimed at n x the scroller's height, but a post is 100dvh, a hair off it on a phone; the error grew post by post until it landed past the slide and the snap yanked it back. It now aims at the measured position of the next slide. Faster too: at most 170ms (lands in ~195ms from lift-off). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
-rw-r--r--src/features/messages.js22
-rw-r--r--tools/pager-tap-slop-test.js24
2 files changed, 38 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index a7dc54c..6dbb4fd 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -12442,7 +12442,7 @@ export function messagesFeature(ctx) {
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 ms = Math.max(80, Math.min(170, Math.abs(dist) / Math.max(3.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);
@@ -12451,6 +12451,13 @@ export function messagesFeature(ctx) {
};
box._glide = requestAnimationFrame(step);
}
+ // Where a slide (or post) sits in its scroller — measured, not counted in
+ // screens: a post is 100dvh, a hair off the scroller's height, and n
+ // screens down landed past it for the snap to yank back.
+ const pagerAt = (box, el, y) => {
+ const b = box.getBoundingClientRect(), r = el.getBoundingClientRect();
+ return y ? box.scrollTop + r.top - b.top : box.scrollLeft + r.left - b.left;
+ };
const pagerHold = (box) => { if (box && box._glide) { cancelAnimationFrame(box._glide); box._glide = 0; } };
function buildPager(want) {
@@ -12503,10 +12510,11 @@ export function messagesFeature(ctx) {
// let go of a drag: on to the next (or the one before), or back
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));
- pagerGlide(box, d.axis, to * size, vel);
+ const stops = [...box.children].filter((c) => c.matches(y ? '.mp-group, .mp-tail' : '.mp-slide')).map((c) => pagerAt(box, c, y));
+ if (!stops.length) return;
+ // the stop it set off from, then one along (or back)
+ const from = stops.reduce((bi, p, i) => (Math.abs(p - d.start) < Math.abs(stops[bi] - d.start) ? i : bi), 0);
+ pagerGlide(box, d.axis, stops[Math.min(stops.length - 1, Math.max(0, from + step))], vel);
}
el.addEventListener('touchend', (e) => {
const d = touch;
@@ -12734,13 +12742,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) pagerGlide(pager.scroller, 'y', to.offsetTop);
+ if (to) pagerGlide(pager.scroller, 'y', pagerAt(pager.scroller, to, true));
return;
}
if (hz) {
e.preventDefault();
const to = !pager.onTail && here && (hz > 0 ? here.nextElementSibling : here.previousElementSibling);
- if (to) pagerGlide(here.parentElement, 'x', to.offsetLeft);
+ if (to) pagerGlide(here.parentElement, 'x', pagerAt(here.parentElement, to, false));
return;
}
if (e.key === ' ' && pager.cur) {
diff --git a/tools/pager-tap-slop-test.js b/tools/pager-tap-slop-test.js
index b0bd339..bcc790c 100644
--- a/tools/pager-tap-slop-test.js
+++ b/tools/pager-tap-slop-test.js
@@ -132,7 +132,29 @@ try {
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('a swipe lands on the next post within 250ms', ms > 0 && ms <= 250, ms + 'ms');
+
+ console.log('\n[no overshoot: a post not quite a screen tall, as 100dvh can be on a phone]');
+ await page.addStyleTag({ content: '.mp-group, .mp-tail { height: calc(100dvh + 12px) !important; }' });
+ await page.evaluate(() => { document.querySelector('.mp-scroll').scrollTop = 0; });
+ await sleep(600);
+ // every frame's scroll, from the finger lifting until well after it lands
+ const glide = async () => {
+ await page.touchscreen.touchStart(195, 650);
+ for (let i = 1; i <= 8; i++) { await page.touchscreen.touchMove(195, 650 - 40 * i); await sleep(16); }
+ await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); window.__trace = []; const t0 = performance.now(); const rec = () => { window.__trace.push(sc.scrollTop); if (performance.now() - t0 < 900) requestAnimationFrame(rec); }; requestAnimationFrame(rec); });
+ await page.touchscreen.touchEnd();
+ await sleep(1100);
+ return page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); const tops = [...sc.querySelectorAll('.mp-group, .mp-tail')].map((g) => g.getBoundingClientRect().top); return { trace: window.__trace, off: Math.min(...tops.map(Math.abs)) }; });
+ };
+ for (let n = 1; n <= 2; n++) {
+ const g = await glide();
+ const end = g.trace[g.trace.length - 1];
+ const past = Math.max(...g.trace.map((x) => x - end));
+ const back = g.trace.some((x, i) => i && x < g.trace[i - 1] - 0.5);
+ check('swipe ' + n + ': straight on to the post, never past it and back', past <= 1 && !back, g.trace[0].toFixed(1) + '→' + end.toFixed(1) + ', past by ' + past.toFixed(1) + 'px' + (back ? ', moved back' : ''));
+ check('…resting square on it', g.off < 1, g.off.toFixed(2) + 'px off');
+ }
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');