From 179c5c3f6de317546f83d00e42bcc344c8189fa5 Mon Sep 17 00:00:00 2001 From: wu wenbin Date: Sun, 18 May 2025 21:50:42 +0800 Subject: [PATCH] feat: upgrade deps --- package/package.json | 16 +- package/src/crop/index.tsx | 682 +++++++++++++++++++++++-------------- package/src/utils/index.ts | 259 +++++++------- 3 files changed, 564 insertions(+), 393 deletions(-) diff --git a/package/package.json b/package/package.json index 0f1e0d7..1de9bf1 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-avatar-crop", - "version": "1.3.5", + "version": "2.2.0", "description": "Crop component to crop profile images", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,18 +22,20 @@ "react": "*", "react-native": "*", "@react-native-masked-view/masked-view": "*", - "react-native-gesture-handler": "*", - "@react-native-community/image-editor": "https://github.com/vemarav/react-native-image-editor", - "react-native-image-size": "*" + "react-native-gesture-handler": "^2.0.0", + "@react-native-community/image-editor": "^4.0.0", + "react-native-image-size": "*", + "react-native-reanimated": "^3.0.0" }, "devDependencies": { "@types/react": "^17.0.15", "@types/react-native": "^0.64.12", "typescript": "^4.3.5", "@react-native-masked-view/masked-view": "*", - "react-native-gesture-handler": "*", - "@react-native-community/image-editor": "https://github.com/vemarav/react-native-image-editor", - "react-native-image-size": "*" + "react-native-gesture-handler": "^2.0.0", + "@react-native-community/image-editor": "^4.0.0", + "react-native-image-size": "*", + "react-native-reanimated": "^3.0.0" }, "jest": { "preset": "react-native", diff --git a/package/src/crop/index.tsx b/package/src/crop/index.tsx index 381caf0..8932863 100644 --- a/package/src/crop/index.tsx +++ b/package/src/crop/index.tsx @@ -1,22 +1,23 @@ -import ImageEditor from '@react-native-community/image-editor'; -import React, {useState, useEffect} from 'react'; -import {Animated, View, Dimensions, StyleSheet} from 'react-native'; -import {State, PinchGestureHandler, PanGestureHandler, GestureEvent} from 'react-native-gesture-handler'; -import MaskedView from '@react-native-masked-view/masked-view'; - +import ImageEditor from '@react-native-community/image-editor' +import React, { useState, useEffect, memo, useMemo, useCallback } from 'react' +import { View, Dimensions, StyleSheet } from 'react-native' +import { GestureDetector, Gesture } from 'react-native-gesture-handler' +import MaskedView from '@react-native-masked-view/masked-view' +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated' import { Size, round, assert, - getValue, getAlpha, isInRange, - computeScale, computeCover, computeContain, translateRangeX, computeImageSize, - computeTranslation, translateRangeY, computeScaledWidth, computeScaledHeight, @@ -24,37 +25,44 @@ import { computeTranslate, computeOffset, computeSize, -} from '../utils'; +} from './utils' -const {width: DEFAULT_WIDTH} = Dimensions.get('window'); -const DEFAULT_ANIM_DURATION = 180; +const { width: DEFAULT_WIDTH } = Dimensions.get('window') +const DEFAULT_ANIM_DURATION = 180 export type CropProps = { - source: {uri: string}; - cropShape?: 'rect' | 'circle'; - cropArea?: Size; - borderWidth?: number; - backgroundColor?: string; - opacity?: number; - width?: number; - height?: number; - maxZoom?: number; - resizeMode?: 'contain' | 'cover'; + source: { uri: string } + cropShape?: 'rect' | 'circle' + cropArea?: Size + borderWidth?: number + backgroundColor?: string + borderColor?: string + opacity?: number + width?: number + height?: number + maxZoom?: number + resizeMode?: 'contain' | 'cover' onCrop: ( cropCallback: (quality?: number) => Promise<{ - uri: string; - width: number; - height: number; - }>, - ) => void; -}; - -const Crop = (props: CropProps): JSX.Element => { + uri: string + width: number + height: number + }> + ) => void + showCornerMarkers?: boolean + placeholder?: React.ReactNode +} + +export const Crop = memo((props: CropProps) => { const { source, cropShape = 'circle', - cropArea = {width: DEFAULT_WIDTH, height: DEFAULT_WIDTH}, + cropArea: cropAreaProp = { + width: DEFAULT_WIDTH, + height: DEFAULT_WIDTH, + }, backgroundColor = '#FFFFFF', + borderColor, opacity = 0.7, width = DEFAULT_WIDTH, height = DEFAULT_WIDTH, @@ -62,282 +70,428 @@ const Crop = (props: CropProps): JSX.Element => { maxZoom = 5, resizeMode = 'contain', onCrop, - } = props; - - cropArea.width = round(cropArea.width, 2); - cropArea.height = round(cropArea.height, 2); - - assert(!isInRange(opacity, 1, 0), 'opacity must be between 0 and 1'); - assert(maxZoom < 1, 'maxZoom must be greater than 1'); - assert(width < cropArea.width, 'width must be greater than crop area width'); - assert(height < cropArea.height, 'height must be greater than crop area height'); - - let _lastScale = 1; - let _lastTranslate = {x: 0, y: 0}; - - const trackScale = new Animated.Value(0); - const [scale] = useState(new Animated.Value(0)); - - const [trackTranslationX] = useState(new Animated.Value(0)); - const [trackTranslationY] = useState(new Animated.Value(0)); - - const [translateX] = useState(new Animated.Value(0)); - const [translateY] = useState(new Animated.Value(0)); + showCornerMarkers = false, + placeholder, + } = props + + const cropArea = useMemo( + () => ({ + width: round(cropAreaProp.width, 2), + height: round(cropAreaProp.height, 2), + }), + [cropAreaProp.width, cropAreaProp.height] + ) + + if (opacity < 0 || opacity > 1) { + throw new Error('opacity must be between 0 and 1') + } + + assert(maxZoom < 1, 'maxZoom must be equal to or greater than 1') + assert(width < cropArea.width, 'width must be greater than or equal to crop area width') + assert( + height < cropArea.height, + 'height must be greater than or equal to crop area height' + ) + + const scale = useSharedValue(1) + const initialScale = useSharedValue(1) + + const translateX = useSharedValue(0) + const translateY = useSharedValue(0) + const initialTranslateX = useSharedValue(0) + const initialTranslateY = useSharedValue(0) + + const imageWidth = useSharedValue(0) + const imageHeight = useSharedValue(0) + const imageRotation = useSharedValue(0) + + const [minZoom, setMinZoom] = useState(1) + const [isLoaded, setIsLoaded] = useState(false) + + const opacityStyle = { + opacity: isLoaded ? 1 : 0, + } - const [minZoom, setMinZoom] = useState(1); + const init = async () => { + try { + const _imageSize = await computeImageSize(source.uri) - const imageSize = {width: NaN, height: NaN, rotation: 0}; + imageWidth.value = _imageSize.width + imageHeight.value = _imageSize.height + imageRotation.value = _imageSize.rotation ?? 0 - const setImageSize = ({width, height, rotation}: {width: number; height: number; rotation?: number}) => { - imageSize.width = width; - imageSize.height = height; - imageSize.rotation = rotation || 0; - }; + const _initialScale = computeContain(_imageSize, cropArea) - const init = async () => { - setImageSize(await computeImageSize(source.uri)); - const _initialScale = computeContain(imageSize, cropArea); + setMinZoom(_initialScale) + scale.value = _initialScale + initialScale.value = _initialScale - setMinZoom(_initialScale); - scale.setValue(_initialScale); + if (resizeMode === 'cover') { + scale.value = computeCover( + _initialScale, + _imageSize, + { width, height }, + cropArea + ) + } - if (resizeMode === 'cover') { - scale.setValue(computeCover(getValue(scale), imageSize, {width, height}, cropArea)); + translateX.value = 0 + translateY.value = 0 + setIsLoaded(true) + onCrop(cropImage) + } catch (e) { + console.error('Failed to load image:', e) + setIsLoaded(true) } - - _lastScale = getValue(scale); - - // reset translation - translateX.setValue(0); - translateY.setValue(0); - addScaleListener(); - addTranslationListeners(); - onCrop(cropImage); - }; + } useEffect(() => { - init(); - - return () => { - removeScaleListeners(); - removeTranslationListeners(); - }; - }); - - // start: pinch gesture handler - - const onPinchGestureEvent = Animated.event([{nativeEvent: {scale: trackScale}}], { - useNativeDriver: false, - }); - - const addScaleListener = () => { - trackScale.addListener(({value}: {value: number}) => { - // value always starts from 0 - scale.setValue(computeScale(value, _lastScale, maxZoom, minZoom)); - }); - }; + init() + }, [source.uri]) + + const translateStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateX: translateX.value }, + { translateY: translateY.value }, + ], + } + }) - const removeScaleListeners = () => { - trackScale.removeAllListeners(); - }; + const scaleStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: scale.value }], + } + }) const resetTranslate = () => { + 'worklet' // after scaling if crop area has blank space then // it will reset to fit image inside the crop area - const scaleValue = getValue(scale); - if (scaleValue < _lastScale) { - const translateXValue = getValue(translateX); - const translateYValue = getValue(translateY); - const {max: maxTranslateX, min: minTranslateX} = translateRangeX(getValue(scale), imageSize, cropArea, minZoom); + const scaleValue = scale.value + const imageSize = { + width: imageWidth.value, + height: imageHeight.value, + rotation: imageRotation.value, + } + const image = imageSize + if (!image || isNaN(image.width)) { + return + } + + if (scaleValue < initialScale.value) { + const translateXValue = translateX.value + const translateYValue = translateY.value + const { max: maxTranslateX, min: minTranslateX } = translateRangeX( + scaleValue, + image, + cropArea, + minZoom + ) if (!isInRange(translateXValue, maxTranslateX, minTranslateX)) { - const toValue = translateXValue > 0 ? maxTranslateX : minTranslateX; - Animated.timing(translateX, { - toValue, + const toValue = translateXValue > 0 ? maxTranslateX : minTranslateX + translateX.value = withTiming(toValue, { duration: DEFAULT_ANIM_DURATION, - useNativeDriver: true, - }).start(() => translateX.setValue(toValue)); + }) } - const {max: maxTranslateY, min: minTranslateY} = translateRangeY(getValue(scale), imageSize, cropArea, minZoom); + const { max: maxTranslateY, min: minTranslateY } = translateRangeY( + scaleValue, + image, + cropArea, + minZoom + ) if (!isInRange(translateYValue, maxTranslateY, minTranslateY)) { - const toValue = translateYValue > 0 ? maxTranslateY : minTranslateY; - Animated.timing(translateY, { - toValue, + const toValue = translateYValue > 0 ? maxTranslateY : minTranslateY + translateY.value = withTiming(toValue, { duration: DEFAULT_ANIM_DURATION, - useNativeDriver: true, - }).start(() => translateY.setValue(toValue)); + }) } } - }; - - const onPinchGestureStateChange = ({nativeEvent}: GestureEvent) => { - if (nativeEvent.oldState === State.ACTIVE) { - resetTranslate(); - // resetTranslate depends on _lastScale - _lastScale = getValue(scale); - } - }; - - // end: pinch gesture handler - - // ================================================================= - - // start: pan gesture handler - - const onPanGestureEvent = Animated.event( - [ - { - nativeEvent: { - translationX: trackTranslationX, - translationY: trackTranslationY, - }, - }, - ], - { - useNativeDriver: false, - }, - ); - - const addTranslationListeners = () => { - trackTranslationX.addListener(({value}: {value: number}) => { - const {max, min} = translateRangeX(getValue(scale), imageSize, cropArea, minZoom); - const last = _lastTranslate.x; - translateX.setValue(computeTranslation(value, last, max, min)); - }); - - trackTranslationY.addListener(({value}: {value: number}) => { - const {max, min} = translateRangeY(getValue(scale), imageSize, cropArea, minZoom); - const last = _lastTranslate.y; - translateY.setValue(computeTranslation(value, last, max, min)); - }); - }; - - const removeTranslationListeners = () => { - translateX.removeAllListeners(); - translateY.removeAllListeners(); - }; - - const onPanGestureStateChange = ({nativeEvent}: GestureEvent) => { - if (nativeEvent.oldState === State.ACTIVE) { - _lastTranslate = {x: getValue(translateX), y: getValue(translateY)}; - } - }; - - // end: pan gesture handler - - const cropImage = async (quality: number = 1): Promise<{uri: string; height: number; width: number}> => { - assert(!isInRange(quality, 1, 0), 'quality must be between 0 and 1'); - - const scaleValue = getValue(scale); - const translateXValue = getValue(translateX); - const translateYValue = getValue(translateY); - - const scaledWidth = computeScaledWidth(scaleValue, imageSize, cropArea, minZoom); - const scaledHeight = computeScaledHeight(scaleValue, imageSize, cropArea, minZoom); - const scaledMultiplier = computeScaledMultiplier(imageSize, scaledWidth); - - const scaledSize = {width: scaledWidth, height: scaledHeight}; - const translate = computeTranslate(imageSize, translateXValue, translateYValue); + } + + const panGesture = Gesture.Pan() + .minPointers(1) + .maxPointers(1) + .onBegin(() => { + initialTranslateX.value = translateX.value + initialTranslateY.value = translateY.value + }) + .onUpdate((event) => { + const imageSize = { + width: imageWidth.value, + height: imageHeight.value, + rotation: imageRotation.value, + } + const { max: maxX, min: minX } = translateRangeX( + scale.value, + imageSize, + cropArea, + minZoom + ) + const { max: maxY, min: minY } = translateRangeY( + scale.value, + imageSize, + cropArea, + minZoom + ) + const newTranslateX = + initialTranslateX.value + event.translationX / scale.value + const newTranslateY = + initialTranslateY.value + event.translationY / scale.value + translateX.value = Math.min(Math.max(newTranslateX, minX), maxX) + translateY.value = Math.min(Math.max(newTranslateY, minY), maxY) + }) + + const pinchGesture = Gesture.Pinch() + .onBegin(() => { + initialScale.value = scale.value + }) + .onChange((event) => { + const newScale = initialScale.value * event.scale + scale.value = Math.min(Math.max(newScale, minZoom), maxZoom) + }) + .onEnd(() => { + resetTranslate() + }) + + const composedGestures = Gesture.Simultaneous(panGesture, pinchGesture) + + const cropImage = useCallback( + async ( + quality: number = 1 + ): Promise<{ uri: string; height: number; width: number }> => { + if (quality < 0 || quality > 1) { + throw new Error('quality must be between 0 and 1') + } - const {max: maxTranslateX} = translateRangeX(getValue(scale), imageSize, cropArea, minZoom); - const {max: maxTranslateY} = translateRangeY(getValue(scale), imageSize, cropArea, minZoom); + const scaleValue = scale.value + const translateXValue = translateX.value + const translateYValue = translateY.value - const offset = computeOffset(scaledSize, imageSize, translate, maxTranslateX, maxTranslateY, scaledMultiplier); - const size = computeSize(cropArea, scaledMultiplier); - const emitSize = computeSize(size, quality); - const cropData = {offset, size, displaySize: emitSize}; + if (!imageWidth.value || !imageHeight.value) { + throw new Error('Invalid image dimensions') + } - try { - const croppedImageUri = await ImageEditor.cropImage(source.uri, cropData); - return {uri: croppedImageUri, ...emitSize}; - } catch (e) { - console.error('Failed to crop image!'); - throw e; - } - }; + const imageSize = { + width: imageWidth.value, + height: imageHeight.value, + rotation: imageRotation.value, + } - const borderRadius = cropShape === 'circle' ? Math.max(cropArea.height, cropArea.width) : 0; + const scaledWidth = computeScaledWidth( + scaleValue, + imageSize, + cropArea, + minZoom + ) + const scaledHeight = computeScaledHeight( + scaleValue, + imageSize, + cropArea, + minZoom + ) + const scaledMultiplier = computeScaledMultiplier(imageSize, scaledWidth) + const scaledSize = { width: scaledWidth, height: scaledHeight } + + const translate = computeTranslate( + imageSize, + translateXValue, + translateYValue + ) + + const { max: maxTranslateX } = translateRangeX( + scaleValue, + imageSize, + cropArea, + minZoom + ) + const { max: maxTranslateY } = translateRangeY( + scaleValue, + imageSize, + cropArea, + minZoom + ) + + const offset = computeOffset( + scaledSize, + imageSize, + translate, + maxTranslateX, + maxTranslateY, + scaledMultiplier + ) + const size = computeSize(cropArea, scaledMultiplier) + const emitSize = computeSize(size, quality) + const cropData = { offset, size, displaySize: emitSize } + + try { + const croppedImageUri = await ImageEditor.cropImage( + source.uri, + cropData + ) + return { + uri: croppedImageUri as unknown as string, + ...emitSize, + } + } catch (e) { + console.error('Crop failed:', e) + throw e + } + }, [ + imageWidth.value, + imageHeight.value, + imageRotation.value, + cropArea.width, + cropArea.height, + translateX.value, + translateY.value, + scale.value, + minZoom, + maxZoom, + ]) + + const borderRadius = + cropShape === 'circle' ? Math.max(cropArea.height, cropArea.width) : 0 return ( - - - - - - - }> - + + - + - - + + } + > + + + {!isLoaded && placeholder && ( + + {placeholder} + + )} + + + - + ...cropArea, + borderWidth: borderWidth, + borderRadius, + borderColor: borderColor ?? backgroundColor, + }} + > + {showCornerMarkers && ( + <> + + + + + + )} - - - ); -}; + + + ) +}) -export default Crop; +Crop.displayName = 'Crop' const styles = StyleSheet.create({ - mask: {flex: 1}, - center: {flex: 1, justifyContent: 'center', alignItems: 'center'}, - transparentMask: {backgroundColor: '#FFFFFF'}, - overlay: {flex: 1, justifyContent: 'center', alignItems: 'center'}, - contain: {resizeMode: 'contain'}, -}); + mask: { + flex: 1, + }, + center: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + zIndex: 0, + }, + transparentMask: { backgroundColor: '#FFFFFF' }, + overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + contain: { resizeMode: 'contain' }, + cover: { + justifyContent: 'center', + alignItems: 'center', + }, + loading: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + zIndex: 1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + cornerBase: { + width: 22, + height: 22, + borderColor: 'white', + position: 'absolute', + borderRadius: 2, + }, + cornerTopLeft: { + borderTopWidth: 4, + borderLeftWidth: 4, + top: -4, + left: -4, + }, + cornerTopRight: { + borderTopWidth: 4, + borderRightWidth: 4, + top: -4, + right: -4, + }, + cornerBottomLeft: { + borderBottomWidth: 4, + borderLeftWidth: 4, + bottom: -4, + left: -4, + }, + cornerBottomRight: { + borderBottomWidth: 4, + borderRightWidth: 4, + bottom: -4, + right: -4, + }, +}) diff --git a/package/src/utils/index.ts b/package/src/utils/index.ts index 38078d3..2776890 100644 --- a/package/src/utils/index.ts +++ b/package/src/utils/index.ts @@ -1,15 +1,15 @@ -import ImageSize from 'react-native-image-size'; +import ImageSize from 'react-native-image-size' export type Size = { - width: number; - height: number; - rotation?: number; -}; + width: number + height: number + rotation?: number +} export type Range = { - max: number; - min: number; -}; + max: number + min: number +} enum Orientation { landscape, @@ -22,191 +22,206 @@ enum Fit { height, } -export const log = (obj: any) => { - console.info(JSON.stringify(obj, null, 2)); -}; - export const assert = (failsTest: boolean, message: string): Error | void => { - if (failsTest) throw new Error(message); -}; + if (failsTest) throw new Error(message) +} export const round = (num: number, precision: number) => { try { - return Number(num.toFixed(precision)); + return Number(num.toFixed(precision)) } catch (e) { - return num; + return num } -}; +} -export const isInRange = (value: number, max: number, min: number): boolean => min <= value && value <= max; +export const isInRange = (value: number, max: number, min: number): boolean => { + 'worklet' + return min <= value && value <= max +} export const getAlpha = (opacity: number): string => { // #12345678 78 is the alpha value and the range is (0 < alpha < 100) if (opacity === 1) { - return ''; + return '' } else { - return `${Math.ceil(opacity * 100)}`.padStart(2, '0').slice(-2); + return `${Math.ceil(opacity * 100)}`.padStart(2, '0').slice(-2) } -}; +} export const getRatio = (imageSize: Size) => { - const {width, height} = imageSize; - return Math.max(width, height) / Math.min(width, height); -}; - -export const getValue = (animated: any): any => Number.parseFloat(JSON.stringify(animated)); + const { width, height } = imageSize + return Math.max(width, height) / Math.min(width, height) +} export const getOrientation = (size: Size) => { if (size.width > size.height) { - return Orientation.landscape; + return Orientation.landscape } else if (size.width < size.height) { - return Orientation.portrait; + return Orientation.portrait } else { - return Orientation.even; + return Orientation.even } -}; +} export const getAspectRatio = (size: Size) => { - return size.height / size.width; -}; + 'worklet' + return size.height / size.width +} export const computeImageSize = async (uri: string): Promise => { - const {width, height, rotation} = await ImageSize.getSize(uri); + const { width, height, rotation } = await ImageSize.getSize(uri) if (rotation === 90 || rotation === 270) { - return {width: height, height: width, rotation}; + return { width: height, height: width, rotation } } else { - return {width, height, rotation}; + return { width, height, rotation } } -}; - -export const computeScale = (current: number, last: number, max: number, min: number) => { - const next = current + last - 1; - if (isInRange(next, max, min)) { - return next; - } - - if (next > max) { - return max; - } - - return min; -}; +} export const computeContain = (imageSize: Size, cropArea: Size) => { - const scale = imageSize.height / cropArea.height / (imageSize.width / cropArea.width); - return scale > 1 ? scale : 1 / scale; -}; + const scale = + imageSize.height / cropArea.height / (imageSize.width / cropArea.width) + return scale > 1 ? scale : 1 / scale +} -export const computeCover = (scale: number, imageSize: Size, size: Size, cropArea: Size) => { - const imageOrientation = getOrientation(imageSize); +export const computeCover = ( + scale: number, + imageSize: Size, + size: Size, + cropArea: Size +) => { + const imageOrientation = getOrientation(imageSize) if (imageOrientation === Orientation.portrait) { - return scale * (size.width / cropArea.width); + return scale * (size.width / cropArea.width) } else { - return scale * (size.height / cropArea.height); + return scale * (size.height / cropArea.height) } -}; +} -export const translateRangeX = (scale: number, imageSize: Size, cropArea: Size, minZoom: number) => { - const cropARatio = getAspectRatio(cropArea); - const imageARatio = getAspectRatio(imageSize); - const initialFit = cropARatio > imageARatio ? Fit.height : Fit.width; +export const translateRangeX = ( + scale: number, + imageSize: Size, + cropArea: Size, + minZoom: number +) => { + 'worklet' + const cropARatio = getAspectRatio(cropArea) + const imageARatio = getAspectRatio(imageSize) + const initialFit = cropARatio > imageARatio ? Fit.height : Fit.width if (initialFit === Fit.width) { - const imageOutsideBoxSize = (cropArea.width * scale) / minZoom - cropArea.width; - return {max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2}; + const imageOutsideBoxSize = + (cropArea.width * scale) / minZoom - cropArea.width + + return { max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2 } } else { - const imageOutsideBoxSize = cropArea.width * scale - cropArea.width; - return {max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2}; + const imageOutsideBoxSize = cropArea.width * scale - cropArea.width + return { max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2 } } -}; +} -export const translateRangeY = (scale: number, imageSize: Size, cropArea: Size, minZoom: number) => { - const cropARatio = getAspectRatio(cropArea); - const imageARatio = getAspectRatio(imageSize); - const initialFit = cropARatio < imageARatio ? Fit.width : Fit.height; +export const translateRangeY = ( + scale: number, + imageSize: Size, + cropArea: Size, + minZoom: number +) => { + 'worklet' + const cropARatio = getAspectRatio(cropArea) + const imageARatio = getAspectRatio(imageSize) + const initialFit = cropARatio < imageARatio ? Fit.width : Fit.height if (initialFit === Fit.height) { - const imageOutsideBoxSize = (cropArea.height * scale) / minZoom - cropArea.height; - return {max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2}; + const imageOutsideBoxSize = + (cropArea.height * scale) / minZoom - cropArea.height + return { max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2 } } else { - const imageOutsideBoxSize = cropArea.height * scale - cropArea.height; - return {max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2}; - } -}; - -export const computeTranslation = (current: number, last: number, max: number, min: number) => { - const next = current + last; - - if (isInRange(next, max, min)) { - return next; - } - - if (next > max) { - return max; + const imageOutsideBoxSize = cropArea.height * scale - cropArea.height + return { max: imageOutsideBoxSize / 2, min: -imageOutsideBoxSize / 2 } } +} - return min; -}; - -export const computeScaledWidth = (scale: number, imageSize: Size, cropArea: Size, minZoom: number): number => { - const {max: maxTranslateX} = translateRangeX(minZoom, imageSize, cropArea, minZoom); - return maxTranslateX > 0 ? cropArea.width * scale : (cropArea.width * scale) / minZoom; -}; +export const computeScaledWidth = ( + scale: number, + imageSize: Size, + cropArea: Size, + minZoom: number +): number => { + const { max: maxTranslateX } = translateRangeX( + minZoom, + imageSize, + cropArea, + minZoom + ) + return maxTranslateX > 0 + ? cropArea.width * scale + : (cropArea.width * scale) / minZoom +} -export const computeScaledHeight = (scale: number, imageSize: Size, cropArea: Size, minZoom: number): number => { - const {max: maxTranslateY} = translateRangeY(minZoom, imageSize, cropArea, minZoom); - return maxTranslateY > 0 ? cropArea.height * scale : (cropArea.height * scale) / minZoom; -}; +export const computeScaledHeight = ( + scale: number, + imageSize: Size, + cropArea: Size, + minZoom: number +): number => { + const { max: maxTranslateY } = translateRangeY( + minZoom, + imageSize, + cropArea, + minZoom + ) + return maxTranslateY > 0 + ? cropArea.height * scale + : (cropArea.height * scale) / minZoom +} export const computeScaledMultiplier = (imageSize: Size, width: number) => { - return imageSize.width / width; -}; + return imageSize.width / width +} export const computeTranslate = (imageSize: Size, x: number, y: number) => { - if (imageSize.rotation === 90) return {x: -x, y: y}; - if (imageSize.rotation === 180) return {x: -x, y: -y}; - if (imageSize.rotation === 270) return {x: x, y: -y}; - return {x, y}; -}; + if (imageSize.rotation === 90) return { x: -x, y: y } + if (imageSize.rotation === 180) return { x: -x, y: -y } + if (imageSize.rotation === 270) return { x: x, y: -y } + return { x, y } +} export const computeOffset = ( scaled: Size, imageSize: Size, - translate: {x: number; y: number}, + translate: { x: number; y: number }, maxTranslateX: number, maxTranslateY: number, - multiplier: number, -): {x: number; y: number} => { - const initialOffsetX = scaled.width - maxTranslateX; - const initialOffsetY = scaled.height - maxTranslateY; - const finalOffsetX = imageSize.width - (initialOffsetX + translate.x) * multiplier; - const finalOffsetY = imageSize.height - (initialOffsetY + translate.y) * multiplier; - const offset = {x: round(finalOffsetX, 3), y: round(finalOffsetY, 3)}; - if (imageSize.rotation == 90 || imageSize.rotation === 270) { - return {x: offset.y, y: offset.x}; + multiplier: number +): { x: number; y: number } => { + const initialOffsetX = scaled.width - maxTranslateX + const initialOffsetY = scaled.height - maxTranslateY + const finalOffsetX = + imageSize.width - (initialOffsetX + translate.x) * multiplier + const finalOffsetY = + imageSize.height - (initialOffsetY + translate.y) * multiplier + const offset = { x: round(finalOffsetX, 3), y: round(finalOffsetY, 3) } + if (imageSize.rotation === 90 || imageSize.rotation === 270) { + return { x: offset.y, y: offset.x } } - return offset; -}; + return offset +} export const computeSize = (size: Size, multiplier: number): Size => { return { width: round(size.width * multiplier, 3), height: round(size.height * multiplier, 3), - }; -}; + } +} export default { - log, Fit, Orientation, assert, - getValue, getAlpha, getRatio, getOrientation, isInRange, - computeScale, computeCover, computeImageSize, translateRangeX, @@ -215,4 +230,4 @@ export default { computeScaledWidth, computeScaledHeight, computeScaledMultiplier, -}; +}