11class AnimationSystem {
22 constructor ( ) {
33 this . prefersReducedMotion = window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ;
4+ this . celebrationEffects = [
5+ this . launchStars . bind ( this ) ,
6+ this . launchConfetti . bind ( this ) ,
7+ this . launchHearts . bind ( this ) ,
8+ this . launchSparkles . bind ( this ) ,
9+ this . launchBalloons . bind ( this ) ,
10+ this . launchFireworks . bind ( this ) ,
11+ ] ;
12+ this . confettiColors = [ '#ef4444' , '#f59e0b' , '#22c55e' , '#3b82f6' , '#a855f7' , '#ec4899' ] ;
413 }
514
615 getStarsContainer ( ) {
716 return document . getElementById ( 'stars-container' ) ;
817 }
918
19+ randomBetween ( min , max ) {
20+ return Math . random ( ) * ( max - min ) + min ;
21+ }
22+
23+ pickRandom ( items ) {
24+ return items [ Math . floor ( Math . random ( ) * items . length ) ] ;
25+ }
26+
27+ viewportWidth ( ) {
28+ return Math . max ( window . innerWidth || 0 , 320 ) ;
29+ }
30+
31+ viewportHeight ( ) {
32+ return Math . max ( window . innerHeight || 0 , 320 ) ;
33+ }
34+
35+ createParticle ( className , text = '' ) {
36+ const particle = document . createElement ( 'div' ) ;
37+ particle . className = className ;
38+ if ( text ) {
39+ particle . textContent = text ;
40+ }
41+ return particle ;
42+ }
43+
1044 createStar ( ) {
11- const star = document . createElement ( 'div' ) ;
12- star . className = 'star' ;
13- star . textContent = '⭐' ;
14- star . style . left = `${ Math . random ( ) * window . innerWidth } px` ;
15- star . style . fontSize = `${ Math . random ( ) * 20 + 20 } px` ;
16- star . style . animationDuration = `${ Math . random ( ) * 1.5 + 0.5 } s` ;
45+ const star = this . createParticle ( 'star' , '⭐' ) ;
46+ star . style . left = `${ Math . random ( ) * this . viewportWidth ( ) } px` ;
47+ star . style . fontSize = `${ this . randomBetween ( 20 , 40 ) } px` ;
48+ star . style . animationDuration = `${ this . randomBetween ( 0.7 , 1.8 ) } s` ;
1749 return star ;
1850 }
1951
52+ createConfetti ( ) {
53+ const confetti = this . createParticle ( 'confetti' ) ;
54+ confetti . style . left = `${ Math . random ( ) * this . viewportWidth ( ) } px` ;
55+ confetti . style . top = '-20px' ;
56+ confetti . style . backgroundColor = this . pickRandom ( this . confettiColors ) ;
57+ confetti . style . width = `${ this . randomBetween ( 8 , 14 ) } px` ;
58+ confetti . style . height = `${ this . randomBetween ( 14 , 24 ) } px` ;
59+ confetti . style . animationDuration = `${ this . randomBetween ( 2.2 , 3.5 ) } s` ;
60+ return confetti ;
61+ }
62+
63+ createHeart ( ) {
64+ const heart = this . createParticle ( 'heart' , this . pickRandom ( [ '💙' , '💚' , '💛' , '💖' ] ) ) ;
65+ heart . style . left = `${ Math . random ( ) * this . viewportWidth ( ) } px` ;
66+ heart . style . top = `${ this . randomBetween ( this . viewportHeight ( ) * 0.65 , this . viewportHeight ( ) * 0.9 ) } px` ;
67+ heart . style . animationDuration = `${ this . randomBetween ( 1.8 , 2.4 ) } s` ;
68+ return heart ;
69+ }
70+
71+ createSparkle ( ) {
72+ const sparkle = this . createParticle ( 'sparkle' , this . pickRandom ( [ '✨' , '✦' , '✧' ] ) ) ;
73+ sparkle . style . left = `${ Math . random ( ) * this . viewportWidth ( ) } px` ;
74+ sparkle . style . top = `${ this . randomBetween ( this . viewportHeight ( ) * 0.2 , this . viewportHeight ( ) * 0.75 ) } px` ;
75+ sparkle . style . animationDuration = `${ this . randomBetween ( 1.0 , 1.8 ) } s` ;
76+ return sparkle ;
77+ }
78+
79+ createBalloon ( ) {
80+ const balloon = this . createParticle ( 'balloon' , this . pickRandom ( [ '🎈' , '🎈' , '🎈' , '🎉' ] ) ) ;
81+ balloon . style . left = `${ Math . random ( ) * this . viewportWidth ( ) } px` ;
82+ balloon . style . top = `${ this . viewportHeight ( ) + this . randomBetween ( 10 , 120 ) } px` ;
83+ balloon . style . animationDuration = `${ this . randomBetween ( 3.5 , 4.8 ) } s` ;
84+ return balloon ;
85+ }
86+
87+ createFireworkSpark ( originX , originY , angle , distance ) {
88+ const spark = this . createParticle ( 'firework-spark' ) ;
89+ spark . style . left = `${ originX } px` ;
90+ spark . style . top = `${ originY } px` ;
91+ spark . style . backgroundColor = this . pickRandom ( this . confettiColors ) ;
92+ spark . style . setProperty ( '--dx' , `${ Math . cos ( angle ) * distance } px` ) ;
93+ spark . style . setProperty ( '--dy' , `${ Math . sin ( angle ) * distance } px` ) ;
94+ spark . style . animationDuration = `${ this . randomBetween ( 0.75 , 1.1 ) } s` ;
95+ return spark ;
96+ }
97+
98+ appendAndCleanup ( container , particle , removeAfterMs ) {
99+ container . appendChild ( particle ) ;
100+ setTimeout ( ( ) => particle . remove ( ) , removeAfterMs ) ;
101+ }
102+
103+ launchStars ( container ) {
104+ for ( let index = 0 ; index < 10 ; index += 1 ) {
105+ this . appendAndCleanup ( container , this . createStar ( ) , 2200 ) ;
106+ }
107+ }
108+
109+ launchConfetti ( container ) {
110+ for ( let index = 0 ; index < 24 ; index += 1 ) {
111+ this . appendAndCleanup ( container , this . createConfetti ( ) , 3800 ) ;
112+ }
113+ }
114+
115+ launchHearts ( container ) {
116+ for ( let index = 0 ; index < 12 ; index += 1 ) {
117+ this . appendAndCleanup ( container , this . createHeart ( ) , 2600 ) ;
118+ }
119+ }
120+
121+ launchSparkles ( container ) {
122+ for ( let index = 0 ; index < 16 ; index += 1 ) {
123+ this . appendAndCleanup ( container , this . createSparkle ( ) , 2200 ) ;
124+ }
125+ }
126+
127+ launchBalloons ( container ) {
128+ for ( let index = 0 ; index < 8 ; index += 1 ) {
129+ this . appendAndCleanup ( container , this . createBalloon ( ) , 5200 ) ;
130+ }
131+ }
132+
133+ launchFireworks ( container ) {
134+ const burstCount = 3 ;
135+ const sparksPerBurst = 10 ;
136+
137+ for ( let burstIndex = 0 ; burstIndex < burstCount ; burstIndex += 1 ) {
138+ const delay = burstIndex * 120 ;
139+ setTimeout ( ( ) => {
140+ const originX = this . randomBetween ( this . viewportWidth ( ) * 0.15 , this . viewportWidth ( ) * 0.85 ) ;
141+ const originY = this . randomBetween ( this . viewportHeight ( ) * 0.2 , this . viewportHeight ( ) * 0.55 ) ;
142+
143+ for ( let sparkIndex = 0 ; sparkIndex < sparksPerBurst ; sparkIndex += 1 ) {
144+ const baseAngle = ( Math . PI * 2 * sparkIndex ) / sparksPerBurst ;
145+ const angle = baseAngle + this . randomBetween ( - 0.18 , 0.18 ) ;
146+ const distance = this . randomBetween ( 45 , 110 ) ;
147+ this . appendAndCleanup (
148+ container ,
149+ this . createFireworkSpark ( originX , originY , angle , distance ) ,
150+ 1300
151+ ) ;
152+ }
153+ } , delay ) ;
154+ }
155+ }
156+
20157 handleCorrectAnswer ( selectedOption , allOptions , callback ) {
21- Array . from ( allOptions ) . forEach ( ( option ) => {
158+ Array . from ( allOptions || [ ] ) . forEach ( ( option ) => {
22159 option . disabled = true ;
23160 option . onclick = null ;
24161 } ) ;
@@ -27,12 +164,9 @@ class AnimationSystem {
27164
28165 if ( ! this . prefersReducedMotion ) {
29166 const starsContainer = this . getStarsContainer ( ) ;
30- if ( starsContainer ) {
31- for ( let index = 0 ; index < 10 ; index += 1 ) {
32- const star = this . createStar ( ) ;
33- starsContainer . appendChild ( star ) ;
34- setTimeout ( ( ) => star . remove ( ) , 2000 ) ;
35- }
167+ if ( starsContainer && this . celebrationEffects . length ) {
168+ const effect = this . pickRandom ( this . celebrationEffects ) ;
169+ effect ( starsContainer ) ;
36170 }
37171 }
38172
0 commit comments