diff --git a/harnesses/gtauri/frontend/dist/index.html b/harnesses/gtauri/frontend/dist/index.html index 18f5c6f..a6e97bd 100644 --- a/harnesses/gtauri/frontend/dist/index.html +++ b/harnesses/gtauri/frontend/dist/index.html @@ -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(); });