@idealyst/animate 1.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +72 -0
- package/src/index.native.ts +66 -0
- package/src/index.ts +64 -0
- package/src/types.ts +208 -0
- package/src/useAnimatedStyle.native.ts +168 -0
- package/src/useAnimatedStyle.ts +165 -0
- package/src/useAnimatedValue.native.ts +169 -0
- package/src/useAnimatedValue.ts +227 -0
- package/src/useGradientBorder.native.tsx +229 -0
- package/src/useGradientBorder.ts +180 -0
- package/src/usePresence.native.ts +157 -0
- package/src/usePresence.ts +151 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGradientBorder - Native implementation
|
|
3
|
+
*
|
|
4
|
+
* Creates animated gradient border effects using react-native-svg
|
|
5
|
+
* and Reanimated for performant animations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo, useEffect } from 'react';
|
|
9
|
+
import { View, StyleSheet } from 'react-native';
|
|
10
|
+
import {
|
|
11
|
+
useSharedValue,
|
|
12
|
+
useAnimatedProps,
|
|
13
|
+
withRepeat,
|
|
14
|
+
withTiming,
|
|
15
|
+
Easing,
|
|
16
|
+
} from 'react-native-reanimated';
|
|
17
|
+
import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
|
|
18
|
+
import Animated from 'react-native-reanimated';
|
|
19
|
+
import { resolveDuration } from '@idealyst/theme/animation';
|
|
20
|
+
import type { UseGradientBorderOptions, UseGradientBorderResult, AnimatableStyle } from './types';
|
|
21
|
+
|
|
22
|
+
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
|
|
23
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
24
|
+
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook that creates an animated gradient border effect.
|
|
28
|
+
* Uses SVG with Reanimated for smooth animations on native.
|
|
29
|
+
*
|
|
30
|
+
* @param options - Configuration for the gradient border
|
|
31
|
+
* @returns Container and content styles to apply
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* const { containerStyle, contentStyle, GradientBorder } = useGradientBorder({
|
|
36
|
+
* colors: ['#3b82f6', '#8b5cf6', '#ec4899'],
|
|
37
|
+
* borderWidth: 2,
|
|
38
|
+
* borderRadius: 8,
|
|
39
|
+
* animation: 'spin',
|
|
40
|
+
* duration: 2000,
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* return (
|
|
44
|
+
* <View style={containerStyle}>
|
|
45
|
+
* <GradientBorder />
|
|
46
|
+
* <View style={contentStyle}>
|
|
47
|
+
* Content here
|
|
48
|
+
* </View>
|
|
49
|
+
* </View>
|
|
50
|
+
* );
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function useGradientBorder(options: UseGradientBorderOptions): UseGradientBorderResult & {
|
|
54
|
+
GradientBorder: React.FC<{ width: number; height: number }>;
|
|
55
|
+
} {
|
|
56
|
+
const {
|
|
57
|
+
colors,
|
|
58
|
+
borderWidth = 2,
|
|
59
|
+
borderRadius = 8,
|
|
60
|
+
duration = 2000,
|
|
61
|
+
animation = 'spin',
|
|
62
|
+
active = true,
|
|
63
|
+
} = options;
|
|
64
|
+
|
|
65
|
+
const durationMs = resolveDuration(duration);
|
|
66
|
+
|
|
67
|
+
// Animation value for rotation (0-360) or progress (0-1)
|
|
68
|
+
const animationValue = useSharedValue(0);
|
|
69
|
+
|
|
70
|
+
// Start animation
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!active) {
|
|
73
|
+
animationValue.value = 0;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
switch (animation) {
|
|
78
|
+
case 'spin':
|
|
79
|
+
animationValue.value = withRepeat(
|
|
80
|
+
withTiming(360, {
|
|
81
|
+
duration: durationMs,
|
|
82
|
+
easing: Easing.linear,
|
|
83
|
+
}),
|
|
84
|
+
-1, // infinite
|
|
85
|
+
false // don't reverse
|
|
86
|
+
);
|
|
87
|
+
break;
|
|
88
|
+
case 'pulse':
|
|
89
|
+
animationValue.value = withRepeat(
|
|
90
|
+
withTiming(1, {
|
|
91
|
+
duration: durationMs / 2,
|
|
92
|
+
easing: Easing.inOut(Easing.ease),
|
|
93
|
+
}),
|
|
94
|
+
-1,
|
|
95
|
+
true // reverse
|
|
96
|
+
);
|
|
97
|
+
break;
|
|
98
|
+
case 'wave':
|
|
99
|
+
animationValue.value = withRepeat(
|
|
100
|
+
withTiming(1, {
|
|
101
|
+
duration: durationMs,
|
|
102
|
+
easing: Easing.inOut(Easing.ease),
|
|
103
|
+
}),
|
|
104
|
+
-1,
|
|
105
|
+
false
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}, [active, animation, durationMs]);
|
|
110
|
+
|
|
111
|
+
// Container style
|
|
112
|
+
const containerStyle = useMemo<AnimatableStyle>(() => {
|
|
113
|
+
return {
|
|
114
|
+
position: 'relative',
|
|
115
|
+
overflow: 'hidden',
|
|
116
|
+
} as AnimatableStyle;
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Content style
|
|
120
|
+
const contentStyle = useMemo<AnimatableStyle>(() => {
|
|
121
|
+
return {
|
|
122
|
+
position: 'absolute',
|
|
123
|
+
top: borderWidth,
|
|
124
|
+
left: borderWidth,
|
|
125
|
+
right: borderWidth,
|
|
126
|
+
bottom: borderWidth,
|
|
127
|
+
borderRadius,
|
|
128
|
+
backgroundColor: 'white', // This should be set by the consumer
|
|
129
|
+
overflow: 'hidden',
|
|
130
|
+
} as AnimatableStyle;
|
|
131
|
+
}, [borderWidth, borderRadius]);
|
|
132
|
+
|
|
133
|
+
// Gradient border component
|
|
134
|
+
const GradientBorder: React.FC<{ width: number; height: number }> = useMemo(() => {
|
|
135
|
+
return ({ width, height }) => {
|
|
136
|
+
// Animated props for the gradient
|
|
137
|
+
const animatedGradientProps = useAnimatedProps(() => {
|
|
138
|
+
if (animation === 'spin') {
|
|
139
|
+
// Rotate the gradient
|
|
140
|
+
const angle = animationValue.value;
|
|
141
|
+
const radians = (angle * Math.PI) / 180;
|
|
142
|
+
const x2 = 0.5 + 0.5 * Math.cos(radians);
|
|
143
|
+
const y2 = 0.5 + 0.5 * Math.sin(radians);
|
|
144
|
+
const x1 = 1 - x2;
|
|
145
|
+
const y1 = 1 - y2;
|
|
146
|
+
return {
|
|
147
|
+
x1: String(x1),
|
|
148
|
+
y1: String(y1),
|
|
149
|
+
x2: String(x2),
|
|
150
|
+
y2: String(y2),
|
|
151
|
+
};
|
|
152
|
+
} else if (animation === 'wave') {
|
|
153
|
+
// Move gradient position
|
|
154
|
+
const progress = animationValue.value;
|
|
155
|
+
return {
|
|
156
|
+
x1: String(-1 + progress * 2),
|
|
157
|
+
y1: '0',
|
|
158
|
+
x2: String(progress * 2),
|
|
159
|
+
y2: '0',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
x1: '0',
|
|
164
|
+
y1: '0',
|
|
165
|
+
x2: '1',
|
|
166
|
+
y2: '1',
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const animatedRectProps = useAnimatedProps(() => {
|
|
171
|
+
if (animation === 'pulse') {
|
|
172
|
+
const opacity = 0.5 + animationValue.value * 0.5;
|
|
173
|
+
return { opacity };
|
|
174
|
+
}
|
|
175
|
+
return { opacity: 1 };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Svg
|
|
180
|
+
width={width}
|
|
181
|
+
height={height}
|
|
182
|
+
style={StyleSheet.absoluteFill}
|
|
183
|
+
>
|
|
184
|
+
<Defs>
|
|
185
|
+
<AnimatedLinearGradient
|
|
186
|
+
id="gradient"
|
|
187
|
+
animatedProps={animatedGradientProps}
|
|
188
|
+
>
|
|
189
|
+
{colors.map((color, index) => (
|
|
190
|
+
<Stop
|
|
191
|
+
key={index}
|
|
192
|
+
offset={`${(index / (colors.length - 1)) * 100}%`}
|
|
193
|
+
stopColor={color}
|
|
194
|
+
/>
|
|
195
|
+
))}
|
|
196
|
+
</AnimatedLinearGradient>
|
|
197
|
+
</Defs>
|
|
198
|
+
<AnimatedRect
|
|
199
|
+
x={0}
|
|
200
|
+
y={0}
|
|
201
|
+
width={width}
|
|
202
|
+
height={height}
|
|
203
|
+
rx={borderRadius + borderWidth}
|
|
204
|
+
ry={borderRadius + borderWidth}
|
|
205
|
+
fill="url(#gradient)"
|
|
206
|
+
animatedProps={animatedRectProps}
|
|
207
|
+
/>
|
|
208
|
+
{/* Inner cutout (white background) */}
|
|
209
|
+
<Rect
|
|
210
|
+
x={borderWidth}
|
|
211
|
+
y={borderWidth}
|
|
212
|
+
width={width - borderWidth * 2}
|
|
213
|
+
height={height - borderWidth * 2}
|
|
214
|
+
rx={borderRadius}
|
|
215
|
+
ry={borderRadius}
|
|
216
|
+
fill="white"
|
|
217
|
+
/>
|
|
218
|
+
</Svg>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
}, [colors, borderWidth, borderRadius, animation, animationValue]);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
containerStyle,
|
|
225
|
+
contentStyle,
|
|
226
|
+
isReady: true,
|
|
227
|
+
GradientBorder,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGradientBorder - Web implementation
|
|
3
|
+
*
|
|
4
|
+
* Creates animated gradient border effects using CSS @property
|
|
5
|
+
* and conic gradients for performant animations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo, useEffect, useRef } from 'react';
|
|
9
|
+
import { resolveDuration, injectGradientCSS } from '@idealyst/theme/animation';
|
|
10
|
+
import type { UseGradientBorderOptions, UseGradientBorderResult, AnimatableStyle } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook that creates an animated gradient border effect.
|
|
14
|
+
* Uses CSS @property for smooth, GPU-accelerated gradient animations.
|
|
15
|
+
*
|
|
16
|
+
* @param options - Configuration for the gradient border
|
|
17
|
+
* @returns Container and content styles to apply
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const { containerStyle, contentStyle, isReady } = useGradientBorder({
|
|
22
|
+
* colors: ['#3b82f6', '#8b5cf6', '#ec4899'],
|
|
23
|
+
* borderWidth: 2,
|
|
24
|
+
* borderRadius: 8,
|
|
25
|
+
* animation: 'spin',
|
|
26
|
+
* duration: 2000,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <div style={containerStyle}>
|
|
31
|
+
* <div style={contentStyle}>
|
|
32
|
+
* Content here
|
|
33
|
+
* </div>
|
|
34
|
+
* </div>
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function useGradientBorder(options: UseGradientBorderOptions): UseGradientBorderResult {
|
|
39
|
+
const {
|
|
40
|
+
colors,
|
|
41
|
+
borderWidth = 2,
|
|
42
|
+
borderRadius = 8,
|
|
43
|
+
duration = 2000,
|
|
44
|
+
animation = 'spin',
|
|
45
|
+
active = true,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
const isReady = useRef(false);
|
|
49
|
+
const durationMs = resolveDuration(duration);
|
|
50
|
+
|
|
51
|
+
// Inject gradient CSS on first use
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
injectGradientCSS();
|
|
54
|
+
isReady.current = true;
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Generate the color string for conic gradient
|
|
58
|
+
const colorString = useMemo(() => {
|
|
59
|
+
// For spinning gradient, distribute colors evenly
|
|
60
|
+
if (animation === 'spin') {
|
|
61
|
+
return colors.join(', ');
|
|
62
|
+
}
|
|
63
|
+
// For pulse/wave, use linear gradient
|
|
64
|
+
return colors.join(', ');
|
|
65
|
+
}, [colors, animation]);
|
|
66
|
+
|
|
67
|
+
// Container style (the gradient background)
|
|
68
|
+
const containerStyle = useMemo<AnimatableStyle>(() => {
|
|
69
|
+
const baseStyle: Record<string, any> = {
|
|
70
|
+
position: 'relative',
|
|
71
|
+
padding: borderWidth,
|
|
72
|
+
borderRadius: borderRadius + borderWidth,
|
|
73
|
+
overflow: 'hidden',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (!active) {
|
|
77
|
+
// When inactive, show a solid border color
|
|
78
|
+
baseStyle.background = colors[0];
|
|
79
|
+
return baseStyle as AnimatableStyle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switch (animation) {
|
|
83
|
+
case 'spin':
|
|
84
|
+
return {
|
|
85
|
+
...baseStyle,
|
|
86
|
+
background: `conic-gradient(from var(--gradient-angle, 0deg), ${colorString})`,
|
|
87
|
+
animation: `spin-gradient ${durationMs}ms linear infinite`,
|
|
88
|
+
} as AnimatableStyle;
|
|
89
|
+
|
|
90
|
+
case 'pulse':
|
|
91
|
+
return {
|
|
92
|
+
...baseStyle,
|
|
93
|
+
background: `linear-gradient(135deg, ${colorString})`,
|
|
94
|
+
animation: `pulse-gradient ${durationMs}ms ease-in-out infinite`,
|
|
95
|
+
} as AnimatableStyle;
|
|
96
|
+
|
|
97
|
+
case 'wave':
|
|
98
|
+
// Wave uses a moving gradient position
|
|
99
|
+
return {
|
|
100
|
+
...baseStyle,
|
|
101
|
+
background: `linear-gradient(
|
|
102
|
+
90deg,
|
|
103
|
+
${colors[0]} var(--gradient-position, 0%),
|
|
104
|
+
${colors[colors.length - 1]} calc(var(--gradient-position, 0%) + 50%),
|
|
105
|
+
${colors[0]} calc(var(--gradient-position, 0%) + 100%)
|
|
106
|
+
)`,
|
|
107
|
+
backgroundSize: '200% 100%',
|
|
108
|
+
animation: `wave-gradient ${durationMs}ms ease-in-out infinite`,
|
|
109
|
+
} as AnimatableStyle;
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
return baseStyle as AnimatableStyle;
|
|
113
|
+
}
|
|
114
|
+
}, [colors, colorString, borderWidth, borderRadius, durationMs, animation, active]);
|
|
115
|
+
|
|
116
|
+
// Content style (the inner container with solid background)
|
|
117
|
+
const contentStyle = useMemo<AnimatableStyle>(() => {
|
|
118
|
+
return {
|
|
119
|
+
borderRadius,
|
|
120
|
+
backgroundColor: 'inherit',
|
|
121
|
+
// Ensure content fills the container minus the border
|
|
122
|
+
width: '100%',
|
|
123
|
+
height: '100%',
|
|
124
|
+
} as AnimatableStyle;
|
|
125
|
+
}, [borderRadius]);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
containerStyle,
|
|
129
|
+
contentStyle,
|
|
130
|
+
isReady: isReady.current,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Utility to create inline gradient border styles without the hook.
|
|
136
|
+
* Useful for static gradient borders or server-side rendering.
|
|
137
|
+
*
|
|
138
|
+
* Note: You must call injectGradientCSS() somewhere in your app for animations to work.
|
|
139
|
+
*/
|
|
140
|
+
export function createGradientBorderStyle(
|
|
141
|
+
colors: string[],
|
|
142
|
+
borderWidth: number = 2,
|
|
143
|
+
borderRadius: number = 8,
|
|
144
|
+
animation: 'spin' | 'pulse' | 'wave' = 'spin',
|
|
145
|
+
durationMs: number = 2000
|
|
146
|
+
): { container: Record<string, any>; content: Record<string, any> } {
|
|
147
|
+
const colorString = colors.join(', ');
|
|
148
|
+
|
|
149
|
+
const container: Record<string, any> = {
|
|
150
|
+
position: 'relative',
|
|
151
|
+
padding: borderWidth,
|
|
152
|
+
borderRadius: borderRadius + borderWidth,
|
|
153
|
+
overflow: 'hidden',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
switch (animation) {
|
|
157
|
+
case 'spin':
|
|
158
|
+
container.background = `conic-gradient(from var(--gradient-angle, 0deg), ${colorString})`;
|
|
159
|
+
container.animation = `spin-gradient ${durationMs}ms linear infinite`;
|
|
160
|
+
break;
|
|
161
|
+
case 'pulse':
|
|
162
|
+
container.background = `linear-gradient(135deg, ${colorString})`;
|
|
163
|
+
container.animation = `pulse-gradient ${durationMs}ms ease-in-out infinite`;
|
|
164
|
+
break;
|
|
165
|
+
case 'wave':
|
|
166
|
+
container.background = `linear-gradient(90deg, ${colors[0]}, ${colors[colors.length - 1]}, ${colors[0]})`;
|
|
167
|
+
container.backgroundSize = '200% 100%';
|
|
168
|
+
container.animation = `wave-gradient ${durationMs}ms ease-in-out infinite`;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const content = {
|
|
173
|
+
borderRadius,
|
|
174
|
+
backgroundColor: 'inherit',
|
|
175
|
+
width: '100%',
|
|
176
|
+
height: '100%',
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return { container, content };
|
|
180
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePresence - Native implementation
|
|
3
|
+
*
|
|
4
|
+
* Manages mount/unmount animations using Reanimated.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
useSharedValue,
|
|
10
|
+
useAnimatedStyle,
|
|
11
|
+
withTiming,
|
|
12
|
+
withSpring,
|
|
13
|
+
withDelay,
|
|
14
|
+
Easing,
|
|
15
|
+
runOnJS,
|
|
16
|
+
} from 'react-native-reanimated';
|
|
17
|
+
import { timingConfig, springConfig, isSpringEasing, resolveDuration } from '@idealyst/theme/animation';
|
|
18
|
+
import type { UsePresenceOptions, UsePresenceResult, AnimatableStyle } from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook that manages presence animations for mount/unmount.
|
|
22
|
+
* The element stays rendered during exit animation.
|
|
23
|
+
*
|
|
24
|
+
* @param isVisible - Whether the element should be visible
|
|
25
|
+
* @param options - Animation configuration with enter/exit styles
|
|
26
|
+
* @returns Object with isPresent, style, and exit function
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import Animated from 'react-native-reanimated';
|
|
31
|
+
*
|
|
32
|
+
* const { isPresent, style } = usePresence(isOpen, {
|
|
33
|
+
* enter: { opacity: 1, transform: [{ translateY: 0 }] },
|
|
34
|
+
* exit: { opacity: 0, transform: [{ translateY: -20 }] },
|
|
35
|
+
* duration: 'normal',
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* return isPresent && <Animated.View style={style}>Content</Animated.View>;
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function usePresence(isVisible: boolean, options: UsePresenceOptions): UsePresenceResult {
|
|
42
|
+
const { enter, exit, initial, duration = 'normal', easing = 'easeOut', delay = 0 } = options;
|
|
43
|
+
|
|
44
|
+
// Track whether the element should be in the DOM
|
|
45
|
+
const [isPresent, setIsPresent] = useState(isVisible);
|
|
46
|
+
// Track initial mount
|
|
47
|
+
const isInitialMount = useRef(true);
|
|
48
|
+
|
|
49
|
+
const durationMs = resolveDuration(duration);
|
|
50
|
+
const useSpring = isSpringEasing(easing);
|
|
51
|
+
|
|
52
|
+
// Shared value for animation progress (0 = exit, 1 = enter)
|
|
53
|
+
const progress = useSharedValue(isVisible ? 1 : 0);
|
|
54
|
+
|
|
55
|
+
// Extract values from enter/exit styles
|
|
56
|
+
const enterOpacity = enter.opacity ?? 1;
|
|
57
|
+
const exitOpacity = exit.opacity ?? 0;
|
|
58
|
+
const initialOpacity = initial?.opacity ?? exitOpacity;
|
|
59
|
+
|
|
60
|
+
// Extract transform values
|
|
61
|
+
const getTransformValue = (
|
|
62
|
+
style: typeof enter | typeof exit,
|
|
63
|
+
key: string,
|
|
64
|
+
defaultValue: number | string
|
|
65
|
+
) => {
|
|
66
|
+
if (!style.transform) return defaultValue;
|
|
67
|
+
const transform = style.transform.find((t) => key in t);
|
|
68
|
+
return transform ? (transform as any)[key] : defaultValue;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const enterTranslateY = getTransformValue(enter, 'translateY', 0) as number;
|
|
72
|
+
const exitTranslateY = getTransformValue(exit, 'translateY', 0) as number;
|
|
73
|
+
const enterTranslateX = getTransformValue(enter, 'translateX', 0) as number;
|
|
74
|
+
const exitTranslateX = getTransformValue(exit, 'translateX', 0) as number;
|
|
75
|
+
const enterScale = getTransformValue(enter, 'scale', 1) as number;
|
|
76
|
+
const exitScale = getTransformValue(exit, 'scale', 1) as number;
|
|
77
|
+
|
|
78
|
+
// Animation helper
|
|
79
|
+
const animateTo = useCallback(
|
|
80
|
+
(target: number, onComplete?: () => void) => {
|
|
81
|
+
'worklet';
|
|
82
|
+
const withCallback = (animation: any) => {
|
|
83
|
+
if (onComplete) {
|
|
84
|
+
return {
|
|
85
|
+
...animation,
|
|
86
|
+
callback: (finished: boolean) => {
|
|
87
|
+
if (finished) {
|
|
88
|
+
runOnJS(onComplete)();
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return animation;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (useSpring) {
|
|
97
|
+
const config = springConfig(easing as any);
|
|
98
|
+
return delay > 0
|
|
99
|
+
? withDelay(delay, withSpring(target, config, onComplete ? (finished) => finished && runOnJS(onComplete)() : undefined))
|
|
100
|
+
: withSpring(target, config, onComplete ? (finished) => finished && runOnJS(onComplete)() : undefined);
|
|
101
|
+
} else {
|
|
102
|
+
const config = timingConfig(duration, easing);
|
|
103
|
+
const timingOptions = {
|
|
104
|
+
duration: config.duration,
|
|
105
|
+
easing: Easing.bezier(config.easing[0], config.easing[1], config.easing[2], config.easing[3]),
|
|
106
|
+
};
|
|
107
|
+
return delay > 0
|
|
108
|
+
? withDelay(delay, withTiming(target, timingOptions, onComplete ? (finished) => finished && runOnJS(onComplete)() : undefined))
|
|
109
|
+
: withTiming(target, timingOptions, onComplete ? (finished) => finished && runOnJS(onComplete)() : undefined);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[useSpring, easing, duration, delay]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Handle visibility changes
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (isVisible) {
|
|
118
|
+
// Entering: mount first, then animate
|
|
119
|
+
setIsPresent(true);
|
|
120
|
+
progress.value = animateTo(1);
|
|
121
|
+
} else if (!isInitialMount.current) {
|
|
122
|
+
// Exiting: animate, then unmount
|
|
123
|
+
progress.value = animateTo(0, () => {
|
|
124
|
+
setIsPresent(false);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isInitialMount.current = false;
|
|
129
|
+
}, [isVisible]);
|
|
130
|
+
|
|
131
|
+
// Manual exit trigger
|
|
132
|
+
const triggerExit = useCallback(() => {
|
|
133
|
+
progress.value = animateTo(0, () => {
|
|
134
|
+
setIsPresent(false);
|
|
135
|
+
});
|
|
136
|
+
}, [animateTo]);
|
|
137
|
+
|
|
138
|
+
// Animated style
|
|
139
|
+
const style = useAnimatedStyle(() => {
|
|
140
|
+
const p = progress.value;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
opacity: exitOpacity + (enterOpacity - exitOpacity) * p,
|
|
144
|
+
transform: [
|
|
145
|
+
{ translateX: exitTranslateX + (enterTranslateX - exitTranslateX) * p },
|
|
146
|
+
{ translateY: exitTranslateY + (enterTranslateY - exitTranslateY) * p },
|
|
147
|
+
{ scale: exitScale + (enterScale - exitScale) * p },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
isPresent,
|
|
154
|
+
style: style as AnimatableStyle,
|
|
155
|
+
exit: triggerExit,
|
|
156
|
+
};
|
|
157
|
+
}
|