Skip to content

Commit ef09415

Browse files
committed
Add celebration animations to AnimationSystem, including stars, confetti, hearts, sparkles, balloons, and fireworks. Introduce helper methods for particle creation and randomization. Update styles for firework sparks and animations.
1 parent 09fc90b commit ef09415

2 files changed

Lines changed: 167 additions & 13 deletions

File tree

animations.js

Lines changed: 147 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,161 @@
11
class 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

styles.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,15 @@ h1 {
439439
animation: balloon-float 4s ease-out forwards;
440440
}
441441

442+
.firework-spark {
443+
position: absolute;
444+
width: 8px;
445+
height: 8px;
446+
border-radius: 999px;
447+
box-shadow: 0 0 10px rgba(255, 255, 255, 0.85);
448+
animation: firework-burst 0.9s ease-out forwards;
449+
}
450+
442451
@keyframes star-fall {
443452
0% {
444453
transform: translateY(-50px) rotate(0deg);
@@ -520,6 +529,17 @@ h1 {
520529
}
521530
}
522531

532+
@keyframes firework-burst {
533+
0% {
534+
transform: translate(0, 0) scale(1);
535+
opacity: 1;
536+
}
537+
100% {
538+
transform: translate(var(--dx, 0), var(--dy, 0)) scale(0.2);
539+
opacity: 0;
540+
}
541+
}
542+
523543
@keyframes shake {
524544
0%,
525545
100% {

0 commit comments

Comments
 (0)