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.
This commit is contained in:
2026-09-12 08:13:21 +07:00
parent 93b5b664d4
commit 98b8021f3b
+74 -4
View File
@@ -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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ReturnType<typeof setTimeout> | 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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(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<ViewfinderHandle, ViewfinderProps>(function Viewfi
onResponderGrant={onLibTouchStart}
onResponderMove={onLibTouchMove}
onResponderRelease={onLibTouchEnd}
onResponderTerminate={onLibTouchEnd}
/>
</View>
);
@@ -2627,6 +2695,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
onResponderGrant={onLibTouchStart}
onResponderMove={onLibTouchMove}
onResponderRelease={onLibTouchEnd}
onResponderTerminate={onLibTouchEnd}
/>
</View>
);
@@ -2777,6 +2846,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
onResponderGrant={onLibTouchStart}
onResponderMove={onLibTouchMove}
onResponderRelease={onLibTouchEnd}
onResponderTerminate={onLibTouchEnd}
/>
</>
) : (