gtauri viewer: velocity-scaled swipe commit (faster flicks)

Flicks now continue the finger's motion instead of taking a fixed
200 ms: commit duration = remaining distance / release velocity
(15 % snappier), clamped 80-240 ms, ease-out. A full-speed flick
releasing past half the screen commits in ~80 ms (was 200). Slow
releases past the 25 % threshold still settle gently (180 ms
smoothstep; 150 ms for keyboard nav).

Verified on the emulator: a 100 ms flick is fully settled before
the first post-release screencap (~150 ms), with only a 5 px
edge artifact between consecutive frames (no residual motion).
This commit is contained in:
Greg Pomerantz 2026-11-03 13:28:43 -05:00
parent ba646fb233
commit cd31b76889

View File

@ -546,26 +546,40 @@ function tweenTo(tx, ty, ms, onDone) {
/// Both images stay on the page strip for the whole animation:
/// the incoming one slides home while the outgoing one slides off
/// screen; only then do the roles swap (returns false at the edges).
function navigate(delta) {
function navigate(delta, vel) {
if (tween && tween.commit) return false; // commit already in flight
const i = ids.indexOf(viewerId);
const ni = i + delta;
if (i < 0 || ni < 0 || ni >= ids.length) return false;
commitTo(ni, delta);
commitTo(ni, delta, vel || 0);
return true;
}
/// Commit a swipe: animate vTx to the outgoing offset (the incoming
/// preview tracks at vTx + delta*W and lands exactly on 0), then swap
/// the elements' roles. The commit tween is locked: input is ignored
/// until it finishes (~200 ms).
function commitTo(ni, delta) {
/// until it finishes. `vel` = release velocity in px/ms: a flick
/// continues the finger's motion (duration scales with the remaining
/// distance at that velocity, ease-out — fast flicks are fast), a
/// slow release settles gently.
function commitTo(ni, delta, vel) {
navSet(ids[ni], delta); // the incoming (no-op if the drag showed it)
const out = -delta * innerWidth;
const x0 = vTx, t0 = performance.now(), ms = 200;
const x0 = vTx, t0 = performance.now();
let ms, ease;
if (vel > 0.35) {
const remain = Math.abs(out - x0);
// Time to cover the remaining distance at the release velocity,
// 15 % snappier than the raw continuation, clamped 80–240 ms.
ms = Math.max(80, Math.min(240, remain / vel * 0.85));
ease = (t) => 1 - Math.pow(1 - t, 3); // ease-out: land soft
} else {
ms = vel > 0 ? 180 : 150; // drag-settle / keyboard
ease = (t) => t * t * (3 - 2 * t);
}
const step = (now) => {
const t = Math.min(1, (now - t0) / ms);
const e = t * t * (3 - 2 * t); // smoothstep: pick up, land soft
const e = ease(t);
vTx = x0 + (out - x0) * e;
vTy = 0;
applyTransform();
@ -690,9 +704,9 @@ const endPt = (e) => {
// Commit past the threshold; at the library edges navigate
// refuses (nothing to commit to) and we snap back instead.
if (dx < -w * 0.25 || g.vx < -0.35) {
if (!navigate(+1)) tweenTo(0, 0, 120, navHide);
if (!navigate(+1, Math.abs(g.vx))) tweenTo(0, 0, 120, navHide);
} else if (dx > w * 0.25 || g.vx > 0.35) {
if (!navigate(-1)) tweenTo(0, 0, 120, navHide);
if (!navigate(-1, Math.abs(g.vx))) tweenTo(0, 0, 120, navHide);
} else {
tweenTo(0, 0, 120, navHide); // not far enough: snap back
}