From f13fd8e0f3188a657bf6bdfc5018b6784a4876cf Mon Sep 17 00:00:00 2001 From: locphamtran Date: Thu, 10 Sep 2026 17:40:50 +0700 Subject: [PATCH] fix(library): drop the leftover zoom in the mark editor, re-anchor the framed crop Two zoom-interaction bugs in the library viewer: - Opening the WATERMARK editor now resets a photo zoom left over from the panel-closed pass. The zoom had shifted the mark's hit box along with the matrix and tripped the `libZoom.s <= 1.01` guards, so after visiting another tab and zooming, the mark could neither be dragged nor pinched. - A polaroid/wall photo window that MOVES (deck grows: a chip slider or the watermark text field) now re-anchors the photo transform by the same u/v the export uses, so the crop inside the frame no longer slides away from the card (at 2.5x the content used to drift 2.5x the card's move). Verified on device (9a6a7277): the mark drags again after tab-switch + zoom; opening WATERMARK returns the photo to the pre-zoom framing; the framed crop tracks the card within ~2 px over a 62 px deck-driven move. --- src/components/Viewfinder.tsx | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) 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