From 602e70a7e156a9d9a7a67f00bbb48dea37f7f5be Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 18:21:00 -0500 Subject: [PATCH] Linearize focused-view pan to the available range and flip Pan Y panX/panY now represent 0..1 fractions of the pannable range rather than absolute image positions, so the slider's full travel always covers edge-to-edge regardless of zoom level. Pan Y is inverted so sliding right moves the view up. Updated both the editor canvas and core/raster.js export so they stay in sync. Co-Authored-By: Claude Sonnet 4.6 --- app/renderer/canvas.js | 10 ++++++---- core/raster.js | 14 ++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/renderer/canvas.js b/app/renderer/canvas.js index 9fe3df0..37c011f 100644 --- a/app/renderer/canvas.js +++ b/app/renderer/canvas.js @@ -109,14 +109,16 @@ class AnnotationCanvas { // ---- coordinate helpers ---- // The focused-view crop region, in coordinates normalized to the full // image (0..1). Identity rect when focused view is off, matching - // core/raster.js applyFocusedView. + // core/raster.js applyFocusedView. panX/panY are 0..1 fractions of the + // available pan range (0 = left/bottom edge, 1 = right/top edge), so the + // slider's full range always pans the crop across the whole image + // regardless of zoom. Y is inverted so panY increases upward. viewRect() { const fv = this.focusedView; if (!fv || !fv.enabled || !(fv.zoom > 1)) return { x: 0, y: 0, w: 1, h: 1 }; const w = 1 / fv.zoom, h = 1 / fv.zoom; - const cx = Math.min(Math.max(fv.panX ?? 0.5, w / 2), 1 - w / 2); - const cy = Math.min(Math.max(fv.panY ?? 0.5, h / 2), 1 - h / 2); - return { x: cx - w / 2, y: cy - h / 2, w, h }; + const panX = fv.panX ?? 0.5, panY = fv.panY ?? 0.5; + return { x: panX * (1 - w), y: (1 - panY) * (1 - h), w, h }; } toNorm(e) { diff --git a/core/raster.js b/core/raster.js index 1317e20..6f085b6 100644 --- a/core/raster.js +++ b/core/raster.js @@ -392,13 +392,19 @@ function renderAnnotations(baseImg, annotations = []) { return img; } -/** Apply focused view (zoom/pan crop, then scale back to original size). */ +/** + * Apply focused view (zoom/pan crop, then scale back to original size). + * panX/panY are 0..1 fractions of the available pan range, not absolute + * image positions: 0 puts the crop at the left/bottom edge, 1 at the + * right/top edge, 0.5 centers it. Y is inverted so panY increases upward. + */ function applyFocusedView(img, fv) { if (!fv || !fv.enabled || !(fv.zoom > 1)) return img; const vw = img.width / fv.zoom, vh = img.height / fv.zoom; - const cx = Math.min(Math.max(fv.panX * img.width, vw / 2), img.width - vw / 2); - const cy = Math.min(Math.max(fv.panY * img.height, vh / 2), img.height - vh / 2); - const region = crop(img, cx - vw / 2, cy - vh / 2, vw, vh); + const panX = fv.panX ?? 0.5, panY = fv.panY ?? 0.5; + const cropX = panX * (img.width - vw); + const cropY = (1 - panY) * (img.height - vh); + const region = crop(img, cropX, cropY, vw, vh); return resize(region, img.width, img.height); }