gtauri viewer: two-image swipe commit — outgoing stays until off-screen
Fix: on release the old image vanished instantly and only the new one slid in. Now both images live on the page strip for the whole commit: vimg (outgoing) slides off-screen while vimg2 (incoming, the drag preview) slides home, over a 200 ms smoothstep; only then do the elements swap roles (vimg.src = the already-decoded preview image, vimg2 hidden after the swap paints — no blank frame). Details: - navigate() returns whether it committed; at the library edges it refuses and the drag snaps back (previously a past-the-edge drag could leave the image stuck mid-strip). - commit tweens are locked: pointer/dblclick input is ignored for the ~200 ms so a second gesture can't fight the animation. - openViewer gains a keepVisible (promoted) path that skips the hide+thumb round-trip; vFullSeq reset per open (safety-net counter could previously exhaust after ~11 opens). - removed the now-dead slideFrom slide-in mechanism (its purpose is subsumed by the two-image commit). Verified on the emulator: mid-commit frame shows both images (outgoing sliding off left, incoming from right); snap-back slides both back; edge drag right on the first image snaps home.
This commit is contained in:
parent
16cd868f22
commit
029780cc09
92
harnesses/gtauri/frontend/dist/index.html
vendored
92
harnesses/gtauri/frontend/dist/index.html
vendored
|
|
@ -383,8 +383,6 @@ function vApply() {
|
|||
|
||||
let viewerId = null, vFullTimer = null, vFullDone = false, vFullSeq = 0;
|
||||
|
||||
let slideFrom = null; // pending slide start offset for a swiped-in image
|
||||
|
||||
/* Live swipe preview: while a horizontal drag is in flight at fit
|
||||
scale, the picture being swiped IN renders on the page strip
|
||||
(vimg2) and follows the finger — no blank side. Its grid thumb is
|
||||
|
|
@ -428,15 +426,7 @@ function fitViewer(w, h) {
|
|||
vimg.style.height = h + "px";
|
||||
vimg.style.visibility = "visible";
|
||||
vload.textContent = "";
|
||||
if (slideFrom != null) {
|
||||
// Incoming swiped image: start where the page strip puts it and
|
||||
// slide home. The tween owns the transform from here on.
|
||||
vTx = slideFrom; vTy = 0;
|
||||
slideFrom = null;
|
||||
tweenTo(0, 0, 150);
|
||||
return;
|
||||
}
|
||||
if (tween) return; // settle/slide owns the transform right now
|
||||
if (tween) return; // a settle/commit animation owns the transform
|
||||
vApply();
|
||||
}
|
||||
|
||||
|
|
@ -457,16 +447,24 @@ function tryShow(id, url, isFull) {
|
|||
im.src = url;
|
||||
}
|
||||
|
||||
function openViewer(id, fromX) {
|
||||
/// openViewer(id, keepVisible): keepVisible = the image was promoted
|
||||
/// by a swipe commit — vimg ALREADY shows its thumbnail, so don't
|
||||
/// hide it or re-request the thumb (only full-res is wanted).
|
||||
function openViewer(id, keepVisible) {
|
||||
viewerId = id;
|
||||
navHide(); // the incoming image takes over from the preview
|
||||
slideFrom = (fromX == null) ? null : fromX;
|
||||
if (!keepVisible) navHide();
|
||||
vFullDone = false;
|
||||
vFullSeq = 0;
|
||||
vScale = 1; vTx = 0; vTy = 0;
|
||||
vload.textContent = "loading…";
|
||||
vimg.style.visibility = "hidden";
|
||||
// 1) INSTANT: the grid thumb (Rust mem cache, ~1 ms).
|
||||
tryShow(id, "http://gallery.localhost/thumb/" + id, false);
|
||||
if (keepVisible) {
|
||||
vimg.style.visibility = "visible";
|
||||
vload.textContent = "";
|
||||
} else {
|
||||
vload.textContent = "loading…";
|
||||
vimg.style.visibility = "hidden";
|
||||
// 1) INSTANT: the grid thumb (Rust mem cache, ~1 ms).
|
||||
tryShow(id, "http://gallery.localhost/thumb/" + id, false);
|
||||
}
|
||||
// 2) Full-res: Rust reads the file OFF the main thread; first request
|
||||
// gets the placeholder, `full-ready` triggers the swap.
|
||||
tryShow(id, "http://gallery.localhost/full/" + id, true);
|
||||
|
|
@ -545,17 +543,52 @@ function tweenTo(tx, ty, ms, onDone) {
|
|||
}
|
||||
|
||||
/// Go to the neighbor `delta` steps down the list (+1 = swipe left).
|
||||
/// The incoming image starts on the page strip where the drag left
|
||||
/// off and slides home.
|
||||
/// 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) {
|
||||
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;
|
||||
const from = vTx + delta * innerWidth;
|
||||
openViewer(ids[ni], from);
|
||||
if (i < 0 || ni < 0 || ni >= ids.length) return false;
|
||||
commitTo(ni, delta);
|
||||
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) {
|
||||
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 step = (now) => {
|
||||
const t = Math.min(1, (now - t0) / ms);
|
||||
const e = t * t * (3 - 2 * t); // smoothstep: pick up, land soft
|
||||
vTx = x0 + (out - x0) * e;
|
||||
vTy = 0;
|
||||
applyTransform();
|
||||
navLayout();
|
||||
if (t < 1) {
|
||||
tween = { raf: requestAnimationFrame(step), commit: true };
|
||||
} else {
|
||||
// vimg2 (incoming) is exactly at 0: it becomes primary.
|
||||
const id = ids[ni];
|
||||
vimg.src = vimg2.src; // cached + decoded → paints immediately
|
||||
vTx = 0;
|
||||
applyTransform();
|
||||
tween = null;
|
||||
openViewer(id, true);
|
||||
// Hide the outgoing preview only once the primary has the bytes.
|
||||
vimg.onload = () => { if (viewerId === id) navHide(); };
|
||||
}
|
||||
};
|
||||
tween = { raf: requestAnimationFrame(step), commit: true };
|
||||
}
|
||||
|
||||
viewer.addEventListener("pointerdown", (e) => {
|
||||
if (tween && tween.commit) return; // a swipe commit is in flight
|
||||
viewer.setPointerCapture(e.pointerId);
|
||||
if (tween) { cancelAnimationFrame(tween.raf); tween = null; }
|
||||
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
|
|
@ -640,15 +673,22 @@ const endPt = (e) => {
|
|||
if (pts.size === 0 && g && g.mode === "swipe") {
|
||||
const dx = e.clientX - g.x0;
|
||||
const w = innerWidth;
|
||||
if (dx < -w * 0.25 || g.vx < -0.35) navigate(+1);
|
||||
else if (dx > w * 0.25 || g.vx > 0.35) navigate(-1);
|
||||
else tweenTo(0, 0, 120, navHide); // not far enough: snap back
|
||||
// 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);
|
||||
} else if (dx > w * 0.25 || g.vx > 0.35) {
|
||||
if (!navigate(-1)) tweenTo(0, 0, 120, navHide);
|
||||
} else {
|
||||
tweenTo(0, 0, 120, navHide); // not far enough: snap back
|
||||
}
|
||||
}
|
||||
gesture = null;
|
||||
};
|
||||
viewer.addEventListener("pointerup", endPt);
|
||||
viewer.addEventListener("pointercancel", endPt);
|
||||
viewer.addEventListener("dblclick", () => {
|
||||
if (tween && tween.commit) return;
|
||||
vScale = fitScale();
|
||||
vTx = 0; vTy = 0; vApply();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user