diff --git a/src/components/ZoomableImage.tsx b/src/components/ZoomableImage.tsx index 4f40c6b..0b613c8 100644 --- a/src/components/ZoomableImage.tsx +++ b/src/components/ZoomableImage.tsx @@ -1,21 +1,25 @@ import React, { useRef } from 'react'; import { Animated, PanResponder } from 'react-native'; -// Double-tap zoom for one preview photo inside the paging strip. +// Pinch / double-tap zoom for one preview photo inside the paging strip. // Core PanResponder + Animated only (no react-native-gesture-handler dep). // While zoomed in, responder termination is refused so the parent ScrollView // cannot steal the gesture — the photo pans instead of paging; swiping to the -// next photo resumes after zooming back out (double tap). +// next photo resumes after zooming back out (double tap or pinch below 1.15). +// Two-finger pinch only zooms once the photo is already zoomed in: at 100% the +// pinch stays inert, so the paging strip still behaves like a plain gallery. // -// ponytail: double tap only — a finished photo is not a map, and the two-finger -// gesture conflicted with paging. Re-add a pinch branch in onPanResponderMove -// (scale = start * dist/startDist, clamped) if pinch is ever wanted back. +// ponytail: pinch zooms around the image center, not the finger midpoint; +// focal zoom needs per-touch location math against the image's screen rect. +// MAX_SCALE is a hard cap — add a dedicated slider/stepper when 4x is not +// enough. interface ZoomableImageProps { uri: string; width: number; } +const MAX_SCALE = 4; const TAP_SCALE = 2.5; const TAP_MS = 300; @@ -30,6 +34,7 @@ export default function ZoomableImage({ uri, width }: ZoomableImageProps) { ty: 0, w: width, h: 0, + pinchStart: null as { scale: number; dist: number } | null, lastDx: 0, lastDy: 0, moved: false, @@ -66,7 +71,9 @@ export default function ZoomableImage({ uri, width }: ZoomableImageProps) { }; const finish = () => { - const wasTap = !st.moved && Date.now() - st.grantAt < 400; + const wasTap = + !st.moved && Date.now() - st.grantAt < 400 && st.pinchStart === null; + st.pinchStart = null; st.lastDx = 0; st.lastDy = 0; st.moved = false; @@ -92,27 +99,52 @@ export default function ZoomableImage({ uri, width }: ZoomableImageProps) { PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: () => true, - // Page the strip while zoom is off; once zoomed the image keeps the - // gesture so a drag pans instead of paging. - onPanResponderTerminationRequest: () => st.scale <= 1.001, + // Page the strip while zoom is off; once zoomed (or mid-pinch) the + // image keeps the gesture for panning/zooming itself. + onPanResponderTerminationRequest: () => + st.scale <= 1.001 && st.pinchStart === null, onPanResponderGrant: () => { st.grantAt = Date.now(); st.moved = false; st.lastDx = 0; st.lastDy = 0; }, - onPanResponderMove: (_e, g) => { - // Single finger only: pan the zoomed photo. A second finger is ignored - // on purpose (double tap is the only way in and out of zoom). - if (Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8) st.moved = true; - if (st.scale > 1.001) { - st.tx += g.dx - st.lastDx; - st.ty += g.dy - st.lastDy; + onPanResponderMove: (e, g) => { + const touches = e.nativeEvent.touches; + if (touches.length >= 2) { + const a = touches[0]; + const b = touches[1]; + const dist = Math.hypot(a.pageX - b.pageX, a.pageY - b.pageY); + if (!st.pinchStart) st.pinchStart = { scale: st.scale, dist: dist || 1 }; + const next = + st.pinchStart.scale > 1.001 + ? Math.max( + 1, + Math.min(MAX_SCALE, st.pinchStart.scale * (dist / st.pinchStart.dist)) + ) + : 1; + st.scale = next; + if (next <= 1.001) { + st.tx = 0; + st.ty = 0; + } clampTx(); apply(); + st.moved = true; + st.lastDx = g.dx; + st.lastDy = g.dy; + } else { + st.pinchStart = null; + if (Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8) st.moved = true; + if (st.scale > 1.001) { + st.tx += g.dx - st.lastDx; + st.ty += g.dy - st.lastDy; + clampTx(); + apply(); + } + st.lastDx = g.dx; + st.lastDy = g.dy; } - st.lastDx = g.dx; - st.lastDy = g.dy; }, onPanResponderRelease: finish, onPanResponderTerminate: finish,