1- import { useEffect , useRef } from 'react' ;
2- import { View , StyleSheet , Animated , Dimensions } from 'react-native' ;
1+ import { useEffect , useState , useRef } from 'react' ;
2+ import { StyleSheet , Animated , Dimensions } from 'react-native' ;
33
44interface ConfettiCelebrationProps {
55 visible : boolean ;
@@ -10,136 +10,137 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
1010const CONFETTI_COUNT = 50 ;
1111const COLORS = [ '#FFD700' , '#FF6B6B' , '#4ECDC4' , '#45B7D1' , '#FFA07A' , '#98D8C8' , '#F7DC6F' ] ;
1212
13- export default function ConfettiCelebration ( { visible, onComplete } : ConfettiCelebrationProps ) {
14- const confettiRefs = useRef < Animated . Value [ ] > ( [ ] ) ;
15- const opacityRef = useRef ( new Animated . Value ( 0 ) ) ;
13+ interface ConfettiPiece {
14+ id : number ;
15+ translateY : Animated . Value ;
16+ translateX : Animated . Value ;
17+ rotation : Animated . Value ;
18+ scale : Animated . Value ;
19+ color : string ;
20+ size : number ;
21+ initialX : number ; // Track initial X position (avoid accessing Animated._value)
22+ }
23+
24+ function createConfettiPieces ( ) : ConfettiPiece [ ] {
25+ return Array . from ( { length : CONFETTI_COUNT } , ( _ , index ) => {
26+ const initialX = Math . random ( ) * SCREEN_WIDTH ;
27+ return {
28+ id : index ,
29+ translateY : new Animated . Value ( - 50 - Math . random ( ) * 100 ) ,
30+ translateX : new Animated . Value ( initialX ) ,
31+ rotation : new Animated . Value ( 0 ) ,
32+ scale : new Animated . Value ( 0.8 + Math . random ( ) * 0.4 ) ,
33+ color : COLORS [ index % COLORS . length ] ,
34+ size : 8 + Math . random ( ) * 8 ,
35+ initialX,
36+ } ;
37+ } ) ;
38+ }
39+
40+ export default function ConfettiCelebration ( { visible } : ConfettiCelebrationProps ) {
41+ const [ confetti , setConfetti ] = useState < ConfettiPiece [ ] > ( [ ] ) ;
42+ const isAnimatingRef = useRef ( false ) ;
43+ const animationRef = useRef < Animated . CompositeAnimation | null > ( null ) ;
1644
1745 useEffect ( ( ) => {
1846 if ( visible ) {
19- // Initialize confetti pieces
20- confettiRefs . current = Array . from ( { length : CONFETTI_COUNT } , ( ) => {
21- const startX = Math . random ( ) * SCREEN_WIDTH ;
22- return {
23- translateY : new Animated . Value ( - 50 ) ,
24- translateX : new Animated . Value ( startX ) ,
25- rotation : new Animated . Value ( 0 ) ,
26- scale : new Animated . Value ( 1 ) ,
27- startX, // Store for reference
28- } ;
29- } ) ;
47+ // Create confetti immediately so it renders
48+ const pieces = createConfettiPieces ( ) ;
49+ setConfetti ( pieces ) ;
50+ isAnimatingRef . current = true ;
51+
52+ // Start animation after a brief delay to ensure render
53+ const startAnimation = ( ) => {
54+ if ( ! isAnimatingRef . current ) return ;
55+
56+ const animations = pieces . map ( ( piece ) => {
57+ // Reset positions for loop - update initialX for each cycle
58+ const newInitialX = Math . random ( ) * SCREEN_WIDTH ;
59+ piece . initialX = newInitialX ;
60+ piece . translateY . setValue ( - 50 - Math . random ( ) * 100 ) ;
61+ piece . translateX . setValue ( newInitialX ) ;
62+ piece . rotation . setValue ( 0 ) ;
63+
64+ const duration = 2500 + Math . random ( ) * 1500 ;
65+ const endX = piece . initialX + ( Math . random ( ) - 0.5 ) * 200 ;
3066
31- // Start animation
32- opacityRef . current . setValue ( 1 ) ;
33-
34- // Animate each confetti piece
35- const animations = confettiRefs . current . map ( ( confetti , index ) => {
36- const duration = 2000 + Math . random ( ) * 1000 ;
37- const delay = index * 20 ;
38- const endX = confetti . startX + ( Math . random ( ) - 0.5 ) * 200 ;
39-
40- return Animated . parallel ( [
41- Animated . timing ( confetti . translateY , {
42- toValue : SCREEN_HEIGHT + 100 ,
43- duration,
44- delay,
45- useNativeDriver : true ,
46- } ) ,
47- Animated . timing ( confetti . translateX , {
48- toValue : endX ,
49- duration,
50- delay,
51- useNativeDriver : true ,
52- } ) ,
53- Animated . timing ( confetti . rotation , {
54- toValue : Math . random ( ) * 720 - 360 ,
55- duration,
56- delay,
57- useNativeDriver : true ,
58- } ) ,
59- Animated . sequence ( [
60- Animated . timing ( confetti . scale , {
61- toValue : 1.2 ,
62- duration : duration * 0.3 ,
63- delay,
67+ return Animated . parallel ( [
68+ Animated . timing ( piece . translateY , {
69+ toValue : SCREEN_HEIGHT + 100 ,
70+ duration,
6471 useNativeDriver : true ,
6572 } ) ,
66- Animated . timing ( confetti . scale , {
67- toValue : 0.8 ,
68- duration : duration * 0.7 ,
73+ Animated . timing ( piece . translateX , {
74+ toValue : endX ,
75+ duration,
6976 useNativeDriver : true ,
7077 } ) ,
71- ] ) ,
72- ] ) ;
73- } ) ;
78+ Animated . timing ( piece . rotation , {
79+ toValue : Math . random ( ) * 720 - 360 ,
80+ duration,
81+ useNativeDriver : true ,
82+ } ) ,
83+ ] ) ;
84+ } ) ;
85+
86+ animationRef . current = Animated . parallel ( animations ) ;
87+ animationRef . current . start ( ( ) => {
88+ // Loop if still visible
89+ if ( isAnimatingRef . current ) {
90+ startAnimation ( ) ;
91+ }
92+ } ) ;
93+ } ;
7494
75- // Fade out after animation
76- Animated . parallel ( [
77- ...animations ,
78- Animated . sequence ( [
79- Animated . delay ( 1500 ) ,
80- Animated . timing ( opacityRef . current , {
81- toValue : 0 ,
82- duration : 500 ,
83- useNativeDriver : true ,
84- } ) ,
85- ] ) ,
86- ] ) . start ( ( ) => {
87- onComplete ?.( ) ;
95+ // Small delay to ensure state update has rendered
96+ requestAnimationFrame ( ( ) => {
97+ startAnimation ( ) ;
8898 } ) ;
8999 } else {
90- // Reset
91- opacityRef . current . setValue ( 0 ) ;
92- confettiRefs . current . forEach ( ( confetti ) => {
93- confetti . translateY . setValue ( - 50 ) ;
94- confetti . translateX . setValue ( confetti . startX ) ;
95- confetti . rotation . setValue ( 0 ) ;
96- confetti . scale . setValue ( 1 ) ;
97- } ) ;
100+ isAnimatingRef . current = false ;
101+ if ( animationRef . current ) {
102+ animationRef . current . stop ( ) ;
103+ animationRef . current = null ;
104+ }
105+ setConfetti ( [ ] ) ;
98106 }
99- } , [ visible , onComplete ] ) ;
100107
101- if ( ! visible ) return null ;
108+ return ( ) => {
109+ isAnimatingRef . current = false ;
110+ if ( animationRef . current ) {
111+ animationRef . current . stop ( ) ;
112+ }
113+ } ;
114+ } , [ visible ] ) ;
115+
116+ if ( ! visible || confetti . length === 0 ) return null ;
102117
103118 return (
104- < Animated . View
105- style = { [ styles . container , { opacity : opacityRef . current } ] }
106- pointerEvents = "none"
107- >
108- { confettiRefs . current . map ( ( confetti , index ) => {
109- const color = COLORS [ index % COLORS . length ] ;
110- const size = 8 + Math . random ( ) * 8 ;
111-
112- return (
113- < Animated . View
114- key = { index }
115- style = { [
116- styles . confetti ,
117- {
118- width : size ,
119- height : size ,
120- backgroundColor : color ,
121- transform : [
122- {
123- translateY : confetti . translateY ,
124- } ,
125- {
126- translateX : confetti . translateX ,
127- } ,
128- {
129- rotate : confetti . rotation . interpolate ( {
130- inputRange : [ - 360 , 360 ] ,
131- outputRange : [ '-360deg' , '360deg' ] ,
132- } ) ,
133- } ,
134- {
135- scale : confetti . scale ,
136- } ,
137- ] ,
138- } ,
139- ] }
140- />
141- ) ;
142- } ) }
119+ < Animated . View style = { styles . container } pointerEvents = "none" >
120+ { confetti . map ( ( piece ) => (
121+ < Animated . View
122+ key = { piece . id }
123+ style = { [
124+ styles . confetti ,
125+ {
126+ width : piece . size ,
127+ height : piece . size ,
128+ backgroundColor : piece . color ,
129+ transform : [
130+ { translateY : piece . translateY } ,
131+ { translateX : piece . translateX } ,
132+ {
133+ rotate : piece . rotation . interpolate ( {
134+ inputRange : [ - 360 , 360 ] ,
135+ outputRange : [ '-360deg' , '360deg' ] ,
136+ } ) ,
137+ } ,
138+ { scale : piece . scale } ,
139+ ] ,
140+ } ,
141+ ] }
142+ />
143+ ) ) }
143144 </ Animated . View >
144145 ) ;
145146}
@@ -156,7 +157,7 @@ const styles = StyleSheet.create({
156157 } ,
157158 confetti : {
158159 position : 'absolute' ,
160+ top : 0 ,
159161 borderRadius : 2 ,
160162 } ,
161163} ) ;
162-
0 commit comments