From 2911dd57f0a58777735eeba4d394189d039523cc Mon Sep 17 00:00:00 2001 From: locphamtran Date: Thu, 10 Sep 2026 15:36:51 +0700 Subject: [PATCH] Let the wall-frame photo be dragged inside its opening --- src/components/Viewfinder.tsx | 38 +++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index 0798b16..b235584 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -922,6 +922,10 @@ const Viewfinder = forwardRef(function Viewfi const [libZoom, setLibZoom] = useState({ s: 1, tx: 0, ty: 0 }); 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 + // move never accumulates rounding across a slow drag. + const panRef = useRef<{ x: number; y: number; tx: number; ty: number } | null>(null); + const panUsedRef = useRef(false); const lastTapRef = useRef<{ x: number; y: number; t: number } | null>(null); const clampZoom = (s: number) => Math.max(1, Math.min(6, s)); // Contain-fit rect of the loaded photo in canvas coordinates (image is @@ -1170,11 +1174,16 @@ const Viewfinder = forwardRef(function Viewfi y <= box.y + box.h + 8; // Library framed (polaroid/wall) returns reuse the SAME responder handlers as // plain library (see the framed JSX below): single-finger drags place/move - // the text watermark while armed, pinch + double-tap repositions the photo - // inside the frame window (watermark stays armed when zoomed so detail can - // be inspected without accidentally moving text — matching plain library). + // the text watermark while armed, and otherwise DRAG the photo inside the + // framed window (same cover clamp as pinch, so the opening never shows the + // card/mat through it); pinch + double-tap zoom that same repositioning. + // The pan only bites once the photo is zoomed past 1x: at the cover scale it + // already fills the opening exactly, so the clamp has nowhere to move. const onLibTouchStart = (e: any) => { const ts = e.nativeEvent.touches; + panRef.current = + ts.length === 1 ? { x: ts[0].locationX, y: ts[0].locationY, tx: libZoom.tx, ty: libZoom.ty } : null; + panUsedRef.current = false; if (ts.length >= 2) { const [a, b] = ts; pinchUsedRef.current = true; @@ -1198,6 +1207,22 @@ const Viewfinder = forwardRef(function Viewfi placeWm(t.pageX ?? t.locationX, t.pageY ?? t.locationY); return; } + // One finger, no pinch running: drag the photo inside the framed window + // (polaroid card / wall-frame opening). Goes through the same clamp as + // pinch, so the opening always stays covered by the picture. + if (ts.length === 1 && !pinchRef.current && zoomWindowRect()) { + const t = ts[0]; + const pan0 = panRef.current; + if (pan0) { + const dx = t.locationX - pan0.x; + const dy = t.locationY - pan0.y; + if (Math.abs(dx) > 6 || Math.abs(dy) > 6) panUsedRef.current = true; + if (panUsedRef.current) { + setLibZoom(clampZoomToWindow(libZoom.s, pan0.tx + dx, pan0.ty + dy)); + } + } + return; + } let p = pinchRef.current; if (ts.length >= 2 && !p) { // Responder grant fires once on the first finger; a second finger that @@ -1229,9 +1254,14 @@ const Viewfinder = forwardRef(function Viewfi }; const onLibTouchEnd = (e: any) => { const wasPinch = pinchUsedRef.current; + const wasPan = panUsedRef.current; pinchRef.current = null; pinchUsedRef.current = false; - if (wasPinch) return; // a double-tap is always a single finger + panRef.current = null; + panUsedRef.current = false; + // A drag is neither a tap nor the first half of a double-tap. + if (wasPan) lastTapRef.current = null; + if (wasPinch || wasPan) return; // a double-tap is always a single finger const x = e.nativeEvent.locationX; const y = e.nativeEvent.locationY; // Mark drawn but its editor CLOSED: a double-tap ON the mark re-opens the