Animate lightbox in and out
This commit is contained in:
@@ -302,11 +302,6 @@ const ImageItem = ({
|
|||||||
committedTransform.value = withClampedSpring(finalTransform)
|
committedTransform.value = withClampedSpring(finalTransform)
|
||||||
})
|
})
|
||||||
|
|
||||||
const innerStyle = useAnimatedStyle(() => ({
|
|
||||||
width: '100%',
|
|
||||||
aspectRatio: imageAspect,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const composedGesture = isScrollViewBeingDragged
|
const composedGesture = isScrollViewBeingDragged
|
||||||
? // If the parent is not at rest, provide a no-op gesture.
|
? // If the parent is not at rest, provide a no-op gesture.
|
||||||
Gesture.Manual()
|
Gesture.Manual()
|
||||||
@@ -317,12 +312,9 @@ const ImageItem = ({
|
|||||||
singleTap,
|
singleTap,
|
||||||
)
|
)
|
||||||
|
|
||||||
const type = imageSrc.type
|
|
||||||
const borderRadius =
|
|
||||||
type === 'circle-avi' ? 1e5 : type === 'rect-avi' ? 20 : 0
|
|
||||||
return (
|
return (
|
||||||
<GestureDetector gesture={composedGesture}>
|
<GestureDetector gesture={composedGesture}>
|
||||||
<Animated.View style={imageStyle} renderToHardwareTextureAndroid>
|
<Animated.View renderToHardwareTextureAndroid>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
// Necessary to make opacity work for both children together.
|
// Necessary to make opacity work for both children together.
|
||||||
@@ -330,11 +322,11 @@ const ImageItem = ({
|
|||||||
style={[styles.container, animatedStyle]}>
|
style={[styles.container, animatedStyle]}>
|
||||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
contentFit="contain"
|
contentFit="cover"
|
||||||
source={{uri: imageSrc.uri}}
|
source={{uri: imageSrc.uri}}
|
||||||
placeholderContentFit="contain"
|
placeholderContentFit="cover"
|
||||||
placeholder={{uri: imageSrc.thumbUri}}
|
placeholder={{uri: imageSrc.thumbUri}}
|
||||||
style={[innerStyle, {borderRadius}]}
|
style={imageStyle}
|
||||||
accessibilityLabel={imageSrc.alt}
|
accessibilityLabel={imageSrc.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import Animated, {
|
|||||||
measure,
|
measure,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
||||||
import {Image, ImageStyle} from 'expo-image'
|
import {Image, ImageStyle} from 'expo-image'
|
||||||
@@ -67,16 +66,6 @@ const ImageItem = ({
|
|||||||
: 1,
|
: 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
|
||||||
const screenSize = measure(safeAreaRef) ?? screenSizeDelayedForJSThreadOnly
|
|
||||||
return {
|
|
||||||
width: screenSize.width,
|
|
||||||
maxHeight: screenSize.height,
|
|
||||||
alignSelf: 'center',
|
|
||||||
aspectRatio: imageAspect,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollHandler = useAnimatedScrollHandler({
|
const scrollHandler = useAnimatedScrollHandler({
|
||||||
onScroll(e) {
|
onScroll(e) {
|
||||||
const nextIsScaled = e.zoomScale > 1
|
const nextIsScaled = e.zoomScale > 1
|
||||||
@@ -143,9 +132,6 @@ const ImageItem = ({
|
|||||||
singleTap,
|
singleTap,
|
||||||
)
|
)
|
||||||
|
|
||||||
const type = imageSrc.type
|
|
||||||
const borderRadius =
|
|
||||||
type === 'circle-avi' ? 1e5 : type === 'rect-avi' ? 20 : 0
|
|
||||||
return (
|
return (
|
||||||
<GestureDetector gesture={composedGesture}>
|
<GestureDetector gesture={composedGesture}>
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
@@ -158,15 +144,14 @@ const ImageItem = ({
|
|||||||
onScroll={scrollHandler}
|
onScroll={scrollHandler}
|
||||||
bounces={scaled}
|
bounces={scaled}
|
||||||
bouncesZoom={true}
|
bouncesZoom={true}
|
||||||
style={imageStyle}
|
|
||||||
centerContent>
|
centerContent>
|
||||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
contentFit="contain"
|
contentFit="cover"
|
||||||
source={{uri: imageSrc.uri}}
|
source={{uri: imageSrc.uri}}
|
||||||
placeholderContentFit="contain"
|
placeholderContentFit="cover"
|
||||||
placeholder={{uri: imageSrc.thumbUri}}
|
placeholder={{uri: imageSrc.thumbUri}}
|
||||||
style={[animatedStyle, {borderRadius}]}
|
style={imageStyle}
|
||||||
accessibilityLabel={imageSrc.alt}
|
accessibilityLabel={imageSrc.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
enableLiveTextInteraction={showControls && !scaled}
|
enableLiveTextInteraction={showControls && !scaled}
|
||||||
|
|||||||
@@ -9,18 +9,26 @@
|
|||||||
// https://github.com/jobtoday/react-native-image-viewing
|
// https://github.com/jobtoday/react-native-image-viewing
|
||||||
|
|
||||||
import React, {useCallback, useState} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import {LayoutAnimation, Platform, StyleSheet, View} from 'react-native'
|
import {
|
||||||
|
LayoutAnimation,
|
||||||
|
PixelRatio,
|
||||||
|
Platform,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
import {Gesture} from 'react-native-gesture-handler'
|
import {Gesture} from 'react-native-gesture-handler'
|
||||||
import PagerView from 'react-native-pager-view'
|
import PagerView from 'react-native-pager-view'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
AnimatedRef,
|
AnimatedRef,
|
||||||
cancelAnimation,
|
cancelAnimation,
|
||||||
|
interpolate,
|
||||||
measure,
|
measure,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
SharedValue,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
|
useDerivedValue,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withDecay,
|
withDecay,
|
||||||
withSpring,
|
withSpring,
|
||||||
@@ -36,17 +44,19 @@ import {Lightbox} from '#/state/lightbox'
|
|||||||
import {Button} from '#/view/com/util/forms/Button'
|
import {Button} from '#/view/com/util/forms/Button'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army'
|
||||||
import {ImageSource} from './@types'
|
import {ImageSource} from './@types'
|
||||||
import ImageDefaultHeader from './components/ImageDefaultHeader'
|
import ImageDefaultHeader from './components/ImageDefaultHeader'
|
||||||
import ImageItem from './components/ImageItem/ImageItem'
|
import ImageItem from './components/ImageItem/ImageItem'
|
||||||
|
|
||||||
|
const PIXEL_RATIO = PixelRatio.get()
|
||||||
const EDGES =
|
const EDGES =
|
||||||
Platform.OS === 'android'
|
Platform.OS === 'android'
|
||||||
? (['top', 'bottom', 'left', 'right'] satisfies Edge[])
|
? (['top', 'bottom', 'left', 'right'] satisfies Edge[])
|
||||||
: (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area
|
: (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area
|
||||||
|
|
||||||
export default function ImageViewRoot({
|
export default function ImageViewRoot({
|
||||||
lightbox,
|
lightbox: nextLightbox,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
onPressSave,
|
onPressSave,
|
||||||
onPressShare,
|
onPressShare,
|
||||||
@@ -57,23 +67,67 @@ export default function ImageViewRoot({
|
|||||||
onPressShare: (uri: string) => void
|
onPressShare: (uri: string) => void
|
||||||
}) {
|
}) {
|
||||||
const ref = useAnimatedRef<View>()
|
const ref = useAnimatedRef<View>()
|
||||||
|
const [activeLightbox, setActiveLightbox] = useState(nextLightbox)
|
||||||
|
const openProgress = useSharedValue(0)
|
||||||
|
|
||||||
|
if (!activeLightbox && nextLightbox) {
|
||||||
|
setActiveLightbox(nextLightbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!nextLightbox) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const canAnimate =
|
||||||
|
!PlatformInfo.getIsReducedMotionEnabled() &&
|
||||||
|
nextLightbox.images.every(img => img.dimensions && img.thumbRect)
|
||||||
|
if (canAnimate) {
|
||||||
|
openProgress.value = withClampedSpring(1)
|
||||||
|
return () => {
|
||||||
|
openProgress.value = withClampedSpring(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openProgress.value = 1
|
||||||
|
return () => {
|
||||||
|
openProgress.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [nextLightbox, openProgress])
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => openProgress.value === 0,
|
||||||
|
(isGone, wasGone) => {
|
||||||
|
if (isGone && !wasGone) {
|
||||||
|
runOnJS(setActiveLightbox)(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const onFlyAway = React.useCallback(() => {
|
||||||
|
'worklet'
|
||||||
|
openProgress.value = 0
|
||||||
|
runOnJS(onRequestClose)()
|
||||||
|
}, [onRequestClose, openProgress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Keep it always mounted to avoid flicker on the first frame.
|
// Keep it always mounted to avoid flicker on the first frame.
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={[styles.screen, !lightbox && styles.screenHidden]}
|
style={[styles.screen, !activeLightbox && styles.screenHidden]}
|
||||||
edges={EDGES}
|
edges={EDGES}
|
||||||
aria-modal
|
aria-modal
|
||||||
accessibilityViewIsModal
|
accessibilityViewIsModal
|
||||||
aria-hidden={!lightbox}>
|
aria-hidden={!activeLightbox}>
|
||||||
<Animated.View ref={ref} style={{flex: 1}} collapsable={false}>
|
<Animated.View ref={ref} style={{flex: 1}} collapsable={false}>
|
||||||
{lightbox && (
|
{activeLightbox && (
|
||||||
<ImageView
|
<ImageView
|
||||||
key={lightbox.id}
|
key={activeLightbox.id}
|
||||||
lightbox={lightbox}
|
lightbox={activeLightbox}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
onPressSave={onPressSave}
|
onPressSave={onPressSave}
|
||||||
onPressShare={onPressShare}
|
onPressShare={onPressShare}
|
||||||
|
onFlyAway={onFlyAway}
|
||||||
safeAreaRef={ref}
|
safeAreaRef={ref}
|
||||||
|
openProgress={openProgress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -86,13 +140,17 @@ function ImageView({
|
|||||||
onRequestClose,
|
onRequestClose,
|
||||||
onPressSave,
|
onPressSave,
|
||||||
onPressShare,
|
onPressShare,
|
||||||
|
onFlyAway,
|
||||||
safeAreaRef,
|
safeAreaRef,
|
||||||
|
openProgress,
|
||||||
}: {
|
}: {
|
||||||
lightbox: Lightbox
|
lightbox: Lightbox
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onPressSave: (uri: string) => void
|
onPressSave: (uri: string) => void
|
||||||
onPressShare: (uri: string) => void
|
onPressShare: (uri: string) => void
|
||||||
|
onFlyAway: () => void
|
||||||
safeAreaRef: AnimatedRef<View>
|
safeAreaRef: AnimatedRef<View>
|
||||||
|
openProgress: SharedValue<number>
|
||||||
}) {
|
}) {
|
||||||
const {images, index: initialImageIndex} = lightbox
|
const {images, index: initialImageIndex} = lightbox
|
||||||
const [isScaled, setIsScaled] = useState(false)
|
const [isScaled, setIsScaled] = useState(false)
|
||||||
@@ -104,7 +162,7 @@ function ImageView({
|
|||||||
const isFlyingAway = useSharedValue(false)
|
const isFlyingAway = useSharedValue(false)
|
||||||
|
|
||||||
const containerStyle = useAnimatedStyle(() => {
|
const containerStyle = useAnimatedStyle(() => {
|
||||||
if (isFlyingAway.value) {
|
if (openProgress.value < 1 || isFlyingAway.value) {
|
||||||
return {pointerEvents: 'none'}
|
return {pointerEvents: 'none'}
|
||||||
}
|
}
|
||||||
return {pointerEvents: 'auto'}
|
return {pointerEvents: 'auto'}
|
||||||
@@ -112,7 +170,9 @@ function ImageView({
|
|||||||
const backdropStyle = useAnimatedStyle(() => {
|
const backdropStyle = useAnimatedStyle(() => {
|
||||||
const screenSize = measure(safeAreaRef)
|
const screenSize = measure(safeAreaRef)
|
||||||
let opacity = 1
|
let opacity = 1
|
||||||
if (screenSize) {
|
if (openProgress.value < 1) {
|
||||||
|
opacity = Math.sqrt(openProgress.value)
|
||||||
|
} else if (screenSize) {
|
||||||
const dragProgress = Math.min(
|
const dragProgress = Math.min(
|
||||||
Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2),
|
Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2),
|
||||||
1,
|
1,
|
||||||
@@ -127,7 +187,7 @@ function ImageView({
|
|||||||
const show = showControls && dismissSwipeTranslateY.value === 0
|
const show = showControls && dismissSwipeTranslateY.value === 0
|
||||||
return {
|
return {
|
||||||
pointerEvents: show ? 'box-none' : 'none',
|
pointerEvents: show ? 'box-none' : 'none',
|
||||||
opacity: withClampedSpring(show ? 1 : 0),
|
opacity: withClampedSpring(show && openProgress.value === 1 ? 1 : 0),
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateY: withClampedSpring(show ? 0 : -30),
|
translateY: withClampedSpring(show ? 0 : -30),
|
||||||
@@ -140,7 +200,7 @@ function ImageView({
|
|||||||
return {
|
return {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
pointerEvents: show ? 'box-none' : 'none',
|
pointerEvents: show ? 'box-none' : 'none',
|
||||||
opacity: withClampedSpring(show ? 1 : 0),
|
opacity: withClampedSpring(show && openProgress.value === 1 ? 1 : 0),
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateY: withClampedSpring(show ? 0 : 30),
|
translateY: withClampedSpring(show ? 0 : 30),
|
||||||
@@ -202,6 +262,7 @@ function ImageView({
|
|||||||
onZoom={onZoom}
|
onZoom={onZoom}
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
|
onFlyAway={onFlyAway}
|
||||||
isScrollViewBeingDragged={isDragging}
|
isScrollViewBeingDragged={isDragging}
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
safeAreaRef={safeAreaRef}
|
safeAreaRef={safeAreaRef}
|
||||||
@@ -209,6 +270,7 @@ function ImageView({
|
|||||||
isFlyingAway={isFlyingAway}
|
isFlyingAway={isFlyingAway}
|
||||||
isActive={i === imageIndex}
|
isActive={i === imageIndex}
|
||||||
dismissSwipeTranslateY={dismissSwipeTranslateY}
|
dismissSwipeTranslateY={dismissSwipeTranslateY}
|
||||||
|
openProgress={openProgress}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -241,24 +303,28 @@ function LightboxImage({
|
|||||||
onTap,
|
onTap,
|
||||||
onZoom,
|
onZoom,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
|
onFlyAway,
|
||||||
isScrollViewBeingDragged,
|
isScrollViewBeingDragged,
|
||||||
isScaled,
|
isScaled,
|
||||||
isFlyingAway,
|
isFlyingAway,
|
||||||
isActive,
|
isActive,
|
||||||
showControls,
|
showControls,
|
||||||
safeAreaRef,
|
safeAreaRef,
|
||||||
|
openProgress,
|
||||||
dismissSwipeTranslateY,
|
dismissSwipeTranslateY,
|
||||||
}: {
|
}: {
|
||||||
imageSrc: ImageSource
|
imageSrc: ImageSource
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onTap: () => void
|
onTap: () => void
|
||||||
onZoom: (scaled: boolean) => void
|
onZoom: (scaled: boolean) => void
|
||||||
|
onFlyAway: () => void
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
isScaled: boolean
|
isScaled: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isFlyingAway: SharedValue<boolean>
|
isFlyingAway: SharedValue<boolean>
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
safeAreaRef: AnimatedRef<View>
|
safeAreaRef: AnimatedRef<View>
|
||||||
|
openProgress: SharedValue<number>
|
||||||
dismissSwipeTranslateY: SharedValue<number>
|
dismissSwipeTranslateY: SharedValue<number>
|
||||||
}) {
|
}) {
|
||||||
const [imageAspect, imageDimensions] = useImageDimensions({
|
const [imageAspect, imageDimensions] = useImageDimensions({
|
||||||
@@ -266,6 +332,51 @@ function LightboxImage({
|
|||||||
knownDimensions: imageSrc.dimensions,
|
knownDimensions: imageSrc.dimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {thumbRect, dimensions} = imageSrc
|
||||||
|
const interpolation = useDerivedValue(() => {
|
||||||
|
'worklet'
|
||||||
|
const screenSize = measure(safeAreaRef)
|
||||||
|
if (!screenSize) {
|
||||||
|
return {transform: [], width: 0, height: 0}
|
||||||
|
}
|
||||||
|
const finalWidth = screenSize.width
|
||||||
|
const finalHeight = imageAspect ? screenSize.width / imageAspect : undefined
|
||||||
|
if (isActive && thumbRect && dimensions && openProgress.value < 1) {
|
||||||
|
return interpolateTransform(
|
||||||
|
openProgress.value,
|
||||||
|
thumbRect,
|
||||||
|
screenSize,
|
||||||
|
dimensions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const translateY = isActive ? dismissSwipeTranslateY.value : 0
|
||||||
|
return {
|
||||||
|
transform: [{translateY}],
|
||||||
|
width: finalWidth,
|
||||||
|
height: finalHeight,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerStyle = useAnimatedStyle(() => {
|
||||||
|
const {transform} = interpolation.value
|
||||||
|
return {
|
||||||
|
flex: 1,
|
||||||
|
transform,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const type = imageSrc.type
|
||||||
|
const borderRadius =
|
||||||
|
type === 'circle-avi' ? 1e5 : type === 'rect-avi' ? 20 : 0
|
||||||
|
const imageStyle = useAnimatedStyle(() => {
|
||||||
|
const {width, height} = interpolation.value
|
||||||
|
return {
|
||||||
|
borderRadius,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const dismissSwipePan = Gesture.Pan()
|
const dismissSwipePan = Gesture.Pan()
|
||||||
.enabled(isActive && !isScaled)
|
.enabled(isActive && !isScaled)
|
||||||
.activeOffsetY([-10, 10])
|
.activeOffsetY([-10, 10])
|
||||||
@@ -303,12 +414,8 @@ function LightboxImage({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageStyle = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
transform: [{translateY: dismissSwipeTranslateY.value}],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return (
|
return (
|
||||||
|
<Animated.View style={containerStyle}>
|
||||||
<ImageItem
|
<ImageItem
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
onTap={onTap}
|
onTap={onTap}
|
||||||
@@ -322,6 +429,7 @@ function LightboxImage({
|
|||||||
imageStyle={imageStyle}
|
imageStyle={imageStyle}
|
||||||
dismissSwipePan={dismissSwipePan}
|
dismissSwipePan={dismissSwipePan}
|
||||||
/>
|
/>
|
||||||
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +584,80 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function interpolatePx(
|
||||||
|
px: number,
|
||||||
|
inputRange: readonly number[],
|
||||||
|
outputRange: readonly number[],
|
||||||
|
) {
|
||||||
|
'worklet'
|
||||||
|
const value = interpolate(px, inputRange, outputRange)
|
||||||
|
return Math.round(value * PIXEL_RATIO) / PIXEL_RATIO
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateTransform(
|
||||||
|
progress: number,
|
||||||
|
thumbnailDims: {
|
||||||
|
pageX: number
|
||||||
|
width: number
|
||||||
|
pageY: number
|
||||||
|
height: number
|
||||||
|
},
|
||||||
|
screenSize: {width: number; height: number},
|
||||||
|
imageDims: {width: number; height: number},
|
||||||
|
) {
|
||||||
|
'worklet'
|
||||||
|
const imageAspect = imageDims.width / imageDims.height
|
||||||
|
const thumbAspect = thumbnailDims.width / thumbnailDims.height
|
||||||
|
let uncroppedInitialWidth
|
||||||
|
let uncroppedInitialHeight
|
||||||
|
if (imageAspect > thumbAspect) {
|
||||||
|
uncroppedInitialWidth = thumbnailDims.height * imageAspect
|
||||||
|
uncroppedInitialHeight = thumbnailDims.height
|
||||||
|
} else {
|
||||||
|
uncroppedInitialWidth = thumbnailDims.width
|
||||||
|
uncroppedInitialHeight = thumbnailDims.width / imageAspect
|
||||||
|
}
|
||||||
|
const finalWidth = screenSize.width
|
||||||
|
const finalHeight = screenSize.width / imageAspect
|
||||||
|
const initialScale = Math.min(
|
||||||
|
uncroppedInitialWidth / finalWidth,
|
||||||
|
uncroppedInitialHeight / finalHeight,
|
||||||
|
)
|
||||||
|
const croppedFinalWidth = thumbnailDims.width / initialScale
|
||||||
|
const croppedFinalHeight = thumbnailDims.height / initialScale
|
||||||
|
const screenCenterX = screenSize.width / 2
|
||||||
|
const screenCenterY = screenSize.height / 2
|
||||||
|
const thumbnailCenterX = thumbnailDims.pageX + thumbnailDims.width / 2
|
||||||
|
const thumbnailCenterY = thumbnailDims.pageY + thumbnailDims.height / 2
|
||||||
|
const initialTranslateX = thumbnailCenterX - screenCenterX
|
||||||
|
const initialTranslateY = thumbnailCenterY - screenCenterY
|
||||||
|
const scale = interpolate(progress, [0, 1], [initialScale, 1])
|
||||||
|
const translateX = interpolatePx(progress, [0, 1], [initialTranslateX, 0])
|
||||||
|
const translateY = interpolatePx(progress, [0, 1], [initialTranslateY, 0])
|
||||||
|
const width = interpolatePx(progress, [0, 1], [croppedFinalWidth, finalWidth])
|
||||||
|
const height = interpolatePx(
|
||||||
|
progress,
|
||||||
|
[0, 1],
|
||||||
|
[croppedFinalHeight, finalHeight],
|
||||||
|
)
|
||||||
|
const cropTranslateX = interpolatePx(
|
||||||
|
progress,
|
||||||
|
[0, 1],
|
||||||
|
[(finalWidth - croppedFinalWidth) / 2, 0],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{translateX},
|
||||||
|
{translateY},
|
||||||
|
{scale},
|
||||||
|
{translateX: cropTranslateX},
|
||||||
|
],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function withClampedSpring(value: any) {
|
function withClampedSpring(value: any) {
|
||||||
'worklet'
|
'worklet'
|
||||||
return withSpring(value, {overshootClamping: true, stiffness: 300})
|
return withSpring(value, {overshootClamping: true, stiffness: 120})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user