gtauri viewer: swap element roles on commit — no resize on takeover
A second drag while the first was settling showed the new image briefly at the wrong size: the commit copied the preview's src into vimg (an async paint — one frame of old content in the new box) and openViewer then reset vScale=1, so the promoted image rendered at thumb scale until full-res landed. Commit now swaps the ELEMENTS' roles instead of copying pixels: the preview element already shows the incoming image at its own size and scale, so finishCommit promotes it in place (geometry adopted from the loaded preview, moved to 0, brought to front, old primary hidden immediately). No src copy, no async paint, no re-layout — the settled frame is the drag frame. P/S role variables replace the fixed vimg/vimg2 references throughout the viewer. openViewer(keepVisible) no longer resets the view state (the commit already set it), and a full-res landing while a slide owns base/vScale is deferred to the safety timer instead of jumping the animation. Verified on the emulator: mid second-drag the promoted image holds its exact fit size (1081 px) in every captured frame (the bug showed ~288), both images visible on the strip, and the chained landing is pixel-identical to two deliberate swipes.
This commit is contained in:
parent
695e2417cc
commit
347b6825cc
95
harnesses/gtauri/frontend/dist/index.html
vendored
95
harnesses/gtauri/frontend/dist/index.html
vendored
|
|
@ -368,6 +368,12 @@ function fitScale() {
|
||||||
return Math.min(innerWidth / base.w, innerHeight / base.h);
|
return Math.min(innerWidth / base.w, innerHeight / base.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// P = the element showing the current image, S = the swipe-preview
|
||||||
|
/// element. A commit PROMOTES S (it already shows the incoming image
|
||||||
|
/// at its own size) and demotes P — the elements swap roles, so no
|
||||||
|
/// src copy / re-decode / re-layout ever happens on a swipe.
|
||||||
|
let P = vimg, S = vimg2;
|
||||||
|
|
||||||
function vApply() {
|
function vApply() {
|
||||||
// Clamp: never zoom out past fit; pan limited to image edges — no
|
// Clamp: never zoom out past fit; pan limited to image edges — no
|
||||||
// blank space around the image, letterboxed axes stay centered.
|
// blank space around the image, letterboxed axes stay centered.
|
||||||
|
|
@ -377,44 +383,44 @@ function vApply() {
|
||||||
const my = Math.max(0, (base.h * vScale - innerHeight) / 2);
|
const my = Math.max(0, (base.h * vScale - innerHeight) / 2);
|
||||||
if (vTx > mx) vTx = mx; else if (vTx < -mx) vTx = -mx;
|
if (vTx > mx) vTx = mx; else if (vTx < -mx) vTx = -mx;
|
||||||
if (vTy > my) vTy = my; else if (vTy < -my) vTy = -my;
|
if (vTy > my) vTy = my; else if (vTy < -my) vTy = -my;
|
||||||
vimg.style.transform =
|
P.style.transform =
|
||||||
"translate(" + (vTx - base.w * vScale / 2) + "px," + (vTy - base.h * vScale / 2) + "px) scale(" + vScale + ")";
|
"translate(" + (vTx - base.w * vScale / 2) + "px," + (vTy - base.h * vScale / 2) + "px) scale(" + vScale + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewerId = null, vFullTimer = null, vFullDone = false, vFullSeq = 0;
|
let viewerId = null, vFullTimer = null, vFullDone = false, vFullSeq = 0;
|
||||||
|
|
||||||
/* Live swipe preview: while a horizontal drag is in flight at fit
|
/* Live swipe preview: while a horizontal drag is in flight at fit
|
||||||
scale, the picture being swiped IN renders on the page strip
|
scale, the picture being swiped IN renders on the page strip (S)
|
||||||
(vimg2) and follows the finger — no blank side. Its grid thumb is
|
and follows the finger — no blank side. Its grid thumb is a mem
|
||||||
a mem hit (~1 ms) in the common case (the grid already requested
|
hit (~1 ms) in the common case (the grid already requested its
|
||||||
its neighbors' thumbs). */
|
neighbors' thumbs). */
|
||||||
let nav = null; // {id, dir(+1 = right/next, -1 = left/prev), w, h, scale, ready}
|
let nav = null; // {id, dir(+1 = right/next, -1 = left/prev), w, h, scale, ready}
|
||||||
function navSet(id, dir) {
|
function navSet(id, dir) {
|
||||||
if (nav && nav.id === id) return;
|
if (nav && nav.id === id) return;
|
||||||
navHide();
|
navHide();
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
nav = { id, dir, w: 0, h: 0, scale: 1, ready: false };
|
nav = { id, dir, w: 0, h: 0, scale: 1, ready: false };
|
||||||
vimg2.style.visibility = "hidden";
|
S.style.visibility = "hidden";
|
||||||
vimg2.onload = () => {
|
S.onload = () => {
|
||||||
if (!nav || nav.id !== id || vimg2.naturalWidth <= 2) return;
|
if (!nav || nav.id !== id || S.naturalWidth <= 2) return;
|
||||||
nav.w = vimg2.naturalWidth; nav.h = vimg2.naturalHeight;
|
nav.w = S.naturalWidth; nav.h = S.naturalHeight;
|
||||||
nav.scale = Math.min(innerWidth / nav.w, innerHeight / nav.h);
|
nav.scale = Math.min(innerWidth / nav.w, innerHeight / nav.h);
|
||||||
nav.ready = true;
|
nav.ready = true;
|
||||||
vimg2.style.width = nav.w + "px";
|
S.style.width = nav.w + "px";
|
||||||
vimg2.style.height = nav.h + "px";
|
S.style.height = nav.h + "px";
|
||||||
vimg2.style.visibility = "visible";
|
S.style.visibility = "visible";
|
||||||
navLayout();
|
navLayout();
|
||||||
};
|
};
|
||||||
vimg2.src = "http://gallery.localhost/thumb/" + id;
|
S.src = "http://gallery.localhost/thumb/" + id;
|
||||||
}
|
}
|
||||||
function navHide() {
|
function navHide() {
|
||||||
nav = null;
|
nav = null;
|
||||||
vimg2.style.visibility = "hidden";
|
S.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
function navLayout() {
|
function navLayout() {
|
||||||
if (!nav || !nav.ready) return;
|
if (!nav || !nav.ready) return;
|
||||||
const s = nav.scale;
|
const s = nav.scale;
|
||||||
vimg2.style.transform =
|
S.style.transform =
|
||||||
"translate(" + (vTx + nav.dir * innerWidth - nav.w * s / 2) + "px," +
|
"translate(" + (vTx + nav.dir * innerWidth - nav.w * s / 2) + "px," +
|
||||||
(0 - nav.h * s / 2) + "px) scale(" + s + ")";
|
(0 - nav.h * s / 2) + "px) scale(" + s + ")";
|
||||||
}
|
}
|
||||||
|
|
@ -422,9 +428,9 @@ function navLayout() {
|
||||||
function fitViewer(w, h) {
|
function fitViewer(w, h) {
|
||||||
base.w = w; base.h = h;
|
base.w = w; base.h = h;
|
||||||
vScale = Math.min(innerWidth / w, innerHeight / h);
|
vScale = Math.min(innerWidth / w, innerHeight / h);
|
||||||
vimg.style.width = w + "px";
|
P.style.width = w + "px";
|
||||||
vimg.style.height = h + "px";
|
P.style.height = h + "px";
|
||||||
vimg.style.visibility = "visible";
|
P.style.visibility = "visible";
|
||||||
vload.textContent = "";
|
vload.textContent = "";
|
||||||
if (tween) return; // a settle/commit animation owns the transform
|
if (tween) return; // a settle/commit animation owns the transform
|
||||||
vApply();
|
vApply();
|
||||||
|
|
@ -437,7 +443,9 @@ function tryShow(id, url, isFull) {
|
||||||
im.addEventListener("load", () => {
|
im.addEventListener("load", () => {
|
||||||
if (viewerId !== id) return;
|
if (viewerId !== id) return;
|
||||||
if (im.naturalWidth <= 2) return;
|
if (im.naturalWidth <= 2) return;
|
||||||
vimg.src = im.src;
|
if (isFull && tween) return; // a slide owns base/vScale right now;
|
||||||
|
// the safety timer re-requests shortly
|
||||||
|
P.src = im.src;
|
||||||
fitViewer(im.naturalWidth, im.naturalHeight);
|
fitViewer(im.naturalWidth, im.naturalHeight);
|
||||||
if (isFull) {
|
if (isFull) {
|
||||||
vFullDone = true;
|
vFullDone = true;
|
||||||
|
|
@ -448,20 +456,21 @@ function tryShow(id, url, isFull) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// openViewer(id, keepVisible): keepVisible = the image was promoted
|
/// openViewer(id, keepVisible): keepVisible = the image was promoted
|
||||||
/// by a swipe commit — vimg ALREADY shows its thumbnail, so don't
|
/// by a swipe commit — P ALREADY shows its thumbnail at the right
|
||||||
/// hide it or re-request the thumb (only full-res is wanted).
|
/// scale/position (finishCommit set it), so don't reset the view or
|
||||||
|
/// re-request the thumb (only full-res is wanted).
|
||||||
function openViewer(id, keepVisible) {
|
function openViewer(id, keepVisible) {
|
||||||
viewerId = id;
|
viewerId = id;
|
||||||
if (!keepVisible) navHide();
|
if (!keepVisible) navHide();
|
||||||
vFullDone = false;
|
vFullDone = false;
|
||||||
vFullSeq = 0;
|
vFullSeq = 0;
|
||||||
vScale = 1; vTx = 0; vTy = 0;
|
|
||||||
if (keepVisible) {
|
if (keepVisible) {
|
||||||
vimg.style.visibility = "visible";
|
P.style.visibility = "visible";
|
||||||
vload.textContent = "";
|
vload.textContent = "";
|
||||||
} else {
|
} else {
|
||||||
|
vScale = 1; vTx = 0; vTy = 0;
|
||||||
vload.textContent = "loading…";
|
vload.textContent = "loading…";
|
||||||
vimg.style.visibility = "hidden";
|
P.style.visibility = "hidden";
|
||||||
// 1) INSTANT: the grid thumb (Rust mem cache, ~1 ms).
|
// 1) INSTANT: the grid thumb (Rust mem cache, ~1 ms).
|
||||||
tryShow(id, "http://gallery.localhost/thumb/" + id, false);
|
tryShow(id, "http://gallery.localhost/thumb/" + id, false);
|
||||||
}
|
}
|
||||||
|
|
@ -477,7 +486,7 @@ function openViewer(id, keepVisible) {
|
||||||
}
|
}
|
||||||
if (++vFullSeq > 10) {
|
if (++vFullSeq > 10) {
|
||||||
if (vFullTimer) { clearInterval(vFullTimer); vFullTimer = null; }
|
if (vFullTimer) { clearInterval(vFullTimer); vFullTimer = null; }
|
||||||
if (viewerId === id && vimg.naturalWidth <= 2) {
|
if (viewerId === id && P.naturalWidth <= 2) {
|
||||||
vload.textContent = "image unavailable";
|
vload.textContent = "image unavailable";
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -493,7 +502,8 @@ function closeViewer() {
|
||||||
navHide();
|
navHide();
|
||||||
if (vFullTimer) { clearInterval(vFullTimer); vFullTimer = null; }
|
if (vFullTimer) { clearInterval(vFullTimer); vFullTimer = null; }
|
||||||
viewer.classList.remove("open");
|
viewer.classList.remove("open");
|
||||||
vimg.src = "";
|
P.src = "";
|
||||||
|
S.src = ""; // release any parked full-res bitmap
|
||||||
}
|
}
|
||||||
document.getElementById("vclose").addEventListener("click", closeViewer);
|
document.getElementById("vclose").addEventListener("click", closeViewer);
|
||||||
addEventListener("keydown", (e) => {
|
addEventListener("keydown", (e) => {
|
||||||
|
|
@ -520,7 +530,7 @@ function atFit() {
|
||||||
|
|
||||||
/// Raw transform write (no clamping) — used by the tween and swipes.
|
/// Raw transform write (no clamping) — used by the tween and swipes.
|
||||||
function applyTransform() {
|
function applyTransform() {
|
||||||
vimg.style.transform =
|
P.style.transform =
|
||||||
"translate(" + (vTx - base.w * vScale / 2) + "px," + (vTy - base.h * vScale / 2) + "px) scale(" + vScale + ")";
|
"translate(" + (vTx - base.w * vScale / 2) + "px," + (vTy - base.h * vScale / 2) + "px) scale(" + vScale + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -595,28 +605,29 @@ function commitTo(ni, delta, vel) {
|
||||||
tween = { raf: requestAnimationFrame(step), commit: true, target: ni };
|
tween = { raf: requestAnimationFrame(step), commit: true, target: ni };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Swap the elements' roles at the settled positions: vimg2 (the
|
/// Settle the commit and swap the elements' roles. S (the preview)
|
||||||
/// incoming image) becomes primary at 0 with its own dimensions (see
|
/// ALREADY shows the incoming image at its own size and scale — it
|
||||||
/// the aspect note there), vimg shows it next paint, the old image is
|
/// is promoted to primary in place (moved to 0, brought to front),
|
||||||
/// gone. Called when the commit tween completes AND on mid-flight
|
/// and the old primary is hidden immediately. No src copy, no async
|
||||||
/// takeover — a new touch/key during a commit finishes it instantly
|
/// paint, no re-layout: the promoted pixels were on screen the whole
|
||||||
/// so the new gesture acts on the landed image (swipes chain).
|
/// drag, so the settled frame is the drag frame — no size jump.
|
||||||
|
/// Called when the commit tween completes AND on mid-flight takeover
|
||||||
|
/// (a touch/key during a commit finishes it instantly so the new
|
||||||
|
/// gesture acts on the landed image — swipes chain).
|
||||||
function finishCommit(ni) {
|
function finishCommit(ni) {
|
||||||
const id = ids[ni];
|
const id = ids[ni];
|
||||||
if (nav && nav.id === id && nav.ready) {
|
if (nav && nav.id === id && nav.ready) {
|
||||||
const nw = nav.w, nh = nav.h, ns = nav.scale;
|
base.w = nav.w; base.h = nav.h;
|
||||||
vimg.src = vimg2.src; // cached + decoded → paints immediately
|
vScale = nav.scale;
|
||||||
base.w = nw; base.h = nh;
|
|
||||||
vScale = ns;
|
|
||||||
vimg.style.width = nw + "px";
|
|
||||||
vimg.style.height = nh + "px";
|
|
||||||
vTx = 0; vTy = 0;
|
vTx = 0; vTy = 0;
|
||||||
|
P.onload = null; S.onload = null; // stale preview/commit handlers
|
||||||
|
P.style.visibility = "hidden"; // park the outgoing image
|
||||||
|
viewer.insertBefore(S, P); // promoted element goes in front
|
||||||
|
[P, S] = [S, P];
|
||||||
applyTransform();
|
applyTransform();
|
||||||
openViewer(id, true);
|
openViewer(id, true);
|
||||||
// Hide the preview only once the primary has the bytes.
|
|
||||||
vimg.onload = () => { if (viewerId === id) navHide(); };
|
|
||||||
} else {
|
} else {
|
||||||
// Preview never made it (keyboard nav / slow thumb): plain open —
|
// Preview never loaded (keyboard nav / slow thumb): plain open —
|
||||||
// the thumb round-trip sets the right dimensions.
|
// the thumb round-trip sets the right dimensions.
|
||||||
openViewer(id, false);
|
openViewer(id, false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user