import * as React from "react" ; import { Animated , StyleSheet , View , TouchableOpacity , requireNativeComponent , Platform , ViewPropTypes , } from "react-native" ; import _ from "lodash" ; import { useRecoilState } from "recoil" ; import { cropSizeState , imageBoundsState , accumulatedPanState } from "./Store" ; import { GestureHandlerRootView , PanGestureHandler , PanGestureHandlerGestureEvent , State , } from "react-native-gesture-handler" ; import { useContext } from "react" ; import { EditorContext } from "./index" ; const horizontalSections = [ "top" , "middle" , "bottom" ] ; const verticalSections = [ "left" , "middle" , "right" ] ; const ImageCropOverlay = ( ) => { const [ selectedFrameSection , setSelectedFrameSection ] = React . useState ( "" ) ; const [ cropSize , setCropSize ] = useRecoilState ( cropSizeState ) ; const [ imageBounds ] = useRecoilState ( imageBoundsState ) ; const [ accumulatedPan , setAccumluatedPan ] = useRecoilState ( accumulatedPanState ) ; const { fixedAspectRatio , lockAspectRatio , minimumCropDimensions } = useContext ( EditorContext ) ; const [ animatedCropSize ] = React . useState ( { width : new Animated . Value ( cropSize . width ) , height : new Animated . Value ( cropSize . height ) , } ) ; const panX = React . useRef ( new Animated . Value ( imageBounds . x ) ) ; const panY = React . useRef ( new Animated . Value ( imageBounds . y ) ) ; React . useEffect ( ( ) => { checkCropBounds ( { translationX : 0 , translationY : 0 , } ) ; animatedCropSize . height . setValue ( cropSize . height ) ; animatedCropSize . width . setValue ( cropSize . width ) ; } , [ cropSize ] ) ; React . useEffect ( ( ) => { let newSize = { width : 0 , height : 0 } ; const { width , height } = imageBounds ; const imageAspectRatio = width / height ; if ( fixedAspectRatio < imageAspectRatio ) { newSize . height = height ; newSize . width = height * fixedAspectRatio ; } else { newSize . width = width ; newSize . height = width / fixedAspectRatio ; } setCropSize ( newSize ) ; } , [ imageBounds ] ) ; const isMovingSection = ( ) => { return ( selectedFrameSection == "topmiddle" || selectedFrameSection == "middleleft" || selectedFrameSection == "middleright" || selectedFrameSection == "middlemiddle" || selectedFrameSection == "bottommiddle" ) ; } ; const isLeft = selectedFrameSection . endsWith ( "left" ) ; const isTop = selectedFrameSection . startsWith ( "top" ) ; const onOverlayMove = ( { nativeEvent } ) => { if ( selectedFrameSection !== "" ) { if ( isMovingSection ( ) ) { Animated . event ( [ { translationX : panX . current , translationY : panY . current , } , ] , { useNativeDriver : false } ) ( nativeEvent ) ; } else { const { x , y } = getTargetCropFrameBounds ( nativeEvent ) ; if ( isTop ) { panY . current . setValue ( - y ) ; } if ( isLeft ) { panX . current . setValue ( - x ) ; } animatedCropSize . width . setValue ( cropSize . width + x ) ; animatedCropSize . height . setValue ( cropSize . height + y ) ; } } else { const { x , y } = nativeEvent ; const { width : initialWidth , height : initialHeight } = cropSize ; let position = "" ; if ( y / initialHeight < 0.333 ) { position = position + "top" ; } else if ( y / initialHeight < 0.667 ) { position = position + "middle" ; } else { position = position + "bottom" ; } if ( x / initialWidth < 0.333 ) { position = position + "left" ; } else if ( x / initialWidth < 0.667 ) { position = position + "middle" ; } else { position = position + "right" ; } setSelectedFrameSection ( position ) ; } } ; const getTargetCropFrameBounds = ( { translationX , translationY , } ) => { let x = 0 ; let y = 0 ; if ( translationX && translationY ) { if ( translationX < translationY ) { x = ( isLeft ? - 1 : 1 ) * translationX ; if ( lockAspectRatio ) { y = x / fixedAspectRatio ; } else { y = ( isTop ? - 1 : 1 ) * translationY ; } } else { y = ( isTop ? - 1 : 1 ) * translationY ; if ( lockAspectRatio ) { x = y * fixedAspectRatio ; } else { x = ( isLeft ? - 1 : 1 ) * translationX ; } } } return { x , y } ; } ; const onOverlayRelease = ( nativeEvent ) => { if ( isMovingSection ( ) ) { checkCropBounds ( nativeEvent ) ; } else { checkResizeBounds ( nativeEvent ) ; } setSelectedFrameSection ( "" ) ; } ; const onHandlerStateChange = ( { nativeEvent , } ) => { if ( nativeEvent . state === State . END ) { onOverlayRelease ( nativeEvent ) ; } } ; const checkCropBounds = ( { translationX , translationY , } ) => { let accDx = accumulatedPan . x + translationX ; if ( accDx <= imageBounds . x ) { accDx = imageBounds . x ; } else if ( accDx + cropSize . width > imageBounds . width + imageBounds . x ) { let limitedXPos = imageBounds . x + imageBounds . width - cropSize . width ; accDx = limitedXPos ; } else { } let accDy = accumulatedPan . y + translationY ; if ( accDy <= imageBounds . y ) { accDy = imageBounds . y ; } else if ( accDy + cropSize . height > imageBounds . height + imageBounds . y ) { let limitedYPos = imageBounds . y + imageBounds . height - cropSize . height ; accDy = limitedYPos ; } else { } panX . current . setValue ( 0 ) ; panY . current . setValue ( 0 ) ; setAccumluatedPan ( { x : accDx , y : accDy } ) ; } ; const checkResizeBounds = ( { translationX , translationY , } ) => { const { width : maxWidth , height : maxHeight } = imageBounds ; const { width : minWidth , height : minHeight } = minimumCropDimensions ; const { x , y } = getTargetCropFrameBounds ( { translationX , translationY } ) ; const animatedWidth = cropSize . width + x ; const animatedHeight = cropSize . height + y ; let finalHeight = animatedHeight ; let finalWidth = animatedWidth ; if ( animatedHeight > maxHeight ) { finalHeight = maxHeight ; if ( lockAspectRatio ) finalWidth = finalHeight * fixedAspectRatio ; } else if ( animatedHeight < minHeight ) { finalHeight = minHeight ; if ( lockAspectRatio ) finalWidth = finalHeight * fixedAspectRatio ; } if ( animatedWidth > maxWidth ) { finalWidth = maxWidth ; if ( lockAspectRatio ) finalHeight = finalWidth / fixedAspectRatio ; } else if ( animatedWidth < minWidth ) { finalWidth = minWidth ; if ( lockAspectRatio ) finalHeight = finalWidth / fixedAspectRatio ; } setAccumluatedPan ( { x : accumulatedPan . x + ( isLeft ? - x : 0 ) , y : accumulatedPan . y + ( isTop ? - y : 0 ) , } ) ; panX . current . setValue ( 0 ) ; panY . current . setValue ( 0 ) ; setCropSize ( { height : finalHeight , width : finalWidth , } ) ; } ; return ( < GestureHandlerRootView style = { styles . container } > < PanGestureHandler onGestureEvent = { onOverlayMove } onHandlerStateChange = { ( e ) => onHandlerStateChange ( e ) } > < Animated . View style = { [ styles . overlay , animatedCropSize , { transform : [ { translateX : Animated . add ( panX . current , accumulatedPan . x ) } , { translateY : Animated . add ( panY . current , accumulatedPan . y ) } , ] , } , ] } > { horizontalSections . map ( ( hsection ) => { return ( < View style = { styles . sectionRow } key = { hsection } > { verticalSections . map ( ( vsection ) => { const key = hsection + vsection ; return ( < View style = { [ styles . defaultSection ] } key = { key } > { key == "topleft" || key == "topright" || key == "bottomleft" || key == "bottomright" ? ( < View style = { [ styles . cornerMarker , hsection == "top" ? { top : - 4 , borderTopWidth : 7 } : { bottom : - 4 , borderBottomWidth : 7 } , vsection == "left" ? { left : - 4 , borderLeftWidth : 7 } : { right : - 4 , borderRightWidth : 7 } , ] } / > ) : null } < /View>); } ) } < /View>); } ) } < /Animated.View> < /PanGestureHandler> < /GestureHandlerRootView>); } ; export { ImageCropOverlay } ; const styles = StyleSheet . create ( { container : { height : "100%" , width : "100%" , position : "absolute" , } , overlay : { height : 40 , width : 40 , backgroundColor : "#33333355" , borderColor : "#ffffff88" , borderWidth : 1 , } , sectionRow : { flexDirection : "row" , flex : 1 , } , defaultSection : { flex : 1 , borderWidth : 0.5 , borderColor : "#ffffff88" , justifyContent : "center" , alignItems : "center" , } , cornerMarker : { position : "absolute" , borderColor : "#ffffff" , height : 30 , width : 30 , } , } ) ;