import { Text } from '@/aesthetics/Text'; import { colors } from '@/aesthetics/styles'; import { useStrings } from '@/common/hooks/useStrings'; import { Canvas, Group, Rect, RoundedRect, rect, rrect } from '@shopify/react-native-skia'; import { Camera as CameraIcon, CameraOff } from 'lucide-react-native'; import type { RefObject } from 'react'; import { useEffect, useState } from 'react'; import { Dimensions, Pressable, StyleSheet, View } from 'react-native'; import { GestureDetector, type ComposedGesture, type GestureType, } from 'react-native-gesture-handler'; import Animated, { cancelAnimation, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withSpring, withTiming, type SharedValue, } from 'react-native-reanimated'; import { Camera as VisionCamera, type CameraDevice, type CameraDeviceFormat, } from 'react-native-vision-camera'; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); const CROP_RADIUS = 24; const MASK_COLOR = colors.camera.cropMask; type CropGestures = { cropResizeTL: GestureType; cropResizeTR: GestureType; cropResizeBL: GestureType; cropResizeBR: GestureType; cropResizeT: GestureType; cropResizeB: GestureType; cropResizeL: GestureType; cropResizeR: GestureType; }; type CameraViewProps = { hasPermission: boolean; requestCameraPermission: () => void; device: CameraDevice | undefined; format: CameraDeviceFormat | undefined; cameraOn: boolean; cameraActive: boolean; cameraRef: RefObject; zoom: number; onInitialized: () => void; gesture: GestureType | ComposedGesture; showCropOverlay: boolean; cropGestures: CropGestures; cropBoxWidth: SharedValue; cropBoxHeight: SharedValue; cropAreaCenterY: number; onEnableCamera: () => void; focusPoint: { x: number; y: number } | null; }; export function CameraView({ hasPermission, requestCameraPermission, device, format, cameraOn, cameraActive, cameraRef, zoom, onInitialized, gesture, showCropOverlay, cropGestures, cropBoxWidth, cropBoxHeight, cropAreaCenterY, onEnableCamera, focusPoint, }: CameraViewProps) { const strings = useStrings(); const clip = useDerivedValue(() => rrect( rect( (SCREEN_WIDTH - cropBoxWidth.value) / 2, cropAreaCenterY - cropBoxHeight.value / 2, cropBoxWidth.value, cropBoxHeight.value ), CROP_RADIUS, CROP_RADIUS ) ); const borderX = useDerivedValue(() => (SCREEN_WIDTH - cropBoxWidth.value) / 2); const borderY = useDerivedValue(() => cropAreaCenterY - cropBoxHeight.value / 2); const hitAreaStyle = useAnimatedStyle(() => ({ position: 'absolute' as const, left: (SCREEN_WIDTH - cropBoxWidth.value) / 2, top: cropAreaCenterY - cropBoxHeight.value / 2, width: cropBoxWidth.value, height: cropBoxHeight.value, })); const [cropMounted, setCropMounted] = useState(false); useEffect(() => { if (!showCropOverlay) return; const id = requestAnimationFrame(() => setCropMounted(true)); return () => { cancelAnimationFrame(id); setCropMounted(false); }; }, [showCropOverlay]); const focusOpacity = useSharedValue(0); const focusScale = useSharedValue(1); useEffect(() => { if (focusPoint) { cancelAnimation(focusOpacity); cancelAnimation(focusScale); focusOpacity.value = 1; focusScale.value = 1.25; focusScale.value = withSpring(1, { damping: 30, stiffness: 500 }); focusOpacity.value = withDelay(1000, withTiming(0, { duration: 500 })); } }, [focusPoint, focusOpacity, focusScale]); const focusIndicatorStyle = useAnimatedStyle(() => ({ opacity: focusOpacity.value, transform: [{ scale: focusScale.value }], })); if (!hasPermission) { return ( {strings.addLog.camera.permissionNeeded} {strings.addLog.camera.enableCamera} ); } if (!device) { return ( {strings.addLog.camera.noCameraAvailable} ); } return ( <> {cropMounted && ( <> )} {focusPoint && ( )} {!cameraOn && } ); } const styles = StyleSheet.create({ cameraContainer: { ...StyleSheet.absoluteFillObject, borderRadius: 24, overflow: 'hidden', }, cameraPlaceholder: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.camera.backgroundAlt, alignItems: 'center', justifyContent: 'center', }, cameraOffOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.camera.backgroundAlt, }, placeholderText: { color: colors.camera.placeholderDim, fontSize: 16, marginTop: 12 }, permissionBtn: { marginTop: 16, paddingHorizontal: 20, paddingVertical: 10, backgroundColor: colors.camera.permissionBg, borderRadius: 8, }, permissionBtnText: { color: colors.camera.text, fontWeight: '600' }, cropHitCornerTL: { position: 'absolute', top: -20, left: -20, width: 50, height: 50 }, cropHitCornerTR: { position: 'absolute', top: -20, right: -20, width: 50, height: 50 }, cropHitCornerBL: { position: 'absolute', bottom: -20, left: -20, width: 50, height: 50 }, cropHitCornerBR: { position: 'absolute', bottom: -20, right: -20, width: 50, height: 50 }, cropHitEdgeT: { position: 'absolute', top: -15, left: 50, right: 50, height: 30 }, cropHitEdgeB: { position: 'absolute', bottom: -15, left: 50, right: 50, height: 30 }, cropHitEdgeL: { position: 'absolute', left: -15, top: 50, bottom: 50, width: 30 }, cropHitEdgeR: { position: 'absolute', right: -15, top: 50, bottom: 50, width: 30 }, focusIndicator: { position: 'absolute', width: 60, height: 60, borderRadius: 30, borderWidth: 2, borderColor: colors.camera.focusBorder, }, });