diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index 8afc95f..2b84ee9 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -975,6 +975,15 @@ const Viewfinder = forwardRef(function Viewfi // point / double-tap again to reset to 100%. Transform is a Skia matrix // P' = scale*P + (tx,ty); gesture math keeps the focal point stationary. const [libZoom, setLibZoom] = useState({ s: 1, tx: 0, ty: 0 }); + // The mark editor works in the UN-zoomed composition: a photo zoom left over + // from the panel-closed pass (zoom in to check a slider, then come back to + // WATERMARK) shifted the mark's hit box with the matrix AND tripped the + // `libZoom.s <= 1.01` guards, so the mark could neither be dragged nor + // pinched. Opening the editor therefore drops the photo zoom. + useEffect(() => { + if (wmEditing) setLibZoom((z) => (z.s <= 1.001 ? z : { s: 1, tx: 0, ty: 0 })); + }, [wmEditing]); + const pinchRef = useRef<{ d: number; s: number; tx: number; ty: number; fx: number; fy: number } | null>(null); const pinchUsedRef = useRef(false); // Framed-window drag: anchor at touch-down (finger + transform origin), so a @@ -1054,6 +1063,32 @@ const Viewfinder = forwardRef(function Viewfi if (selectedFrame === 'wallframe' && wallGeo && wallImage) return wallGeo.window; return null; }; + // Framed photo windows move when the deck grows (slider) or shrinks: the + // photo content is scaled about its own top-left, so a plain move of the + // window under a zoomed photo slides the visible crop away from the card. + // Re-anchor the transform when the window rect changes so the SAME part of + // the photo stays inside the frame (same u/v math the export uses). + const winRectRef = useRef<{ x: number; y: number; w: number; h: number } | null>(null); + useEffect(() => { + const win = zoomWindowRect(); + const prev = winRectRef.current; + winRectRef.current = win ? { ...win } : null; + if (!prev || !win) return; + if (prev.x === win.x && prev.y === win.y && prev.w === win.w && prev.h === win.h) return; + setLibZoom((z) => { + if (z.s <= 1.001) return z; + const tlx = z.tx + (z.s - 1) * prev.x; + const tly = z.ty + (z.s - 1) * prev.y; + const u = (prev.w / 2 - tlx) / (z.s * prev.w); + const v = (prev.h / 2 - tly) / (z.s * prev.h); + return { + s: z.s, + tx: win.w / 2 - u * z.s * win.w - (z.s - 1) * win.x, + ty: win.h / 2 - v * z.s * win.h - (z.s - 1) * win.y, + }; + }); + }); + // Clamp pan/zoom so the photo always keeps COVERING the framed window (never // drags far enough to reveal the card/mat through the transparent opening). // Transform is P' = s*P + t in canvas coords; with the photo drawn over the