react-native-puff-pop 1.0.7 → 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 +138 -1
- package/lib/module/index.js +319 -39
- package/lib/typescript/src/index.d.ts +85 -3
- package/package.json +1 -1
- package/src/index.tsx +488 -47
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,10 +22,116 @@ 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,
|
|
29
135
|
// Exit animation settings
|
|
30
136
|
exitEffect, exitDuration, exitEasing, exitDelay = 0,
|
|
31
137
|
// Custom initial values
|
|
@@ -33,7 +139,11 @@ initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslate
|
|
|
33
139
|
// Reverse mode
|
|
34
140
|
reverse = false,
|
|
35
141
|
// Animation intensity
|
|
36
|
-
intensity = 1,
|
|
142
|
+
intensity = 1,
|
|
143
|
+
// Anchor point
|
|
144
|
+
anchorPoint = 'center',
|
|
145
|
+
// Spring animation
|
|
146
|
+
useSpring = false, springConfig, }) {
|
|
37
147
|
// Clamp intensity between 0 and 1
|
|
38
148
|
const clampedIntensity = Math.max(0, Math.min(1, intensity));
|
|
39
149
|
// Helper to get initial value with custom override, reverse, and intensity support
|
|
@@ -90,19 +200,10 @@ intensity = 1, }) {
|
|
|
90
200
|
// Effective duration (0 if reduce motion is enabled)
|
|
91
201
|
const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
|
|
92
202
|
const effectiveExitDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : (exitDuration ?? duration);
|
|
93
|
-
// Helper to get effect flags for any effect type
|
|
94
|
-
const getEffectFlags = useCallback((eff) => ({
|
|
95
|
-
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(eff),
|
|
96
|
-
hasRotate: ['rotate', 'rotateScale'].includes(eff),
|
|
97
|
-
hasFlip: eff === 'flip',
|
|
98
|
-
hasTranslateX: ['slideLeft', 'slideRight'].includes(eff),
|
|
99
|
-
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(eff),
|
|
100
|
-
hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(eff),
|
|
101
|
-
}), []);
|
|
102
203
|
// Memoize effect type checks to avoid repeated includes() calls
|
|
103
|
-
const effectFlags = useMemo(() => getEffectFlags(effect), [effect
|
|
204
|
+
const effectFlags = useMemo(() => getEffectFlags(effect), [effect]);
|
|
104
205
|
// Exit effect flags (use exitEffect if specified, otherwise same as enter effect)
|
|
105
|
-
const exitEffectFlags = useMemo(() => exitEffect ? getEffectFlags(exitEffect) : effectFlags, [exitEffect,
|
|
206
|
+
const exitEffectFlags = useMemo(() => exitEffect ? getEffectFlags(exitEffect) : effectFlags, [exitEffect, effectFlags]);
|
|
106
207
|
// Memoize interpolations to avoid recreating on every render
|
|
107
208
|
const rotateInterpolation = useMemo(() => rotate.interpolate({
|
|
108
209
|
inputRange: [-360, 0, 360],
|
|
@@ -135,51 +236,87 @@ intensity = 1, }) {
|
|
|
135
236
|
// When skeleton is false, we animate height which doesn't support native driver
|
|
136
237
|
// So we must use JS driver for all animations in that case
|
|
137
238
|
const useNative = skeleton;
|
|
138
|
-
|
|
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 = {
|
|
139
248
|
duration: currentDuration,
|
|
140
249
|
easing: easingFn,
|
|
141
250
|
useNativeDriver: useNative,
|
|
142
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
|
+
};
|
|
143
266
|
const animations = [];
|
|
144
|
-
// Opacity animation
|
|
267
|
+
// Opacity animation (always use timing for opacity for smoother fade)
|
|
145
268
|
animations.push(Animated.timing(opacity, {
|
|
146
269
|
toValue: toVisible ? 1 : 0,
|
|
147
|
-
...
|
|
270
|
+
...timingConfig,
|
|
148
271
|
}));
|
|
149
272
|
// Scale animation
|
|
150
273
|
if (currentFlags.hasScale) {
|
|
151
274
|
const targetScale = toVisible ? 1 : getInitialScaleValue(currentEffect);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
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));
|
|
157
287
|
}
|
|
158
288
|
// Rotate animation
|
|
159
289
|
if (currentFlags.hasRotate || currentFlags.hasFlip) {
|
|
160
290
|
const targetRotate = toVisible ? 0 : getInitialRotateValue(currentEffect);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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));
|
|
165
300
|
}
|
|
166
301
|
// TranslateX animation
|
|
167
302
|
if (currentFlags.hasTranslateX) {
|
|
168
303
|
const targetX = toVisible ? 0 : getInitialTranslateXValue(currentEffect);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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));
|
|
173
313
|
}
|
|
174
314
|
// TranslateY animation
|
|
175
315
|
if (currentFlags.hasTranslateY) {
|
|
176
316
|
const targetY = toVisible ? 0 : getInitialTranslateYValue(currentEffect);
|
|
177
|
-
animations.push(
|
|
178
|
-
toValue: targetY,
|
|
179
|
-
...config,
|
|
180
|
-
}));
|
|
317
|
+
animations.push(createAnimation(translateY, targetY));
|
|
181
318
|
}
|
|
182
|
-
// Height animation for non-skeleton mode
|
|
319
|
+
// Height animation for non-skeleton mode (always use timing)
|
|
183
320
|
if (!skeleton && measuredHeight !== null) {
|
|
184
321
|
const targetHeight = toVisible ? measuredHeight : 0;
|
|
185
322
|
animations.push(Animated.timing(animatedHeight, {
|
|
@@ -295,6 +432,8 @@ intensity = 1, }) {
|
|
|
295
432
|
animatedHeight,
|
|
296
433
|
loop,
|
|
297
434
|
loopDelay,
|
|
435
|
+
useSpring,
|
|
436
|
+
springConfig,
|
|
298
437
|
]);
|
|
299
438
|
// Handle initial mount animation
|
|
300
439
|
useEffect(() => {
|
|
@@ -320,11 +459,33 @@ intensity = 1, }) {
|
|
|
320
459
|
}
|
|
321
460
|
};
|
|
322
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]);
|
|
323
472
|
// Memoize transform array to avoid recreating on every render
|
|
324
473
|
// IMPORTANT: All hooks must be called before any conditional returns
|
|
325
474
|
const transform = useMemo(() => {
|
|
326
475
|
const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
|
|
476
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
327
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
|
|
328
489
|
if (hasScale) {
|
|
329
490
|
transforms.push({ scale });
|
|
330
491
|
}
|
|
@@ -334,6 +495,16 @@ intensity = 1, }) {
|
|
|
334
495
|
if (hasFlip) {
|
|
335
496
|
transforms.push({ rotateY: flipInterpolation });
|
|
336
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
|
|
337
508
|
if (hasTranslateX) {
|
|
338
509
|
transforms.push({ translateX });
|
|
339
510
|
}
|
|
@@ -341,7 +512,7 @@ intensity = 1, }) {
|
|
|
341
512
|
transforms.push({ translateY });
|
|
342
513
|
}
|
|
343
514
|
return transforms.length > 0 ? transforms : undefined;
|
|
344
|
-
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
|
|
515
|
+
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY, anchorPoint, anchorOffset]);
|
|
345
516
|
// Memoize animated style
|
|
346
517
|
const animatedStyle = useMemo(() => ({
|
|
347
518
|
opacity,
|
|
@@ -363,6 +534,8 @@ intensity = 1, }) {
|
|
|
363
534
|
}
|
|
364
535
|
return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], testID: testID, children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
|
|
365
536
|
}
|
|
537
|
+
// Memoize PuffPop to prevent unnecessary re-renders
|
|
538
|
+
export const PuffPop = memo(PuffPopComponent, arePuffPopPropsEqual);
|
|
366
539
|
/**
|
|
367
540
|
* Get initial scale value based on effect
|
|
368
541
|
*/
|
|
@@ -371,6 +544,7 @@ function getInitialScale(effect, _reverse = false) {
|
|
|
371
544
|
switch (effect) {
|
|
372
545
|
case 'scale':
|
|
373
546
|
case 'rotateScale':
|
|
547
|
+
case 'elastic':
|
|
374
548
|
return 0;
|
|
375
549
|
case 'bounce':
|
|
376
550
|
return 0.3;
|
|
@@ -378,6 +552,8 @@ function getInitialScale(effect, _reverse = false) {
|
|
|
378
552
|
return 0.5;
|
|
379
553
|
case 'flip':
|
|
380
554
|
return 0.8;
|
|
555
|
+
case 'pulse':
|
|
556
|
+
return 0.6;
|
|
381
557
|
default:
|
|
382
558
|
return 1;
|
|
383
559
|
}
|
|
@@ -394,6 +570,10 @@ function getInitialRotate(effect, reverse = false) {
|
|
|
394
570
|
return -180 * multiplier;
|
|
395
571
|
case 'flip':
|
|
396
572
|
return -180 * multiplier;
|
|
573
|
+
case 'swing':
|
|
574
|
+
return -15 * multiplier;
|
|
575
|
+
case 'wobble':
|
|
576
|
+
return -5 * multiplier;
|
|
397
577
|
default:
|
|
398
578
|
return 0;
|
|
399
579
|
}
|
|
@@ -408,6 +588,10 @@ function getInitialTranslateX(effect, reverse = false) {
|
|
|
408
588
|
return 100 * multiplier;
|
|
409
589
|
case 'slideRight':
|
|
410
590
|
return -100 * multiplier;
|
|
591
|
+
case 'shake':
|
|
592
|
+
return -10 * multiplier;
|
|
593
|
+
case 'wobble':
|
|
594
|
+
return -25 * multiplier;
|
|
411
595
|
default:
|
|
412
596
|
return 0;
|
|
413
597
|
}
|
|
@@ -440,6 +624,68 @@ const styles = StyleSheet.create({
|
|
|
440
624
|
},
|
|
441
625
|
groupContainer: {},
|
|
442
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
|
+
}
|
|
443
689
|
/**
|
|
444
690
|
* PuffPopGroup - Animate multiple children with staggered entrance effects
|
|
445
691
|
*
|
|
@@ -452,15 +698,19 @@ const styles = StyleSheet.create({
|
|
|
452
698
|
* </PuffPopGroup>
|
|
453
699
|
* ```
|
|
454
700
|
*/
|
|
455
|
-
|
|
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,
|
|
456
702
|
// Custom initial values
|
|
457
703
|
initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY,
|
|
458
704
|
// Reverse mode
|
|
459
705
|
reverse,
|
|
460
706
|
// Animation intensity
|
|
461
707
|
intensity,
|
|
708
|
+
// Anchor point
|
|
709
|
+
anchorPoint,
|
|
710
|
+
// Spring animation
|
|
711
|
+
useSpring, springConfig,
|
|
462
712
|
// Exit animation settings
|
|
463
|
-
exitEffect, exitDuration, exitEasing, exitDelay, }) {
|
|
713
|
+
exitEffect, exitDuration, exitEasing, exitDelay, exitStaggerDelay = 0, exitStaggerDirection = 'reverse', }) {
|
|
464
714
|
const childArray = Children.toArray(children);
|
|
465
715
|
const childCount = childArray.length;
|
|
466
716
|
const completedCount = useRef(0);
|
|
@@ -489,6 +739,34 @@ exitEffect, exitDuration, exitEasing, exitDelay, }) {
|
|
|
489
739
|
}
|
|
490
740
|
return initialDelay + delayIndex * staggerDelay;
|
|
491
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]);
|
|
492
770
|
// Handle individual child animation complete
|
|
493
771
|
const handleChildComplete = useCallback(() => {
|
|
494
772
|
completedCount.current += 1;
|
|
@@ -519,6 +797,8 @@ exitEffect, exitDuration, exitEasing, exitDelay, }) {
|
|
|
519
797
|
}
|
|
520
798
|
return baseStyle;
|
|
521
799
|
}, [horizontal, gap]);
|
|
522
|
-
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, exitEffect: exitEffect, exitDuration: exitDuration, exitEasing: exitEasing, exitDelay:
|
|
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))) }));
|
|
523
801
|
}
|
|
802
|
+
// Memoize PuffPopGroup to prevent unnecessary re-renders
|
|
803
|
+
export const PuffPopGroup = memo(PuffPopGroupComponent, arePuffPopGroupPropsEqual);
|
|
524
804
|
export default PuffPop;
|
|
@@ -3,11 +3,42 @@ import { type StyleProp, type ViewStyle } from 'react-native';
|
|
|
3
3
|
/**
|
|
4
4
|
* Animation effect types for PuffPop
|
|
5
5
|
*/
|
|
6
|
-
export type PuffPopEffect = 'scale' | 'rotate' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'bounce' | 'flip' | 'zoom' | 'rotateScale';
|
|
6
|
+
export type PuffPopEffect = 'scale' | 'rotate' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'bounce' | 'flip' | 'zoom' | 'rotateScale' | 'shake' | 'pulse' | 'swing' | 'wobble' | 'elastic';
|
|
7
7
|
/**
|
|
8
8
|
* Easing function types
|
|
9
9
|
*/
|
|
10
10
|
export type PuffPopEasing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'spring' | 'bounce';
|
|
11
|
+
/**
|
|
12
|
+
* Anchor point for scale/rotate transformations
|
|
13
|
+
* Determines the origin point of the transformation
|
|
14
|
+
*/
|
|
15
|
+
export type PuffPopAnchorPoint = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
|
16
|
+
/**
|
|
17
|
+
* Spring animation configuration
|
|
18
|
+
* Used when useSpring is true for physics-based animations
|
|
19
|
+
*/
|
|
20
|
+
export interface PuffPopSpringConfig {
|
|
21
|
+
/**
|
|
22
|
+
* Controls the spring stiffness. Higher values = faster, snappier animation
|
|
23
|
+
* @default 100
|
|
24
|
+
*/
|
|
25
|
+
tension?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Controls the spring damping. Higher values = less oscillation
|
|
28
|
+
* @default 10
|
|
29
|
+
*/
|
|
30
|
+
friction?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Animation speed multiplier
|
|
33
|
+
* @default 12
|
|
34
|
+
*/
|
|
35
|
+
speed?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Controls the bounciness. Higher values = more bouncy
|
|
38
|
+
* @default 8
|
|
39
|
+
*/
|
|
40
|
+
bounciness?: number;
|
|
41
|
+
}
|
|
11
42
|
export interface PuffPopProps {
|
|
12
43
|
/**
|
|
13
44
|
* Children to animate
|
|
@@ -145,11 +176,32 @@ export interface PuffPopProps {
|
|
|
145
176
|
* @default 1
|
|
146
177
|
*/
|
|
147
178
|
intensity?: number;
|
|
179
|
+
/**
|
|
180
|
+
* Anchor point for scale/rotate transformations
|
|
181
|
+
* Determines the origin point of the transformation
|
|
182
|
+
* - 'center' = default center point
|
|
183
|
+
* - 'top', 'bottom', 'left', 'right' = edge centers
|
|
184
|
+
* - 'topLeft', 'topRight', 'bottomLeft', 'bottomRight' = corners
|
|
185
|
+
* @default 'center'
|
|
186
|
+
*/
|
|
187
|
+
anchorPoint?: PuffPopAnchorPoint;
|
|
188
|
+
/**
|
|
189
|
+
* Use spring physics-based animation instead of timing
|
|
190
|
+
* Provides more natural, bouncy animations
|
|
191
|
+
* @default false
|
|
192
|
+
*/
|
|
193
|
+
useSpring?: boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Spring animation configuration
|
|
196
|
+
* Only used when useSpring is true
|
|
197
|
+
*/
|
|
198
|
+
springConfig?: PuffPopSpringConfig;
|
|
148
199
|
}
|
|
149
200
|
/**
|
|
150
201
|
* PuffPop - Animate children with beautiful entrance effects
|
|
151
202
|
*/
|
|
152
|
-
|
|
203
|
+
declare function PuffPopComponent({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, onAnimationStart, style, animateOnMount, loop, loopDelay, respectReduceMotion, testID, exitEffect, exitDuration, exitEasing, exitDelay, initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY, reverse, intensity, anchorPoint, useSpring, springConfig, }: PuffPopProps): ReactElement;
|
|
204
|
+
export declare const PuffPop: import("react").MemoExoticComponent<typeof PuffPopComponent>;
|
|
153
205
|
/**
|
|
154
206
|
* Props for PuffPopGroup component
|
|
155
207
|
*/
|
|
@@ -268,6 +320,20 @@ export interface PuffPopGroupProps {
|
|
|
268
320
|
* @default 1
|
|
269
321
|
*/
|
|
270
322
|
intensity?: number;
|
|
323
|
+
/**
|
|
324
|
+
* Anchor point for scale/rotate transformations for all children
|
|
325
|
+
* @default 'center'
|
|
326
|
+
*/
|
|
327
|
+
anchorPoint?: PuffPopAnchorPoint;
|
|
328
|
+
/**
|
|
329
|
+
* Use spring physics-based animation for all children
|
|
330
|
+
* @default false
|
|
331
|
+
*/
|
|
332
|
+
useSpring?: boolean;
|
|
333
|
+
/**
|
|
334
|
+
* Spring animation configuration for all children
|
|
335
|
+
*/
|
|
336
|
+
springConfig?: PuffPopSpringConfig;
|
|
271
337
|
/**
|
|
272
338
|
* Animation effect type when hiding (exit animation) for all children
|
|
273
339
|
* If not specified, uses the reverse of the enter effect
|
|
@@ -288,6 +354,21 @@ export interface PuffPopGroupProps {
|
|
|
288
354
|
* @default 0
|
|
289
355
|
*/
|
|
290
356
|
exitDelay?: number;
|
|
357
|
+
/**
|
|
358
|
+
* Delay between each child's exit animation start in milliseconds
|
|
359
|
+
* If not specified, all children exit simultaneously
|
|
360
|
+
* @default 0
|
|
361
|
+
*/
|
|
362
|
+
exitStaggerDelay?: number;
|
|
363
|
+
/**
|
|
364
|
+
* Direction of exit stagger animation
|
|
365
|
+
* - 'forward': First to last child
|
|
366
|
+
* - 'reverse': Last to first child (most natural for exit)
|
|
367
|
+
* - 'center': From center outward
|
|
368
|
+
* - 'edges': From edges toward center
|
|
369
|
+
* @default 'reverse'
|
|
370
|
+
*/
|
|
371
|
+
exitStaggerDirection?: 'forward' | 'reverse' | 'center' | 'edges';
|
|
291
372
|
}
|
|
292
373
|
/**
|
|
293
374
|
* PuffPopGroup - Animate multiple children with staggered entrance effects
|
|
@@ -301,5 +382,6 @@ export interface PuffPopGroupProps {
|
|
|
301
382
|
* </PuffPopGroup>
|
|
302
383
|
* ```
|
|
303
384
|
*/
|
|
304
|
-
|
|
385
|
+
declare function PuffPopGroupComponent({ children, effect, duration, staggerDelay, initialDelay, easing, skeleton, visible, animateOnMount, onAnimationComplete, onAnimationStart, style, respectReduceMotion, testID, staggerDirection, horizontal, gap, initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY, reverse, intensity, anchorPoint, useSpring, springConfig, exitEffect, exitDuration, exitEasing, exitDelay, exitStaggerDelay, exitStaggerDirection, }: PuffPopGroupProps): ReactElement;
|
|
386
|
+
export declare const PuffPopGroup: import("react").MemoExoticComponent<typeof PuffPopGroupComponent>;
|
|
305
387
|
export default PuffPop;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-puff-pop",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A React Native animation library for revealing children components with beautiful puff and pop effects",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|