From 98b8021f3b4d2351cad5740dbdf31d4b01cfac87 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sat, 12 Sep 2026 08:13:21 +0700 Subject: [PATCH] Show the picture with the last edit undone while the finger holds it Pressing and holding on the photo with a LIGHT/WB/FX panel open asks App for a preview drawn without the edit that gesture just made; lifting the finger asks for the edited look back. Pegged to one gesture, not one pointer-move: App only re-snapshots the 'before' state after a 700ms pause, so the whole drag compares against its own starting point. Preview-only - the sliders, RECIPE and the export keep reading the real adjustments. Moving past TAP_SLOP, a second finger, or a release all end the peek, and a peek is not a tap, so tap-to-focus and double-tap zoom are untouched. --- src/components/Viewfinder.tsx | 78 +++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index de7a4d1..1f77a24 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -106,6 +106,11 @@ interface ViewfinderProps { value: number; onChange: (v: number) => void; } | null; + // Hold-to-compare: while armed (a LIGHT/WB/FX panel is open) pressing and + // holding anywhere on the picture asks App to draw it with the last edit + // undone; the finger coming off asks for the edited look back, so the user + // can flick between the before and the after. Absent prop = feature off. + onComparePeek?: (on: boolean) => void; // Custom text watermark (WATERMARK tab -> CUSTOM chip). Drawn live on the // image like the GPS mark; dragging/tapping the image moves it. Fractions are // 0..1 of the photo/window area. Absent prop = feature off. @@ -231,6 +236,7 @@ const Viewfinder = forwardRef(function Viewfi onExposureChange, meteringAdjustments, imageAdjustTarget, + onComparePeek, customWm = { enabled: false, text: '', x: 0.5, y: 0.5 }, onWmPositionChange, gpsWm, @@ -625,6 +631,55 @@ const Viewfinder = forwardRef(function Viewfi // Priority: an open panel param row (imageAdjustTarget) > EV (AE locked). // A plain tap (no movement) always falls through to tap-to-focus. const TAP_SLOP = 14; // px of movement before a touch counts as a drag + + // ---- Hold-to-compare (see onComparePeek) --------------------------------- + // The timer only arms when App has something to compare against; moving past + // TAP_SLOP (or a second finger) hands the touch back to its normal job — + // adjust / pinch / pan — and drops the peek. + const HOLD_MS = 350; + const holdTimerRef = useRef | null>(null); + const holdAnchorRef = useRef<{ x: number; y: number } | null>(null); + const peekingRef = useRef(false); + const onComparePeekRef = useRef(onComparePeek); + onComparePeekRef.current = onComparePeek; + const endPeek = () => { + if (!peekingRef.current) return; + peekingRef.current = false; + onComparePeekRef.current?.(false); + }; + const cancelHold = () => { + if (holdTimerRef.current) { + clearTimeout(holdTimerRef.current); + holdTimerRef.current = null; + } + }; + const startHold = (x: number, y: number) => { + cancelHold(); + holdAnchorRef.current = { x, y }; + if (!onComparePeekRef.current) return; + holdTimerRef.current = setTimeout(() => { + holdTimerRef.current = null; + peekingRef.current = true; + onComparePeekRef.current?.(true); + }, HOLD_MS); + }; + // Ends any hold and reports whether this gesture WAS peeking — a peek is not + // a tap, so its release must skip the tap actions below. + const endHold = () => { + cancelHold(); + holdAnchorRef.current = null; + const was = peekingRef.current; + endPeek(); + return was; + }; + const holdNoteMove = (x: number, y: number) => { + const a = holdAnchorRef.current; + if (!a) return; + if (Math.abs(x - a.x) <= TAP_SLOP && Math.abs(y - a.y) <= TAP_SLOP) return; + holdAnchorRef.current = null; + cancelHold(); + endPeek(); + }; const imageAdjustTargetRef = useRef(imageAdjustTarget); imageAdjustTargetRef.current = imageAdjustTarget; type ImgDragSession = { @@ -676,9 +731,14 @@ const Viewfinder = forwardRef(function Viewfi panUsedRef.current = false; if (gts.length < 2) pinchRef.current = null; imgDragRef.current = { y0: pageYOf(e), kind, v0, param, moved: false, lastSent: null }; + const ht = e?.nativeEvent?.touches?.[0] ?? e?.nativeEvent; + startHold(ht?.pageX ?? 0, pageYOf(e)); }; const onImageDragMove = (e: any) => { const g = imgDragRef.current; + const holdTs = e?.nativeEvent?.touches ?? []; + if (holdTs.length >= 2) endHold(); + else holdNoteMove(holdTs[0]?.pageX ?? 0, pageYOf(e)); // Framed window (polaroid card / wall opening) on the live camera: two // fingers pinch the picture and drag it inside the frame — exactly the crop // the export takes (frameWindowZoom). One finger drags it too, but only a @@ -808,6 +868,7 @@ const Viewfinder = forwardRef(function Viewfi const onImageDragEnd = (e: any) => { const g = imgDragRef.current; imgDragRef.current = null; + const peeked = endHold(); wmPinchRef.current = null; wmDragTgtRef.current = null; wmDragRef.current = null; @@ -826,8 +887,9 @@ const Viewfinder = forwardRef(function Viewfi commitWm(); return; } - if (!g.moved) { - // Plain tap: lock focus at the tapped point (drag never re-locks). + if (!g.moved && !peeked) { + // Plain tap: lock focus at the tapped point (drag never re-locks). A + // hold-to-compare is not a tap either. handleTapFocus(e.nativeEvent.locationX, e.nativeEvent.locationY); } }; @@ -1710,6 +1772,8 @@ const Viewfinder = forwardRef(function Viewfi return; } const ts = e.nativeEvent.touches; + if (ts.length === 1) startHold(ts[0].pageX, ts[0].pageY); + else endHold(); panRef.current = ts.length === 1 ? { @@ -1764,6 +1828,8 @@ const Viewfinder = forwardRef(function Viewfi return; } const ts = e.nativeEvent.touches; + if (ts.length === 1) holdNoteMove(ts[0].pageX, ts[0].pageY); + else endHold(); // WATERMARK tab open: two fingers scale the mark under them (or do // nothing) — the photo itself only zooms once the editor is closed. if (ts.length >= 2 && wmEditing) { @@ -1881,6 +1947,7 @@ const Viewfinder = forwardRef(function Viewfi cropDragRef.current = null; return; } + const peeked = endHold(); const wasPinch = pinchUsedRef.current; const wasPan = panUsedRef.current; // Decided at touch-down; the reset below would lose it. @@ -1895,8 +1962,8 @@ const Viewfinder = forwardRef(function Viewfi panRef.current = null; panUsedRef.current = false; // A drag is neither a tap nor the first half of a double-tap. - if (wasPan || wmMoved) lastTapRef.current = null; - if (wasPinch || wasPan) return; // a double-tap is always a single finger + if (wasPan || wmMoved || peeked) lastTapRef.current = null; + if (wasPinch || wasPan || peeked) 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 @@ -2548,6 +2615,7 @@ const Viewfinder = forwardRef(function Viewfi onResponderGrant={onLibTouchStart} onResponderMove={onLibTouchMove} onResponderRelease={onLibTouchEnd} + onResponderTerminate={onLibTouchEnd} /> ); @@ -2627,6 +2695,7 @@ const Viewfinder = forwardRef(function Viewfi onResponderGrant={onLibTouchStart} onResponderMove={onLibTouchMove} onResponderRelease={onLibTouchEnd} + onResponderTerminate={onLibTouchEnd} /> ); @@ -2777,6 +2846,7 @@ const Viewfinder = forwardRef(function Viewfi onResponderGrant={onLibTouchStart} onResponderMove={onLibTouchMove} onResponderRelease={onLibTouchEnd} + onResponderTerminate={onLibTouchEnd} /> ) : (