From 8f700e7f66f8ab31ffc889f3779b044dba23f4dc Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Sun, 1 Nov 2026 17:44:14 -0500 Subject: [PATCH] gtauri: clamp viewer pan/zoom to image edges Pan is now limited to the image bounds (|offset| <= (rendered - viewport)/2 per axis), so the image can never be dragged off-screen leaving blank space around it; on a letterboxed axis panning is locked to centered. Pinch can no longer zoom out past fit. Clamping lives in vApply() so pan, pinch and reset all share it; still compositor-only (transform only). --- harnesses/gtauri/frontend/dist/index.html | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/harnesses/gtauri/frontend/dist/index.html b/harnesses/gtauri/frontend/dist/index.html index a82a890..1750e0b 100644 --- a/harnesses/gtauri/frontend/dist/index.html +++ b/harnesses/gtauri/frontend/dist/index.html @@ -269,7 +269,21 @@ requestAnimationFrame(frTick); let vScale = 1, vTx = 0, vTy = 0; const base = { w: 0, h: 0 }; +function fitScale() { + return Math.min(innerWidth / base.w, innerHeight / base.h); +} + function vApply() { + // Clamp: never zoom out past fit, and pan is limited to the image + // edges — the image can never be dragged off-screen leaving blank + // space around it. On a letterboxed axis (image smaller than the + // viewport in that dimension) panning is locked to centered. + const s = fitScale(); + if (vScale < s) vScale = s; + const mx = Math.max(0, (base.w * vScale - innerWidth) / 2); + const my = Math.max(0, (base.h * vScale - innerHeight) / 2); + if (vTx > mx) vTx = mx; else if (vTx < -mx) vTx = -mx; + if (vTy > my) vTy = my; else if (vTy < -my) vTy = -my; vimg.style.transform = "translate(" + (vTx - base.w * vScale / 2) + "px," + (vTy - base.h * vScale / 2) + "px) scale(" + vScale + ")"; } @@ -370,7 +384,7 @@ viewer.addEventListener("pointermove", (e) => { p.x = e.clientX; p.y = e.clientY; const [a, b] = [...pts.values()]; const d = Math.hypot(a.x - b.x, a.y - b.y); - if (pinchD0 > 0) vScale = Math.max(0.05, pinchS0 * d / pinchD0); + if (pinchD0 > 0) vScale = Math.max(fitScale(), pinchS0 * d / pinchD0); vApply(); } }); @@ -378,7 +392,7 @@ const endPt = (e) => { pts.delete(e.pointerId); }; viewer.addEventListener("pointerup", endPt); viewer.addEventListener("pointercancel", endPt); viewer.addEventListener("dblclick", () => { - vScale = Math.min(innerWidth / base.w, innerHeight / base.h); + vScale = fitScale(); vTx = 0; vTy = 0; vApply(); });