react-native-puff-pop 1.0.6 → 1.0.8
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/README.md +331 -1
- package/lib/module/index.js +411 -69
- package/lib/typescript/src/index.d.ts +198 -3
- package/package.json +1 -1
- package/src/index.tsx +759 -77
package/lib/module/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState, useCallback, useMemo, Children, } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, useMemo, memo, Children, } from 'react';
|
|
3
3
|
import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
|
|
4
4
|
/**
|
|
5
5
|
* Get easing function based on type
|
|
@@ -22,16 +22,160 @@ function getEasing(type) {
|
|
|
22
22
|
return Easing.out(Easing.ease);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Get effect flags for any effect type
|
|
27
|
+
* Returns flags indicating which transforms are needed for the effect
|
|
28
|
+
*/
|
|
29
|
+
function getEffectFlags(eff) {
|
|
30
|
+
return {
|
|
31
|
+
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip', 'pulse', 'elastic'].includes(eff),
|
|
32
|
+
hasRotate: ['rotate', 'rotateScale', 'swing', 'wobble'].includes(eff),
|
|
33
|
+
hasFlip: eff === 'flip',
|
|
34
|
+
hasTranslateX: ['slideLeft', 'slideRight', 'shake', 'wobble'].includes(eff),
|
|
35
|
+
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(eff),
|
|
36
|
+
hasRotateEffect: ['rotate', 'rotateScale', 'flip', 'swing', 'wobble'].includes(eff),
|
|
37
|
+
// Special effects that need sequence animation
|
|
38
|
+
isShake: eff === 'shake',
|
|
39
|
+
isPulse: eff === 'pulse',
|
|
40
|
+
isSwing: eff === 'swing',
|
|
41
|
+
isWobble: eff === 'wobble',
|
|
42
|
+
isElastic: eff === 'elastic',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get anchor point offset multipliers
|
|
47
|
+
* Returns { x: -1 to 1, y: -1 to 1 } where 0 is center
|
|
48
|
+
*/
|
|
49
|
+
function getAnchorPointOffset(anchorPoint) {
|
|
50
|
+
switch (anchorPoint) {
|
|
51
|
+
case 'top':
|
|
52
|
+
return { x: 0, y: -0.5 };
|
|
53
|
+
case 'bottom':
|
|
54
|
+
return { x: 0, y: 0.5 };
|
|
55
|
+
case 'left':
|
|
56
|
+
return { x: -0.5, y: 0 };
|
|
57
|
+
case 'right':
|
|
58
|
+
return { x: 0.5, y: 0 };
|
|
59
|
+
case 'topLeft':
|
|
60
|
+
return { x: -0.5, y: -0.5 };
|
|
61
|
+
case 'topRight':
|
|
62
|
+
return { x: 0.5, y: -0.5 };
|
|
63
|
+
case 'bottomLeft':
|
|
64
|
+
return { x: -0.5, y: 0.5 };
|
|
65
|
+
case 'bottomRight':
|
|
66
|
+
return { x: 0.5, y: 0.5 };
|
|
67
|
+
case 'center':
|
|
68
|
+
default:
|
|
69
|
+
return { x: 0, y: 0 };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Props comparison function for PuffPop memoization
|
|
74
|
+
* Performs shallow comparison of props to prevent unnecessary re-renders
|
|
75
|
+
*/
|
|
76
|
+
function arePuffPopPropsEqual(prevProps, nextProps) {
|
|
77
|
+
// Compare primitive props
|
|
78
|
+
if (prevProps.effect !== nextProps.effect ||
|
|
79
|
+
prevProps.duration !== nextProps.duration ||
|
|
80
|
+
prevProps.delay !== nextProps.delay ||
|
|
81
|
+
prevProps.easing !== nextProps.easing ||
|
|
82
|
+
prevProps.skeleton !== nextProps.skeleton ||
|
|
83
|
+
prevProps.visible !== nextProps.visible ||
|
|
84
|
+
prevProps.animateOnMount !== nextProps.animateOnMount ||
|
|
85
|
+
prevProps.loop !== nextProps.loop ||
|
|
86
|
+
prevProps.loopDelay !== nextProps.loopDelay ||
|
|
87
|
+
prevProps.respectReduceMotion !== nextProps.respectReduceMotion ||
|
|
88
|
+
prevProps.testID !== nextProps.testID ||
|
|
89
|
+
prevProps.reverse !== nextProps.reverse ||
|
|
90
|
+
prevProps.intensity !== nextProps.intensity ||
|
|
91
|
+
prevProps.anchorPoint !== nextProps.anchorPoint ||
|
|
92
|
+
prevProps.useSpring !== nextProps.useSpring ||
|
|
93
|
+
prevProps.exitEffect !== nextProps.exitEffect ||
|
|
94
|
+
prevProps.exitDuration !== nextProps.exitDuration ||
|
|
95
|
+
prevProps.exitEasing !== nextProps.exitEasing ||
|
|
96
|
+
prevProps.exitDelay !== nextProps.exitDelay ||
|
|
97
|
+
prevProps.initialOpacity !== nextProps.initialOpacity ||
|
|
98
|
+
prevProps.initialScale !== nextProps.initialScale ||
|
|
99
|
+
prevProps.initialRotate !== nextProps.initialRotate ||
|
|
100
|
+
prevProps.initialTranslateX !== nextProps.initialTranslateX ||
|
|
101
|
+
prevProps.initialTranslateY !== nextProps.initialTranslateY) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
// Compare springConfig object (shallow)
|
|
105
|
+
if (prevProps.springConfig !== nextProps.springConfig) {
|
|
106
|
+
if (!prevProps.springConfig ||
|
|
107
|
+
!nextProps.springConfig ||
|
|
108
|
+
prevProps.springConfig.tension !== nextProps.springConfig.tension ||
|
|
109
|
+
prevProps.springConfig.friction !== nextProps.springConfig.friction ||
|
|
110
|
+
prevProps.springConfig.speed !== nextProps.springConfig.speed ||
|
|
111
|
+
prevProps.springConfig.bounciness !== nextProps.springConfig.bounciness) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Compare callbacks (reference equality - if changed, should re-render)
|
|
116
|
+
if (prevProps.onAnimationStart !== nextProps.onAnimationStart ||
|
|
117
|
+
prevProps.onAnimationComplete !== nextProps.onAnimationComplete) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// Style comparison - if style prop changes, re-render
|
|
121
|
+
// Note: Deep comparison of style is expensive, so we use reference equality
|
|
122
|
+
if (prevProps.style !== nextProps.style) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// Children comparison - if children change, re-render
|
|
126
|
+
if (prevProps.children !== nextProps.children) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
25
131
|
/**
|
|
26
132
|
* PuffPop - Animate children with beautiful entrance effects
|
|
27
133
|
*/
|
|
28
|
-
|
|
134
|
+
function PuffPopComponent({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, onAnimationStart, style, animateOnMount = true, loop = false, loopDelay = 0, respectReduceMotion = true, testID,
|
|
135
|
+
// Exit animation settings
|
|
136
|
+
exitEffect, exitDuration, exitEasing, exitDelay = 0,
|
|
137
|
+
// Custom initial values
|
|
138
|
+
initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY,
|
|
139
|
+
// Reverse mode
|
|
140
|
+
reverse = false,
|
|
141
|
+
// Animation intensity
|
|
142
|
+
intensity = 1,
|
|
143
|
+
// Anchor point
|
|
144
|
+
anchorPoint = 'center',
|
|
145
|
+
// Spring animation
|
|
146
|
+
useSpring = false, springConfig, }) {
|
|
147
|
+
// Clamp intensity between 0 and 1
|
|
148
|
+
const clampedIntensity = Math.max(0, Math.min(1, intensity));
|
|
149
|
+
// Helper to get initial value with custom override, reverse, and intensity support
|
|
150
|
+
const getInitialOpacityValue = () => initialOpacity ?? 0;
|
|
151
|
+
const getInitialScaleValue = (eff) => {
|
|
152
|
+
if (initialScale !== undefined)
|
|
153
|
+
return initialScale;
|
|
154
|
+
const baseScale = getInitialScale(eff, reverse);
|
|
155
|
+
// Scale goes from baseScale to 1, so we interpolate: 1 - (1 - baseScale) * intensity
|
|
156
|
+
return 1 - (1 - baseScale) * clampedIntensity;
|
|
157
|
+
};
|
|
158
|
+
const getInitialRotateValue = (eff) => {
|
|
159
|
+
if (initialRotate !== undefined)
|
|
160
|
+
return initialRotate;
|
|
161
|
+
return getInitialRotate(eff, reverse) * clampedIntensity;
|
|
162
|
+
};
|
|
163
|
+
const getInitialTranslateXValue = (eff) => {
|
|
164
|
+
if (initialTranslateX !== undefined)
|
|
165
|
+
return initialTranslateX;
|
|
166
|
+
return getInitialTranslateX(eff, reverse) * clampedIntensity;
|
|
167
|
+
};
|
|
168
|
+
const getInitialTranslateYValue = (eff) => {
|
|
169
|
+
if (initialTranslateY !== undefined)
|
|
170
|
+
return initialTranslateY;
|
|
171
|
+
return getInitialTranslateY(eff, reverse) * clampedIntensity;
|
|
172
|
+
};
|
|
29
173
|
// Animation values
|
|
30
|
-
const opacity = useRef(new Animated.Value(animateOnMount ?
|
|
31
|
-
const scale = useRef(new Animated.Value(animateOnMount ?
|
|
32
|
-
const rotate = useRef(new Animated.Value(animateOnMount ?
|
|
33
|
-
const translateX = useRef(new Animated.Value(animateOnMount ?
|
|
34
|
-
const translateY = useRef(new Animated.Value(animateOnMount ?
|
|
174
|
+
const opacity = useRef(new Animated.Value(animateOnMount ? getInitialOpacityValue() : 1)).current;
|
|
175
|
+
const scale = useRef(new Animated.Value(animateOnMount ? getInitialScaleValue(effect) : 1)).current;
|
|
176
|
+
const rotate = useRef(new Animated.Value(animateOnMount ? getInitialRotateValue(effect) : 0)).current;
|
|
177
|
+
const translateX = useRef(new Animated.Value(animateOnMount ? getInitialTranslateXValue(effect) : 0)).current;
|
|
178
|
+
const translateY = useRef(new Animated.Value(animateOnMount ? getInitialTranslateYValue(effect) : 0)).current;
|
|
35
179
|
// For non-skeleton mode
|
|
36
180
|
const [measuredHeight, setMeasuredHeight] = useState(null);
|
|
37
181
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
|
@@ -55,15 +199,11 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
55
199
|
}, [respectReduceMotion]);
|
|
56
200
|
// Effective duration (0 if reduce motion is enabled)
|
|
57
201
|
const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
|
|
202
|
+
const effectiveExitDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : (exitDuration ?? duration);
|
|
58
203
|
// Memoize effect type checks to avoid repeated includes() calls
|
|
59
|
-
const effectFlags = useMemo(() => (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
hasFlip: effect === 'flip',
|
|
63
|
-
hasTranslateX: ['slideLeft', 'slideRight'].includes(effect),
|
|
64
|
-
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(effect),
|
|
65
|
-
hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
|
|
66
|
-
}), [effect]);
|
|
204
|
+
const effectFlags = useMemo(() => getEffectFlags(effect), [effect]);
|
|
205
|
+
// Exit effect flags (use exitEffect if specified, otherwise same as enter effect)
|
|
206
|
+
const exitEffectFlags = useMemo(() => exitEffect ? getEffectFlags(exitEffect) : effectFlags, [exitEffect, effectFlags]);
|
|
67
207
|
// Memoize interpolations to avoid recreating on every render
|
|
68
208
|
const rotateInterpolation = useMemo(() => rotate.interpolate({
|
|
69
209
|
inputRange: [-360, 0, 360],
|
|
@@ -86,60 +226,102 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
86
226
|
if (toVisible && onAnimationStart) {
|
|
87
227
|
onAnimationStart();
|
|
88
228
|
}
|
|
89
|
-
|
|
229
|
+
// Determine which effect settings to use based on direction
|
|
230
|
+
const currentEffect = toVisible ? effect : (exitEffect ?? effect);
|
|
231
|
+
const currentDuration = toVisible ? effectiveDuration : effectiveExitDuration;
|
|
232
|
+
const currentEasing = toVisible ? easing : (exitEasing ?? easing);
|
|
233
|
+
const currentDelay = toVisible ? delay : exitDelay;
|
|
234
|
+
const currentFlags = toVisible ? effectFlags : exitEffectFlags;
|
|
235
|
+
const easingFn = getEasing(currentEasing);
|
|
90
236
|
// When skeleton is false, we animate height which doesn't support native driver
|
|
91
237
|
// So we must use JS driver for all animations in that case
|
|
92
238
|
const useNative = skeleton;
|
|
93
|
-
|
|
94
|
-
|
|
239
|
+
// Spring configuration
|
|
240
|
+
const springConf = {
|
|
241
|
+
tension: springConfig?.tension ?? 100,
|
|
242
|
+
friction: springConfig?.friction ?? 10,
|
|
243
|
+
speed: springConfig?.speed,
|
|
244
|
+
bounciness: springConfig?.bounciness,
|
|
245
|
+
useNativeDriver: useNative,
|
|
246
|
+
};
|
|
247
|
+
const timingConfig = {
|
|
248
|
+
duration: currentDuration,
|
|
95
249
|
easing: easingFn,
|
|
96
250
|
useNativeDriver: useNative,
|
|
97
251
|
};
|
|
252
|
+
// Helper to create animation (spring or timing)
|
|
253
|
+
const createAnimation = (value, toValue, customEasing) => {
|
|
254
|
+
if (useSpring) {
|
|
255
|
+
return Animated.spring(value, {
|
|
256
|
+
toValue,
|
|
257
|
+
...springConf,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return Animated.timing(value, {
|
|
261
|
+
toValue,
|
|
262
|
+
...timingConfig,
|
|
263
|
+
...(customEasing ? { easing: customEasing } : {}),
|
|
264
|
+
});
|
|
265
|
+
};
|
|
98
266
|
const animations = [];
|
|
99
|
-
// Opacity animation
|
|
267
|
+
// Opacity animation (always use timing for opacity for smoother fade)
|
|
100
268
|
animations.push(Animated.timing(opacity, {
|
|
101
269
|
toValue: toVisible ? 1 : 0,
|
|
102
|
-
...
|
|
270
|
+
...timingConfig,
|
|
103
271
|
}));
|
|
104
272
|
// Scale animation
|
|
105
|
-
if (
|
|
106
|
-
const targetScale = toVisible ? 1 :
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
273
|
+
if (currentFlags.hasScale) {
|
|
274
|
+
const targetScale = toVisible ? 1 : getInitialScaleValue(currentEffect);
|
|
275
|
+
// Special easing for different effects
|
|
276
|
+
let scaleEasing = easingFn;
|
|
277
|
+
if (currentEffect === 'bounce') {
|
|
278
|
+
scaleEasing = Easing.bounce;
|
|
279
|
+
}
|
|
280
|
+
else if (currentEffect === 'elastic') {
|
|
281
|
+
scaleEasing = Easing.elastic(1.5);
|
|
282
|
+
}
|
|
283
|
+
else if (currentEffect === 'pulse') {
|
|
284
|
+
scaleEasing = Easing.out(Easing.back(3));
|
|
285
|
+
}
|
|
286
|
+
animations.push(createAnimation(scale, targetScale, scaleEasing));
|
|
112
287
|
}
|
|
113
288
|
// Rotate animation
|
|
114
|
-
if (
|
|
115
|
-
const targetRotate = toVisible ? 0 :
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
289
|
+
if (currentFlags.hasRotate || currentFlags.hasFlip) {
|
|
290
|
+
const targetRotate = toVisible ? 0 : getInitialRotateValue(currentEffect);
|
|
291
|
+
// Special easing for swing and wobble
|
|
292
|
+
let rotateEasing = easingFn;
|
|
293
|
+
if (currentEffect === 'swing') {
|
|
294
|
+
rotateEasing = Easing.elastic(1.2);
|
|
295
|
+
}
|
|
296
|
+
else if (currentEffect === 'wobble') {
|
|
297
|
+
rotateEasing = Easing.elastic(1.5);
|
|
298
|
+
}
|
|
299
|
+
animations.push(createAnimation(rotate, targetRotate, rotateEasing));
|
|
120
300
|
}
|
|
121
301
|
// TranslateX animation
|
|
122
|
-
if (
|
|
123
|
-
const targetX = toVisible ? 0 :
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
302
|
+
if (currentFlags.hasTranslateX) {
|
|
303
|
+
const targetX = toVisible ? 0 : getInitialTranslateXValue(currentEffect);
|
|
304
|
+
// Special easing for shake and wobble
|
|
305
|
+
let translateXEasing = easingFn;
|
|
306
|
+
if (currentEffect === 'shake') {
|
|
307
|
+
translateXEasing = Easing.elastic(3);
|
|
308
|
+
}
|
|
309
|
+
else if (currentEffect === 'wobble') {
|
|
310
|
+
translateXEasing = Easing.elastic(1.5);
|
|
311
|
+
}
|
|
312
|
+
animations.push(createAnimation(translateX, targetX, translateXEasing));
|
|
128
313
|
}
|
|
129
314
|
// TranslateY animation
|
|
130
|
-
if (
|
|
131
|
-
const targetY = toVisible ? 0 :
|
|
132
|
-
animations.push(
|
|
133
|
-
toValue: targetY,
|
|
134
|
-
...config,
|
|
135
|
-
}));
|
|
315
|
+
if (currentFlags.hasTranslateY) {
|
|
316
|
+
const targetY = toVisible ? 0 : getInitialTranslateYValue(currentEffect);
|
|
317
|
+
animations.push(createAnimation(translateY, targetY));
|
|
136
318
|
}
|
|
137
|
-
// Height animation for non-skeleton mode
|
|
319
|
+
// Height animation for non-skeleton mode (always use timing)
|
|
138
320
|
if (!skeleton && measuredHeight !== null) {
|
|
139
321
|
const targetHeight = toVisible ? measuredHeight : 0;
|
|
140
322
|
animations.push(Animated.timing(animatedHeight, {
|
|
141
323
|
toValue: targetHeight,
|
|
142
|
-
duration:
|
|
324
|
+
duration: currentDuration,
|
|
143
325
|
easing: easingFn,
|
|
144
326
|
useNativeDriver: false,
|
|
145
327
|
}));
|
|
@@ -148,20 +330,20 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
148
330
|
const parallelAnimation = Animated.parallel(animations);
|
|
149
331
|
// Reset values function for looping
|
|
150
332
|
const resetValues = () => {
|
|
151
|
-
opacity.setValue(
|
|
152
|
-
scale.setValue(
|
|
153
|
-
rotate.setValue(
|
|
154
|
-
translateX.setValue(
|
|
155
|
-
translateY.setValue(
|
|
333
|
+
opacity.setValue(getInitialOpacityValue());
|
|
334
|
+
scale.setValue(getInitialScaleValue(effect));
|
|
335
|
+
rotate.setValue(getInitialRotateValue(effect));
|
|
336
|
+
translateX.setValue(getInitialTranslateXValue(effect));
|
|
337
|
+
translateY.setValue(getInitialTranslateYValue(effect));
|
|
156
338
|
if (!skeleton && measuredHeight !== null) {
|
|
157
339
|
animatedHeight.setValue(0);
|
|
158
340
|
}
|
|
159
341
|
};
|
|
160
342
|
// Build the animation sequence
|
|
161
343
|
let animation;
|
|
162
|
-
if (
|
|
344
|
+
if (currentDelay > 0) {
|
|
163
345
|
animation = Animated.sequence([
|
|
164
|
-
Animated.delay(
|
|
346
|
+
Animated.delay(currentDelay),
|
|
165
347
|
parallelAnimation,
|
|
166
348
|
]);
|
|
167
349
|
}
|
|
@@ -230,9 +412,14 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
230
412
|
}, [
|
|
231
413
|
delay,
|
|
232
414
|
effectiveDuration,
|
|
415
|
+
effectiveExitDuration,
|
|
233
416
|
easing,
|
|
234
417
|
effect,
|
|
235
418
|
effectFlags,
|
|
419
|
+
exitEffect,
|
|
420
|
+
exitEffectFlags,
|
|
421
|
+
exitEasing,
|
|
422
|
+
exitDelay,
|
|
236
423
|
measuredHeight,
|
|
237
424
|
onAnimationComplete,
|
|
238
425
|
onAnimationStart,
|
|
@@ -245,6 +432,8 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
245
432
|
animatedHeight,
|
|
246
433
|
loop,
|
|
247
434
|
loopDelay,
|
|
435
|
+
useSpring,
|
|
436
|
+
springConfig,
|
|
248
437
|
]);
|
|
249
438
|
// Handle initial mount animation
|
|
250
439
|
useEffect(() => {
|
|
@@ -270,11 +459,33 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
270
459
|
}
|
|
271
460
|
};
|
|
272
461
|
}, []);
|
|
462
|
+
// Calculate anchor point offset (using 100px as base size for skeleton mode)
|
|
463
|
+
const anchorOffset = useMemo(() => {
|
|
464
|
+
const offset = getAnchorPointOffset(anchorPoint);
|
|
465
|
+
// Use measured height if available, otherwise use 100px as base
|
|
466
|
+
const baseSize = measuredHeight ?? 100;
|
|
467
|
+
return {
|
|
468
|
+
x: offset.x * baseSize,
|
|
469
|
+
y: offset.y * baseSize,
|
|
470
|
+
};
|
|
471
|
+
}, [anchorPoint, measuredHeight]);
|
|
273
472
|
// Memoize transform array to avoid recreating on every render
|
|
274
473
|
// IMPORTANT: All hooks must be called before any conditional returns
|
|
275
474
|
const transform = useMemo(() => {
|
|
276
475
|
const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
|
|
476
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
477
|
const transforms = [];
|
|
478
|
+
const needsAnchorOffset = anchorPoint !== 'center' && (hasScale || hasRotate || hasFlip);
|
|
479
|
+
// Step 1: Move to anchor point (negative offset)
|
|
480
|
+
if (needsAnchorOffset) {
|
|
481
|
+
if (anchorOffset.x !== 0) {
|
|
482
|
+
transforms.push({ translateX: -anchorOffset.x });
|
|
483
|
+
}
|
|
484
|
+
if (anchorOffset.y !== 0) {
|
|
485
|
+
transforms.push({ translateY: -anchorOffset.y });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Step 2: Apply scale/rotate transforms
|
|
278
489
|
if (hasScale) {
|
|
279
490
|
transforms.push({ scale });
|
|
280
491
|
}
|
|
@@ -284,6 +495,16 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
284
495
|
if (hasFlip) {
|
|
285
496
|
transforms.push({ rotateY: flipInterpolation });
|
|
286
497
|
}
|
|
498
|
+
// Step 3: Move back from anchor point (positive offset)
|
|
499
|
+
if (needsAnchorOffset) {
|
|
500
|
+
if (anchorOffset.x !== 0) {
|
|
501
|
+
transforms.push({ translateX: anchorOffset.x });
|
|
502
|
+
}
|
|
503
|
+
if (anchorOffset.y !== 0) {
|
|
504
|
+
transforms.push({ translateY: anchorOffset.y });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Step 4: Apply other translate transforms
|
|
287
508
|
if (hasTranslateX) {
|
|
288
509
|
transforms.push({ translateX });
|
|
289
510
|
}
|
|
@@ -291,7 +512,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
291
512
|
transforms.push({ translateY });
|
|
292
513
|
}
|
|
293
514
|
return transforms.length > 0 ? transforms : undefined;
|
|
294
|
-
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
|
|
515
|
+
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY, anchorPoint, anchorOffset]);
|
|
295
516
|
// Memoize animated style
|
|
296
517
|
const animatedStyle = useMemo(() => ({
|
|
297
518
|
opacity,
|
|
@@ -313,13 +534,17 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
313
534
|
}
|
|
314
535
|
return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], testID: testID, children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
|
|
315
536
|
}
|
|
537
|
+
// Memoize PuffPop to prevent unnecessary re-renders
|
|
538
|
+
export const PuffPop = memo(PuffPopComponent, arePuffPopPropsEqual);
|
|
316
539
|
/**
|
|
317
540
|
* Get initial scale value based on effect
|
|
318
541
|
*/
|
|
319
|
-
function getInitialScale(effect) {
|
|
542
|
+
function getInitialScale(effect, _reverse = false) {
|
|
543
|
+
// Scale doesn't change with reverse (parameter kept for consistent API)
|
|
320
544
|
switch (effect) {
|
|
321
545
|
case 'scale':
|
|
322
546
|
case 'rotateScale':
|
|
547
|
+
case 'elastic':
|
|
323
548
|
return 0;
|
|
324
549
|
case 'bounce':
|
|
325
550
|
return 0.3;
|
|
@@ -327,6 +552,8 @@ function getInitialScale(effect) {
|
|
|
327
552
|
return 0.5;
|
|
328
553
|
case 'flip':
|
|
329
554
|
return 0.8;
|
|
555
|
+
case 'pulse':
|
|
556
|
+
return 0.6;
|
|
330
557
|
default:
|
|
331
558
|
return 1;
|
|
332
559
|
}
|
|
@@ -334,14 +561,19 @@ function getInitialScale(effect) {
|
|
|
334
561
|
/**
|
|
335
562
|
* Get initial rotate value based on effect
|
|
336
563
|
*/
|
|
337
|
-
function getInitialRotate(effect) {
|
|
564
|
+
function getInitialRotate(effect, reverse = false) {
|
|
565
|
+
const multiplier = reverse ? -1 : 1;
|
|
338
566
|
switch (effect) {
|
|
339
567
|
case 'rotate':
|
|
340
|
-
return -360;
|
|
568
|
+
return -360 * multiplier;
|
|
341
569
|
case 'rotateScale':
|
|
342
|
-
return -180;
|
|
570
|
+
return -180 * multiplier;
|
|
343
571
|
case 'flip':
|
|
344
|
-
return -180;
|
|
572
|
+
return -180 * multiplier;
|
|
573
|
+
case 'swing':
|
|
574
|
+
return -15 * multiplier;
|
|
575
|
+
case 'wobble':
|
|
576
|
+
return -5 * multiplier;
|
|
345
577
|
default:
|
|
346
578
|
return 0;
|
|
347
579
|
}
|
|
@@ -349,12 +581,17 @@ function getInitialRotate(effect) {
|
|
|
349
581
|
/**
|
|
350
582
|
* Get initial translateX value based on effect
|
|
351
583
|
*/
|
|
352
|
-
function getInitialTranslateX(effect) {
|
|
584
|
+
function getInitialTranslateX(effect, reverse = false) {
|
|
585
|
+
const multiplier = reverse ? -1 : 1;
|
|
353
586
|
switch (effect) {
|
|
354
587
|
case 'slideLeft':
|
|
355
|
-
return 100;
|
|
588
|
+
return 100 * multiplier;
|
|
356
589
|
case 'slideRight':
|
|
357
|
-
return -100;
|
|
590
|
+
return -100 * multiplier;
|
|
591
|
+
case 'shake':
|
|
592
|
+
return -10 * multiplier;
|
|
593
|
+
case 'wobble':
|
|
594
|
+
return -25 * multiplier;
|
|
358
595
|
default:
|
|
359
596
|
return 0;
|
|
360
597
|
}
|
|
@@ -362,14 +599,15 @@ function getInitialTranslateX(effect) {
|
|
|
362
599
|
/**
|
|
363
600
|
* Get initial translateY value based on effect
|
|
364
601
|
*/
|
|
365
|
-
function getInitialTranslateY(effect) {
|
|
602
|
+
function getInitialTranslateY(effect, reverse = false) {
|
|
603
|
+
const multiplier = reverse ? -1 : 1;
|
|
366
604
|
switch (effect) {
|
|
367
605
|
case 'slideUp':
|
|
368
|
-
return 50;
|
|
606
|
+
return 50 * multiplier;
|
|
369
607
|
case 'slideDown':
|
|
370
|
-
return -50;
|
|
608
|
+
return -50 * multiplier;
|
|
371
609
|
case 'bounce':
|
|
372
|
-
return 30;
|
|
610
|
+
return 30 * multiplier;
|
|
373
611
|
default:
|
|
374
612
|
return 0;
|
|
375
613
|
}
|
|
@@ -386,6 +624,68 @@ const styles = StyleSheet.create({
|
|
|
386
624
|
},
|
|
387
625
|
groupContainer: {},
|
|
388
626
|
});
|
|
627
|
+
/**
|
|
628
|
+
* Props comparison function for PuffPopGroup memoization
|
|
629
|
+
* Performs shallow comparison of props to prevent unnecessary re-renders
|
|
630
|
+
*/
|
|
631
|
+
function arePuffPopGroupPropsEqual(prevProps, nextProps) {
|
|
632
|
+
// Compare primitive props
|
|
633
|
+
if (prevProps.effect !== nextProps.effect ||
|
|
634
|
+
prevProps.duration !== nextProps.duration ||
|
|
635
|
+
prevProps.staggerDelay !== nextProps.staggerDelay ||
|
|
636
|
+
prevProps.initialDelay !== nextProps.initialDelay ||
|
|
637
|
+
prevProps.easing !== nextProps.easing ||
|
|
638
|
+
prevProps.skeleton !== nextProps.skeleton ||
|
|
639
|
+
prevProps.visible !== nextProps.visible ||
|
|
640
|
+
prevProps.animateOnMount !== nextProps.animateOnMount ||
|
|
641
|
+
prevProps.respectReduceMotion !== nextProps.respectReduceMotion ||
|
|
642
|
+
prevProps.testID !== nextProps.testID ||
|
|
643
|
+
prevProps.staggerDirection !== nextProps.staggerDirection ||
|
|
644
|
+
prevProps.horizontal !== nextProps.horizontal ||
|
|
645
|
+
prevProps.gap !== nextProps.gap ||
|
|
646
|
+
prevProps.reverse !== nextProps.reverse ||
|
|
647
|
+
prevProps.intensity !== nextProps.intensity ||
|
|
648
|
+
prevProps.anchorPoint !== nextProps.anchorPoint ||
|
|
649
|
+
prevProps.useSpring !== nextProps.useSpring ||
|
|
650
|
+
prevProps.exitEffect !== nextProps.exitEffect ||
|
|
651
|
+
prevProps.exitDuration !== nextProps.exitDuration ||
|
|
652
|
+
prevProps.exitEasing !== nextProps.exitEasing ||
|
|
653
|
+
prevProps.exitDelay !== nextProps.exitDelay ||
|
|
654
|
+
prevProps.exitStaggerDelay !== nextProps.exitStaggerDelay ||
|
|
655
|
+
prevProps.exitStaggerDirection !== nextProps.exitStaggerDirection ||
|
|
656
|
+
prevProps.initialOpacity !== nextProps.initialOpacity ||
|
|
657
|
+
prevProps.initialScale !== nextProps.initialScale ||
|
|
658
|
+
prevProps.initialRotate !== nextProps.initialRotate ||
|
|
659
|
+
prevProps.initialTranslateX !== nextProps.initialTranslateX ||
|
|
660
|
+
prevProps.initialTranslateY !== nextProps.initialTranslateY) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
// Compare springConfig object (shallow)
|
|
664
|
+
if (prevProps.springConfig !== nextProps.springConfig) {
|
|
665
|
+
if (!prevProps.springConfig ||
|
|
666
|
+
!nextProps.springConfig ||
|
|
667
|
+
prevProps.springConfig.tension !== nextProps.springConfig.tension ||
|
|
668
|
+
prevProps.springConfig.friction !== nextProps.springConfig.friction ||
|
|
669
|
+
prevProps.springConfig.speed !== nextProps.springConfig.speed ||
|
|
670
|
+
prevProps.springConfig.bounciness !== nextProps.springConfig.bounciness) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Compare callbacks
|
|
675
|
+
if (prevProps.onAnimationStart !== nextProps.onAnimationStart ||
|
|
676
|
+
prevProps.onAnimationComplete !== nextProps.onAnimationComplete) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
// Style comparison
|
|
680
|
+
if (prevProps.style !== nextProps.style) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
// Children comparison - if children change, re-render
|
|
684
|
+
if (prevProps.children !== nextProps.children) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
389
689
|
/**
|
|
390
690
|
* PuffPopGroup - Animate multiple children with staggered entrance effects
|
|
391
691
|
*
|
|
@@ -398,7 +698,19 @@ const styles = StyleSheet.create({
|
|
|
398
698
|
* </PuffPopGroup>
|
|
399
699
|
* ```
|
|
400
700
|
*/
|
|
401
|
-
|
|
701
|
+
function PuffPopGroupComponent({ children, effect = 'scale', duration = 400, staggerDelay = 100, initialDelay = 0, easing = 'easeOut', skeleton = true, visible = true, animateOnMount = true, onAnimationComplete, onAnimationStart, style, respectReduceMotion = true, testID, staggerDirection = 'forward', horizontal = false, gap,
|
|
702
|
+
// Custom initial values
|
|
703
|
+
initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY,
|
|
704
|
+
// Reverse mode
|
|
705
|
+
reverse,
|
|
706
|
+
// Animation intensity
|
|
707
|
+
intensity,
|
|
708
|
+
// Anchor point
|
|
709
|
+
anchorPoint,
|
|
710
|
+
// Spring animation
|
|
711
|
+
useSpring, springConfig,
|
|
712
|
+
// Exit animation settings
|
|
713
|
+
exitEffect, exitDuration, exitEasing, exitDelay, exitStaggerDelay = 0, exitStaggerDirection = 'reverse', }) {
|
|
402
714
|
const childArray = Children.toArray(children);
|
|
403
715
|
const childCount = childArray.length;
|
|
404
716
|
const completedCount = useRef(0);
|
|
@@ -427,6 +739,34 @@ export function PuffPopGroup({ children, effect = 'scale', duration = 400, stagg
|
|
|
427
739
|
}
|
|
428
740
|
return initialDelay + delayIndex * staggerDelay;
|
|
429
741
|
}, [childCount, initialDelay, staggerDelay, staggerDirection]);
|
|
742
|
+
// Calculate exit delay for each child based on exit stagger direction
|
|
743
|
+
const getChildExitDelay = useCallback((index) => {
|
|
744
|
+
if (exitStaggerDelay === 0) {
|
|
745
|
+
return exitDelay ?? 0;
|
|
746
|
+
}
|
|
747
|
+
let delayIndex;
|
|
748
|
+
switch (exitStaggerDirection) {
|
|
749
|
+
case 'forward':
|
|
750
|
+
delayIndex = index;
|
|
751
|
+
break;
|
|
752
|
+
case 'center': {
|
|
753
|
+
const center = (childCount - 1) / 2;
|
|
754
|
+
delayIndex = Math.abs(index - center);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case 'edges': {
|
|
758
|
+
const center = (childCount - 1) / 2;
|
|
759
|
+
delayIndex = center - Math.abs(index - center);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case 'reverse':
|
|
763
|
+
default:
|
|
764
|
+
// Reverse is default for exit (last in, first out)
|
|
765
|
+
delayIndex = childCount - 1 - index;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
return (exitDelay ?? 0) + delayIndex * exitStaggerDelay;
|
|
769
|
+
}, [childCount, exitDelay, exitStaggerDelay, exitStaggerDirection]);
|
|
430
770
|
// Handle individual child animation complete
|
|
431
771
|
const handleChildComplete = useCallback(() => {
|
|
432
772
|
completedCount.current += 1;
|
|
@@ -457,6 +797,8 @@ export function PuffPopGroup({ children, effect = 'scale', duration = 400, stagg
|
|
|
457
797
|
}
|
|
458
798
|
return baseStyle;
|
|
459
799
|
}, [horizontal, gap]);
|
|
460
|
-
return (_jsx(View, { style: [styles.groupContainer, containerStyle, style], testID: testID, children: childArray.map((child, index) => (_jsx(PuffPop, { effect: effect, duration: duration, delay: getChildDelay(index), easing: easing, skeleton: skeleton, visible: visible, animateOnMount: animateOnMount, onAnimationComplete: handleChildComplete, onAnimationStart: index === 0 ? handleChildStart : undefined, respectReduceMotion: respectReduceMotion, children: child }, index))) }));
|
|
800
|
+
return (_jsx(View, { style: [styles.groupContainer, containerStyle, style], testID: testID, children: childArray.map((child, index) => (_jsx(PuffPop, { effect: effect, duration: duration, delay: getChildDelay(index), easing: easing, skeleton: skeleton, visible: visible, animateOnMount: animateOnMount, onAnimationComplete: handleChildComplete, onAnimationStart: index === 0 ? handleChildStart : undefined, respectReduceMotion: respectReduceMotion, initialOpacity: initialOpacity, initialScale: initialScale, initialRotate: initialRotate, initialTranslateX: initialTranslateX, initialTranslateY: initialTranslateY, reverse: reverse, intensity: intensity, anchorPoint: anchorPoint, useSpring: useSpring, springConfig: springConfig, exitEffect: exitEffect, exitDuration: exitDuration, exitEasing: exitEasing, exitDelay: getChildExitDelay(index), children: child }, index))) }));
|
|
461
801
|
}
|
|
802
|
+
// Memoize PuffPopGroup to prevent unnecessary re-renders
|
|
803
|
+
export const PuffPopGroup = memo(PuffPopGroupComponent, arePuffPopGroupPropsEqual);
|
|
462
804
|
export default PuffPop;
|