Skip to content

Instantly share code, notes, and snippets.

@dentemm
Created October 25, 2025 11:14
Show Gist options
  • Save dentemm/77f8cdff676264e8a69a6785231628b1 to your computer and use it in GitHub Desktop.
Save dentemm/77f8cdff676264e8a69a6785231628b1 to your computer and use it in GitHub Desktop.
import React from 'react';
import {View, StyleSheet, useWindowDimensions} from 'react-native';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
clamp,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
const MIN_SCALE = 0.5;
const MAX_SCALE = 3;
const RECTANGLE_SIZE = 200;
export default function PinchZoomDemo() {
const {width: screenWidth, height: screenHeight} = useWindowDimensions();
const translateX = useSharedValue(screenWidth / 2 - RECTANGLE_SIZE / 2); // Center the rectangle
const translateY = useSharedValue(screenHeight / 2 - RECTANGLE_SIZE / 2);
const scale = useSharedValue(1);
const startScale = useSharedValue(1);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const prevPanX = useSharedValue(0);
const prevPanY = useSharedValue(0);
const isDragging = useSharedValue(false);
const rectangleAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{translateX: translateX.value},
{translateY: translateY.value},
{scale: scale.value},
],
};
});
const panGesture = Gesture.Pan()
.maxPointers(1)
.minDistance(6)
.onStart(() => {
'worklet';
prevPanX.value = 0;
prevPanY.value = 0;
isDragging.value = true;
})
.onUpdate(event => {
'worklet';
const deltaX = event.translationX - prevPanX.value;
const deltaY = event.translationY - prevPanY.value;
translateX.value += deltaX;
translateY.value += deltaY;
prevPanX.value = event.translationX;
prevPanY.value = event.translationY;
})
.onEnd(() => {
'worklet';
isDragging.value = false;
prevPanX.value = 0;
prevPanY.value = 0;
})
.onFinalize(() => {
'worklet';
isDragging.value = false;
prevPanX.value = 0;
prevPanY.value = 0;
});
const pinchGesture = Gesture.Pinch()
.onStart(event => {
'worklet';
startScale.value = scale.value;
focalX.value = event.focalX;
focalY.value = event.focalY;
})
.onUpdate(event => {
'worklet';
const zoomSensitivity = 1;
const rawScale = 1 + (event.scale - 1) * zoomSensitivity;
const nextScale = clamp(
startScale.value * rawScale,
MIN_SCALE,
MAX_SCALE,
);
// Simple focal point zooming approach
// The focal point should stay at the same screen position while the rectangle scales
// Apply the new scale
scale.value = nextScale;
// Adjust the translation to keep the focal point fixed
// The focal point is at: translateX + focalX * scale
// We want to keep this screen position fixed, so:
const currentFocalScreenX = translateX.value + focalX.value * scale.value;
const currentFocalScreenY = translateY.value + focalY.value * scale.value;
// Calculate the new translation to maintain the focal point
translateX.value = currentFocalScreenX - focalX.value * nextScale;
translateY.value = currentFocalScreenY - focalY.value * nextScale;
})
.onEnd(() => {
'worklet';
// Snap to bounds if needed
if (scale.value < MIN_SCALE) {
scale.value = withSpring(MIN_SCALE, {damping: 18, stiffness: 180});
} else if (scale.value > MAX_SCALE) {
scale.value = withSpring(MAX_SCALE, {damping: 18, stiffness: 180});
}
});
// ========================
// Compose gestures (pan + pinch)
// ========================
const composedGesture = Gesture.Simultaneous(panGesture, pinchGesture);
// ========================
// Render
// ========================
return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.canvas}>
<GestureDetector gesture={composedGesture}>
<Animated.View
style={[
styles.rectangle,
rectangleAnimatedStyle,
{
width: RECTANGLE_SIZE,
height: RECTANGLE_SIZE,
},
]}
/>
</GestureDetector>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
canvas: {
flex: 1,
position: 'relative',
},
rectangle: {
position: 'absolute',
backgroundColor: '#3b82f6',
borderRadius: 12,
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment