gtauri viewer: mid-commit touch/key takes over the swipe (chained swipes)

A second swipe while the new image was still sliding in got dropped
(pointerdown bailed while the commit tween was in flight). Now a
touch — or arrow key — during a commit takes it over: the commit
finishes instantly (roles swap at the settle positions, same
dimension-adoption path as natural completion) and the input starts a
fresh gesture on the landed image, so rapid successive swipes chain
through multiple pictures. A plain tap during a commit simply settles
it early.

Verified on the emulator: two 100 ms flicks 120 ms apart (the second
lands mid-commit) end on the exact same image — pixel-identical — as
two deliberate slow swipes.
This commit is contained in:
Greg Pomerantz 2026-11-03 13:47:39 -05:00
parent cd31b76889
commit 695e2417cc

View File

@ -547,7 +547,8 @@ function tweenTo(tx, ty, ms, onDone) {
/// 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, vel) {
if (tween && tween.commit) return false; // commit already in flight
// Key nav during a commit takes it over the same way a touch does.
if (tween && tween.commit) takeover();
const i = ids.indexOf(viewerId);
const ni = i + delta;
if (i < 0 || ni < 0 || ni >= ids.length) return false;
@ -585,38 +586,55 @@ function commitTo(ni, delta, vel) {
applyTransform();
navLayout();
if (t < 1) {
tween = { raf: requestAnimationFrame(step), commit: true };
tween = { raf: requestAnimationFrame(step), commit: true, target: ni };
} else {
const id = ids[ni];
tween = null;
if (nav && nav.id === id && nav.ready) {
// vimg2 (incoming) is exactly at 0: it becomes primary. Adopt
// its dimensions NOW — otherwise the new pixels render in the
// OLD image's width/height box (wrong aspect) until full-res
// lands and fitViewer() fixes it (visible stretch → snap).
const nw = nav.w, nh = nav.h, ns = nav.scale;
vimg.src = vimg2.src; // cached + decoded → paints immediately
base.w = nw; base.h = nh;
vScale = ns;
vimg.style.width = nw + "px";
vimg.style.height = nh + "px";
vTx = 0; vTy = 0;
applyTransform();
openViewer(id, true);
// Hide the preview only once the primary has the bytes.
vimg.onload = () => { if (viewerId === id) navHide(); };
} else {
// Preview never made it (keyboard nav / slow thumb): plain
// open — the thumb round-trip sets the right dimensions.
openViewer(id, false);
}
finishCommit(ni);
}
};
tween = { raf: requestAnimationFrame(step), commit: true };
tween = { raf: requestAnimationFrame(step), commit: true, target: ni };
}
/// Swap the elements' roles at the settled positions: vimg2 (the
/// incoming image) becomes primary at 0 with its own dimensions (see
/// the aspect note there), vimg shows it next paint, the old image is
/// gone. Called when the commit tween completes AND on mid-flight
/// takeover — a new touch/key during a commit finishes it instantly
/// so the new gesture acts on the landed image (swipes chain).
function finishCommit(ni) {
const id = ids[ni];
if (nav && nav.id === id && nav.ready) {
const nw = nav.w, nh = nav.h, ns = nav.scale;
vimg.src = vimg2.src; // cached + decoded → paints immediately
base.w = nw; base.h = nh;
vScale = ns;
vimg.style.width = nw + "px";
vimg.style.height = nh + "px";
vTx = 0; vTy = 0;
applyTransform();
openViewer(id, true);
// Hide the preview only once the primary has the bytes.
vimg.onload = () => { if (viewerId === id) navHide(); };
} else {
// Preview never made it (keyboard nav / slow thumb): plain open —
// the thumb round-trip sets the right dimensions.
openViewer(id, false);
}
}
/// Instantly settle an in-flight commit (touch/key takeover).
function takeover() {
const target = tween.target;
cancelAnimationFrame(tween.raf);
tween = null;
finishCommit(target);
}
viewer.addEventListener("pointerdown", (e) => {
if (tween && tween.commit) return; // a swipe commit is in flight
// A touch during a swipe commit takes it over: finish the commit
// now and start this gesture on the landed image — a second swipe
// while the new image is sliding in acts as another swipe.
if (tween && tween.commit) takeover();
viewer.setPointerCapture(e.pointerId);
if (tween) { cancelAnimationFrame(tween.raf); tween = null; }
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
@ -716,7 +734,6 @@ const endPt = (e) => {
viewer.addEventListener("pointerup", endPt);
viewer.addEventListener("pointercancel", endPt);
viewer.addEventListener("dblclick", () => {
if (tween && tween.commit) return;
vScale = fitScale();
vTx = 0; vTy = 0; vApply();
});