From 494e9dfcdd5f87cc32d053de836d6d0e4d85e3c5 Mon Sep 17 00:00:00 2001 From: jay Date: Tue, 30 Jun 2026 16:20:12 -0400 Subject: [PATCH] art: drag-to-pan the zoomed artwork (persistent position; zoom holds the spot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per request: the inspector now pans only while the mouse button is held (grab/grabbing), using a persistent translate rather than cursor-follow — so you place a detail where you want it and it stays put. Zooming (slider/±/arrows) scales the translate by the same ratio, keeping the viewport-centred spot fixed so you can keep magnifying that exact area without it recentering. Pan is clamped to the image bounds (pointer-capture drag); 1× recenters. Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/art/+page.svelte | 71 ++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/frontend/src/routes/art/+page.svelte b/frontend/src/routes/art/+page.svelte index 7f1cd55..c4988c3 100644 --- a/frontend/src/routes/art/+page.svelte +++ b/frontend/src/routes/art/+page.svelte @@ -28,27 +28,54 @@ let landscape = $state(false); // Desktop zoom: a gallery inspector inside the lightbox. Variable 1×–4× via a floating - // toolbar (− / slider / + / % / Fit); moving the cursor pans (transform-origin tracks the - // pointer). Mobile keeps native pinch — the Zoom button + toolbar are hidden there. + // toolbar (− / slider / + / % / Fit); DRAG the artwork to pan (translate persists), so you + // can place a spot and keep zooming into it — zoom holds the viewport-centred point fixed. + // Mobile keeps native pinch — the Zoom button + toolbar are hidden there. let zoomed = $state(false); - let zoomLevel = $state(1.5); // scale factor; enter at a gentle 1.5× - let ox = $state(50), oy = $state(50); // transform-origin (%) — the panned-to point - function enterZoom() { zoomLevel = 1.5; ox = 50; oy = 50; zoomed = true; } - function fit() { zoomed = false; } // back to the framed gallery view - function setZoom(v) { zoomLevel = Math.max(1, Math.min(4, Math.round(v * 10) / 10)); } - function panZoom(e) { - const r = e.currentTarget.getBoundingClientRect(); - ox = Math.max(0, Math.min(100, ((e.clientX - r.left) / r.width) * 100)); - oy = Math.max(0, Math.min(100, ((e.clientY - r.top) / r.height) * 100)); + let zoomLevel = $state(1.5); // scale factor; enter at a gentle 1.5× + let tx = $state(0), ty = $state(0); // pan offset in px (translate) + let dragging = $state(false); + let zoomBoxEl, zoomImgEl; // refs for clamping pan to the image bounds + let lastX = 0, lastY = 0; + + function enterZoom() { zoomLevel = 1.5; tx = 0; ty = 0; zoomed = true; } + function fit() { zoomed = false; } // back to the framed gallery view + function clampPan() { + if (!zoomImgEl || !zoomBoxEl) return; + const maxX = Math.max(0, (zoomImgEl.offsetWidth * zoomLevel - zoomBoxEl.clientWidth) / 2); + const maxY = Math.max(0, (zoomImgEl.offsetHeight * zoomLevel - zoomBoxEl.clientHeight) / 2); + tx = Math.max(-maxX, Math.min(maxX, tx)); + ty = Math.max(-maxY, Math.min(maxY, ty)); + } + function setZoom(v) { + const z = Math.max(1, Math.min(4, Math.round(v * 10) / 10)); + if (z === zoomLevel) return; + const ratio = z / zoomLevel; // keep the centred spot fixed as we zoom + tx *= ratio; ty *= ratio; zoomLevel = z; + clampPan(); + } + function dragStart(e) { + dragging = true; lastX = e.clientX; lastY = e.clientY; + e.currentTarget.setPointerCapture?.(e.pointerId); + } + function dragMove(e) { + if (!dragging) return; + tx += e.clientX - lastX; ty += e.clientY - lastY; + lastX = e.clientX; lastY = e.clientY; + clampPan(); + } + function dragEnd(e) { + dragging = false; + e.currentTarget.releasePointerCapture?.(e.pointerId); } function onKey(e) { if (e.key !== 'Escape') return; - if (zoomed) zoomed = false; // Escape steps out of inspection first, then closes + if (zoomed) zoomed = false; // Escape steps out of inspection first, then closes else if (zoom) zoom = false; } // Leaving the lightbox always resets the inspector, so re-opening starts framed. - $effect(() => { if (!zoom) { zoomed = false; zoomLevel = 1.5; } }); + $effect(() => { if (!zoom) { zoomed = false; zoomLevel = 1.5; tx = 0; ty = 0; } }); // Move focus to the lightbox when it opens, so Escape/Enter work and focus is trapped sanely. $effect(() => { if (zoom && lightboxEl) lightboxEl.focus(); }); @@ -244,14 +271,17 @@ {#if zoomed} - -