Linearize focused-view pan to the available range and flip Pan Y
Template tests / tests (push) Successful in 1m39s
Template tests / tests (pull_request) Successful in 1m39s

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 <[email protected]>
This commit is contained in:
2026-06-13 18:21:00 -05:00
co-authored by Claude Sonnet 4.6
parent f23b49c0d1
commit 602e70a7e1
2 changed files with 16 additions and 8 deletions
+6 -4
View File
@@ -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) {
+10 -4
View File
@@ -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);
}