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/src/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
useCallback,
|
|
6
6
|
useMemo,
|
|
7
|
+
memo,
|
|
7
8
|
Children,
|
|
8
9
|
type ReactNode,
|
|
9
10
|
type ReactElement,
|
|
@@ -33,7 +34,12 @@ export type PuffPopEffect =
|
|
|
33
34
|
| 'bounce' // Bounce effect with overshoot
|
|
34
35
|
| 'flip' // 3D flip effect
|
|
35
36
|
| 'zoom' // Zoom with slight overshoot
|
|
36
|
-
| 'rotateScale'
|
|
37
|
+
| 'rotateScale' // Rotate + Scale combined
|
|
38
|
+
| 'shake' // Shake left-right (흔들림 효과)
|
|
39
|
+
| 'pulse' // Pulse scale effect (맥박처럼 커졌다 작아짐)
|
|
40
|
+
| 'swing' // Swing like pendulum (시계추처럼 흔들림)
|
|
41
|
+
| 'wobble' // Wobble with tilt (기울어지며 이동)
|
|
42
|
+
| 'elastic'; // Elastic stretch effect (탄성 효과)
|
|
37
43
|
|
|
38
44
|
/**
|
|
39
45
|
* Easing function types
|
|
@@ -46,6 +52,51 @@ export type PuffPopEasing =
|
|
|
46
52
|
| 'spring'
|
|
47
53
|
| 'bounce';
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Anchor point for scale/rotate transformations
|
|
57
|
+
* Determines the origin point of the transformation
|
|
58
|
+
*/
|
|
59
|
+
export type PuffPopAnchorPoint =
|
|
60
|
+
| 'center' // Default center point
|
|
61
|
+
| 'top' // Top center
|
|
62
|
+
| 'bottom' // Bottom center
|
|
63
|
+
| 'left' // Left center
|
|
64
|
+
| 'right' // Right center
|
|
65
|
+
| 'topLeft' // Top left corner
|
|
66
|
+
| 'topRight' // Top right corner
|
|
67
|
+
| 'bottomLeft' // Bottom left corner
|
|
68
|
+
| 'bottomRight'; // Bottom right corner
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Spring animation configuration
|
|
72
|
+
* Used when useSpring is true for physics-based animations
|
|
73
|
+
*/
|
|
74
|
+
export interface PuffPopSpringConfig {
|
|
75
|
+
/**
|
|
76
|
+
* Controls the spring stiffness. Higher values = faster, snappier animation
|
|
77
|
+
* @default 100
|
|
78
|
+
*/
|
|
79
|
+
tension?: number;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Controls the spring damping. Higher values = less oscillation
|
|
83
|
+
* @default 10
|
|
84
|
+
*/
|
|
85
|
+
friction?: number;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Animation speed multiplier
|
|
89
|
+
* @default 12
|
|
90
|
+
*/
|
|
91
|
+
speed?: number;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Controls the bounciness. Higher values = more bouncy
|
|
95
|
+
* @default 8
|
|
96
|
+
*/
|
|
97
|
+
bounciness?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
49
100
|
export interface PuffPopProps {
|
|
50
101
|
/**
|
|
51
102
|
* Children to animate
|
|
@@ -135,6 +186,114 @@ export interface PuffPopProps {
|
|
|
135
186
|
* Test ID for testing purposes
|
|
136
187
|
*/
|
|
137
188
|
testID?: string;
|
|
189
|
+
|
|
190
|
+
// ============ Exit Animation Settings ============
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Animation effect type when hiding (exit animation)
|
|
194
|
+
* If not specified, uses the reverse of the enter effect
|
|
195
|
+
*/
|
|
196
|
+
exitEffect?: PuffPopEffect;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Animation duration for exit animation in milliseconds
|
|
200
|
+
* If not specified, uses the same duration as enter animation
|
|
201
|
+
*/
|
|
202
|
+
exitDuration?: number;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Easing function for exit animation
|
|
206
|
+
* If not specified, uses the same easing as enter animation
|
|
207
|
+
*/
|
|
208
|
+
exitEasing?: PuffPopEasing;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Delay before exit animation starts in milliseconds
|
|
212
|
+
* @default 0
|
|
213
|
+
*/
|
|
214
|
+
exitDelay?: number;
|
|
215
|
+
|
|
216
|
+
// ============ Custom Initial Values ============
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Custom initial opacity value (0-1)
|
|
220
|
+
* Overrides the default initial opacity for the effect
|
|
221
|
+
*/
|
|
222
|
+
initialOpacity?: number;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Custom initial scale value
|
|
226
|
+
* Overrides the default initial scale for effects like 'scale', 'zoom', 'bounce'
|
|
227
|
+
*/
|
|
228
|
+
initialScale?: number;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Custom initial rotation value in degrees
|
|
232
|
+
* Overrides the default initial rotation for effects like 'rotate', 'rotateScale'
|
|
233
|
+
*/
|
|
234
|
+
initialRotate?: number;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Custom initial translateX value in pixels
|
|
238
|
+
* Overrides the default initial translateX for effects like 'slideLeft', 'slideRight'
|
|
239
|
+
*/
|
|
240
|
+
initialTranslateX?: number;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Custom initial translateY value in pixels
|
|
244
|
+
* Overrides the default initial translateY for effects like 'slideUp', 'slideDown'
|
|
245
|
+
*/
|
|
246
|
+
initialTranslateY?: number;
|
|
247
|
+
|
|
248
|
+
// ============ Reverse Mode ============
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* If true, reverses the animation direction
|
|
252
|
+
* - slideUp becomes slide from top
|
|
253
|
+
* - slideLeft becomes slide from left
|
|
254
|
+
* - rotate spins clockwise instead of counter-clockwise
|
|
255
|
+
* @default false
|
|
256
|
+
*/
|
|
257
|
+
reverse?: boolean;
|
|
258
|
+
|
|
259
|
+
// ============ Animation Intensity ============
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Animation intensity multiplier (0-1)
|
|
263
|
+
* Controls how far/much elements move during animation
|
|
264
|
+
* - 1 = full animation (default)
|
|
265
|
+
* - 0.5 = half the movement distance
|
|
266
|
+
* - 0 = no movement (instant appear)
|
|
267
|
+
* @default 1
|
|
268
|
+
*/
|
|
269
|
+
intensity?: number;
|
|
270
|
+
|
|
271
|
+
// ============ Anchor Point ============
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Anchor point for scale/rotate transformations
|
|
275
|
+
* Determines the origin point of the transformation
|
|
276
|
+
* - 'center' = default center point
|
|
277
|
+
* - 'top', 'bottom', 'left', 'right' = edge centers
|
|
278
|
+
* - 'topLeft', 'topRight', 'bottomLeft', 'bottomRight' = corners
|
|
279
|
+
* @default 'center'
|
|
280
|
+
*/
|
|
281
|
+
anchorPoint?: PuffPopAnchorPoint;
|
|
282
|
+
|
|
283
|
+
// ============ Spring Animation ============
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Use spring physics-based animation instead of timing
|
|
287
|
+
* Provides more natural, bouncy animations
|
|
288
|
+
* @default false
|
|
289
|
+
*/
|
|
290
|
+
useSpring?: boolean;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Spring animation configuration
|
|
294
|
+
* Only used when useSpring is true
|
|
295
|
+
*/
|
|
296
|
+
springConfig?: PuffPopSpringConfig;
|
|
138
297
|
}
|
|
139
298
|
|
|
140
299
|
/**
|
|
@@ -159,10 +318,133 @@ function getEasing(type: PuffPopEasing): (value: number) => number {
|
|
|
159
318
|
}
|
|
160
319
|
}
|
|
161
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Get effect flags for any effect type
|
|
323
|
+
* Returns flags indicating which transforms are needed for the effect
|
|
324
|
+
*/
|
|
325
|
+
function getEffectFlags(eff: PuffPopEffect) {
|
|
326
|
+
return {
|
|
327
|
+
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip', 'pulse', 'elastic'].includes(eff),
|
|
328
|
+
hasRotate: ['rotate', 'rotateScale', 'swing', 'wobble'].includes(eff),
|
|
329
|
+
hasFlip: eff === 'flip',
|
|
330
|
+
hasTranslateX: ['slideLeft', 'slideRight', 'shake', 'wobble'].includes(eff),
|
|
331
|
+
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(eff),
|
|
332
|
+
hasRotateEffect: ['rotate', 'rotateScale', 'flip', 'swing', 'wobble'].includes(eff),
|
|
333
|
+
// Special effects that need sequence animation
|
|
334
|
+
isShake: eff === 'shake',
|
|
335
|
+
isPulse: eff === 'pulse',
|
|
336
|
+
isSwing: eff === 'swing',
|
|
337
|
+
isWobble: eff === 'wobble',
|
|
338
|
+
isElastic: eff === 'elastic',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get anchor point offset multipliers
|
|
344
|
+
* Returns { x: -1 to 1, y: -1 to 1 } where 0 is center
|
|
345
|
+
*/
|
|
346
|
+
function getAnchorPointOffset(anchorPoint: PuffPopAnchorPoint): { x: number; y: number } {
|
|
347
|
+
switch (anchorPoint) {
|
|
348
|
+
case 'top':
|
|
349
|
+
return { x: 0, y: -0.5 };
|
|
350
|
+
case 'bottom':
|
|
351
|
+
return { x: 0, y: 0.5 };
|
|
352
|
+
case 'left':
|
|
353
|
+
return { x: -0.5, y: 0 };
|
|
354
|
+
case 'right':
|
|
355
|
+
return { x: 0.5, y: 0 };
|
|
356
|
+
case 'topLeft':
|
|
357
|
+
return { x: -0.5, y: -0.5 };
|
|
358
|
+
case 'topRight':
|
|
359
|
+
return { x: 0.5, y: -0.5 };
|
|
360
|
+
case 'bottomLeft':
|
|
361
|
+
return { x: -0.5, y: 0.5 };
|
|
362
|
+
case 'bottomRight':
|
|
363
|
+
return { x: 0.5, y: 0.5 };
|
|
364
|
+
case 'center':
|
|
365
|
+
default:
|
|
366
|
+
return { x: 0, y: 0 };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Props comparison function for PuffPop memoization
|
|
372
|
+
* Performs shallow comparison of props to prevent unnecessary re-renders
|
|
373
|
+
*/
|
|
374
|
+
function arePuffPopPropsEqual(
|
|
375
|
+
prevProps: PuffPopProps,
|
|
376
|
+
nextProps: PuffPopProps
|
|
377
|
+
): boolean {
|
|
378
|
+
// Compare primitive props
|
|
379
|
+
if (
|
|
380
|
+
prevProps.effect !== nextProps.effect ||
|
|
381
|
+
prevProps.duration !== nextProps.duration ||
|
|
382
|
+
prevProps.delay !== nextProps.delay ||
|
|
383
|
+
prevProps.easing !== nextProps.easing ||
|
|
384
|
+
prevProps.skeleton !== nextProps.skeleton ||
|
|
385
|
+
prevProps.visible !== nextProps.visible ||
|
|
386
|
+
prevProps.animateOnMount !== nextProps.animateOnMount ||
|
|
387
|
+
prevProps.loop !== nextProps.loop ||
|
|
388
|
+
prevProps.loopDelay !== nextProps.loopDelay ||
|
|
389
|
+
prevProps.respectReduceMotion !== nextProps.respectReduceMotion ||
|
|
390
|
+
prevProps.testID !== nextProps.testID ||
|
|
391
|
+
prevProps.reverse !== nextProps.reverse ||
|
|
392
|
+
prevProps.intensity !== nextProps.intensity ||
|
|
393
|
+
prevProps.anchorPoint !== nextProps.anchorPoint ||
|
|
394
|
+
prevProps.useSpring !== nextProps.useSpring ||
|
|
395
|
+
prevProps.exitEffect !== nextProps.exitEffect ||
|
|
396
|
+
prevProps.exitDuration !== nextProps.exitDuration ||
|
|
397
|
+
prevProps.exitEasing !== nextProps.exitEasing ||
|
|
398
|
+
prevProps.exitDelay !== nextProps.exitDelay ||
|
|
399
|
+
prevProps.initialOpacity !== nextProps.initialOpacity ||
|
|
400
|
+
prevProps.initialScale !== nextProps.initialScale ||
|
|
401
|
+
prevProps.initialRotate !== nextProps.initialRotate ||
|
|
402
|
+
prevProps.initialTranslateX !== nextProps.initialTranslateX ||
|
|
403
|
+
prevProps.initialTranslateY !== nextProps.initialTranslateY
|
|
404
|
+
) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Compare springConfig object (shallow)
|
|
409
|
+
if (prevProps.springConfig !== nextProps.springConfig) {
|
|
410
|
+
if (
|
|
411
|
+
!prevProps.springConfig ||
|
|
412
|
+
!nextProps.springConfig ||
|
|
413
|
+
prevProps.springConfig.tension !== nextProps.springConfig.tension ||
|
|
414
|
+
prevProps.springConfig.friction !== nextProps.springConfig.friction ||
|
|
415
|
+
prevProps.springConfig.speed !== nextProps.springConfig.speed ||
|
|
416
|
+
prevProps.springConfig.bounciness !== nextProps.springConfig.bounciness
|
|
417
|
+
) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Compare callbacks (reference equality - if changed, should re-render)
|
|
423
|
+
if (
|
|
424
|
+
prevProps.onAnimationStart !== nextProps.onAnimationStart ||
|
|
425
|
+
prevProps.onAnimationComplete !== nextProps.onAnimationComplete
|
|
426
|
+
) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Style comparison - if style prop changes, re-render
|
|
431
|
+
// Note: Deep comparison of style is expensive, so we use reference equality
|
|
432
|
+
if (prevProps.style !== nextProps.style) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Children comparison - if children change, re-render
|
|
437
|
+
if (prevProps.children !== nextProps.children) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
162
444
|
/**
|
|
163
445
|
* PuffPop - Animate children with beautiful entrance effects
|
|
164
446
|
*/
|
|
165
|
-
|
|
447
|
+
function PuffPopComponent({
|
|
166
448
|
children,
|
|
167
449
|
effect = 'scale',
|
|
168
450
|
duration = 400,
|
|
@@ -178,13 +460,57 @@ export function PuffPop({
|
|
|
178
460
|
loopDelay = 0,
|
|
179
461
|
respectReduceMotion = true,
|
|
180
462
|
testID,
|
|
463
|
+
// Exit animation settings
|
|
464
|
+
exitEffect,
|
|
465
|
+
exitDuration,
|
|
466
|
+
exitEasing,
|
|
467
|
+
exitDelay = 0,
|
|
468
|
+
// Custom initial values
|
|
469
|
+
initialOpacity,
|
|
470
|
+
initialScale,
|
|
471
|
+
initialRotate,
|
|
472
|
+
initialTranslateX,
|
|
473
|
+
initialTranslateY,
|
|
474
|
+
// Reverse mode
|
|
475
|
+
reverse = false,
|
|
476
|
+
// Animation intensity
|
|
477
|
+
intensity = 1,
|
|
478
|
+
// Anchor point
|
|
479
|
+
anchorPoint = 'center',
|
|
480
|
+
// Spring animation
|
|
481
|
+
useSpring = false,
|
|
482
|
+
springConfig,
|
|
181
483
|
}: PuffPopProps): ReactElement {
|
|
484
|
+
// Clamp intensity between 0 and 1
|
|
485
|
+
const clampedIntensity = Math.max(0, Math.min(1, intensity));
|
|
486
|
+
|
|
487
|
+
// Helper to get initial value with custom override, reverse, and intensity support
|
|
488
|
+
const getInitialOpacityValue = () => initialOpacity ?? 0;
|
|
489
|
+
const getInitialScaleValue = (eff: PuffPopEffect) => {
|
|
490
|
+
if (initialScale !== undefined) return initialScale;
|
|
491
|
+
const baseScale = getInitialScale(eff, reverse);
|
|
492
|
+
// Scale goes from baseScale to 1, so we interpolate: 1 - (1 - baseScale) * intensity
|
|
493
|
+
return 1 - (1 - baseScale) * clampedIntensity;
|
|
494
|
+
};
|
|
495
|
+
const getInitialRotateValue = (eff: PuffPopEffect) => {
|
|
496
|
+
if (initialRotate !== undefined) return initialRotate;
|
|
497
|
+
return getInitialRotate(eff, reverse) * clampedIntensity;
|
|
498
|
+
};
|
|
499
|
+
const getInitialTranslateXValue = (eff: PuffPopEffect) => {
|
|
500
|
+
if (initialTranslateX !== undefined) return initialTranslateX;
|
|
501
|
+
return getInitialTranslateX(eff, reverse) * clampedIntensity;
|
|
502
|
+
};
|
|
503
|
+
const getInitialTranslateYValue = (eff: PuffPopEffect) => {
|
|
504
|
+
if (initialTranslateY !== undefined) return initialTranslateY;
|
|
505
|
+
return getInitialTranslateY(eff, reverse) * clampedIntensity;
|
|
506
|
+
};
|
|
507
|
+
|
|
182
508
|
// Animation values
|
|
183
|
-
const opacity = useRef(new Animated.Value(animateOnMount ?
|
|
184
|
-
const scale = useRef(new Animated.Value(animateOnMount ?
|
|
185
|
-
const rotate = useRef(new Animated.Value(animateOnMount ?
|
|
186
|
-
const translateX = useRef(new Animated.Value(animateOnMount ?
|
|
187
|
-
const translateY = useRef(new Animated.Value(animateOnMount ?
|
|
509
|
+
const opacity = useRef(new Animated.Value(animateOnMount ? getInitialOpacityValue() : 1)).current;
|
|
510
|
+
const scale = useRef(new Animated.Value(animateOnMount ? getInitialScaleValue(effect) : 1)).current;
|
|
511
|
+
const rotate = useRef(new Animated.Value(animateOnMount ? getInitialRotateValue(effect) : 0)).current;
|
|
512
|
+
const translateX = useRef(new Animated.Value(animateOnMount ? getInitialTranslateXValue(effect) : 0)).current;
|
|
513
|
+
const translateY = useRef(new Animated.Value(animateOnMount ? getInitialTranslateYValue(effect) : 0)).current;
|
|
188
514
|
|
|
189
515
|
// For non-skeleton mode
|
|
190
516
|
const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);
|
|
@@ -218,16 +544,15 @@ export function PuffPop({
|
|
|
218
544
|
|
|
219
545
|
// Effective duration (0 if reduce motion is enabled)
|
|
220
546
|
const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
|
|
547
|
+
const effectiveExitDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : (exitDuration ?? duration);
|
|
221
548
|
|
|
222
549
|
// Memoize effect type checks to avoid repeated includes() calls
|
|
223
|
-
const effectFlags = useMemo(() => (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
|
|
230
|
-
}), [effect]);
|
|
550
|
+
const effectFlags = useMemo(() => getEffectFlags(effect), [effect]);
|
|
551
|
+
|
|
552
|
+
// Exit effect flags (use exitEffect if specified, otherwise same as enter effect)
|
|
553
|
+
const exitEffectFlags = useMemo(() =>
|
|
554
|
+
exitEffect ? getEffectFlags(exitEffect) : effectFlags
|
|
555
|
+
, [exitEffect, effectFlags]);
|
|
231
556
|
|
|
232
557
|
// Memoize interpolations to avoid recreating on every render
|
|
233
558
|
const rotateInterpolation = useMemo(() =>
|
|
@@ -261,78 +586,116 @@ export function PuffPop({
|
|
|
261
586
|
onAnimationStart();
|
|
262
587
|
}
|
|
263
588
|
|
|
264
|
-
|
|
589
|
+
// Determine which effect settings to use based on direction
|
|
590
|
+
const currentEffect = toVisible ? effect : (exitEffect ?? effect);
|
|
591
|
+
const currentDuration = toVisible ? effectiveDuration : effectiveExitDuration;
|
|
592
|
+
const currentEasing = toVisible ? easing : (exitEasing ?? easing);
|
|
593
|
+
const currentDelay = toVisible ? delay : exitDelay;
|
|
594
|
+
const currentFlags = toVisible ? effectFlags : exitEffectFlags;
|
|
595
|
+
|
|
596
|
+
const easingFn = getEasing(currentEasing);
|
|
265
597
|
// When skeleton is false, we animate height which doesn't support native driver
|
|
266
598
|
// So we must use JS driver for all animations in that case
|
|
267
599
|
const useNative = skeleton;
|
|
268
|
-
|
|
269
|
-
|
|
600
|
+
|
|
601
|
+
// Spring configuration
|
|
602
|
+
const springConf = {
|
|
603
|
+
tension: springConfig?.tension ?? 100,
|
|
604
|
+
friction: springConfig?.friction ?? 10,
|
|
605
|
+
speed: springConfig?.speed,
|
|
606
|
+
bounciness: springConfig?.bounciness,
|
|
607
|
+
useNativeDriver: useNative,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const timingConfig = {
|
|
611
|
+
duration: currentDuration,
|
|
270
612
|
easing: easingFn,
|
|
271
613
|
useNativeDriver: useNative,
|
|
272
614
|
};
|
|
273
615
|
|
|
616
|
+
// Helper to create animation (spring or timing)
|
|
617
|
+
const createAnimation = (
|
|
618
|
+
value: Animated.Value,
|
|
619
|
+
toValue: number,
|
|
620
|
+
customEasing?: (t: number) => number
|
|
621
|
+
): Animated.CompositeAnimation => {
|
|
622
|
+
if (useSpring) {
|
|
623
|
+
return Animated.spring(value, {
|
|
624
|
+
toValue,
|
|
625
|
+
...springConf,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return Animated.timing(value, {
|
|
629
|
+
toValue,
|
|
630
|
+
...timingConfig,
|
|
631
|
+
...(customEasing ? { easing: customEasing } : {}),
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
|
|
274
635
|
const animations: Animated.CompositeAnimation[] = [];
|
|
275
636
|
|
|
276
|
-
// Opacity animation
|
|
637
|
+
// Opacity animation (always use timing for opacity for smoother fade)
|
|
277
638
|
animations.push(
|
|
278
639
|
Animated.timing(opacity, {
|
|
279
640
|
toValue: toVisible ? 1 : 0,
|
|
280
|
-
...
|
|
641
|
+
...timingConfig,
|
|
281
642
|
})
|
|
282
643
|
);
|
|
283
644
|
|
|
284
645
|
// Scale animation
|
|
285
|
-
if (
|
|
286
|
-
const targetScale = toVisible ? 1 :
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
)
|
|
646
|
+
if (currentFlags.hasScale) {
|
|
647
|
+
const targetScale = toVisible ? 1 : getInitialScaleValue(currentEffect);
|
|
648
|
+
// Special easing for different effects
|
|
649
|
+
let scaleEasing = easingFn;
|
|
650
|
+
if (currentEffect === 'bounce') {
|
|
651
|
+
scaleEasing = Easing.bounce;
|
|
652
|
+
} else if (currentEffect === 'elastic') {
|
|
653
|
+
scaleEasing = Easing.elastic(1.5);
|
|
654
|
+
} else if (currentEffect === 'pulse') {
|
|
655
|
+
scaleEasing = Easing.out(Easing.back(3));
|
|
656
|
+
}
|
|
657
|
+
animations.push(createAnimation(scale, targetScale, scaleEasing));
|
|
294
658
|
}
|
|
295
659
|
|
|
296
660
|
// Rotate animation
|
|
297
|
-
if (
|
|
298
|
-
const targetRotate = toVisible ? 0 :
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
661
|
+
if (currentFlags.hasRotate || currentFlags.hasFlip) {
|
|
662
|
+
const targetRotate = toVisible ? 0 : getInitialRotateValue(currentEffect);
|
|
663
|
+
// Special easing for swing and wobble
|
|
664
|
+
let rotateEasing = easingFn;
|
|
665
|
+
if (currentEffect === 'swing') {
|
|
666
|
+
rotateEasing = Easing.elastic(1.2);
|
|
667
|
+
} else if (currentEffect === 'wobble') {
|
|
668
|
+
rotateEasing = Easing.elastic(1.5);
|
|
669
|
+
}
|
|
670
|
+
animations.push(createAnimation(rotate, targetRotate, rotateEasing));
|
|
305
671
|
}
|
|
306
672
|
|
|
307
673
|
// TranslateX animation
|
|
308
|
-
if (
|
|
309
|
-
const targetX = toVisible ? 0 :
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
674
|
+
if (currentFlags.hasTranslateX) {
|
|
675
|
+
const targetX = toVisible ? 0 : getInitialTranslateXValue(currentEffect);
|
|
676
|
+
// Special easing for shake and wobble
|
|
677
|
+
let translateXEasing = easingFn;
|
|
678
|
+
if (currentEffect === 'shake') {
|
|
679
|
+
translateXEasing = Easing.elastic(3);
|
|
680
|
+
} else if (currentEffect === 'wobble') {
|
|
681
|
+
translateXEasing = Easing.elastic(1.5);
|
|
682
|
+
}
|
|
683
|
+
animations.push(createAnimation(translateX, targetX, translateXEasing));
|
|
316
684
|
}
|
|
317
685
|
|
|
318
686
|
// TranslateY animation
|
|
319
|
-
if (
|
|
320
|
-
const targetY = toVisible ? 0 :
|
|
321
|
-
animations.push(
|
|
322
|
-
Animated.timing(translateY, {
|
|
323
|
-
toValue: targetY,
|
|
324
|
-
...config,
|
|
325
|
-
})
|
|
326
|
-
);
|
|
687
|
+
if (currentFlags.hasTranslateY) {
|
|
688
|
+
const targetY = toVisible ? 0 : getInitialTranslateYValue(currentEffect);
|
|
689
|
+
animations.push(createAnimation(translateY, targetY));
|
|
327
690
|
}
|
|
328
691
|
|
|
329
|
-
// Height animation for non-skeleton mode
|
|
692
|
+
// Height animation for non-skeleton mode (always use timing)
|
|
330
693
|
if (!skeleton && measuredHeight !== null) {
|
|
331
694
|
const targetHeight = toVisible ? measuredHeight : 0;
|
|
332
695
|
animations.push(
|
|
333
696
|
Animated.timing(animatedHeight, {
|
|
334
697
|
toValue: targetHeight,
|
|
335
|
-
duration:
|
|
698
|
+
duration: currentDuration,
|
|
336
699
|
easing: easingFn,
|
|
337
700
|
useNativeDriver: false,
|
|
338
701
|
})
|
|
@@ -344,11 +707,11 @@ export function PuffPop({
|
|
|
344
707
|
|
|
345
708
|
// Reset values function for looping
|
|
346
709
|
const resetValues = () => {
|
|
347
|
-
opacity.setValue(
|
|
348
|
-
scale.setValue(
|
|
349
|
-
rotate.setValue(
|
|
350
|
-
translateX.setValue(
|
|
351
|
-
translateY.setValue(
|
|
710
|
+
opacity.setValue(getInitialOpacityValue());
|
|
711
|
+
scale.setValue(getInitialScaleValue(effect));
|
|
712
|
+
rotate.setValue(getInitialRotateValue(effect));
|
|
713
|
+
translateX.setValue(getInitialTranslateXValue(effect));
|
|
714
|
+
translateY.setValue(getInitialTranslateYValue(effect));
|
|
352
715
|
if (!skeleton && measuredHeight !== null) {
|
|
353
716
|
animatedHeight.setValue(0);
|
|
354
717
|
}
|
|
@@ -357,9 +720,9 @@ export function PuffPop({
|
|
|
357
720
|
// Build the animation sequence
|
|
358
721
|
let animation: Animated.CompositeAnimation;
|
|
359
722
|
|
|
360
|
-
if (
|
|
723
|
+
if (currentDelay > 0) {
|
|
361
724
|
animation = Animated.sequence([
|
|
362
|
-
Animated.delay(
|
|
725
|
+
Animated.delay(currentDelay),
|
|
363
726
|
parallelAnimation,
|
|
364
727
|
]);
|
|
365
728
|
} else {
|
|
@@ -430,9 +793,14 @@ export function PuffPop({
|
|
|
430
793
|
[
|
|
431
794
|
delay,
|
|
432
795
|
effectiveDuration,
|
|
796
|
+
effectiveExitDuration,
|
|
433
797
|
easing,
|
|
434
798
|
effect,
|
|
435
799
|
effectFlags,
|
|
800
|
+
exitEffect,
|
|
801
|
+
exitEffectFlags,
|
|
802
|
+
exitEasing,
|
|
803
|
+
exitDelay,
|
|
436
804
|
measuredHeight,
|
|
437
805
|
onAnimationComplete,
|
|
438
806
|
onAnimationStart,
|
|
@@ -445,6 +813,8 @@ export function PuffPop({
|
|
|
445
813
|
animatedHeight,
|
|
446
814
|
loop,
|
|
447
815
|
loopDelay,
|
|
816
|
+
useSpring,
|
|
817
|
+
springConfig,
|
|
448
818
|
]
|
|
449
819
|
);
|
|
450
820
|
|
|
@@ -475,12 +845,36 @@ export function PuffPop({
|
|
|
475
845
|
};
|
|
476
846
|
}, []);
|
|
477
847
|
|
|
848
|
+
// Calculate anchor point offset (using 100px as base size for skeleton mode)
|
|
849
|
+
const anchorOffset = useMemo(() => {
|
|
850
|
+
const offset = getAnchorPointOffset(anchorPoint);
|
|
851
|
+
// Use measured height if available, otherwise use 100px as base
|
|
852
|
+
const baseSize = measuredHeight ?? 100;
|
|
853
|
+
return {
|
|
854
|
+
x: offset.x * baseSize,
|
|
855
|
+
y: offset.y * baseSize,
|
|
856
|
+
};
|
|
857
|
+
}, [anchorPoint, measuredHeight]);
|
|
858
|
+
|
|
478
859
|
// Memoize transform array to avoid recreating on every render
|
|
479
860
|
// IMPORTANT: All hooks must be called before any conditional returns
|
|
480
861
|
const transform = useMemo(() => {
|
|
481
862
|
const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
|
|
482
|
-
|
|
863
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
864
|
+
const transforms: any[] = [];
|
|
865
|
+
const needsAnchorOffset = anchorPoint !== 'center' && (hasScale || hasRotate || hasFlip);
|
|
866
|
+
|
|
867
|
+
// Step 1: Move to anchor point (negative offset)
|
|
868
|
+
if (needsAnchorOffset) {
|
|
869
|
+
if (anchorOffset.x !== 0) {
|
|
870
|
+
transforms.push({ translateX: -anchorOffset.x });
|
|
871
|
+
}
|
|
872
|
+
if (anchorOffset.y !== 0) {
|
|
873
|
+
transforms.push({ translateY: -anchorOffset.y });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
483
876
|
|
|
877
|
+
// Step 2: Apply scale/rotate transforms
|
|
484
878
|
if (hasScale) {
|
|
485
879
|
transforms.push({ scale });
|
|
486
880
|
}
|
|
@@ -493,6 +887,17 @@ export function PuffPop({
|
|
|
493
887
|
transforms.push({ rotateY: flipInterpolation });
|
|
494
888
|
}
|
|
495
889
|
|
|
890
|
+
// Step 3: Move back from anchor point (positive offset)
|
|
891
|
+
if (needsAnchorOffset) {
|
|
892
|
+
if (anchorOffset.x !== 0) {
|
|
893
|
+
transforms.push({ translateX: anchorOffset.x });
|
|
894
|
+
}
|
|
895
|
+
if (anchorOffset.y !== 0) {
|
|
896
|
+
transforms.push({ translateY: anchorOffset.y });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Step 4: Apply other translate transforms
|
|
496
901
|
if (hasTranslateX) {
|
|
497
902
|
transforms.push({ translateX });
|
|
498
903
|
}
|
|
@@ -502,7 +907,7 @@ export function PuffPop({
|
|
|
502
907
|
}
|
|
503
908
|
|
|
504
909
|
return transforms.length > 0 ? transforms : undefined;
|
|
505
|
-
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
|
|
910
|
+
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY, anchorPoint, anchorOffset]);
|
|
506
911
|
|
|
507
912
|
// Memoize animated style
|
|
508
913
|
const animatedStyle = useMemo(() => ({
|
|
@@ -540,13 +945,18 @@ export function PuffPop({
|
|
|
540
945
|
);
|
|
541
946
|
}
|
|
542
947
|
|
|
948
|
+
// Memoize PuffPop to prevent unnecessary re-renders
|
|
949
|
+
export const PuffPop = memo(PuffPopComponent, arePuffPopPropsEqual);
|
|
950
|
+
|
|
543
951
|
/**
|
|
544
952
|
* Get initial scale value based on effect
|
|
545
953
|
*/
|
|
546
|
-
function getInitialScale(effect: PuffPopEffect): number {
|
|
954
|
+
function getInitialScale(effect: PuffPopEffect, _reverse = false): number {
|
|
955
|
+
// Scale doesn't change with reverse (parameter kept for consistent API)
|
|
547
956
|
switch (effect) {
|
|
548
957
|
case 'scale':
|
|
549
958
|
case 'rotateScale':
|
|
959
|
+
case 'elastic':
|
|
550
960
|
return 0;
|
|
551
961
|
case 'bounce':
|
|
552
962
|
return 0.3;
|
|
@@ -554,6 +964,8 @@ function getInitialScale(effect: PuffPopEffect): number {
|
|
|
554
964
|
return 0.5;
|
|
555
965
|
case 'flip':
|
|
556
966
|
return 0.8;
|
|
967
|
+
case 'pulse':
|
|
968
|
+
return 0.6;
|
|
557
969
|
default:
|
|
558
970
|
return 1;
|
|
559
971
|
}
|
|
@@ -562,14 +974,19 @@ function getInitialScale(effect: PuffPopEffect): number {
|
|
|
562
974
|
/**
|
|
563
975
|
* Get initial rotate value based on effect
|
|
564
976
|
*/
|
|
565
|
-
function getInitialRotate(effect: PuffPopEffect): number {
|
|
977
|
+
function getInitialRotate(effect: PuffPopEffect, reverse = false): number {
|
|
978
|
+
const multiplier = reverse ? -1 : 1;
|
|
566
979
|
switch (effect) {
|
|
567
980
|
case 'rotate':
|
|
568
|
-
return -360;
|
|
981
|
+
return -360 * multiplier;
|
|
569
982
|
case 'rotateScale':
|
|
570
|
-
return -180;
|
|
983
|
+
return -180 * multiplier;
|
|
571
984
|
case 'flip':
|
|
572
|
-
return -180;
|
|
985
|
+
return -180 * multiplier;
|
|
986
|
+
case 'swing':
|
|
987
|
+
return -15 * multiplier;
|
|
988
|
+
case 'wobble':
|
|
989
|
+
return -5 * multiplier;
|
|
573
990
|
default:
|
|
574
991
|
return 0;
|
|
575
992
|
}
|
|
@@ -578,12 +995,17 @@ function getInitialRotate(effect: PuffPopEffect): number {
|
|
|
578
995
|
/**
|
|
579
996
|
* Get initial translateX value based on effect
|
|
580
997
|
*/
|
|
581
|
-
function getInitialTranslateX(effect: PuffPopEffect): number {
|
|
998
|
+
function getInitialTranslateX(effect: PuffPopEffect, reverse = false): number {
|
|
999
|
+
const multiplier = reverse ? -1 : 1;
|
|
582
1000
|
switch (effect) {
|
|
583
1001
|
case 'slideLeft':
|
|
584
|
-
return 100;
|
|
1002
|
+
return 100 * multiplier;
|
|
585
1003
|
case 'slideRight':
|
|
586
|
-
return -100;
|
|
1004
|
+
return -100 * multiplier;
|
|
1005
|
+
case 'shake':
|
|
1006
|
+
return -10 * multiplier;
|
|
1007
|
+
case 'wobble':
|
|
1008
|
+
return -25 * multiplier;
|
|
587
1009
|
default:
|
|
588
1010
|
return 0;
|
|
589
1011
|
}
|
|
@@ -592,14 +1014,15 @@ function getInitialTranslateX(effect: PuffPopEffect): number {
|
|
|
592
1014
|
/**
|
|
593
1015
|
* Get initial translateY value based on effect
|
|
594
1016
|
*/
|
|
595
|
-
function getInitialTranslateY(effect: PuffPopEffect): number {
|
|
1017
|
+
function getInitialTranslateY(effect: PuffPopEffect, reverse = false): number {
|
|
1018
|
+
const multiplier = reverse ? -1 : 1;
|
|
596
1019
|
switch (effect) {
|
|
597
1020
|
case 'slideUp':
|
|
598
|
-
return 50;
|
|
1021
|
+
return 50 * multiplier;
|
|
599
1022
|
case 'slideDown':
|
|
600
|
-
return -50;
|
|
1023
|
+
return -50 * multiplier;
|
|
601
1024
|
case 'bounce':
|
|
602
|
-
return 30;
|
|
1025
|
+
return 30 * multiplier;
|
|
603
1026
|
default:
|
|
604
1027
|
return 0;
|
|
605
1028
|
}
|
|
@@ -721,6 +1144,191 @@ export interface PuffPopGroupProps {
|
|
|
721
1144
|
* Gap between children (uses flexbox gap)
|
|
722
1145
|
*/
|
|
723
1146
|
gap?: number;
|
|
1147
|
+
|
|
1148
|
+
// ============ Custom Initial Values ============
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Custom initial opacity value (0-1) for all children
|
|
1152
|
+
*/
|
|
1153
|
+
initialOpacity?: number;
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Custom initial scale value for all children
|
|
1157
|
+
*/
|
|
1158
|
+
initialScale?: number;
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Custom initial rotation value in degrees for all children
|
|
1162
|
+
*/
|
|
1163
|
+
initialRotate?: number;
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Custom initial translateX value in pixels for all children
|
|
1167
|
+
*/
|
|
1168
|
+
initialTranslateX?: number;
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Custom initial translateY value in pixels for all children
|
|
1172
|
+
*/
|
|
1173
|
+
initialTranslateY?: number;
|
|
1174
|
+
|
|
1175
|
+
// ============ Reverse Mode ============
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* If true, reverses the animation direction for all children
|
|
1179
|
+
* @default false
|
|
1180
|
+
*/
|
|
1181
|
+
reverse?: boolean;
|
|
1182
|
+
|
|
1183
|
+
// ============ Animation Intensity ============
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Animation intensity multiplier (0-1) for all children
|
|
1187
|
+
* Controls how far/much elements move during animation
|
|
1188
|
+
* @default 1
|
|
1189
|
+
*/
|
|
1190
|
+
intensity?: number;
|
|
1191
|
+
|
|
1192
|
+
// ============ Anchor Point ============
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Anchor point for scale/rotate transformations for all children
|
|
1196
|
+
* @default 'center'
|
|
1197
|
+
*/
|
|
1198
|
+
anchorPoint?: PuffPopAnchorPoint;
|
|
1199
|
+
|
|
1200
|
+
// ============ Spring Animation ============
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Use spring physics-based animation for all children
|
|
1204
|
+
* @default false
|
|
1205
|
+
*/
|
|
1206
|
+
useSpring?: boolean;
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Spring animation configuration for all children
|
|
1210
|
+
*/
|
|
1211
|
+
springConfig?: PuffPopSpringConfig;
|
|
1212
|
+
|
|
1213
|
+
// ============ Exit Animation Settings ============
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Animation effect type when hiding (exit animation) for all children
|
|
1217
|
+
* If not specified, uses the reverse of the enter effect
|
|
1218
|
+
*/
|
|
1219
|
+
exitEffect?: PuffPopEffect;
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Animation duration for exit animation in milliseconds for all children
|
|
1223
|
+
* If not specified, uses the same duration as enter animation
|
|
1224
|
+
*/
|
|
1225
|
+
exitDuration?: number;
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Easing function for exit animation for all children
|
|
1229
|
+
* If not specified, uses the same easing as enter animation
|
|
1230
|
+
*/
|
|
1231
|
+
exitEasing?: PuffPopEasing;
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Delay before exit animation starts in milliseconds
|
|
1235
|
+
* @default 0
|
|
1236
|
+
*/
|
|
1237
|
+
exitDelay?: number;
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Delay between each child's exit animation start in milliseconds
|
|
1241
|
+
* If not specified, all children exit simultaneously
|
|
1242
|
+
* @default 0
|
|
1243
|
+
*/
|
|
1244
|
+
exitStaggerDelay?: number;
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Direction of exit stagger animation
|
|
1248
|
+
* - 'forward': First to last child
|
|
1249
|
+
* - 'reverse': Last to first child (most natural for exit)
|
|
1250
|
+
* - 'center': From center outward
|
|
1251
|
+
* - 'edges': From edges toward center
|
|
1252
|
+
* @default 'reverse'
|
|
1253
|
+
*/
|
|
1254
|
+
exitStaggerDirection?: 'forward' | 'reverse' | 'center' | 'edges';
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Props comparison function for PuffPopGroup memoization
|
|
1259
|
+
* Performs shallow comparison of props to prevent unnecessary re-renders
|
|
1260
|
+
*/
|
|
1261
|
+
function arePuffPopGroupPropsEqual(
|
|
1262
|
+
prevProps: PuffPopGroupProps,
|
|
1263
|
+
nextProps: PuffPopGroupProps
|
|
1264
|
+
): boolean {
|
|
1265
|
+
// Compare primitive props
|
|
1266
|
+
if (
|
|
1267
|
+
prevProps.effect !== nextProps.effect ||
|
|
1268
|
+
prevProps.duration !== nextProps.duration ||
|
|
1269
|
+
prevProps.staggerDelay !== nextProps.staggerDelay ||
|
|
1270
|
+
prevProps.initialDelay !== nextProps.initialDelay ||
|
|
1271
|
+
prevProps.easing !== nextProps.easing ||
|
|
1272
|
+
prevProps.skeleton !== nextProps.skeleton ||
|
|
1273
|
+
prevProps.visible !== nextProps.visible ||
|
|
1274
|
+
prevProps.animateOnMount !== nextProps.animateOnMount ||
|
|
1275
|
+
prevProps.respectReduceMotion !== nextProps.respectReduceMotion ||
|
|
1276
|
+
prevProps.testID !== nextProps.testID ||
|
|
1277
|
+
prevProps.staggerDirection !== nextProps.staggerDirection ||
|
|
1278
|
+
prevProps.horizontal !== nextProps.horizontal ||
|
|
1279
|
+
prevProps.gap !== nextProps.gap ||
|
|
1280
|
+
prevProps.reverse !== nextProps.reverse ||
|
|
1281
|
+
prevProps.intensity !== nextProps.intensity ||
|
|
1282
|
+
prevProps.anchorPoint !== nextProps.anchorPoint ||
|
|
1283
|
+
prevProps.useSpring !== nextProps.useSpring ||
|
|
1284
|
+
prevProps.exitEffect !== nextProps.exitEffect ||
|
|
1285
|
+
prevProps.exitDuration !== nextProps.exitDuration ||
|
|
1286
|
+
prevProps.exitEasing !== nextProps.exitEasing ||
|
|
1287
|
+
prevProps.exitDelay !== nextProps.exitDelay ||
|
|
1288
|
+
prevProps.exitStaggerDelay !== nextProps.exitStaggerDelay ||
|
|
1289
|
+
prevProps.exitStaggerDirection !== nextProps.exitStaggerDirection ||
|
|
1290
|
+
prevProps.initialOpacity !== nextProps.initialOpacity ||
|
|
1291
|
+
prevProps.initialScale !== nextProps.initialScale ||
|
|
1292
|
+
prevProps.initialRotate !== nextProps.initialRotate ||
|
|
1293
|
+
prevProps.initialTranslateX !== nextProps.initialTranslateX ||
|
|
1294
|
+
prevProps.initialTranslateY !== nextProps.initialTranslateY
|
|
1295
|
+
) {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Compare springConfig object (shallow)
|
|
1300
|
+
if (prevProps.springConfig !== nextProps.springConfig) {
|
|
1301
|
+
if (
|
|
1302
|
+
!prevProps.springConfig ||
|
|
1303
|
+
!nextProps.springConfig ||
|
|
1304
|
+
prevProps.springConfig.tension !== nextProps.springConfig.tension ||
|
|
1305
|
+
prevProps.springConfig.friction !== nextProps.springConfig.friction ||
|
|
1306
|
+
prevProps.springConfig.speed !== nextProps.springConfig.speed ||
|
|
1307
|
+
prevProps.springConfig.bounciness !== nextProps.springConfig.bounciness
|
|
1308
|
+
) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Compare callbacks
|
|
1314
|
+
if (
|
|
1315
|
+
prevProps.onAnimationStart !== nextProps.onAnimationStart ||
|
|
1316
|
+
prevProps.onAnimationComplete !== nextProps.onAnimationComplete
|
|
1317
|
+
) {
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Style comparison
|
|
1322
|
+
if (prevProps.style !== nextProps.style) {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Children comparison - if children change, re-render
|
|
1327
|
+
if (prevProps.children !== nextProps.children) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return true;
|
|
724
1332
|
}
|
|
725
1333
|
|
|
726
1334
|
/**
|
|
@@ -735,7 +1343,7 @@ export interface PuffPopGroupProps {
|
|
|
735
1343
|
* </PuffPopGroup>
|
|
736
1344
|
* ```
|
|
737
1345
|
*/
|
|
738
|
-
|
|
1346
|
+
function PuffPopGroupComponent({
|
|
739
1347
|
children,
|
|
740
1348
|
effect = 'scale',
|
|
741
1349
|
duration = 400,
|
|
@@ -753,6 +1361,28 @@ export function PuffPopGroup({
|
|
|
753
1361
|
staggerDirection = 'forward',
|
|
754
1362
|
horizontal = false,
|
|
755
1363
|
gap,
|
|
1364
|
+
// Custom initial values
|
|
1365
|
+
initialOpacity,
|
|
1366
|
+
initialScale,
|
|
1367
|
+
initialRotate,
|
|
1368
|
+
initialTranslateX,
|
|
1369
|
+
initialTranslateY,
|
|
1370
|
+
// Reverse mode
|
|
1371
|
+
reverse,
|
|
1372
|
+
// Animation intensity
|
|
1373
|
+
intensity,
|
|
1374
|
+
// Anchor point
|
|
1375
|
+
anchorPoint,
|
|
1376
|
+
// Spring animation
|
|
1377
|
+
useSpring,
|
|
1378
|
+
springConfig,
|
|
1379
|
+
// Exit animation settings
|
|
1380
|
+
exitEffect,
|
|
1381
|
+
exitDuration,
|
|
1382
|
+
exitEasing,
|
|
1383
|
+
exitDelay,
|
|
1384
|
+
exitStaggerDelay = 0,
|
|
1385
|
+
exitStaggerDirection = 'reverse',
|
|
756
1386
|
}: PuffPopGroupProps): ReactElement {
|
|
757
1387
|
const childArray = Children.toArray(children);
|
|
758
1388
|
const childCount = childArray.length;
|
|
@@ -789,6 +1419,41 @@ export function PuffPopGroup({
|
|
|
789
1419
|
[childCount, initialDelay, staggerDelay, staggerDirection]
|
|
790
1420
|
);
|
|
791
1421
|
|
|
1422
|
+
// Calculate exit delay for each child based on exit stagger direction
|
|
1423
|
+
const getChildExitDelay = useCallback(
|
|
1424
|
+
(index: number): number => {
|
|
1425
|
+
if (exitStaggerDelay === 0) {
|
|
1426
|
+
return exitDelay ?? 0;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
let delayIndex: number;
|
|
1430
|
+
|
|
1431
|
+
switch (exitStaggerDirection) {
|
|
1432
|
+
case 'forward':
|
|
1433
|
+
delayIndex = index;
|
|
1434
|
+
break;
|
|
1435
|
+
case 'center': {
|
|
1436
|
+
const center = (childCount - 1) / 2;
|
|
1437
|
+
delayIndex = Math.abs(index - center);
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
case 'edges': {
|
|
1441
|
+
const center = (childCount - 1) / 2;
|
|
1442
|
+
delayIndex = center - Math.abs(index - center);
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
case 'reverse':
|
|
1446
|
+
default:
|
|
1447
|
+
// Reverse is default for exit (last in, first out)
|
|
1448
|
+
delayIndex = childCount - 1 - index;
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
return (exitDelay ?? 0) + delayIndex * exitStaggerDelay;
|
|
1453
|
+
},
|
|
1454
|
+
[childCount, exitDelay, exitStaggerDelay, exitStaggerDirection]
|
|
1455
|
+
);
|
|
1456
|
+
|
|
792
1457
|
// Handle individual child animation complete
|
|
793
1458
|
const handleChildComplete = useCallback(() => {
|
|
794
1459
|
completedCount.current += 1;
|
|
@@ -838,6 +1503,20 @@ export function PuffPopGroup({
|
|
|
838
1503
|
onAnimationComplete={handleChildComplete}
|
|
839
1504
|
onAnimationStart={index === 0 ? handleChildStart : undefined}
|
|
840
1505
|
respectReduceMotion={respectReduceMotion}
|
|
1506
|
+
initialOpacity={initialOpacity}
|
|
1507
|
+
initialScale={initialScale}
|
|
1508
|
+
initialRotate={initialRotate}
|
|
1509
|
+
initialTranslateX={initialTranslateX}
|
|
1510
|
+
initialTranslateY={initialTranslateY}
|
|
1511
|
+
reverse={reverse}
|
|
1512
|
+
intensity={intensity}
|
|
1513
|
+
anchorPoint={anchorPoint}
|
|
1514
|
+
useSpring={useSpring}
|
|
1515
|
+
springConfig={springConfig}
|
|
1516
|
+
exitEffect={exitEffect}
|
|
1517
|
+
exitDuration={exitDuration}
|
|
1518
|
+
exitEasing={exitEasing}
|
|
1519
|
+
exitDelay={getChildExitDelay(index)}
|
|
841
1520
|
>
|
|
842
1521
|
{child}
|
|
843
1522
|
</PuffPop>
|
|
@@ -846,5 +1525,8 @@ export function PuffPopGroup({
|
|
|
846
1525
|
);
|
|
847
1526
|
}
|
|
848
1527
|
|
|
1528
|
+
// Memoize PuffPopGroup to prevent unnecessary re-renders
|
|
1529
|
+
export const PuffPopGroup = memo(PuffPopGroupComponent, arePuffPopGroupPropsEqual);
|
|
1530
|
+
|
|
849
1531
|
export default PuffPop;
|
|
850
1532
|
|