aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-15 09:10:51 -0700
committerAdam Soltys <[email protected]>2026-10-15 09:10:51 -0700
commit2f1998d67c6878026dbd9666807e8ea24cbbfdd6 (patch)
tree6d06c3af046ffc5653049d2fb66eb2667fabbf22
parent6ecc636c4a16b343f07d65c953c678762a1f175a (diff)
downloadcoinosv3-2f1998d67c6878026dbd9666807e8ea24cbbfdd6.tar.gz
coinosv3-2f1998d67c6878026dbd9666807e8ea24cbbfdd6.zip
Media pager: a flick flies from an ordinary brisk swipe, and further. A flight took a hard fling (3 px/ms, coasting 500 ms) — Adam, 2026-10-16: "too hard to get it to fly". Now from 1.6 px/ms, coasting 800 ms (so a brisk swipe is two posts, a hard flick five or six), and the finger's speed is its average over its last 80 ms rather than its last five samples — at a phone's 120 Hz those were a third of that, and the finger slowing as it lifts hid the flick. tools/pager-swipe-fly-test.js: the quick flick is three big moves on a schedule from the start (a move's handling in a headless browser takes 30–50 ms, so a paced flick came out a stroll), flying three to six posts; a 320px swipe over 130ms is still one post; pager-tap-slop and profile-pager tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
-rw-r--r--src/features/messages.js7
-rw-r--r--tools/pager-swipe-fly-test.js27
2 files changed, 20 insertions, 14 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 4a154c8..7e92374 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -12763,7 +12763,10 @@ export function messagesFeature(ctx) {
// fling would have coasted — the speed times FLY_TAU, in screens — from
// FLY_V up; a slower swipe is the one post it was. The ends are not
// flown past: a fling that would, lands on the last (or first) post.
- const FLY_V = 3, FLY_TAU = 500, FLY_MAX = 6; // px/ms; ms; posts
+ // (3 px/ms and a 500 ms coast took a hard flick to fly at all — Adam,
+ // 2026-10-16: "too hard to get it to fly".)
+ const FLY_V = 1.6, FLY_TAU = 800, FLY_MAX = 6; // px/ms; ms; posts
+ const TRACK_MS = 80; // the speed is the finger's over its last moment, not its last few samples
let touch = null, pendingTap = 0, tapDoneAt = 0;
el.addEventListener('touchstart', (e) => {
const t = e.touches[0];
@@ -12791,7 +12794,7 @@ export function messagesFeature(ctx) {
d.far = Math.max(d.far || 0, Math.hypot(dx, dy));
if (d.axis === 'y') d.box.scrollTop = d.start - delta; else d.box.scrollLeft = d.start - delta;
d.track.push({ t: performance.now(), v: delta }); // (when handled: synthetic moves can share a timeStamp)
- if (d.track.length > 5) d.track.shift();
+ while (d.track.length > 2 && d.track[d.track.length - 1].t - d.track[1].t >= TRACK_MS) d.track.shift();
}, { passive: false });
// let go of a drag: on to the next (or the one before), or back
function settle(d, step, vel = 0) {
diff --git a/tools/pager-swipe-fly-test.js b/tools/pager-swipe-fly-test.js
index 63af647..36fa816 100644
--- a/tools/pager-swipe-fly-test.js
+++ b/tools/pager-swipe-fly-test.js
@@ -35,9 +35,10 @@ const waitFor = async (fn, arg, ms = 10000) => { for (let i = 0; i < ms / 100; i
const at = () => page.evaluate(() => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 1 && Math.abs(r.left) < 1; }); return s ? Number(/p(\d+)\.png/.exec(s.getAttribute('data-k'))[1]) : 'between'; });
const settled = async () => { let last = null; for (let i = 0; i < 40; i++) { const t = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop); if (t === last) { await sleep(100); if (await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop) === t) return; } last = t; await sleep(50); } };
// a swipe up the screen: px of travel over about `ms` milliseconds. A
-// quick one is dispatched inside the page, paced to the millisecond: the
-// browser's own touch simulation takes a round trip per move, so a flick
-// through it is never faster than a stroll.
+// quick one is dispatched inside the page, as fast as the handlers take
+// the moves (about 30–50 ms each here, headless): the browser's own touch
+// simulation takes a round trip per move on top, so a flick through it is
+// never faster than a stroll.
const swipe = async (px, ms, steps = 6) => {
if (ms >= 100) {
await page.touchscreen.touchStart(195, 700);
@@ -49,9 +50,11 @@ const swipe = async (px, ms, steps = 6) => {
const el = document.querySelector('.mp-scroll'), target = document.elementFromPoint(195, 700) || el;
const touch = (y) => new Touch({ identifier: 1, target, clientX: 195, clientY: y, pageX: 195, pageY: y });
const fire = (type, y, up) => target.dispatchEvent(new TouchEvent(type, { bubbles: true, cancelable: true, touches: up ? [] : [touch(y)], changedTouches: [touch(y)], targetTouches: up ? [] : [touch(y)] }));
- const spin = (t) => { const t0 = performance.now(); while (performance.now() - t0 < t); };
+ // on a schedule from the start, not a pause after each move: a move's
+ // handling (the slides following the finger) takes its own time
+ const t0 = performance.now();
fire('touchstart', 700);
- for (let i = 1; i <= steps; i++) { spin(ms / steps); fire('touchmove', 700 - px * i / steps); }
+ for (let i = 1; i <= steps; i++) { while (performance.now() < t0 + ms * i / steps); fire('touchmove', 700 - px * i / steps); }
fire('touchend', 700 - px, true);
}, [px, ms, steps]);
};
@@ -88,24 +91,24 @@ try {
console.log('\n[a fast flick: several posts, landing snapped]');
const trace = async (sw) => { await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); window.__tr = []; const t0 = performance.now(); const rec = () => { window.__tr.push(sc.scrollTop); if (performance.now() - t0 < 1500) requestAnimationFrame(rec); }; requestAnimationFrame(rec); }); await sw(); await sleep(1600); return page.evaluate(() => window.__tr); };
- const tr = await trace(() => swipe(420, 72)); await settled(); // ~5.8 px/ms: three posts' worth of coast
+ const tr = await trace(() => swipe(600, 80, 3)); await settled(); // three big moves: 3–6 px/ms as their handling takes, three posts' worth of coast and up
const landed = await at();
- check('a 420px flick in 72ms flies three posts on', landed === 5, String(landed));
+ check('a 600px flick in a few frames flies three to six posts on', landed >= 5 && landed <= 8, String(landed));
check('…landing square on a post', landed !== 'between');
check('…in a glide, never back', tr.every((x, i) => !i || x >= tr[i - 1] - 0.5), tr.slice(0, 12).map((x) => Math.round(x)).join(' '));
const seen = await page.evaluate(() => document.querySelector('.mp-position').textContent);
check('the counter says where it landed', seen === landed + ' / ' + N, seen);
console.log('\n[a flick near the end: the last post, not past it]');
- await page.keyboard.press('ArrowDown'); await settled(); await page.keyboard.press('ArrowDown'); await settled();
+ while ((await at()) < 7) { await page.keyboard.press('ArrowDown'); await settled(); }
const before = await at();
- await swipe(420, 72); await settled();
- check('from post ' + before + ' (three posts\' worth would be past the end), a flick lands on the last post', before === 7 && (await at()) === N, String(await at()));
- await swipe(420, 72); await settled();
+ await swipe(600, 80, 3); await settled();
+ check('from post ' + before + ' (a flight\'s worth would be past the end), a flick lands on the last post', before >= 7 && (await at()) === N, String(await at()));
+ await swipe(600, 80, 3); await settled();
check('…and one more flick at the end wraps to the first', (await at()) === 1, String(await at()));
console.log('\n[a finger down mid-flight catches it]');
- await swipe(420, 72);
+ await swipe(600, 80, 3);
await sleep(120);
const mid = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop);
await page.touchscreen.touchStart(195, 500);