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/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
|
|
@@ -216,6 +267,33 @@ export interface PuffPopProps {
|
|
|
216
267
|
* @default 1
|
|
217
268
|
*/
|
|
218
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;
|
|
219
297
|
}
|
|
220
298
|
|
|
221
299
|
/**
|
|
@@ -240,10 +318,133 @@ function getEasing(type: PuffPopEasing): (value: number) => number {
|
|
|
240
318
|
}
|
|
241
319
|
}
|
|
242
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
|
+
|
|
243
444
|
/**
|
|
244
445
|
* PuffPop - Animate children with beautiful entrance effects
|
|
245
446
|
*/
|
|
246
|
-
|
|
447
|
+
function PuffPopComponent({
|
|
247
448
|
children,
|
|
248
449
|
effect = 'scale',
|
|
249
450
|
duration = 400,
|
|
@@ -274,6 +475,11 @@ export function PuffPop({
|
|
|
274
475
|
reverse = false,
|
|
275
476
|
// Animation intensity
|
|
276
477
|
intensity = 1,
|
|
478
|
+
// Anchor point
|
|
479
|
+
anchorPoint = 'center',
|
|
480
|
+
// Spring animation
|
|
481
|
+
useSpring = false,
|
|
482
|
+
springConfig,
|
|
277
483
|
}: PuffPopProps): ReactElement {
|
|
278
484
|
// Clamp intensity between 0 and 1
|
|
279
485
|
const clampedIntensity = Math.max(0, Math.min(1, intensity));
|
|
@@ -340,23 +546,13 @@ export function PuffPop({
|
|
|
340
546
|
const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
|
|
341
547
|
const effectiveExitDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : (exitDuration ?? duration);
|
|
342
548
|
|
|
343
|
-
// Helper to get effect flags for any effect type
|
|
344
|
-
const getEffectFlags = useCallback((eff: PuffPopEffect) => ({
|
|
345
|
-
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(eff),
|
|
346
|
-
hasRotate: ['rotate', 'rotateScale'].includes(eff),
|
|
347
|
-
hasFlip: eff === 'flip',
|
|
348
|
-
hasTranslateX: ['slideLeft', 'slideRight'].includes(eff),
|
|
349
|
-
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(eff),
|
|
350
|
-
hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(eff),
|
|
351
|
-
}), []);
|
|
352
|
-
|
|
353
549
|
// Memoize effect type checks to avoid repeated includes() calls
|
|
354
|
-
const effectFlags = useMemo(() => getEffectFlags(effect), [effect
|
|
550
|
+
const effectFlags = useMemo(() => getEffectFlags(effect), [effect]);
|
|
355
551
|
|
|
356
552
|
// Exit effect flags (use exitEffect if specified, otherwise same as enter effect)
|
|
357
553
|
const exitEffectFlags = useMemo(() =>
|
|
358
554
|
exitEffect ? getEffectFlags(exitEffect) : effectFlags
|
|
359
|
-
, [exitEffect,
|
|
555
|
+
, [exitEffect, effectFlags]);
|
|
360
556
|
|
|
361
557
|
// Memoize interpolations to avoid recreating on every render
|
|
362
558
|
const rotateInterpolation = useMemo(() =>
|
|
@@ -401,68 +597,99 @@ export function PuffPop({
|
|
|
401
597
|
// When skeleton is false, we animate height which doesn't support native driver
|
|
402
598
|
// So we must use JS driver for all animations in that case
|
|
403
599
|
const useNative = skeleton;
|
|
404
|
-
|
|
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 = {
|
|
405
611
|
duration: currentDuration,
|
|
406
612
|
easing: easingFn,
|
|
407
613
|
useNativeDriver: useNative,
|
|
408
614
|
};
|
|
409
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
|
+
|
|
410
635
|
const animations: Animated.CompositeAnimation[] = [];
|
|
411
636
|
|
|
412
|
-
// Opacity animation
|
|
637
|
+
// Opacity animation (always use timing for opacity for smoother fade)
|
|
413
638
|
animations.push(
|
|
414
639
|
Animated.timing(opacity, {
|
|
415
640
|
toValue: toVisible ? 1 : 0,
|
|
416
|
-
...
|
|
641
|
+
...timingConfig,
|
|
417
642
|
})
|
|
418
643
|
);
|
|
419
644
|
|
|
420
645
|
// Scale animation
|
|
421
646
|
if (currentFlags.hasScale) {
|
|
422
647
|
const targetScale = toVisible ? 1 : getInitialScaleValue(currentEffect);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
)
|
|
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));
|
|
430
658
|
}
|
|
431
659
|
|
|
432
660
|
// Rotate animation
|
|
433
661
|
if (currentFlags.hasRotate || currentFlags.hasFlip) {
|
|
434
662
|
const targetRotate = toVisible ? 0 : getInitialRotateValue(currentEffect);
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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));
|
|
441
671
|
}
|
|
442
672
|
|
|
443
673
|
// TranslateX animation
|
|
444
674
|
if (currentFlags.hasTranslateX) {
|
|
445
675
|
const targetX = toVisible ? 0 : getInitialTranslateXValue(currentEffect);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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));
|
|
452
684
|
}
|
|
453
685
|
|
|
454
686
|
// TranslateY animation
|
|
455
687
|
if (currentFlags.hasTranslateY) {
|
|
456
688
|
const targetY = toVisible ? 0 : getInitialTranslateYValue(currentEffect);
|
|
457
|
-
animations.push(
|
|
458
|
-
Animated.timing(translateY, {
|
|
459
|
-
toValue: targetY,
|
|
460
|
-
...config,
|
|
461
|
-
})
|
|
462
|
-
);
|
|
689
|
+
animations.push(createAnimation(translateY, targetY));
|
|
463
690
|
}
|
|
464
691
|
|
|
465
|
-
// Height animation for non-skeleton mode
|
|
692
|
+
// Height animation for non-skeleton mode (always use timing)
|
|
466
693
|
if (!skeleton && measuredHeight !== null) {
|
|
467
694
|
const targetHeight = toVisible ? measuredHeight : 0;
|
|
468
695
|
animations.push(
|
|
@@ -586,6 +813,8 @@ export function PuffPop({
|
|
|
586
813
|
animatedHeight,
|
|
587
814
|
loop,
|
|
588
815
|
loopDelay,
|
|
816
|
+
useSpring,
|
|
817
|
+
springConfig,
|
|
589
818
|
]
|
|
590
819
|
);
|
|
591
820
|
|
|
@@ -616,12 +845,36 @@ export function PuffPop({
|
|
|
616
845
|
};
|
|
617
846
|
}, []);
|
|
618
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
|
+
|
|
619
859
|
// Memoize transform array to avoid recreating on every render
|
|
620
860
|
// IMPORTANT: All hooks must be called before any conditional returns
|
|
621
861
|
const transform = useMemo(() => {
|
|
622
862
|
const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
|
|
623
|
-
|
|
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
|
+
}
|
|
624
876
|
|
|
877
|
+
// Step 2: Apply scale/rotate transforms
|
|
625
878
|
if (hasScale) {
|
|
626
879
|
transforms.push({ scale });
|
|
627
880
|
}
|
|
@@ -634,6 +887,17 @@ export function PuffPop({
|
|
|
634
887
|
transforms.push({ rotateY: flipInterpolation });
|
|
635
888
|
}
|
|
636
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
|
|
637
901
|
if (hasTranslateX) {
|
|
638
902
|
transforms.push({ translateX });
|
|
639
903
|
}
|
|
@@ -643,7 +907,7 @@ export function PuffPop({
|
|
|
643
907
|
}
|
|
644
908
|
|
|
645
909
|
return transforms.length > 0 ? transforms : undefined;
|
|
646
|
-
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
|
|
910
|
+
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY, anchorPoint, anchorOffset]);
|
|
647
911
|
|
|
648
912
|
// Memoize animated style
|
|
649
913
|
const animatedStyle = useMemo(() => ({
|
|
@@ -681,6 +945,9 @@ export function PuffPop({
|
|
|
681
945
|
);
|
|
682
946
|
}
|
|
683
947
|
|
|
948
|
+
// Memoize PuffPop to prevent unnecessary re-renders
|
|
949
|
+
export const PuffPop = memo(PuffPopComponent, arePuffPopPropsEqual);
|
|
950
|
+
|
|
684
951
|
/**
|
|
685
952
|
* Get initial scale value based on effect
|
|
686
953
|
*/
|
|
@@ -689,6 +956,7 @@ function getInitialScale(effect: PuffPopEffect, _reverse = false): number {
|
|
|
689
956
|
switch (effect) {
|
|
690
957
|
case 'scale':
|
|
691
958
|
case 'rotateScale':
|
|
959
|
+
case 'elastic':
|
|
692
960
|
return 0;
|
|
693
961
|
case 'bounce':
|
|
694
962
|
return 0.3;
|
|
@@ -696,6 +964,8 @@ function getInitialScale(effect: PuffPopEffect, _reverse = false): number {
|
|
|
696
964
|
return 0.5;
|
|
697
965
|
case 'flip':
|
|
698
966
|
return 0.8;
|
|
967
|
+
case 'pulse':
|
|
968
|
+
return 0.6;
|
|
699
969
|
default:
|
|
700
970
|
return 1;
|
|
701
971
|
}
|
|
@@ -713,6 +983,10 @@ function getInitialRotate(effect: PuffPopEffect, reverse = false): number {
|
|
|
713
983
|
return -180 * multiplier;
|
|
714
984
|
case 'flip':
|
|
715
985
|
return -180 * multiplier;
|
|
986
|
+
case 'swing':
|
|
987
|
+
return -15 * multiplier;
|
|
988
|
+
case 'wobble':
|
|
989
|
+
return -5 * multiplier;
|
|
716
990
|
default:
|
|
717
991
|
return 0;
|
|
718
992
|
}
|
|
@@ -728,6 +1002,10 @@ function getInitialTranslateX(effect: PuffPopEffect, reverse = false): number {
|
|
|
728
1002
|
return 100 * multiplier;
|
|
729
1003
|
case 'slideRight':
|
|
730
1004
|
return -100 * multiplier;
|
|
1005
|
+
case 'shake':
|
|
1006
|
+
return -10 * multiplier;
|
|
1007
|
+
case 'wobble':
|
|
1008
|
+
return -25 * multiplier;
|
|
731
1009
|
default:
|
|
732
1010
|
return 0;
|
|
733
1011
|
}
|
|
@@ -911,6 +1189,27 @@ export interface PuffPopGroupProps {
|
|
|
911
1189
|
*/
|
|
912
1190
|
intensity?: number;
|
|
913
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
|
+
|
|
914
1213
|
// ============ Exit Animation Settings ============
|
|
915
1214
|
|
|
916
1215
|
/**
|
|
@@ -936,6 +1235,100 @@ export interface PuffPopGroupProps {
|
|
|
936
1235
|
* @default 0
|
|
937
1236
|
*/
|
|
938
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;
|
|
939
1332
|
}
|
|
940
1333
|
|
|
941
1334
|
/**
|
|
@@ -950,7 +1343,7 @@ export interface PuffPopGroupProps {
|
|
|
950
1343
|
* </PuffPopGroup>
|
|
951
1344
|
* ```
|
|
952
1345
|
*/
|
|
953
|
-
|
|
1346
|
+
function PuffPopGroupComponent({
|
|
954
1347
|
children,
|
|
955
1348
|
effect = 'scale',
|
|
956
1349
|
duration = 400,
|
|
@@ -978,11 +1371,18 @@ export function PuffPopGroup({
|
|
|
978
1371
|
reverse,
|
|
979
1372
|
// Animation intensity
|
|
980
1373
|
intensity,
|
|
1374
|
+
// Anchor point
|
|
1375
|
+
anchorPoint,
|
|
1376
|
+
// Spring animation
|
|
1377
|
+
useSpring,
|
|
1378
|
+
springConfig,
|
|
981
1379
|
// Exit animation settings
|
|
982
1380
|
exitEffect,
|
|
983
1381
|
exitDuration,
|
|
984
1382
|
exitEasing,
|
|
985
1383
|
exitDelay,
|
|
1384
|
+
exitStaggerDelay = 0,
|
|
1385
|
+
exitStaggerDirection = 'reverse',
|
|
986
1386
|
}: PuffPopGroupProps): ReactElement {
|
|
987
1387
|
const childArray = Children.toArray(children);
|
|
988
1388
|
const childCount = childArray.length;
|
|
@@ -1019,6 +1419,41 @@ export function PuffPopGroup({
|
|
|
1019
1419
|
[childCount, initialDelay, staggerDelay, staggerDirection]
|
|
1020
1420
|
);
|
|
1021
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
|
+
|
|
1022
1457
|
// Handle individual child animation complete
|
|
1023
1458
|
const handleChildComplete = useCallback(() => {
|
|
1024
1459
|
completedCount.current += 1;
|
|
@@ -1075,10 +1510,13 @@ export function PuffPopGroup({
|
|
|
1075
1510
|
initialTranslateY={initialTranslateY}
|
|
1076
1511
|
reverse={reverse}
|
|
1077
1512
|
intensity={intensity}
|
|
1513
|
+
anchorPoint={anchorPoint}
|
|
1514
|
+
useSpring={useSpring}
|
|
1515
|
+
springConfig={springConfig}
|
|
1078
1516
|
exitEffect={exitEffect}
|
|
1079
1517
|
exitDuration={exitDuration}
|
|
1080
1518
|
exitEasing={exitEasing}
|
|
1081
|
-
exitDelay={
|
|
1519
|
+
exitDelay={getChildExitDelay(index)}
|
|
1082
1520
|
>
|
|
1083
1521
|
{child}
|
|
1084
1522
|
</PuffPop>
|
|
@@ -1087,5 +1525,8 @@ export function PuffPopGroup({
|
|
|
1087
1525
|
);
|
|
1088
1526
|
}
|
|
1089
1527
|
|
|
1528
|
+
// Memoize PuffPopGroup to prevent unnecessary re-renders
|
|
1529
|
+
export const PuffPopGroup = memo(PuffPopGroupComponent, arePuffPopGroupPropsEqual);
|
|
1530
|
+
|
|
1090
1531
|
export default PuffPop;
|
|
1091
1532
|
|