@idealyst/animate 1.2.58 → 1.2.60
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 +3 -3
- package/src/index.native.ts +1 -0
- package/src/index.ts +1 -0
- package/src/normalizeTransform.ts +79 -0
- package/src/types.ts +48 -3
- package/src/useAnimatedStyle.native.ts +113 -22
- package/src/useAnimatedStyle.ts +36 -60
- package/src/usePresence.native.ts +33 -12
- package/src/usePresence.ts +18 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/animate",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.60",
|
|
4
4
|
"description": "Cross-platform animation hooks for React and React Native",
|
|
5
5
|
"readme": "README.md",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"publish:npm": "npm publish"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@idealyst/theme": "^1.2.
|
|
33
|
+
"@idealyst/theme": "^1.2.60",
|
|
34
34
|
"react": ">=16.8.0",
|
|
35
35
|
"react-native": ">=0.60.0",
|
|
36
36
|
"react-native-reanimated": ">=3.0.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@idealyst/theme": "^1.2.
|
|
51
|
+
"@idealyst/theme": "^1.2.60",
|
|
52
52
|
"@types/react": "^19.1.0",
|
|
53
53
|
"react": "^19.1.0",
|
|
54
54
|
"react-native": "^0.80.1",
|
package/src/index.native.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform normalization utilities.
|
|
3
|
+
*
|
|
4
|
+
* Converts the simplified TransformObject syntax to the React Native
|
|
5
|
+
* array format used internally by both web and native implementations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TransformObject, TransformProperty } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type guard to detect if a transform is using the new object syntax
|
|
12
|
+
* vs the legacy array format.
|
|
13
|
+
*/
|
|
14
|
+
export function isTransformObject(
|
|
15
|
+
transform: TransformObject | TransformProperty[] | undefined
|
|
16
|
+
): transform is TransformObject {
|
|
17
|
+
return transform !== undefined && !Array.isArray(transform);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a TransformObject to the React Native array format.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* normalizeTransform({ x: 10, y: 20, scale: 1.2, rotate: 45 })
|
|
25
|
+
* // Returns: [{ translateX: 10 }, { translateY: 20 }, { scale: 1.2 }, { rotate: '45deg' }]
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeTransform(transform: TransformObject): TransformProperty[] {
|
|
28
|
+
const result: TransformProperty[] = [];
|
|
29
|
+
|
|
30
|
+
// Perspective should come first for proper 3D transforms
|
|
31
|
+
if (transform.perspective !== undefined) {
|
|
32
|
+
result.push({ perspective: transform.perspective });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Translation
|
|
36
|
+
if (transform.x !== undefined) {
|
|
37
|
+
result.push({ translateX: transform.x });
|
|
38
|
+
}
|
|
39
|
+
if (transform.y !== undefined) {
|
|
40
|
+
result.push({ translateY: transform.y });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Scale
|
|
44
|
+
if (transform.scale !== undefined) {
|
|
45
|
+
result.push({ scale: transform.scale });
|
|
46
|
+
}
|
|
47
|
+
if (transform.scaleX !== undefined) {
|
|
48
|
+
result.push({ scaleX: transform.scaleX });
|
|
49
|
+
}
|
|
50
|
+
if (transform.scaleY !== undefined) {
|
|
51
|
+
result.push({ scaleY: transform.scaleY });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Rotation - convert number to degrees string
|
|
55
|
+
if (transform.rotate !== undefined) {
|
|
56
|
+
const rotation =
|
|
57
|
+
typeof transform.rotate === 'number' ? `${transform.rotate}deg` : transform.rotate;
|
|
58
|
+
result.push({ rotate: rotation });
|
|
59
|
+
}
|
|
60
|
+
if (transform.rotateX !== undefined) {
|
|
61
|
+
result.push({ rotateX: transform.rotateX });
|
|
62
|
+
}
|
|
63
|
+
if (transform.rotateY !== undefined) {
|
|
64
|
+
result.push({ rotateY: transform.rotateY });
|
|
65
|
+
}
|
|
66
|
+
if (transform.rotateZ !== undefined) {
|
|
67
|
+
result.push({ rotateZ: transform.rotateZ });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skew
|
|
71
|
+
if (transform.skewX !== undefined) {
|
|
72
|
+
result.push({ skewX: transform.skewX });
|
|
73
|
+
}
|
|
74
|
+
if (transform.skewY !== undefined) {
|
|
75
|
+
result.push({ skewY: transform.skewY });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Duration, EasingKey, SpringType } from '@idealyst/theme/animation'
|
|
|
9
9
|
export type AnimatableStyle = ViewStyle | TextStyle | ImageStyle;
|
|
10
10
|
export type StyleValue = string | number | undefined;
|
|
11
11
|
|
|
12
|
-
// Transform types (React Native style)
|
|
12
|
+
// Transform types (React Native array style - legacy)
|
|
13
13
|
export type TransformProperty =
|
|
14
14
|
| { perspective: number }
|
|
15
15
|
| { rotate: string }
|
|
@@ -24,6 +24,41 @@ export type TransformProperty =
|
|
|
24
24
|
| { skewX: string }
|
|
25
25
|
| { skewY: string };
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Simplified transform object syntax.
|
|
29
|
+
* Cleaner alternative to the React Native array format.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Instead of: transform: [{ translateX: 10 }, { translateY: 20 }, { scale: 1.2 }]
|
|
33
|
+
* // Use: transform: { x: 10, y: 20, scale: 1.2 }
|
|
34
|
+
*/
|
|
35
|
+
export interface TransformObject {
|
|
36
|
+
/** translateX - horizontal movement in pixels */
|
|
37
|
+
x?: number;
|
|
38
|
+
/** translateY - vertical movement in pixels */
|
|
39
|
+
y?: number;
|
|
40
|
+
/** Uniform scale (1 = normal, 2 = double, 0.5 = half) */
|
|
41
|
+
scale?: number;
|
|
42
|
+
/** Horizontal scale */
|
|
43
|
+
scaleX?: number;
|
|
44
|
+
/** Vertical scale */
|
|
45
|
+
scaleY?: number;
|
|
46
|
+
/** Rotation in degrees (number) or string ("45deg") */
|
|
47
|
+
rotate?: number | string;
|
|
48
|
+
/** X-axis rotation (string, e.g., "45deg") */
|
|
49
|
+
rotateX?: string;
|
|
50
|
+
/** Y-axis rotation (string, e.g., "45deg") */
|
|
51
|
+
rotateY?: string;
|
|
52
|
+
/** Z-axis rotation (string, e.g., "45deg") */
|
|
53
|
+
rotateZ?: string;
|
|
54
|
+
/** X-axis skew (string, e.g., "10deg") */
|
|
55
|
+
skewX?: string;
|
|
56
|
+
/** Y-axis skew (string, e.g., "10deg") */
|
|
57
|
+
skewY?: string;
|
|
58
|
+
/** Perspective for 3D transforms */
|
|
59
|
+
perspective?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
// Animatable properties (subset that can be animated smoothly)
|
|
28
63
|
export interface AnimatableProperties {
|
|
29
64
|
// Opacity
|
|
@@ -55,8 +90,18 @@ export interface AnimatableProperties {
|
|
|
55
90
|
bottom?: number | string;
|
|
56
91
|
left?: number | string;
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Transform - preferred for performance (GPU accelerated).
|
|
95
|
+
* Accepts either the new object syntax or legacy array format.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // New object syntax (recommended)
|
|
99
|
+
* transform: { x: 10, y: 20, scale: 1.2, rotate: 45 }
|
|
100
|
+
*
|
|
101
|
+
* // Legacy array syntax (still supported)
|
|
102
|
+
* transform: [{ translateX: 10 }, { translateY: 20 }, { scale: 1.2 }]
|
|
103
|
+
*/
|
|
104
|
+
transform?: TransformObject | TransformProperty[];
|
|
60
105
|
}
|
|
61
106
|
|
|
62
107
|
// Animation configuration
|
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
Easing,
|
|
16
16
|
} from 'react-native-reanimated';
|
|
17
17
|
import { timingConfig, springConfig, isSpringEasing } from '@idealyst/theme/animation';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
AnimatableProperties,
|
|
20
|
+
UseAnimatedStyleOptions,
|
|
21
|
+
AnimatableStyle,
|
|
22
|
+
TransformProperty,
|
|
23
|
+
} from './types';
|
|
24
|
+
import { isTransformObject, normalizeTransform } from './normalizeTransform';
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Hook that returns an animated style object.
|
|
@@ -29,14 +35,21 @@ import type { AnimatableProperties, UseAnimatedStyleOptions, AnimatableStyle } f
|
|
|
29
35
|
* ```tsx
|
|
30
36
|
* import Animated from 'react-native-reanimated';
|
|
31
37
|
*
|
|
38
|
+
* // New object syntax (recommended)
|
|
32
39
|
* const style = useAnimatedStyle({
|
|
33
40
|
* opacity: isVisible ? 1 : 0,
|
|
34
|
-
* transform:
|
|
41
|
+
* transform: { y: isVisible ? 0 : 20 },
|
|
35
42
|
* }, {
|
|
36
43
|
* duration: 'normal',
|
|
37
44
|
* easing: 'easeOut',
|
|
38
45
|
* });
|
|
39
46
|
*
|
|
47
|
+
* // Legacy array syntax (still supported)
|
|
48
|
+
* const legacyStyle = useAnimatedStyle({
|
|
49
|
+
* opacity: isVisible ? 1 : 0,
|
|
50
|
+
* transform: [{ translateY: isVisible ? 0 : 20 }],
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
40
53
|
* return <Animated.View style={style}>Content</Animated.View>;
|
|
41
54
|
* ```
|
|
42
55
|
*/
|
|
@@ -53,6 +66,15 @@ export function useAnimatedStyle(
|
|
|
53
66
|
const useSpring = native?.useSpring ?? isSpringEasing(finalEasing);
|
|
54
67
|
const springType = native?.springType ?? (isSpringEasing(finalEasing) ? finalEasing : 'spring');
|
|
55
68
|
|
|
69
|
+
// Normalize transform to array format if using object syntax
|
|
70
|
+
const normalizedTransform = useMemo((): TransformProperty[] | undefined => {
|
|
71
|
+
if (!style.transform) return undefined;
|
|
72
|
+
if (isTransformObject(style.transform)) {
|
|
73
|
+
return normalizeTransform(style.transform);
|
|
74
|
+
}
|
|
75
|
+
return style.transform;
|
|
76
|
+
}, [style.transform]);
|
|
77
|
+
|
|
56
78
|
// Create shared values for each animatable property
|
|
57
79
|
const opacity = useSharedValue(style.opacity ?? 1);
|
|
58
80
|
const backgroundColor = useSharedValue(style.backgroundColor ?? 'transparent');
|
|
@@ -60,6 +82,14 @@ export function useAnimatedStyle(
|
|
|
60
82
|
const borderWidth = useSharedValue(style.borderWidth ?? 0);
|
|
61
83
|
const borderRadius = useSharedValue(style.borderRadius ?? 0);
|
|
62
84
|
|
|
85
|
+
// Layout properties
|
|
86
|
+
const top = useSharedValue(style.top ?? 0);
|
|
87
|
+
const right = useSharedValue(style.right ?? 0);
|
|
88
|
+
const bottom = useSharedValue(style.bottom ?? 0);
|
|
89
|
+
const left = useSharedValue(style.left ?? 0);
|
|
90
|
+
const width = useSharedValue(style.width ?? 'auto');
|
|
91
|
+
const height = useSharedValue(style.height ?? 'auto');
|
|
92
|
+
|
|
63
93
|
// Transform values
|
|
64
94
|
const translateX = useSharedValue(0);
|
|
65
95
|
const translateY = useSharedValue(0);
|
|
@@ -67,8 +97,14 @@ export function useAnimatedStyle(
|
|
|
67
97
|
const scaleX = useSharedValue(1);
|
|
68
98
|
const scaleY = useSharedValue(1);
|
|
69
99
|
const rotate = useSharedValue('0deg');
|
|
100
|
+
const rotateX = useSharedValue('0deg');
|
|
101
|
+
const rotateY = useSharedValue('0deg');
|
|
102
|
+
const rotateZ = useSharedValue('0deg');
|
|
103
|
+
const skewX = useSharedValue('0deg');
|
|
104
|
+
const skewY = useSharedValue('0deg');
|
|
105
|
+
const perspective = useSharedValue(1000);
|
|
70
106
|
|
|
71
|
-
// Extract transform values from
|
|
107
|
+
// Extract transform values from normalized array
|
|
72
108
|
const transforms = useMemo(() => {
|
|
73
109
|
const result = {
|
|
74
110
|
translateX: 0,
|
|
@@ -77,10 +113,16 @@ export function useAnimatedStyle(
|
|
|
77
113
|
scaleX: 1,
|
|
78
114
|
scaleY: 1,
|
|
79
115
|
rotate: '0deg',
|
|
116
|
+
rotateX: '0deg',
|
|
117
|
+
rotateY: '0deg',
|
|
118
|
+
rotateZ: '0deg',
|
|
119
|
+
skewX: '0deg',
|
|
120
|
+
skewY: '0deg',
|
|
121
|
+
perspective: 1000,
|
|
80
122
|
};
|
|
81
123
|
|
|
82
|
-
if (
|
|
83
|
-
|
|
124
|
+
if (normalizedTransform) {
|
|
125
|
+
normalizedTransform.forEach((t) => {
|
|
84
126
|
const [key, value] = Object.entries(t)[0];
|
|
85
127
|
if (key in result) {
|
|
86
128
|
(result as any)[key] = value;
|
|
@@ -89,11 +131,10 @@ export function useAnimatedStyle(
|
|
|
89
131
|
}
|
|
90
132
|
|
|
91
133
|
return result;
|
|
92
|
-
}, [
|
|
134
|
+
}, [normalizedTransform]);
|
|
93
135
|
|
|
94
|
-
//
|
|
95
|
-
const animate = (
|
|
96
|
-
'worklet';
|
|
136
|
+
// Create animation wrapper - called from JS thread, returns animation modifier
|
|
137
|
+
const animate = (targetValue: any, isString = false) => {
|
|
97
138
|
if (useSpring && !isString) {
|
|
98
139
|
const config = springConfig(springType as any);
|
|
99
140
|
return finalDelay > 0
|
|
@@ -113,55 +154,105 @@ export function useAnimatedStyle(
|
|
|
113
154
|
|
|
114
155
|
// Update shared values when style changes
|
|
115
156
|
useEffect(() => {
|
|
157
|
+
// Visual properties
|
|
116
158
|
if (style.opacity !== undefined) {
|
|
117
|
-
opacity.value = animate(
|
|
159
|
+
opacity.value = animate(style.opacity);
|
|
118
160
|
}
|
|
119
161
|
if (style.backgroundColor !== undefined) {
|
|
120
|
-
backgroundColor.value = animate(
|
|
162
|
+
backgroundColor.value = animate(style.backgroundColor, true);
|
|
121
163
|
}
|
|
122
164
|
if (style.borderColor !== undefined) {
|
|
123
|
-
borderColor.value = animate(
|
|
165
|
+
borderColor.value = animate(style.borderColor, true);
|
|
124
166
|
}
|
|
125
167
|
if (style.borderWidth !== undefined) {
|
|
126
|
-
borderWidth.value = animate(
|
|
168
|
+
borderWidth.value = animate(style.borderWidth);
|
|
127
169
|
}
|
|
128
170
|
if (style.borderRadius !== undefined) {
|
|
129
|
-
borderRadius.value = animate(
|
|
171
|
+
borderRadius.value = animate(style.borderRadius);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Layout properties
|
|
175
|
+
if (style.top !== undefined) {
|
|
176
|
+
top.value = animate(style.top);
|
|
177
|
+
}
|
|
178
|
+
if (style.right !== undefined) {
|
|
179
|
+
right.value = animate(style.right);
|
|
180
|
+
}
|
|
181
|
+
if (style.bottom !== undefined) {
|
|
182
|
+
bottom.value = animate(style.bottom);
|
|
183
|
+
}
|
|
184
|
+
if (style.left !== undefined) {
|
|
185
|
+
left.value = animate(style.left);
|
|
186
|
+
}
|
|
187
|
+
if (style.width !== undefined) {
|
|
188
|
+
width.value = animate(style.width);
|
|
189
|
+
}
|
|
190
|
+
if (style.height !== undefined) {
|
|
191
|
+
height.value = animate(style.height);
|
|
130
192
|
}
|
|
131
193
|
|
|
132
194
|
// Update transform values
|
|
133
|
-
translateX.value = animate(
|
|
134
|
-
translateY.value = animate(
|
|
135
|
-
scale.value = animate(
|
|
136
|
-
scaleX.value = animate(
|
|
137
|
-
scaleY.value = animate(
|
|
138
|
-
rotate.value = animate(
|
|
195
|
+
translateX.value = animate(transforms.translateX);
|
|
196
|
+
translateY.value = animate(transforms.translateY);
|
|
197
|
+
scale.value = animate(transforms.scale);
|
|
198
|
+
scaleX.value = animate(transforms.scaleX);
|
|
199
|
+
scaleY.value = animate(transforms.scaleY);
|
|
200
|
+
rotate.value = animate(transforms.rotate, true);
|
|
201
|
+
rotateX.value = animate(transforms.rotateX, true);
|
|
202
|
+
rotateY.value = animate(transforms.rotateY, true);
|
|
203
|
+
rotateZ.value = animate(transforms.rotateZ, true);
|
|
204
|
+
skewX.value = animate(transforms.skewX, true);
|
|
205
|
+
skewY.value = animate(transforms.skewY, true);
|
|
206
|
+
perspective.value = animate(transforms.perspective);
|
|
139
207
|
}, [
|
|
140
208
|
style.opacity,
|
|
141
209
|
style.backgroundColor,
|
|
142
210
|
style.borderColor,
|
|
143
211
|
style.borderWidth,
|
|
144
212
|
style.borderRadius,
|
|
213
|
+
style.top,
|
|
214
|
+
style.right,
|
|
215
|
+
style.bottom,
|
|
216
|
+
style.left,
|
|
217
|
+
style.width,
|
|
218
|
+
style.height,
|
|
145
219
|
transforms,
|
|
146
220
|
]);
|
|
147
221
|
|
|
148
|
-
// Create animated style
|
|
222
|
+
// Create animated style - only include properties that were specified
|
|
149
223
|
const animatedStyle = useReanimatedStyle(() => {
|
|
150
|
-
|
|
224
|
+
'worklet';
|
|
225
|
+
const result: Record<string, any> = {
|
|
151
226
|
opacity: opacity.value,
|
|
152
227
|
backgroundColor: backgroundColor.value,
|
|
153
228
|
borderColor: borderColor.value,
|
|
154
229
|
borderWidth: borderWidth.value,
|
|
155
230
|
borderRadius: borderRadius.value,
|
|
156
231
|
transform: [
|
|
232
|
+
{ perspective: perspective.value },
|
|
157
233
|
{ translateX: translateX.value },
|
|
158
234
|
{ translateY: translateY.value },
|
|
159
235
|
{ scale: scale.value },
|
|
160
236
|
{ scaleX: scaleX.value },
|
|
161
237
|
{ scaleY: scaleY.value },
|
|
162
238
|
{ rotate: rotate.value },
|
|
239
|
+
{ rotateX: rotateX.value },
|
|
240
|
+
{ rotateY: rotateY.value },
|
|
241
|
+
{ rotateZ: rotateZ.value },
|
|
242
|
+
{ skewX: skewX.value },
|
|
243
|
+
{ skewY: skewY.value },
|
|
163
244
|
],
|
|
164
245
|
};
|
|
246
|
+
|
|
247
|
+
// Only include layout properties if they were specified (avoid overriding with defaults)
|
|
248
|
+
if (style.top !== undefined) result.top = top.value;
|
|
249
|
+
if (style.right !== undefined) result.right = right.value;
|
|
250
|
+
if (style.bottom !== undefined) result.bottom = bottom.value;
|
|
251
|
+
if (style.left !== undefined) result.left = left.value;
|
|
252
|
+
if (style.width !== undefined) result.width = width.value;
|
|
253
|
+
if (style.height !== undefined) result.height = height.value;
|
|
254
|
+
|
|
255
|
+
return result;
|
|
165
256
|
});
|
|
166
257
|
|
|
167
258
|
return animatedStyle as AnimatableStyle;
|
package/src/useAnimatedStyle.ts
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
* Uses CSS transitions for smooth, performant animations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useMemo
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
import { resolveDuration, resolveEasing } from '@idealyst/theme/animation';
|
|
10
|
+
import type {
|
|
11
|
+
AnimatableProperties,
|
|
12
|
+
UseAnimatedStyleOptions,
|
|
13
|
+
AnimatableStyle,
|
|
14
|
+
TransformProperty,
|
|
15
|
+
} from './types';
|
|
16
|
+
import { isTransformObject, normalizeTransform } from './normalizeTransform';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Hook that returns an animated style object.
|
|
@@ -19,14 +25,21 @@ import type { AnimatableProperties, UseAnimatedStyleOptions, AnimatableStyle } f
|
|
|
19
25
|
*
|
|
20
26
|
* @example
|
|
21
27
|
* ```tsx
|
|
28
|
+
* // New object syntax (recommended)
|
|
22
29
|
* const style = useAnimatedStyle({
|
|
23
30
|
* opacity: isVisible ? 1 : 0,
|
|
24
|
-
* transform:
|
|
31
|
+
* transform: { y: isVisible ? 0 : 20 },
|
|
25
32
|
* }, {
|
|
26
33
|
* duration: 'normal',
|
|
27
34
|
* easing: 'easeOut',
|
|
28
35
|
* });
|
|
29
36
|
*
|
|
37
|
+
* // Legacy array syntax (still supported)
|
|
38
|
+
* const legacyStyle = useAnimatedStyle({
|
|
39
|
+
* opacity: isVisible ? 1 : 0,
|
|
40
|
+
* transform: [{ translateY: isVisible ? 0 : 20 }],
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
30
43
|
* return <div style={style}>Content</div>;
|
|
31
44
|
* ```
|
|
32
45
|
*/
|
|
@@ -41,20 +54,22 @@ export function useAnimatedStyle(
|
|
|
41
54
|
const finalEasing = web?.easing ?? easing;
|
|
42
55
|
const finalDelay = web?.delay ?? delay;
|
|
43
56
|
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
// Normalize transform to array format if using object syntax
|
|
58
|
+
const normalizedTransform = useMemo((): TransformProperty[] | undefined => {
|
|
59
|
+
if (!style.transform) return undefined;
|
|
60
|
+
if (isTransformObject(style.transform)) {
|
|
61
|
+
return normalizeTransform(style.transform);
|
|
62
|
+
}
|
|
63
|
+
return style.transform;
|
|
64
|
+
}, [style.transform]);
|
|
50
65
|
|
|
51
66
|
// Convert transform array to CSS transform string
|
|
52
67
|
const transformString = useMemo(() => {
|
|
53
|
-
if (!
|
|
68
|
+
if (!normalizedTransform) {
|
|
54
69
|
return undefined;
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
return
|
|
72
|
+
return normalizedTransform
|
|
58
73
|
.map((t) => {
|
|
59
74
|
const [key, value] = Object.entries(t)[0];
|
|
60
75
|
// Handle different transform types
|
|
@@ -79,63 +94,23 @@ export function useAnimatedStyle(
|
|
|
79
94
|
}
|
|
80
95
|
})
|
|
81
96
|
.join(' ');
|
|
82
|
-
}, [
|
|
97
|
+
}, [normalizedTransform]);
|
|
83
98
|
|
|
84
|
-
// Build the transition string
|
|
99
|
+
// Build the transition string - use 'all' for simplicity and to catch any property
|
|
85
100
|
const transition = useMemo(() => {
|
|
86
101
|
// Use custom transition if provided
|
|
87
102
|
if (web?.transition) {
|
|
88
103
|
return web.transition;
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
// Get all animatable property names from the style
|
|
92
|
-
const properties = Object.keys(style).filter((key) => key !== 'transform');
|
|
93
|
-
if (style.transform) {
|
|
94
|
-
properties.push('transform');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (properties.length === 0) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Map RN property names to CSS property names
|
|
102
|
-
const cssProperties = properties.map((prop) => {
|
|
103
|
-
switch (prop) {
|
|
104
|
-
case 'backgroundColor':
|
|
105
|
-
return 'background-color';
|
|
106
|
-
case 'borderColor':
|
|
107
|
-
return 'border-color';
|
|
108
|
-
case 'borderWidth':
|
|
109
|
-
return 'border-width';
|
|
110
|
-
case 'borderRadius':
|
|
111
|
-
return 'border-radius';
|
|
112
|
-
case 'borderTopLeftRadius':
|
|
113
|
-
return 'border-top-left-radius';
|
|
114
|
-
case 'borderTopRightRadius':
|
|
115
|
-
return 'border-top-right-radius';
|
|
116
|
-
case 'borderBottomLeftRadius':
|
|
117
|
-
return 'border-bottom-left-radius';
|
|
118
|
-
case 'borderBottomRightRadius':
|
|
119
|
-
return 'border-bottom-right-radius';
|
|
120
|
-
case 'maxHeight':
|
|
121
|
-
return 'max-height';
|
|
122
|
-
case 'maxWidth':
|
|
123
|
-
return 'max-width';
|
|
124
|
-
case 'minHeight':
|
|
125
|
-
return 'min-height';
|
|
126
|
-
case 'minWidth':
|
|
127
|
-
return 'min-width';
|
|
128
|
-
default:
|
|
129
|
-
return prop;
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
106
|
const durationMs = resolveDuration(finalDuration);
|
|
134
107
|
const easingCss = resolveEasing(finalEasing);
|
|
135
108
|
const delayStr = finalDelay > 0 ? ` ${finalDelay}ms` : '';
|
|
136
109
|
|
|
137
|
-
|
|
138
|
-
|
|
110
|
+
// Use 'all' to animate any property that changes
|
|
111
|
+
// This is simpler and catches properties we might not explicitly list
|
|
112
|
+
return `all ${durationMs}ms ${easingCss}${delayStr}`;
|
|
113
|
+
}, [finalDuration, finalEasing, finalDelay, web?.transition]);
|
|
139
114
|
|
|
140
115
|
// Build the final style object
|
|
141
116
|
const animatedStyle = useMemo(() => {
|
|
@@ -153,8 +128,9 @@ export function useAnimatedStyle(
|
|
|
153
128
|
result.transform = transformString;
|
|
154
129
|
}
|
|
155
130
|
|
|
156
|
-
//
|
|
157
|
-
|
|
131
|
+
// Always include transition - CSS handles the timing correctly
|
|
132
|
+
// The transition property must be present BEFORE values change for animation to work
|
|
133
|
+
if (transition) {
|
|
158
134
|
result.transition = transition;
|
|
159
135
|
}
|
|
160
136
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages mount/unmount animations using Reanimated.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
7
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
8
8
|
import {
|
|
9
9
|
useSharedValue,
|
|
10
10
|
useAnimatedStyle,
|
|
@@ -15,7 +15,14 @@ import {
|
|
|
15
15
|
runOnJS,
|
|
16
16
|
} from 'react-native-reanimated';
|
|
17
17
|
import { timingConfig, springConfig, isSpringEasing, resolveDuration } from '@idealyst/theme/animation';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
UsePresenceOptions,
|
|
20
|
+
UsePresenceResult,
|
|
21
|
+
AnimatableStyle,
|
|
22
|
+
AnimatableProperties,
|
|
23
|
+
TransformProperty,
|
|
24
|
+
} from './types';
|
|
25
|
+
import { isTransformObject, normalizeTransform } from './normalizeTransform';
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
28
|
* Hook that manages presence animations for mount/unmount.
|
|
@@ -57,23 +64,37 @@ export function usePresence(isVisible: boolean, options: UsePresenceOptions): Us
|
|
|
57
64
|
const exitOpacity = exit.opacity ?? 0;
|
|
58
65
|
const initialOpacity = initial?.opacity ?? exitOpacity;
|
|
59
66
|
|
|
60
|
-
//
|
|
67
|
+
// Normalize transforms to array format
|
|
68
|
+
const normalizedEnterTransform = useMemo((): TransformProperty[] => {
|
|
69
|
+
if (!enter.transform) return [];
|
|
70
|
+
return isTransformObject(enter.transform)
|
|
71
|
+
? normalizeTransform(enter.transform)
|
|
72
|
+
: enter.transform;
|
|
73
|
+
}, [enter.transform]);
|
|
74
|
+
|
|
75
|
+
const normalizedExitTransform = useMemo((): TransformProperty[] => {
|
|
76
|
+
if (!exit.transform) return [];
|
|
77
|
+
return isTransformObject(exit.transform)
|
|
78
|
+
? normalizeTransform(exit.transform)
|
|
79
|
+
: exit.transform;
|
|
80
|
+
}, [exit.transform]);
|
|
81
|
+
|
|
82
|
+
// Extract transform values from normalized arrays
|
|
61
83
|
const getTransformValue = (
|
|
62
|
-
|
|
84
|
+
transforms: TransformProperty[],
|
|
63
85
|
key: string,
|
|
64
86
|
defaultValue: number | string
|
|
65
87
|
) => {
|
|
66
|
-
|
|
67
|
-
const transform = style.transform.find((t) => key in t);
|
|
88
|
+
const transform = transforms.find((t) => key in t);
|
|
68
89
|
return transform ? (transform as any)[key] : defaultValue;
|
|
69
90
|
};
|
|
70
91
|
|
|
71
|
-
const enterTranslateY = getTransformValue(
|
|
72
|
-
const exitTranslateY = getTransformValue(
|
|
73
|
-
const enterTranslateX = getTransformValue(
|
|
74
|
-
const exitTranslateX = getTransformValue(
|
|
75
|
-
const enterScale = getTransformValue(
|
|
76
|
-
const exitScale = getTransformValue(
|
|
92
|
+
const enterTranslateY = getTransformValue(normalizedEnterTransform, 'translateY', 0) as number;
|
|
93
|
+
const exitTranslateY = getTransformValue(normalizedExitTransform, 'translateY', 0) as number;
|
|
94
|
+
const enterTranslateX = getTransformValue(normalizedEnterTransform, 'translateX', 0) as number;
|
|
95
|
+
const exitTranslateX = getTransformValue(normalizedExitTransform, 'translateX', 0) as number;
|
|
96
|
+
const enterScale = getTransformValue(normalizedEnterTransform, 'scale', 1) as number;
|
|
97
|
+
const exitScale = getTransformValue(normalizedExitTransform, 'scale', 1) as number;
|
|
77
98
|
|
|
78
99
|
// Animation helper
|
|
79
100
|
const animateTo = useCallback(
|
package/src/usePresence.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
9
9
|
import { resolveDuration, resolveEasing } from '@idealyst/theme/animation';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
UsePresenceOptions,
|
|
12
|
+
UsePresenceResult,
|
|
13
|
+
AnimatableStyle,
|
|
14
|
+
AnimatableProperties,
|
|
15
|
+
TransformProperty,
|
|
16
|
+
} from './types';
|
|
17
|
+
import { isTransformObject, normalizeTransform } from './normalizeTransform';
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Hook that manages presence animations for mount/unmount.
|
|
@@ -87,11 +94,18 @@ export function usePresence(isVisible: boolean, options: UsePresenceOptions): Us
|
|
|
87
94
|
}, durationMs + delay);
|
|
88
95
|
}, [durationMs, delay]);
|
|
89
96
|
|
|
90
|
-
// Convert transform
|
|
91
|
-
const transformToString = (
|
|
97
|
+
// Convert transform to CSS transform string (handles both object and array formats)
|
|
98
|
+
const transformToString = (
|
|
99
|
+
transform: AnimatableProperties['transform']
|
|
100
|
+
): string | undefined => {
|
|
92
101
|
if (!transform) return undefined;
|
|
93
102
|
|
|
94
|
-
|
|
103
|
+
// Normalize if it's an object
|
|
104
|
+
const normalizedTransform: TransformProperty[] = isTransformObject(transform)
|
|
105
|
+
? normalizeTransform(transform)
|
|
106
|
+
: transform;
|
|
107
|
+
|
|
108
|
+
return normalizedTransform
|
|
95
109
|
.map((t) => {
|
|
96
110
|
const [key, value] = Object.entries(t)[0];
|
|
97
111
|
switch (key) {
|