import AsyncStorage from '@react-native-async-storage/async-storage'; import { useCallback, useEffect, useMemo } from 'react'; import { Dimensions } from 'react-native'; import { Gesture } from 'react-native-gesture-handler'; import { runOnJS, useSharedValue } from 'react-native-reanimated'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CROP_DIMS_KEY = 'palace_camera_crop_dims'; type UseCropGesturesParams = { insetsTop: number; }; export function useCropGestures({ insetsTop }: UseCropGesturesParams) { const defaultSize = SCREEN_WIDTH * 0.55; const cropBoxWidth = useSharedValue(defaultSize); const cropBoxHeight = useSharedValue(defaultSize); const screenHeight = Dimensions.get('window').height; const maxCropSize = Math.min(SCREEN_WIDTH - 40, screenHeight * 0.45); const minCropSize = 100; const cropAreaTop = insetsTop + 12; const cropAreaCenterY = cropAreaTop + maxCropSize / 2; useEffect(() => { let active = true; const raf = requestAnimationFrame(() => { AsyncStorage.getItem(CROP_DIMS_KEY).then(cropDimsData => { if (!active || !cropDimsData) return; try { const { width, height } = JSON.parse(cropDimsData); if (typeof width === 'number' && typeof height === 'number') { cropBoxWidth.value = width; cropBoxHeight.value = height; } } catch { /* ignore */ } }); }); return () => { active = false; cancelAnimationFrame(raf); }; }, [cropBoxWidth, cropBoxHeight]); const saveCropDims = useCallback((w: number, h: number) => { AsyncStorage.setItem(CROP_DIMS_KEY, JSON.stringify({ width: w, height: h })); }, []); const offsetW = useSharedValue(0); const offsetH = useSharedValue(0); const clamp = (val: number, min: number, max: number) => { 'worklet'; return Math.max(min, Math.min(val, max)); }; const createCornerGesture = (xDir: number, yDir: number) => Gesture.Pan() .onStart(() => { 'worklet'; offsetW.value = cropBoxWidth.value; offsetH.value = cropBoxHeight.value; }) .onUpdate(e => { 'worklet'; cropBoxWidth.value = clamp( offsetW.value + e.translationX * xDir * 2, minCropSize, maxCropSize ); cropBoxHeight.value = clamp( offsetH.value + e.translationY * yDir * 2, minCropSize, maxCropSize ); }) .onEnd(() => { 'worklet'; runOnJS(saveCropDims)(cropBoxWidth.value, cropBoxHeight.value); }); const createEdgeGesture = (axis: 'x' | 'y', dir: number) => Gesture.Pan() .onStart(() => { 'worklet'; offsetW.value = cropBoxWidth.value; offsetH.value = cropBoxHeight.value; }) .onUpdate(e => { 'worklet'; if (axis === 'x') { cropBoxWidth.value = clamp( offsetW.value + e.translationX * dir * 2, minCropSize, maxCropSize ); } else { cropBoxHeight.value = clamp( offsetH.value + e.translationY * dir * 2, minCropSize, maxCropSize ); } }) .onEnd(() => { 'worklet'; runOnJS(saveCropDims)(cropBoxWidth.value, cropBoxHeight.value); }); const cropGestures = useMemo( () => ({ cropResizeTL: createCornerGesture(-1, -1), cropResizeTR: createCornerGesture(1, -1), cropResizeBL: createCornerGesture(-1, 1), cropResizeBR: createCornerGesture(1, 1), cropResizeT: createEdgeGesture('y', -1), cropResizeB: createEdgeGesture('y', 1), cropResizeL: createEdgeGesture('x', -1), cropResizeR: createEdgeGesture('x', 1), }), [] ); return { cropBoxWidth, cropBoxHeight, cropGestures, cropAreaCenterY, }; }