festive-effects 1.0.0
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 +163 -0
- package/dist/animations/index.d.ts +91 -0
- package/dist/components/FestiveEffects.d.ts +19 -0
- package/dist/config/index.d.ts +18 -0
- package/dist/effects/ChineseNewYearEffect.d.ts +32 -0
- package/dist/effects/ChristmasEffect.d.ts +32 -0
- package/dist/effects/DiwaliEffect.d.ts +32 -0
- package/dist/effects/EasterEffect.d.ts +32 -0
- package/dist/effects/EidEffect.d.ts +32 -0
- package/dist/effects/HalloweenEffect.d.ts +32 -0
- package/dist/effects/HoliEffect.d.ts +32 -0
- package/dist/effects/IndependenceEffect.d.ts +32 -0
- package/dist/effects/NewYearEffect.d.ts +31 -0
- package/dist/effects/StPatricksEffect.d.ts +32 -0
- package/dist/effects/ThanksgivingEffect.d.ts +32 -0
- package/dist/effects/ValentineEffect.d.ts +32 -0
- package/dist/effects/index.d.ts +15 -0
- package/dist/hooks/index.d.ts +70 -0
- package/dist/index.d.ts +754 -0
- package/dist/index.esm.js +2009 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2074 -0
- package/dist/index.js.map +1 -0
- package/dist/particles/index.d.ts +37 -0
- package/dist/registry/index.d.ts +69 -0
- package/dist/types/index.d.ts +144 -0
- package/package.json +85 -0
|
@@ -0,0 +1,2009 @@
|
|
|
1
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
|
|
5
|
+
// Core types for Festive Effects library
|
|
6
|
+
/**
|
|
7
|
+
* Array of all valid festival types for validation
|
|
8
|
+
*/
|
|
9
|
+
const FESTIVAL_TYPES = [
|
|
10
|
+
'christmas',
|
|
11
|
+
'newyear',
|
|
12
|
+
'valentine',
|
|
13
|
+
'easter',
|
|
14
|
+
'halloween',
|
|
15
|
+
'thanksgiving',
|
|
16
|
+
'diwali',
|
|
17
|
+
'chinesenewyear',
|
|
18
|
+
'holi',
|
|
19
|
+
'eid',
|
|
20
|
+
'stpatricks',
|
|
21
|
+
'independence',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Configuration for Festive Effects library
|
|
25
|
+
/**
|
|
26
|
+
* Intensity configuration for particle effects
|
|
27
|
+
* Defines particle multipliers and max counts for each intensity level
|
|
28
|
+
*/
|
|
29
|
+
const INTENSITY_CONFIG = {
|
|
30
|
+
low: { particleMultiplier: 0.5, maxParticles: 30 },
|
|
31
|
+
medium: { particleMultiplier: 1.0, maxParticles: 60 },
|
|
32
|
+
high: { particleMultiplier: 2.0, maxParticles: 120 },
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Calculate the actual particle count based on base count and intensity
|
|
36
|
+
* @param baseCount - The base particle count from festival config
|
|
37
|
+
* @param intensity - The intensity level
|
|
38
|
+
* @returns The calculated particle count, capped at maxParticles
|
|
39
|
+
*/
|
|
40
|
+
function getParticleCount(baseCount, intensity = 'medium') {
|
|
41
|
+
const settings = INTENSITY_CONFIG[intensity];
|
|
42
|
+
const calculated = Math.floor(baseCount * settings.particleMultiplier);
|
|
43
|
+
return Math.min(calculated, settings.maxParticles);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Festival configurations for all 12 supported festivals
|
|
47
|
+
* Each config defines colors, particle types, animation style, and physics
|
|
48
|
+
*/
|
|
49
|
+
const FESTIVAL_CONFIGS = {
|
|
50
|
+
christmas: {
|
|
51
|
+
baseParticleCount: 50,
|
|
52
|
+
colors: ['#FFFFFF', '#E8F4FF', '#B8D4E8', '#87CEEB'],
|
|
53
|
+
particleTypes: ['snowflake'],
|
|
54
|
+
animationType: 'fall',
|
|
55
|
+
physics: { speed: 3, drift: 50, rotation: 360, scale: [0.5, 1.5] },
|
|
56
|
+
},
|
|
57
|
+
newyear: {
|
|
58
|
+
baseParticleCount: 40,
|
|
59
|
+
colors: ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'],
|
|
60
|
+
particleTypes: ['firework', 'confetti', 'spark'],
|
|
61
|
+
animationType: 'explode',
|
|
62
|
+
physics: { speed: 5, drift: 100, rotation: 720, scale: [0.3, 1.2] },
|
|
63
|
+
},
|
|
64
|
+
valentine: {
|
|
65
|
+
baseParticleCount: 35,
|
|
66
|
+
colors: ['#FF6B6B', '#EE5A5A', '#FF8E8E', '#FFB6C1', '#FF69B4'],
|
|
67
|
+
particleTypes: ['heart'],
|
|
68
|
+
animationType: 'rise',
|
|
69
|
+
physics: { speed: 2, drift: 30, rotation: 15, scale: [0.6, 1.4] },
|
|
70
|
+
},
|
|
71
|
+
easter: {
|
|
72
|
+
baseParticleCount: 30,
|
|
73
|
+
colors: ['#FFB6C1', '#98FB98', '#87CEEB', '#DDA0DD', '#F0E68C'],
|
|
74
|
+
particleTypes: ['egg', 'flower'],
|
|
75
|
+
animationType: 'float',
|
|
76
|
+
physics: { speed: 1.5, drift: 20, rotation: 10, scale: [0.7, 1.3] },
|
|
77
|
+
},
|
|
78
|
+
halloween: {
|
|
79
|
+
baseParticleCount: 35,
|
|
80
|
+
colors: ['#FF6600', '#8B008B', '#000000', '#FFD700', '#32CD32'],
|
|
81
|
+
particleTypes: ['bat', 'pumpkin', 'spider'],
|
|
82
|
+
animationType: 'float',
|
|
83
|
+
physics: { speed: 2, drift: 60, rotation: 30, scale: [0.5, 1.5] },
|
|
84
|
+
},
|
|
85
|
+
thanksgiving: {
|
|
86
|
+
baseParticleCount: 40,
|
|
87
|
+
colors: ['#D2691E', '#FF8C00', '#B8860B', '#CD853F', '#8B4513'],
|
|
88
|
+
particleTypes: ['leaf'],
|
|
89
|
+
animationType: 'fall',
|
|
90
|
+
physics: { speed: 2, drift: 80, rotation: 540, scale: [0.6, 1.4] },
|
|
91
|
+
},
|
|
92
|
+
diwali: {
|
|
93
|
+
baseParticleCount: 45,
|
|
94
|
+
colors: ['#FFD700', '#FF8C00', '#FF4500', '#FFA500', '#FFFF00'],
|
|
95
|
+
particleTypes: ['diya', 'spark', 'rangoli'],
|
|
96
|
+
animationType: 'float',
|
|
97
|
+
physics: { speed: 1, drift: 15, rotation: 5, scale: [0.5, 1.2] },
|
|
98
|
+
},
|
|
99
|
+
chinesenewyear: {
|
|
100
|
+
baseParticleCount: 40,
|
|
101
|
+
colors: ['#FF0000', '#FFD700', '#FF4500', '#DC143C'],
|
|
102
|
+
particleTypes: ['lantern', 'firework', 'dragon'],
|
|
103
|
+
animationType: 'rise',
|
|
104
|
+
physics: { speed: 1.5, drift: 25, rotation: 10, scale: [0.6, 1.3] },
|
|
105
|
+
},
|
|
106
|
+
holi: {
|
|
107
|
+
baseParticleCount: 50,
|
|
108
|
+
colors: ['#FF1493', '#00FF00', '#FFFF00', '#FF4500', '#9400D3', '#00BFFF'],
|
|
109
|
+
particleTypes: ['color-splash'],
|
|
110
|
+
animationType: 'scatter',
|
|
111
|
+
physics: { speed: 4, drift: 150, rotation: 180, scale: [0.4, 2.0] },
|
|
112
|
+
},
|
|
113
|
+
eid: {
|
|
114
|
+
baseParticleCount: 35,
|
|
115
|
+
colors: ['#FFD700', '#C0C0C0', '#228B22', '#FFFFFF'],
|
|
116
|
+
particleTypes: ['moon', 'star'],
|
|
117
|
+
animationType: 'float',
|
|
118
|
+
physics: { speed: 0.8, drift: 10, rotation: 5, scale: [0.5, 1.5] },
|
|
119
|
+
},
|
|
120
|
+
stpatricks: {
|
|
121
|
+
baseParticleCount: 40,
|
|
122
|
+
colors: ['#228B22', '#32CD32', '#00FF00', '#FFD700'],
|
|
123
|
+
particleTypes: ['shamrock', 'rainbow'],
|
|
124
|
+
animationType: 'fall',
|
|
125
|
+
physics: { speed: 2, drift: 40, rotation: 180, scale: [0.6, 1.3] },
|
|
126
|
+
},
|
|
127
|
+
independence: {
|
|
128
|
+
baseParticleCount: 45,
|
|
129
|
+
colors: ['#FF0000', '#FFFFFF', '#0000FF'], // Default US colors, customizable
|
|
130
|
+
particleTypes: ['firework', 'spark', 'confetti'],
|
|
131
|
+
animationType: 'explode',
|
|
132
|
+
physics: { speed: 5, drift: 120, rotation: 360, scale: [0.3, 1.5] },
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Custom hooks for Festive Effects
|
|
137
|
+
// Contains accessibility and utility hooks
|
|
138
|
+
/**
|
|
139
|
+
* Hook to detect if the user prefers reduced motion
|
|
140
|
+
* Listens to the prefers-reduced-motion media query
|
|
141
|
+
*
|
|
142
|
+
* @returns boolean - true if user prefers reduced motion, false otherwise
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* const prefersReducedMotion = useReducedMotion();
|
|
147
|
+
* if (prefersReducedMotion) {
|
|
148
|
+
* // Don't render animations
|
|
149
|
+
* return null;
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function useReducedMotion() {
|
|
154
|
+
// Default to false for SSR
|
|
155
|
+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
// Check if window is available (client-side)
|
|
158
|
+
if (typeof window === 'undefined') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Create media query for prefers-reduced-motion
|
|
162
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
163
|
+
// Set initial value
|
|
164
|
+
setPrefersReducedMotion(mediaQuery.matches);
|
|
165
|
+
// Handler for media query changes
|
|
166
|
+
const handleChange = (event) => {
|
|
167
|
+
setPrefersReducedMotion(event.matches);
|
|
168
|
+
};
|
|
169
|
+
// Add listener for changes
|
|
170
|
+
// Use addEventListener for modern browsers, addListener for older ones
|
|
171
|
+
if (mediaQuery.addEventListener) {
|
|
172
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Fallback for older browsers
|
|
176
|
+
mediaQuery.addListener(handleChange);
|
|
177
|
+
}
|
|
178
|
+
// Cleanup listener on unmount
|
|
179
|
+
return () => {
|
|
180
|
+
if (mediaQuery.removeEventListener) {
|
|
181
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Fallback for older browsers
|
|
185
|
+
mediaQuery.removeListener(handleChange);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}, []);
|
|
189
|
+
return prefersReducedMotion;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Hook to detect document visibility changes
|
|
193
|
+
* Returns true when the document is visible, false when hidden
|
|
194
|
+
*
|
|
195
|
+
* This is useful for pausing animations when the user switches tabs
|
|
196
|
+
* to save resources and improve performance.
|
|
197
|
+
*
|
|
198
|
+
* @returns boolean - true if document is visible, false if hidden
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```tsx
|
|
202
|
+
* const isVisible = useDocumentVisibility();
|
|
203
|
+
* if (!isVisible) {
|
|
204
|
+
* // Pause animations
|
|
205
|
+
* }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
function useDocumentVisibility() {
|
|
209
|
+
// Default to true for SSR (assume visible)
|
|
210
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
// Check if document is available (client-side)
|
|
213
|
+
if (typeof document === 'undefined') {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Set initial value based on current visibility state
|
|
217
|
+
setIsVisible(!document.hidden);
|
|
218
|
+
// Handler for visibility changes
|
|
219
|
+
const handleVisibilityChange = () => {
|
|
220
|
+
setIsVisible(!document.hidden);
|
|
221
|
+
};
|
|
222
|
+
// Add listener for visibility changes
|
|
223
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
224
|
+
// Cleanup listener on unmount
|
|
225
|
+
return () => {
|
|
226
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
227
|
+
};
|
|
228
|
+
}, []);
|
|
229
|
+
return isVisible;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// SVG Particle System for Festive Effects
|
|
233
|
+
// Contains all particle SVG shapes and the Particle component
|
|
234
|
+
/**
|
|
235
|
+
* SVG definitions for all 18 particle types
|
|
236
|
+
* Each SVG is designed to work with currentColor for dynamic coloring
|
|
237
|
+
*/
|
|
238
|
+
const PARTICLE_SVGS = {
|
|
239
|
+
snowflake: `<svg viewBox="0 0 24 24"><path d="M12 0L12 24M0 12L24 12M4 4L20 20M20 4L4 20" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
|
|
240
|
+
heart: `<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="currentColor"/></svg>`,
|
|
241
|
+
bat: `<svg viewBox="0 0 24 24"><path d="M12 4C8 4 4 8 2 12c2-2 4-2 6 0-1 2-1 4 0 6 1-2 3-3 4-3s3 1 4 3c1-2 1-4 0-6 2-2 4-2 6 0-2-4-6-8-10-8z" fill="currentColor"/></svg>`,
|
|
242
|
+
pumpkin: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="14" rx="8" ry="7" fill="currentColor"/><path d="M12 7V4M10 4h4" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
|
|
243
|
+
leaf: `<svg viewBox="0 0 24 24"><path d="M12 2C6 8 4 14 4 18c0 2 2 4 8 4s8-2 8-4c0-4-2-10-8-16z" fill="currentColor"/><path d="M12 22V8" stroke="rgba(0,0,0,0.3)" stroke-width="1" fill="none"/></svg>`,
|
|
244
|
+
diya: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="18" rx="8" ry="4" fill="currentColor"/><path d="M12 14c-2 0-3-2-3-4s1-4 3-4 3 2 3 4-1 4-3 4z" fill="#FFD700"/></svg>`,
|
|
245
|
+
lantern: `<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="14" rx="2" fill="currentColor"/><path d="M8 4h8M12 4V2" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
|
|
246
|
+
moon: `<svg viewBox="0 0 24 24"><path d="M12 3a9 9 0 109 9c0-5-4-9-9-9z" fill="currentColor"/></svg>`,
|
|
247
|
+
star: `<svg viewBox="0 0 24 24"><path d="M12 2l3 7h7l-5.5 4.5 2 7L12 16l-6.5 4.5 2-7L2 9h7z" fill="currentColor"/></svg>`,
|
|
248
|
+
shamrock: `<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" fill="currentColor"/><circle cx="7" cy="14" r="4" fill="currentColor"/><circle cx="17" cy="14" r="4" fill="currentColor"/><path d="M12 16v6" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
|
|
249
|
+
egg: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="14" rx="7" ry="9" fill="currentColor"/></svg>`,
|
|
250
|
+
flower: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3" fill="#FFD700"/><circle cx="12" cy="6" r="3" fill="currentColor"/><circle cx="18" cy="12" r="3" fill="currentColor"/><circle cx="12" cy="18" r="3" fill="currentColor"/><circle cx="6" cy="12" r="3" fill="currentColor"/></svg>`,
|
|
251
|
+
confetti: `<svg viewBox="0 0 24 24"><rect x="8" y="4" width="8" height="16" rx="1" fill="currentColor"/></svg>`,
|
|
252
|
+
spark: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>`,
|
|
253
|
+
firework: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2" fill="currentColor"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4M4 4l3 3M17 17l3 3M4 20l3-3M17 7l3-3" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
|
|
254
|
+
'color-splash': `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.8"/></svg>`,
|
|
255
|
+
spider: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="currentColor"/><path d="M8 8L4 4M16 8L20 4M8 16L4 20M16 16L20 20M6 12H2M18 12h4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
|
|
256
|
+
rainbow: `<svg viewBox="0 0 24 24"><path d="M2 20a10 10 0 0120 0" stroke="currentColor" stroke-width="4" fill="none"/></svg>`,
|
|
257
|
+
dragon: `<svg viewBox="0 0 24 24"><path d="M4 12c2-4 6-6 10-4 2 1 4 4 6 4-1 2-3 4-6 4-4 0-8-2-10-4z" fill="currentColor"/></svg>`,
|
|
258
|
+
rangoli: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>`,
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Get animation variants based on animation type
|
|
262
|
+
*/
|
|
263
|
+
function getAnimationProps(particle, animationType, physics) {
|
|
264
|
+
const { delay, duration, custom } = particle;
|
|
265
|
+
const driftAmount = custom.drift ?? physics.drift * (Math.random() - 0.5) * 2;
|
|
266
|
+
const rotationAmount = custom.rotation ?? physics.rotation * (Math.random() - 0.5) * 2;
|
|
267
|
+
switch (animationType) {
|
|
268
|
+
case 'fall':
|
|
269
|
+
return {
|
|
270
|
+
initial: { y: -50, x: particle.x, opacity: 0, rotate: 0 },
|
|
271
|
+
animate: {
|
|
272
|
+
y: typeof window !== 'undefined' ? window.innerHeight + 50 : 1000,
|
|
273
|
+
x: particle.x + driftAmount,
|
|
274
|
+
opacity: [0, 1, 1, 0],
|
|
275
|
+
rotate: rotationAmount,
|
|
276
|
+
},
|
|
277
|
+
transition: {
|
|
278
|
+
duration,
|
|
279
|
+
ease: 'linear',
|
|
280
|
+
repeat: Infinity,
|
|
281
|
+
delay,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
case 'rise':
|
|
285
|
+
return {
|
|
286
|
+
initial: { y: typeof window !== 'undefined' ? window.innerHeight + 50 : 1000, x: particle.x, opacity: 0, scale: 0 },
|
|
287
|
+
animate: {
|
|
288
|
+
y: -50,
|
|
289
|
+
x: [particle.x, particle.x + driftAmount / 2, particle.x - driftAmount / 2, particle.x],
|
|
290
|
+
opacity: [0, 1, 1, 0],
|
|
291
|
+
scale: [0, 1, 1.2, 1, 0],
|
|
292
|
+
},
|
|
293
|
+
transition: {
|
|
294
|
+
duration,
|
|
295
|
+
ease: 'easeOut',
|
|
296
|
+
repeat: Infinity,
|
|
297
|
+
delay,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
case 'explode':
|
|
301
|
+
const angle = custom.angle ?? Math.random() * Math.PI * 2;
|
|
302
|
+
const distance = custom.distance ?? 100 + Math.random() * 150;
|
|
303
|
+
return {
|
|
304
|
+
initial: { x: particle.x, y: particle.y, scale: 0, opacity: 1 },
|
|
305
|
+
animate: {
|
|
306
|
+
x: particle.x + Math.cos(angle) * distance,
|
|
307
|
+
y: particle.y + Math.sin(angle) * distance,
|
|
308
|
+
scale: [0, 1.5, 0],
|
|
309
|
+
opacity: [1, 1, 0],
|
|
310
|
+
},
|
|
311
|
+
transition: {
|
|
312
|
+
duration: duration * 0.5,
|
|
313
|
+
ease: [0.25, 0.46, 0.45, 0.94],
|
|
314
|
+
repeat: Infinity,
|
|
315
|
+
repeatDelay: duration * 0.5,
|
|
316
|
+
delay,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
case 'float':
|
|
320
|
+
return {
|
|
321
|
+
initial: { x: particle.x, y: particle.y, opacity: 0, scale: 0.8 },
|
|
322
|
+
animate: {
|
|
323
|
+
x: [particle.x, particle.x + driftAmount, particle.x - driftAmount, particle.x],
|
|
324
|
+
y: [particle.y, particle.y - 20, particle.y + 10, particle.y],
|
|
325
|
+
opacity: [0, 1, 1, 0],
|
|
326
|
+
scale: [0.8, 1, 1.1, 1, 0.8],
|
|
327
|
+
rotate: [0, rotationAmount / 4, -rotationAmount / 4, 0],
|
|
328
|
+
},
|
|
329
|
+
transition: {
|
|
330
|
+
duration,
|
|
331
|
+
ease: 'easeInOut',
|
|
332
|
+
repeat: Infinity,
|
|
333
|
+
delay,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
case 'scatter':
|
|
337
|
+
const scatterAngle = custom.angle ?? Math.random() * Math.PI * 2;
|
|
338
|
+
const scatterDistance = custom.distance ?? 50 + Math.random() * 100;
|
|
339
|
+
return {
|
|
340
|
+
initial: { x: particle.x, y: particle.y, scale: 0, opacity: 0.8 },
|
|
341
|
+
animate: {
|
|
342
|
+
x: particle.x + Math.cos(scatterAngle) * scatterDistance,
|
|
343
|
+
y: particle.y + Math.sin(scatterAngle) * scatterDistance + 50,
|
|
344
|
+
scale: [0, 2, 0],
|
|
345
|
+
opacity: [0.8, 0.6, 0],
|
|
346
|
+
rotate: rotationAmount,
|
|
347
|
+
},
|
|
348
|
+
transition: {
|
|
349
|
+
duration: duration * 0.7,
|
|
350
|
+
ease: 'easeOut',
|
|
351
|
+
repeat: Infinity,
|
|
352
|
+
repeatDelay: duration * 0.3,
|
|
353
|
+
delay,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
default:
|
|
357
|
+
return {
|
|
358
|
+
initial: { opacity: 0 },
|
|
359
|
+
animate: { opacity: 1 },
|
|
360
|
+
transition: { duration: 1 },
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Particle component that renders an SVG particle with Framer Motion animations
|
|
366
|
+
*/
|
|
367
|
+
const Particle = ({ particle, animationType, physics, }) => {
|
|
368
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
369
|
+
const animationProps = getAnimationProps(particle, animationType, physics);
|
|
370
|
+
return React.createElement(motion.div, {
|
|
371
|
+
key: particle.id,
|
|
372
|
+
style: {
|
|
373
|
+
position: 'absolute',
|
|
374
|
+
width: particle.size,
|
|
375
|
+
height: particle.size,
|
|
376
|
+
color: particle.color,
|
|
377
|
+
pointerEvents: 'none',
|
|
378
|
+
},
|
|
379
|
+
initial: animationProps.initial,
|
|
380
|
+
animate: animationProps.animate,
|
|
381
|
+
transition: animationProps.transition,
|
|
382
|
+
dangerouslySetInnerHTML: { __html: svgString },
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Generate a unique ID for a particle
|
|
387
|
+
*/
|
|
388
|
+
function generateId() {
|
|
389
|
+
return `particle-${Math.random().toString(36).substring(2, 11)}`;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get a random item from an array
|
|
393
|
+
*/
|
|
394
|
+
function randomFromArray(arr) {
|
|
395
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get a random number within a range
|
|
399
|
+
*/
|
|
400
|
+
function randomInRange(min, max) {
|
|
401
|
+
return min + Math.random() * (max - min);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Generate particles for a festival effect
|
|
405
|
+
* @param count - Number of particles to generate
|
|
406
|
+
* @param viewport - Viewport dimensions
|
|
407
|
+
* @param particleTypes - Array of particle types to use
|
|
408
|
+
* @param colors - Array of colors to use
|
|
409
|
+
* @param physics - Physics configuration
|
|
410
|
+
* @param animationType - Type of animation
|
|
411
|
+
* @returns Array of ParticleData objects
|
|
412
|
+
*/
|
|
413
|
+
function generateParticles(count, viewport, particleTypes, colors, physics, animationType) {
|
|
414
|
+
const particles = [];
|
|
415
|
+
for (let i = 0; i < count; i++) {
|
|
416
|
+
const type = randomFromArray(particleTypes);
|
|
417
|
+
const color = randomFromArray(colors);
|
|
418
|
+
const size = randomInRange(physics.scale[0] * 20, physics.scale[1] * 20);
|
|
419
|
+
// Position calculation based on animation type
|
|
420
|
+
let x;
|
|
421
|
+
let y;
|
|
422
|
+
switch (animationType) {
|
|
423
|
+
case 'fall':
|
|
424
|
+
// Start from random x position at top
|
|
425
|
+
x = randomInRange(0, viewport.width);
|
|
426
|
+
y = randomInRange(-100, -20);
|
|
427
|
+
break;
|
|
428
|
+
case 'rise':
|
|
429
|
+
// Start from random x position at bottom
|
|
430
|
+
x = randomInRange(0, viewport.width);
|
|
431
|
+
y = viewport.height + randomInRange(20, 100);
|
|
432
|
+
break;
|
|
433
|
+
case 'explode':
|
|
434
|
+
// Start from random positions across viewport
|
|
435
|
+
x = randomInRange(viewport.width * 0.2, viewport.width * 0.8);
|
|
436
|
+
y = randomInRange(viewport.height * 0.2, viewport.height * 0.6);
|
|
437
|
+
break;
|
|
438
|
+
case 'float':
|
|
439
|
+
// Random positions across viewport
|
|
440
|
+
x = randomInRange(0, viewport.width);
|
|
441
|
+
y = randomInRange(0, viewport.height);
|
|
442
|
+
break;
|
|
443
|
+
case 'scatter':
|
|
444
|
+
// Start from center-ish positions
|
|
445
|
+
x = randomInRange(viewport.width * 0.3, viewport.width * 0.7);
|
|
446
|
+
y = randomInRange(viewport.height * 0.3, viewport.height * 0.7);
|
|
447
|
+
break;
|
|
448
|
+
default:
|
|
449
|
+
x = randomInRange(0, viewport.width);
|
|
450
|
+
y = randomInRange(0, viewport.height);
|
|
451
|
+
}
|
|
452
|
+
// Calculate delay and duration based on physics speed
|
|
453
|
+
const baseDelay = i * (1 / count) * 3; // Stagger particles
|
|
454
|
+
const delay = baseDelay + randomInRange(0, 0.5);
|
|
455
|
+
const baseDuration = 10 / physics.speed;
|
|
456
|
+
const duration = baseDuration + randomInRange(-1, 1);
|
|
457
|
+
// Custom properties for animation variations
|
|
458
|
+
const custom = {
|
|
459
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
460
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
461
|
+
angle: Math.random() * Math.PI * 2,
|
|
462
|
+
distance: 50 + Math.random() * 150,
|
|
463
|
+
};
|
|
464
|
+
particles.push({
|
|
465
|
+
id: generateId(),
|
|
466
|
+
x,
|
|
467
|
+
y,
|
|
468
|
+
size,
|
|
469
|
+
color,
|
|
470
|
+
type,
|
|
471
|
+
delay,
|
|
472
|
+
duration,
|
|
473
|
+
custom,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return particles;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Calculate the actual particle count based on base count and intensity
|
|
480
|
+
* @param baseCount - The base particle count from festival config
|
|
481
|
+
* @param intensity - The intensity level
|
|
482
|
+
* @returns The calculated particle count, capped at maxParticles
|
|
483
|
+
*/
|
|
484
|
+
function calculateParticleCount(baseCount, intensity = 'medium') {
|
|
485
|
+
const settings = INTENSITY_CONFIG[intensity];
|
|
486
|
+
const calculated = Math.floor(baseCount * settings.particleMultiplier);
|
|
487
|
+
return Math.min(calculated, settings.maxParticles);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Check if a festival type is valid
|
|
492
|
+
*/
|
|
493
|
+
function isValidFestival(festival) {
|
|
494
|
+
return FESTIVAL_TYPES.includes(festival);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* FestiveEffects - Main component for rendering festival-themed visual effects
|
|
498
|
+
*
|
|
499
|
+
* Renders a full-screen overlay with animated particles for various festivals.
|
|
500
|
+
* Uses Framer Motion for smooth, GPU-accelerated animations.
|
|
501
|
+
*
|
|
502
|
+
* Features:
|
|
503
|
+
* - Automatic cleanup of timeouts and event listeners on unmount
|
|
504
|
+
* - Pauses animations when browser tab is not visible (performance optimization)
|
|
505
|
+
* - Respects user's reduced motion preferences
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```tsx
|
|
509
|
+
* <FestiveEffects festival="christmas" intensity="medium" />
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
const FestiveEffects = ({ festival, intensity = 'medium', duration, zIndex = 9999, respectReducedMotion = true, colors, }) => {
|
|
513
|
+
const [particles, setParticles] = useState([]);
|
|
514
|
+
const [isActive, setIsActive] = useState(true);
|
|
515
|
+
const [viewport, setViewport] = useState({ width: 0, height: 0 });
|
|
516
|
+
const prefersReducedMotion = useReducedMotion();
|
|
517
|
+
const isDocumentVisible = useDocumentVisibility();
|
|
518
|
+
// Refs for cleanup tracking
|
|
519
|
+
const durationTimeoutRef = useRef(null);
|
|
520
|
+
const animationFrameRef = useRef(null);
|
|
521
|
+
const isMountedRef = useRef(true);
|
|
522
|
+
// Cleanup function to cancel all pending operations
|
|
523
|
+
const cleanup = useCallback(() => {
|
|
524
|
+
// Clear duration timeout
|
|
525
|
+
if (durationTimeoutRef.current) {
|
|
526
|
+
clearTimeout(durationTimeoutRef.current);
|
|
527
|
+
durationTimeoutRef.current = null;
|
|
528
|
+
}
|
|
529
|
+
// Cancel any pending animation frames
|
|
530
|
+
if (animationFrameRef.current) {
|
|
531
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
532
|
+
animationFrameRef.current = null;
|
|
533
|
+
}
|
|
534
|
+
}, []);
|
|
535
|
+
// Track mounted state for async operations
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
isMountedRef.current = true;
|
|
538
|
+
return () => {
|
|
539
|
+
isMountedRef.current = false;
|
|
540
|
+
cleanup();
|
|
541
|
+
};
|
|
542
|
+
}, [cleanup]);
|
|
543
|
+
// Update viewport dimensions with cleanup
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
if (typeof window === 'undefined')
|
|
546
|
+
return;
|
|
547
|
+
const updateViewport = () => {
|
|
548
|
+
if (isMountedRef.current) {
|
|
549
|
+
setViewport({
|
|
550
|
+
width: window.innerWidth,
|
|
551
|
+
height: window.innerHeight,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
updateViewport();
|
|
556
|
+
window.addEventListener('resize', updateViewport);
|
|
557
|
+
return () => {
|
|
558
|
+
window.removeEventListener('resize', updateViewport);
|
|
559
|
+
};
|
|
560
|
+
}, []);
|
|
561
|
+
// Handle duration timeout with proper cleanup
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
if (duration && duration > 0) {
|
|
564
|
+
durationTimeoutRef.current = setTimeout(() => {
|
|
565
|
+
if (isMountedRef.current) {
|
|
566
|
+
setIsActive(false);
|
|
567
|
+
}
|
|
568
|
+
}, duration);
|
|
569
|
+
}
|
|
570
|
+
return () => {
|
|
571
|
+
if (durationTimeoutRef.current) {
|
|
572
|
+
clearTimeout(durationTimeoutRef.current);
|
|
573
|
+
durationTimeoutRef.current = null;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}, [duration]);
|
|
577
|
+
// Generate particles when viewport is ready and effect is active
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
if (!isActive || viewport.width === 0 || viewport.height === 0) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!isValidFestival(festival)) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const config = FESTIVAL_CONFIGS[festival];
|
|
586
|
+
const effectColors = colors && colors.length > 0 ? colors : config.colors;
|
|
587
|
+
const particleCount = getParticleCount(config.baseParticleCount, intensity);
|
|
588
|
+
const newParticles = generateParticles(particleCount, viewport, config.particleTypes, effectColors, config.physics, config.animationType);
|
|
589
|
+
if (isMountedRef.current) {
|
|
590
|
+
setParticles(newParticles);
|
|
591
|
+
}
|
|
592
|
+
}, [festival, intensity, colors, viewport, isActive]);
|
|
593
|
+
// Validate festival prop
|
|
594
|
+
if (!isValidFestival(festival)) {
|
|
595
|
+
console.warn(`[FestiveEffects] Invalid festival prop: "${festival}". ` +
|
|
596
|
+
`Valid options are: ${FESTIVAL_TYPES.join(', ')}`);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
// Respect reduced motion preference
|
|
600
|
+
if (respectReducedMotion && prefersReducedMotion) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
// Don't render if effect is not active
|
|
604
|
+
if (!isActive) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
// Don't render if viewport is not ready
|
|
608
|
+
if (viewport.width === 0 || viewport.height === 0) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
// Pause animations when document is not visible (tab is in background)
|
|
612
|
+
// This improves performance by not rendering animations that can't be seen
|
|
613
|
+
const shouldRenderParticles = isDocumentVisible;
|
|
614
|
+
const config = FESTIVAL_CONFIGS[festival];
|
|
615
|
+
return (jsx("div", { "data-testid": "festive-effects-overlay", style: {
|
|
616
|
+
position: 'fixed',
|
|
617
|
+
top: 0,
|
|
618
|
+
left: 0,
|
|
619
|
+
width: '100%',
|
|
620
|
+
height: '100%',
|
|
621
|
+
pointerEvents: 'none',
|
|
622
|
+
zIndex,
|
|
623
|
+
overflow: 'hidden',
|
|
624
|
+
}, children: jsx(AnimatePresence, { children: shouldRenderParticles && particles.map((particle) => (jsx(Particle, { particle: particle, animationType: config.animationType, physics: config.physics }, particle.id))) }) }));
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Animation System for Festive Effects
|
|
628
|
+
// Contains Framer Motion variants for each animation type
|
|
629
|
+
/**
|
|
630
|
+
* Fall animation variants - used for snow, leaves, shamrocks
|
|
631
|
+
* Particles fall from top to bottom with horizontal drift and rotation
|
|
632
|
+
*/
|
|
633
|
+
const fallVariants = {
|
|
634
|
+
initial: {
|
|
635
|
+
y: -50,
|
|
636
|
+
opacity: 0,
|
|
637
|
+
rotate: 0,
|
|
638
|
+
},
|
|
639
|
+
animate: (custom) => ({
|
|
640
|
+
y: custom.viewportHeight + 50,
|
|
641
|
+
x: custom.drift,
|
|
642
|
+
opacity: [0, 1, 1, 0],
|
|
643
|
+
rotate: custom.rotation,
|
|
644
|
+
}),
|
|
645
|
+
};
|
|
646
|
+
/**
|
|
647
|
+
* Get transition for fall animation
|
|
648
|
+
*/
|
|
649
|
+
function getFallTransition(custom) {
|
|
650
|
+
return {
|
|
651
|
+
duration: custom.duration,
|
|
652
|
+
ease: 'linear',
|
|
653
|
+
repeat: Infinity,
|
|
654
|
+
delay: custom.delay,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Rise animation variants - used for hearts, lanterns
|
|
659
|
+
* Particles rise from bottom to top with gentle sway
|
|
660
|
+
*/
|
|
661
|
+
const riseVariants = {
|
|
662
|
+
initial: (custom) => ({
|
|
663
|
+
y: custom.viewportHeight + 50,
|
|
664
|
+
opacity: 0,
|
|
665
|
+
scale: 0,
|
|
666
|
+
}),
|
|
667
|
+
animate: (custom) => ({
|
|
668
|
+
y: -50,
|
|
669
|
+
x: [0, custom.drift / 2, -custom.drift / 2, 0],
|
|
670
|
+
opacity: [0, 1, 1, 0],
|
|
671
|
+
scale: [0, 1, 1.2, 1, 0],
|
|
672
|
+
}),
|
|
673
|
+
};
|
|
674
|
+
/**
|
|
675
|
+
* Get transition for rise animation
|
|
676
|
+
*/
|
|
677
|
+
function getRiseTransition(custom) {
|
|
678
|
+
return {
|
|
679
|
+
duration: custom.duration,
|
|
680
|
+
ease: 'easeOut',
|
|
681
|
+
repeat: Infinity,
|
|
682
|
+
delay: custom.delay,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Explode animation variants - used for fireworks, confetti
|
|
687
|
+
* Particles burst outward from center point
|
|
688
|
+
*/
|
|
689
|
+
const explodeVariants = {
|
|
690
|
+
initial: {
|
|
691
|
+
scale: 0,
|
|
692
|
+
opacity: 1,
|
|
693
|
+
},
|
|
694
|
+
animate: (custom) => ({
|
|
695
|
+
x: Math.cos(custom.angle) * custom.distance,
|
|
696
|
+
y: Math.sin(custom.angle) * custom.distance,
|
|
697
|
+
scale: [0, 1.5, 0],
|
|
698
|
+
opacity: [1, 1, 0],
|
|
699
|
+
}),
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Get transition for explode animation
|
|
703
|
+
*/
|
|
704
|
+
function getExplodeTransition(custom) {
|
|
705
|
+
return {
|
|
706
|
+
duration: custom.duration * 0.5,
|
|
707
|
+
ease: [0.25, 0.46, 0.45, 0.94],
|
|
708
|
+
repeat: Infinity,
|
|
709
|
+
repeatDelay: custom.duration * 0.5,
|
|
710
|
+
delay: custom.delay,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Float animation variants - used for bats, diyas, moons
|
|
715
|
+
* Particles float with gentle bobbing motion
|
|
716
|
+
*/
|
|
717
|
+
const floatVariants = {
|
|
718
|
+
initial: {
|
|
719
|
+
opacity: 0,
|
|
720
|
+
scale: 0.8,
|
|
721
|
+
},
|
|
722
|
+
animate: (custom) => ({
|
|
723
|
+
x: [0, custom.drift, -custom.drift, 0],
|
|
724
|
+
y: [0, -20, 10, 0],
|
|
725
|
+
opacity: [0, 1, 1, 0],
|
|
726
|
+
scale: [0.8, 1, 1.1, 1, 0.8],
|
|
727
|
+
rotate: [0, custom.rotation / 4, -custom.rotation / 4, 0],
|
|
728
|
+
}),
|
|
729
|
+
};
|
|
730
|
+
/**
|
|
731
|
+
* Get transition for float animation
|
|
732
|
+
*/
|
|
733
|
+
function getFloatTransition(custom) {
|
|
734
|
+
return {
|
|
735
|
+
duration: custom.duration,
|
|
736
|
+
ease: 'easeInOut',
|
|
737
|
+
repeat: Infinity,
|
|
738
|
+
delay: custom.delay,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Scatter animation variants - used for holi colors
|
|
743
|
+
* Particles scatter outward with fading effect
|
|
744
|
+
*/
|
|
745
|
+
const scatterVariants = {
|
|
746
|
+
initial: {
|
|
747
|
+
scale: 0,
|
|
748
|
+
opacity: 0.8,
|
|
749
|
+
},
|
|
750
|
+
animate: (custom) => ({
|
|
751
|
+
x: Math.cos(custom.angle) * custom.distance,
|
|
752
|
+
y: Math.sin(custom.angle) * custom.distance + 50,
|
|
753
|
+
scale: [0, 2, 0],
|
|
754
|
+
opacity: [0.8, 0.6, 0],
|
|
755
|
+
rotate: custom.rotation,
|
|
756
|
+
}),
|
|
757
|
+
};
|
|
758
|
+
/**
|
|
759
|
+
* Get transition for scatter animation
|
|
760
|
+
*/
|
|
761
|
+
function getScatterTransition(custom) {
|
|
762
|
+
return {
|
|
763
|
+
duration: custom.duration * 0.7,
|
|
764
|
+
ease: 'easeOut',
|
|
765
|
+
repeat: Infinity,
|
|
766
|
+
repeatDelay: custom.duration * 0.3,
|
|
767
|
+
delay: custom.delay,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Map of animation types to their variants
|
|
772
|
+
*/
|
|
773
|
+
const ANIMATION_VARIANTS = {
|
|
774
|
+
fall: fallVariants,
|
|
775
|
+
rise: riseVariants,
|
|
776
|
+
explode: explodeVariants,
|
|
777
|
+
float: floatVariants,
|
|
778
|
+
scatter: scatterVariants,
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* Get the appropriate transition function for an animation type
|
|
782
|
+
*/
|
|
783
|
+
function getTransitionForType(animationType, custom) {
|
|
784
|
+
switch (animationType) {
|
|
785
|
+
case 'fall':
|
|
786
|
+
return getFallTransition(custom);
|
|
787
|
+
case 'rise':
|
|
788
|
+
return getRiseTransition(custom);
|
|
789
|
+
case 'explode':
|
|
790
|
+
return getExplodeTransition(custom);
|
|
791
|
+
case 'float':
|
|
792
|
+
return getFloatTransition(custom);
|
|
793
|
+
case 'scatter':
|
|
794
|
+
return getScatterTransition(custom);
|
|
795
|
+
default:
|
|
796
|
+
return { duration: 1 };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Create custom animation data from particle properties
|
|
801
|
+
*/
|
|
802
|
+
function createAnimationCustomData(x, y, physics, delay, duration, viewportHeight) {
|
|
803
|
+
return {
|
|
804
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
805
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
806
|
+
delay,
|
|
807
|
+
duration,
|
|
808
|
+
angle: Math.random() * Math.PI * 2,
|
|
809
|
+
distance: 50 + Math.random() * 150,
|
|
810
|
+
x,
|
|
811
|
+
y,
|
|
812
|
+
viewportHeight,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Get animation props for a particle based on animation type
|
|
817
|
+
* Returns initial, animate, and transition properties for Framer Motion
|
|
818
|
+
*/
|
|
819
|
+
function getAnimationPropsForType(animationType, custom) {
|
|
820
|
+
const transition = getTransitionForType(animationType, custom);
|
|
821
|
+
// Get initial and animate values based on animation type
|
|
822
|
+
// We compute these directly rather than using variant functions
|
|
823
|
+
// to avoid TypeScript issues with Framer Motion's TargetResolver signature
|
|
824
|
+
let initial;
|
|
825
|
+
let animate;
|
|
826
|
+
switch (animationType) {
|
|
827
|
+
case 'fall':
|
|
828
|
+
initial = { y: -50, opacity: 0, rotate: 0 };
|
|
829
|
+
animate = {
|
|
830
|
+
y: custom.viewportHeight + 50,
|
|
831
|
+
x: custom.drift,
|
|
832
|
+
opacity: [0, 1, 1, 0],
|
|
833
|
+
rotate: custom.rotation,
|
|
834
|
+
};
|
|
835
|
+
break;
|
|
836
|
+
case 'rise':
|
|
837
|
+
initial = { y: custom.viewportHeight + 50, opacity: 0, scale: 0 };
|
|
838
|
+
animate = {
|
|
839
|
+
y: -50,
|
|
840
|
+
x: [0, custom.drift / 2, -custom.drift / 2, 0],
|
|
841
|
+
opacity: [0, 1, 1, 0],
|
|
842
|
+
scale: [0, 1, 1.2, 1, 0],
|
|
843
|
+
};
|
|
844
|
+
break;
|
|
845
|
+
case 'explode':
|
|
846
|
+
initial = { scale: 0, opacity: 1 };
|
|
847
|
+
animate = {
|
|
848
|
+
x: Math.cos(custom.angle) * custom.distance,
|
|
849
|
+
y: Math.sin(custom.angle) * custom.distance,
|
|
850
|
+
scale: [0, 1.5, 0],
|
|
851
|
+
opacity: [1, 1, 0],
|
|
852
|
+
};
|
|
853
|
+
break;
|
|
854
|
+
case 'float':
|
|
855
|
+
initial = { opacity: 0, scale: 0.8 };
|
|
856
|
+
animate = {
|
|
857
|
+
x: [0, custom.drift, -custom.drift, 0],
|
|
858
|
+
y: [0, -20, 10, 0],
|
|
859
|
+
opacity: [0, 1, 1, 0],
|
|
860
|
+
scale: [0.8, 1, 1.1, 1, 0.8],
|
|
861
|
+
rotate: [0, custom.rotation / 4, -custom.rotation / 4, 0],
|
|
862
|
+
};
|
|
863
|
+
break;
|
|
864
|
+
case 'scatter':
|
|
865
|
+
initial = { scale: 0, opacity: 0.8 };
|
|
866
|
+
animate = {
|
|
867
|
+
x: Math.cos(custom.angle) * custom.distance,
|
|
868
|
+
y: Math.sin(custom.angle) * custom.distance + 50,
|
|
869
|
+
scale: [0, 2, 0],
|
|
870
|
+
opacity: [0.8, 0.6, 0],
|
|
871
|
+
rotate: custom.rotation,
|
|
872
|
+
};
|
|
873
|
+
break;
|
|
874
|
+
default:
|
|
875
|
+
initial = { opacity: 0 };
|
|
876
|
+
animate = { opacity: 1 };
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
initial,
|
|
880
|
+
animate,
|
|
881
|
+
transition,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Festival Registry for Festive Effects
|
|
886
|
+
// Manages registration and retrieval of festival effects
|
|
887
|
+
/**
|
|
888
|
+
* Create a festival effect from a festival type
|
|
889
|
+
* @param festivalType - The type of festival
|
|
890
|
+
* @returns A FestivalEffect instance
|
|
891
|
+
*/
|
|
892
|
+
function createFestivalEffect(festivalType) {
|
|
893
|
+
const config = FESTIVAL_CONFIGS[festivalType];
|
|
894
|
+
return {
|
|
895
|
+
name: festivalType,
|
|
896
|
+
generateParticles(count, viewport) {
|
|
897
|
+
return generateParticles(count, viewport, config.particleTypes, config.colors, config.physics, config.animationType);
|
|
898
|
+
},
|
|
899
|
+
getConfig() {
|
|
900
|
+
return config;
|
|
901
|
+
},
|
|
902
|
+
renderParticle(particle) {
|
|
903
|
+
const props = {
|
|
904
|
+
particle,
|
|
905
|
+
animationType: config.animationType,
|
|
906
|
+
physics: config.physics,
|
|
907
|
+
};
|
|
908
|
+
return React.createElement(Particle, { key: particle.id, ...props });
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Registry for managing festival effects
|
|
914
|
+
* Provides methods to register, retrieve, and list festival effects
|
|
915
|
+
*/
|
|
916
|
+
class FestivalRegistry {
|
|
917
|
+
constructor() {
|
|
918
|
+
this.effects = new Map();
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Register a festival effect
|
|
922
|
+
* @param effect - The festival effect to register
|
|
923
|
+
*/
|
|
924
|
+
register(effect) {
|
|
925
|
+
this.effects.set(effect.name, effect);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get a festival effect by type
|
|
929
|
+
* @param festival - The festival type to retrieve
|
|
930
|
+
* @returns The festival effect or undefined if not found
|
|
931
|
+
*/
|
|
932
|
+
get(festival) {
|
|
933
|
+
return this.effects.get(festival);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Check if a festival effect is registered
|
|
937
|
+
* @param festival - The festival type to check
|
|
938
|
+
* @returns True if the effect is registered
|
|
939
|
+
*/
|
|
940
|
+
has(festival) {
|
|
941
|
+
return this.effects.has(festival);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* List all registered festival types
|
|
945
|
+
* @returns Array of registered festival types
|
|
946
|
+
*/
|
|
947
|
+
list() {
|
|
948
|
+
return Array.from(this.effects.keys());
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Get the number of registered effects
|
|
952
|
+
* @returns The count of registered effects
|
|
953
|
+
*/
|
|
954
|
+
get size() {
|
|
955
|
+
return this.effects.size;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Clear all registered effects
|
|
959
|
+
*/
|
|
960
|
+
clear() {
|
|
961
|
+
this.effects.clear();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Default registry instance with all 12 festival effects pre-registered
|
|
966
|
+
*/
|
|
967
|
+
const defaultRegistry = new FestivalRegistry();
|
|
968
|
+
// Register all 12 festival effects
|
|
969
|
+
const ALL_FESTIVALS = [
|
|
970
|
+
'christmas',
|
|
971
|
+
'newyear',
|
|
972
|
+
'valentine',
|
|
973
|
+
'easter',
|
|
974
|
+
'halloween',
|
|
975
|
+
'thanksgiving',
|
|
976
|
+
'diwali',
|
|
977
|
+
'chinesenewyear',
|
|
978
|
+
'holi',
|
|
979
|
+
'eid',
|
|
980
|
+
'stpatricks',
|
|
981
|
+
'independence',
|
|
982
|
+
];
|
|
983
|
+
ALL_FESTIVALS.forEach((festivalType) => {
|
|
984
|
+
defaultRegistry.register(createFestivalEffect(festivalType));
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Christmas effect configuration
|
|
989
|
+
*/
|
|
990
|
+
const CHRISTMAS_CONFIG = {
|
|
991
|
+
baseParticleCount: 50,
|
|
992
|
+
colors: ['#FFFFFF', '#E8F4FF', '#B8D4E8', '#87CEEB'],
|
|
993
|
+
particleTypes: ['snowflake'],
|
|
994
|
+
animationType: 'fall',
|
|
995
|
+
physics: {
|
|
996
|
+
speed: 3,
|
|
997
|
+
drift: 50,
|
|
998
|
+
rotation: 360,
|
|
999
|
+
scale: [0.5, 1.5],
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
/**
|
|
1003
|
+
* Individual snowflake particle with falling animation
|
|
1004
|
+
*/
|
|
1005
|
+
const ChristmasParticle = ({ particle, viewportHeight }) => {
|
|
1006
|
+
const { drift, rotation } = particle.custom;
|
|
1007
|
+
return (jsx(motion.div, { style: {
|
|
1008
|
+
position: 'absolute',
|
|
1009
|
+
left: particle.x,
|
|
1010
|
+
width: particle.size,
|
|
1011
|
+
height: particle.size,
|
|
1012
|
+
color: particle.color,
|
|
1013
|
+
pointerEvents: 'none',
|
|
1014
|
+
}, initial: { y: -50, opacity: 0, rotate: 0 }, animate: {
|
|
1015
|
+
y: viewportHeight + 50,
|
|
1016
|
+
x: drift,
|
|
1017
|
+
opacity: [0, 1, 1, 0],
|
|
1018
|
+
rotate: rotation,
|
|
1019
|
+
}, transition: {
|
|
1020
|
+
duration: particle.duration,
|
|
1021
|
+
ease: 'linear',
|
|
1022
|
+
repeat: Infinity,
|
|
1023
|
+
delay: particle.delay,
|
|
1024
|
+
}, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS.snowflake } }));
|
|
1025
|
+
};
|
|
1026
|
+
/**
|
|
1027
|
+
* Generate snowflake particles for Christmas effect
|
|
1028
|
+
*/
|
|
1029
|
+
function generateChristmasParticles(count, viewport, colors) {
|
|
1030
|
+
const effectColors = colors && colors.length > 0 ? colors : CHRISTMAS_CONFIG.colors;
|
|
1031
|
+
const physics = CHRISTMAS_CONFIG.physics;
|
|
1032
|
+
const particles = [];
|
|
1033
|
+
for (let i = 0; i < count; i++) {
|
|
1034
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1035
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1036
|
+
particles.push({
|
|
1037
|
+
id: `christmas-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1038
|
+
x: Math.random() * viewport.width,
|
|
1039
|
+
y: -50 - Math.random() * 80,
|
|
1040
|
+
size,
|
|
1041
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1042
|
+
type: 'snowflake',
|
|
1043
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1044
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1045
|
+
custom: {
|
|
1046
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1047
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
return particles;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Christmas Effect Component
|
|
1055
|
+
* Renders falling snowflakes with drift and rotation
|
|
1056
|
+
* White/blue color palette
|
|
1057
|
+
*/
|
|
1058
|
+
const ChristmasEffect = ({ particles, viewport }) => {
|
|
1059
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(ChristmasParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* New Year effect configuration
|
|
1064
|
+
*/
|
|
1065
|
+
const NEWYEAR_CONFIG = {
|
|
1066
|
+
baseParticleCount: 40,
|
|
1067
|
+
colors: ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'],
|
|
1068
|
+
particleTypes: ['firework', 'confetti', 'spark'],
|
|
1069
|
+
animationType: 'explode',
|
|
1070
|
+
physics: {
|
|
1071
|
+
speed: 5,
|
|
1072
|
+
drift: 100,
|
|
1073
|
+
rotation: 720,
|
|
1074
|
+
scale: [0.3, 1.2],
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* Individual particle with explode animation for fireworks/confetti/sparks
|
|
1079
|
+
*/
|
|
1080
|
+
const NewYearParticle = ({ particle }) => {
|
|
1081
|
+
const { angle, distance } = particle.custom;
|
|
1082
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1083
|
+
return (jsx(motion.div, { style: {
|
|
1084
|
+
position: 'absolute',
|
|
1085
|
+
left: particle.x,
|
|
1086
|
+
top: particle.y,
|
|
1087
|
+
width: particle.size,
|
|
1088
|
+
height: particle.size,
|
|
1089
|
+
color: particle.color,
|
|
1090
|
+
pointerEvents: 'none',
|
|
1091
|
+
}, initial: { scale: 0, opacity: 1 }, animate: {
|
|
1092
|
+
x: Math.cos(angle) * distance,
|
|
1093
|
+
y: Math.sin(angle) * distance,
|
|
1094
|
+
scale: [0, 1.5, 0],
|
|
1095
|
+
opacity: [1, 1, 0],
|
|
1096
|
+
}, transition: {
|
|
1097
|
+
duration: particle.duration * 0.5,
|
|
1098
|
+
ease: [0.25, 0.46, 0.45, 0.94],
|
|
1099
|
+
repeat: Infinity,
|
|
1100
|
+
repeatDelay: particle.duration * 0.5,
|
|
1101
|
+
delay: particle.delay,
|
|
1102
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1103
|
+
};
|
|
1104
|
+
/**
|
|
1105
|
+
* Generate particles for New Year effect
|
|
1106
|
+
*/
|
|
1107
|
+
function generateNewYearParticles(count, viewport, colors) {
|
|
1108
|
+
const effectColors = colors && colors.length > 0 ? colors : NEWYEAR_CONFIG.colors;
|
|
1109
|
+
const physics = NEWYEAR_CONFIG.physics;
|
|
1110
|
+
const particles = [];
|
|
1111
|
+
const particleTypes = NEWYEAR_CONFIG.particleTypes;
|
|
1112
|
+
for (let i = 0; i < count; i++) {
|
|
1113
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1114
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1115
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1116
|
+
particles.push({
|
|
1117
|
+
id: `newyear-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1118
|
+
x: viewport.width * 0.2 + Math.random() * viewport.width * 0.6,
|
|
1119
|
+
y: viewport.height * 0.2 + Math.random() * viewport.height * 0.4,
|
|
1120
|
+
size,
|
|
1121
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1122
|
+
type,
|
|
1123
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1124
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1125
|
+
custom: {
|
|
1126
|
+
angle: Math.random() * Math.PI * 2,
|
|
1127
|
+
distance: 100 + Math.random() * 150,
|
|
1128
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
return particles;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* New Year Effect Component
|
|
1136
|
+
* Renders exploding fireworks with sparks and falling confetti
|
|
1137
|
+
*/
|
|
1138
|
+
const NewYearEffect = ({ particles }) => {
|
|
1139
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(NewYearParticle, { particle: particle }, particle.id))) }));
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Valentine effect configuration
|
|
1144
|
+
*/
|
|
1145
|
+
const VALENTINE_CONFIG = {
|
|
1146
|
+
baseParticleCount: 35,
|
|
1147
|
+
colors: ['#FF6B6B', '#EE5A5A', '#FF8E8E', '#FFB6C1', '#FF69B4'],
|
|
1148
|
+
particleTypes: ['heart'],
|
|
1149
|
+
animationType: 'rise',
|
|
1150
|
+
physics: {
|
|
1151
|
+
speed: 2,
|
|
1152
|
+
drift: 30,
|
|
1153
|
+
rotation: 15,
|
|
1154
|
+
scale: [0.6, 1.4],
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
/**
|
|
1158
|
+
* Individual heart particle with rising animation and gentle sway
|
|
1159
|
+
*/
|
|
1160
|
+
const ValentineParticle = ({ particle, viewportHeight }) => {
|
|
1161
|
+
const { drift } = particle.custom;
|
|
1162
|
+
return (jsx(motion.div, { style: {
|
|
1163
|
+
position: 'absolute',
|
|
1164
|
+
left: particle.x,
|
|
1165
|
+
width: particle.size,
|
|
1166
|
+
height: particle.size,
|
|
1167
|
+
color: particle.color,
|
|
1168
|
+
pointerEvents: 'none',
|
|
1169
|
+
}, initial: { y: viewportHeight + 50, opacity: 0, scale: 0 }, animate: {
|
|
1170
|
+
y: -50,
|
|
1171
|
+
x: [0, drift / 2, -drift / 2, 0],
|
|
1172
|
+
opacity: [0, 1, 1, 0],
|
|
1173
|
+
scale: [0, 1, 1.2, 1, 0],
|
|
1174
|
+
}, transition: {
|
|
1175
|
+
duration: particle.duration,
|
|
1176
|
+
ease: 'easeOut',
|
|
1177
|
+
repeat: Infinity,
|
|
1178
|
+
delay: particle.delay,
|
|
1179
|
+
}, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS.heart } }));
|
|
1180
|
+
};
|
|
1181
|
+
/**
|
|
1182
|
+
* Generate heart particles for Valentine effect
|
|
1183
|
+
*/
|
|
1184
|
+
function generateValentineParticles(count, viewport, colors) {
|
|
1185
|
+
const effectColors = colors && colors.length > 0 ? colors : VALENTINE_CONFIG.colors;
|
|
1186
|
+
const physics = VALENTINE_CONFIG.physics;
|
|
1187
|
+
const particles = [];
|
|
1188
|
+
for (let i = 0; i < count; i++) {
|
|
1189
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1190
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1191
|
+
particles.push({
|
|
1192
|
+
id: `valentine-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1193
|
+
x: Math.random() * viewport.width,
|
|
1194
|
+
y: viewport.height + 50 + Math.random() * 80,
|
|
1195
|
+
size,
|
|
1196
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1197
|
+
type: 'heart',
|
|
1198
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1199
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1200
|
+
custom: {
|
|
1201
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1202
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
return particles;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Valentine Effect Component
|
|
1210
|
+
* Renders rising hearts with gentle sway
|
|
1211
|
+
* Pink/red color palette
|
|
1212
|
+
*/
|
|
1213
|
+
const ValentineEffect = ({ particles, viewport }) => {
|
|
1214
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(ValentineParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Easter effect configuration
|
|
1219
|
+
*/
|
|
1220
|
+
const EASTER_CONFIG = {
|
|
1221
|
+
baseParticleCount: 30,
|
|
1222
|
+
colors: ['#FFB6C1', '#98FB98', '#87CEEB', '#DDA0DD', '#F0E68C'],
|
|
1223
|
+
particleTypes: ['egg', 'flower'],
|
|
1224
|
+
animationType: 'float',
|
|
1225
|
+
physics: {
|
|
1226
|
+
speed: 1.5,
|
|
1227
|
+
drift: 20,
|
|
1228
|
+
rotation: 10,
|
|
1229
|
+
scale: [0.7, 1.3],
|
|
1230
|
+
},
|
|
1231
|
+
};
|
|
1232
|
+
/**
|
|
1233
|
+
* Individual egg/flower particle with floating animation
|
|
1234
|
+
*/
|
|
1235
|
+
const EasterParticle = ({ particle }) => {
|
|
1236
|
+
const { drift, rotation } = particle.custom;
|
|
1237
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1238
|
+
return (jsx(motion.div, { style: {
|
|
1239
|
+
position: 'absolute',
|
|
1240
|
+
left: particle.x,
|
|
1241
|
+
top: particle.y,
|
|
1242
|
+
width: particle.size,
|
|
1243
|
+
height: particle.size,
|
|
1244
|
+
color: particle.color,
|
|
1245
|
+
pointerEvents: 'none',
|
|
1246
|
+
}, initial: { opacity: 0, scale: 0.8 }, animate: {
|
|
1247
|
+
x: [0, drift, -drift, 0],
|
|
1248
|
+
y: [0, -20, 10, 0],
|
|
1249
|
+
opacity: [0, 1, 1, 0],
|
|
1250
|
+
scale: [0.8, 1, 1.1, 1, 0.8],
|
|
1251
|
+
rotate: [0, rotation / 4, -rotation / 4, 0],
|
|
1252
|
+
}, transition: {
|
|
1253
|
+
duration: particle.duration,
|
|
1254
|
+
ease: 'easeInOut',
|
|
1255
|
+
repeat: Infinity,
|
|
1256
|
+
delay: particle.delay,
|
|
1257
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1258
|
+
};
|
|
1259
|
+
/**
|
|
1260
|
+
* Generate particles for Easter effect
|
|
1261
|
+
*/
|
|
1262
|
+
function generateEasterParticles(count, viewport, colors) {
|
|
1263
|
+
const effectColors = colors && colors.length > 0 ? colors : EASTER_CONFIG.colors;
|
|
1264
|
+
const physics = EASTER_CONFIG.physics;
|
|
1265
|
+
const particles = [];
|
|
1266
|
+
const particleTypes = EASTER_CONFIG.particleTypes;
|
|
1267
|
+
for (let i = 0; i < count; i++) {
|
|
1268
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1269
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1270
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1271
|
+
particles.push({
|
|
1272
|
+
id: `easter-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1273
|
+
x: Math.random() * viewport.width,
|
|
1274
|
+
y: Math.random() * viewport.height,
|
|
1275
|
+
size,
|
|
1276
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1277
|
+
type,
|
|
1278
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1279
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1280
|
+
custom: {
|
|
1281
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1282
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
return particles;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Easter Effect Component
|
|
1290
|
+
* Renders floating eggs and spring flowers
|
|
1291
|
+
* Pastel color palette
|
|
1292
|
+
*/
|
|
1293
|
+
const EasterEffect = ({ particles }) => {
|
|
1294
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(EasterParticle, { particle: particle }, particle.id))) }));
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Halloween effect configuration
|
|
1299
|
+
*/
|
|
1300
|
+
const HALLOWEEN_CONFIG = {
|
|
1301
|
+
baseParticleCount: 35,
|
|
1302
|
+
colors: ['#FF6600', '#8B008B', '#000000', '#FFD700', '#32CD32'],
|
|
1303
|
+
particleTypes: ['bat', 'pumpkin', 'spider'],
|
|
1304
|
+
animationType: 'float',
|
|
1305
|
+
physics: {
|
|
1306
|
+
speed: 2,
|
|
1307
|
+
drift: 60,
|
|
1308
|
+
rotation: 30,
|
|
1309
|
+
scale: [0.5, 1.5],
|
|
1310
|
+
},
|
|
1311
|
+
};
|
|
1312
|
+
/**
|
|
1313
|
+
* Individual bat/pumpkin/spider particle with erratic floating animation
|
|
1314
|
+
*/
|
|
1315
|
+
const HalloweenParticle = ({ particle }) => {
|
|
1316
|
+
const { drift, rotation } = particle.custom;
|
|
1317
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1318
|
+
// Bats have more erratic movement
|
|
1319
|
+
const isBat = particle.type === 'bat';
|
|
1320
|
+
const driftMultiplier = isBat ? 1.5 : 1;
|
|
1321
|
+
const rotationMultiplier = isBat ? 2 : 1;
|
|
1322
|
+
return (jsx(motion.div, { style: {
|
|
1323
|
+
position: 'absolute',
|
|
1324
|
+
left: particle.x,
|
|
1325
|
+
top: particle.y,
|
|
1326
|
+
width: particle.size,
|
|
1327
|
+
height: particle.size,
|
|
1328
|
+
color: particle.color,
|
|
1329
|
+
pointerEvents: 'none',
|
|
1330
|
+
}, initial: { opacity: 0, scale: 0.8 }, animate: {
|
|
1331
|
+
x: [0, drift * driftMultiplier, -drift * driftMultiplier, drift * 0.5, 0],
|
|
1332
|
+
y: [0, -30, 15, -20, 0],
|
|
1333
|
+
opacity: [0, 1, 1, 1, 0],
|
|
1334
|
+
scale: [0.8, 1, 1.1, 1, 0.8],
|
|
1335
|
+
rotate: [0, rotation * rotationMultiplier / 4, -rotation * rotationMultiplier / 4, rotation / 8, 0],
|
|
1336
|
+
}, transition: {
|
|
1337
|
+
duration: particle.duration,
|
|
1338
|
+
ease: 'easeInOut',
|
|
1339
|
+
repeat: Infinity,
|
|
1340
|
+
delay: particle.delay,
|
|
1341
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1342
|
+
};
|
|
1343
|
+
/**
|
|
1344
|
+
* Generate particles for Halloween effect
|
|
1345
|
+
*/
|
|
1346
|
+
function generateHalloweenParticles(count, viewport, colors) {
|
|
1347
|
+
const effectColors = colors && colors.length > 0 ? colors : HALLOWEEN_CONFIG.colors;
|
|
1348
|
+
const physics = HALLOWEEN_CONFIG.physics;
|
|
1349
|
+
const particles = [];
|
|
1350
|
+
const particleTypes = HALLOWEEN_CONFIG.particleTypes;
|
|
1351
|
+
for (let i = 0; i < count; i++) {
|
|
1352
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1353
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1354
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1355
|
+
particles.push({
|
|
1356
|
+
id: `halloween-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1357
|
+
x: Math.random() * viewport.width,
|
|
1358
|
+
y: Math.random() * viewport.height,
|
|
1359
|
+
size,
|
|
1360
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1361
|
+
type,
|
|
1362
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1363
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1364
|
+
custom: {
|
|
1365
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1366
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return particles;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Halloween Effect Component
|
|
1374
|
+
* Renders flying bats with erratic movement, floating pumpkins and spiders
|
|
1375
|
+
* Orange/purple/black color palette
|
|
1376
|
+
*/
|
|
1377
|
+
const HalloweenEffect = ({ particles }) => {
|
|
1378
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(HalloweenParticle, { particle: particle }, particle.id))) }));
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Thanksgiving effect configuration
|
|
1383
|
+
*/
|
|
1384
|
+
const THANKSGIVING_CONFIG = {
|
|
1385
|
+
baseParticleCount: 40,
|
|
1386
|
+
colors: ['#D2691E', '#FF8C00', '#B8860B', '#CD853F', '#8B4513'],
|
|
1387
|
+
particleTypes: ['leaf'],
|
|
1388
|
+
animationType: 'fall',
|
|
1389
|
+
physics: {
|
|
1390
|
+
speed: 2,
|
|
1391
|
+
drift: 80,
|
|
1392
|
+
rotation: 540,
|
|
1393
|
+
scale: [0.6, 1.4],
|
|
1394
|
+
},
|
|
1395
|
+
};
|
|
1396
|
+
/**
|
|
1397
|
+
* Individual leaf particle with tumbling fall animation
|
|
1398
|
+
*/
|
|
1399
|
+
const ThanksgivingParticle = ({ particle, viewportHeight }) => {
|
|
1400
|
+
const { drift, rotation } = particle.custom;
|
|
1401
|
+
return (jsx(motion.div, { style: {
|
|
1402
|
+
position: 'absolute',
|
|
1403
|
+
left: particle.x,
|
|
1404
|
+
width: particle.size,
|
|
1405
|
+
height: particle.size,
|
|
1406
|
+
color: particle.color,
|
|
1407
|
+
pointerEvents: 'none',
|
|
1408
|
+
}, initial: { y: -50, opacity: 0, rotate: 0 }, animate: {
|
|
1409
|
+
y: viewportHeight + 50,
|
|
1410
|
+
x: [0, drift * 0.3, -drift * 0.2, drift * 0.4, drift],
|
|
1411
|
+
opacity: [0, 1, 1, 1, 0],
|
|
1412
|
+
rotate: rotation,
|
|
1413
|
+
}, transition: {
|
|
1414
|
+
duration: particle.duration,
|
|
1415
|
+
ease: 'linear',
|
|
1416
|
+
repeat: Infinity,
|
|
1417
|
+
delay: particle.delay,
|
|
1418
|
+
}, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS.leaf } }));
|
|
1419
|
+
};
|
|
1420
|
+
/**
|
|
1421
|
+
* Generate leaf particles for Thanksgiving effect
|
|
1422
|
+
*/
|
|
1423
|
+
function generateThanksgivingParticles(count, viewport, colors) {
|
|
1424
|
+
const effectColors = colors && colors.length > 0 ? colors : THANKSGIVING_CONFIG.colors;
|
|
1425
|
+
const physics = THANKSGIVING_CONFIG.physics;
|
|
1426
|
+
const particles = [];
|
|
1427
|
+
for (let i = 0; i < count; i++) {
|
|
1428
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1429
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1430
|
+
particles.push({
|
|
1431
|
+
id: `thanksgiving-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1432
|
+
x: Math.random() * viewport.width,
|
|
1433
|
+
y: -50 - Math.random() * 80,
|
|
1434
|
+
size,
|
|
1435
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1436
|
+
type: 'leaf',
|
|
1437
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1438
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1439
|
+
custom: {
|
|
1440
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1441
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
return particles;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Thanksgiving Effect Component
|
|
1449
|
+
* Renders falling autumn leaves with tumbling rotation
|
|
1450
|
+
* Orange/brown color palette
|
|
1451
|
+
*/
|
|
1452
|
+
const ThanksgivingEffect = ({ particles, viewport }) => {
|
|
1453
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(ThanksgivingParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Diwali effect configuration
|
|
1458
|
+
*/
|
|
1459
|
+
const DIWALI_CONFIG = {
|
|
1460
|
+
baseParticleCount: 45,
|
|
1461
|
+
colors: ['#FFD700', '#FF8C00', '#FF4500', '#FFA500', '#FFFF00'],
|
|
1462
|
+
particleTypes: ['diya', 'spark', 'rangoli'],
|
|
1463
|
+
animationType: 'float',
|
|
1464
|
+
physics: {
|
|
1465
|
+
speed: 1,
|
|
1466
|
+
drift: 15,
|
|
1467
|
+
rotation: 5,
|
|
1468
|
+
scale: [0.5, 1.2],
|
|
1469
|
+
},
|
|
1470
|
+
};
|
|
1471
|
+
/**
|
|
1472
|
+
* Individual diya/spark/rangoli particle with glowing float animation
|
|
1473
|
+
*/
|
|
1474
|
+
const DiwaliParticle = ({ particle }) => {
|
|
1475
|
+
const { drift, rotation } = particle.custom;
|
|
1476
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1477
|
+
// Diyas have flickering effect, sparks have more movement
|
|
1478
|
+
const isDiya = particle.type === 'diya';
|
|
1479
|
+
const isSpark = particle.type === 'spark';
|
|
1480
|
+
return (jsx(motion.div, { style: {
|
|
1481
|
+
position: 'absolute',
|
|
1482
|
+
left: particle.x,
|
|
1483
|
+
top: particle.y,
|
|
1484
|
+
width: particle.size,
|
|
1485
|
+
height: particle.size,
|
|
1486
|
+
color: particle.color,
|
|
1487
|
+
pointerEvents: 'none',
|
|
1488
|
+
filter: isDiya ? 'drop-shadow(0 0 8px #FFD700)' : isSpark ? 'drop-shadow(0 0 4px #FFA500)' : undefined,
|
|
1489
|
+
}, initial: { opacity: 0, scale: 0.8 }, animate: {
|
|
1490
|
+
x: [0, drift, -drift, 0],
|
|
1491
|
+
y: [0, -10, 5, 0],
|
|
1492
|
+
opacity: isDiya ? [0, 1, 0.8, 1, 0] : [0, 1, 1, 0],
|
|
1493
|
+
scale: isDiya ? [0.8, 1, 0.95, 1.05, 0.8] : [0.8, 1, 1.1, 1, 0.8],
|
|
1494
|
+
rotate: [0, rotation / 4, -rotation / 4, 0],
|
|
1495
|
+
}, transition: {
|
|
1496
|
+
duration: particle.duration,
|
|
1497
|
+
ease: 'easeInOut',
|
|
1498
|
+
repeat: Infinity,
|
|
1499
|
+
delay: particle.delay,
|
|
1500
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1501
|
+
};
|
|
1502
|
+
/**
|
|
1503
|
+
* Generate particles for Diwali effect
|
|
1504
|
+
*/
|
|
1505
|
+
function generateDiwaliParticles(count, viewport, colors) {
|
|
1506
|
+
const effectColors = colors && colors.length > 0 ? colors : DIWALI_CONFIG.colors;
|
|
1507
|
+
const physics = DIWALI_CONFIG.physics;
|
|
1508
|
+
const particles = [];
|
|
1509
|
+
const particleTypes = DIWALI_CONFIG.particleTypes;
|
|
1510
|
+
for (let i = 0; i < count; i++) {
|
|
1511
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1512
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1513
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1514
|
+
particles.push({
|
|
1515
|
+
id: `diwali-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1516
|
+
x: Math.random() * viewport.width,
|
|
1517
|
+
y: Math.random() * viewport.height,
|
|
1518
|
+
size,
|
|
1519
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1520
|
+
type,
|
|
1521
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1522
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1523
|
+
custom: {
|
|
1524
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1525
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1526
|
+
},
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return particles;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Diwali Effect Component
|
|
1533
|
+
* Renders glowing diyas with flickering, golden sparkles and rangoli patterns
|
|
1534
|
+
* Golden/orange color palette
|
|
1535
|
+
*/
|
|
1536
|
+
const DiwaliEffect = ({ particles }) => {
|
|
1537
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(DiwaliParticle, { particle: particle }, particle.id))) }));
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Chinese New Year effect configuration
|
|
1542
|
+
*/
|
|
1543
|
+
const CHINESENEWYEAR_CONFIG = {
|
|
1544
|
+
baseParticleCount: 40,
|
|
1545
|
+
colors: ['#FF0000', '#FFD700', '#FF4500', '#DC143C'],
|
|
1546
|
+
particleTypes: ['lantern', 'firework', 'dragon'],
|
|
1547
|
+
animationType: 'rise',
|
|
1548
|
+
physics: {
|
|
1549
|
+
speed: 1.5,
|
|
1550
|
+
drift: 25,
|
|
1551
|
+
rotation: 10,
|
|
1552
|
+
scale: [0.6, 1.3],
|
|
1553
|
+
},
|
|
1554
|
+
};
|
|
1555
|
+
/**
|
|
1556
|
+
* Individual lantern/firework/dragon particle with rising animation
|
|
1557
|
+
*/
|
|
1558
|
+
const ChineseNewYearParticle = ({ particle, viewportHeight }) => {
|
|
1559
|
+
const { drift, angle, distance } = particle.custom;
|
|
1560
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1561
|
+
// Fireworks explode, lanterns and dragons rise
|
|
1562
|
+
const isFirework = particle.type === 'firework';
|
|
1563
|
+
const isLantern = particle.type === 'lantern';
|
|
1564
|
+
if (isFirework) {
|
|
1565
|
+
return (jsx(motion.div, { style: {
|
|
1566
|
+
position: 'absolute',
|
|
1567
|
+
left: particle.x,
|
|
1568
|
+
top: particle.y,
|
|
1569
|
+
width: particle.size,
|
|
1570
|
+
height: particle.size,
|
|
1571
|
+
color: particle.color,
|
|
1572
|
+
pointerEvents: 'none',
|
|
1573
|
+
}, initial: { scale: 0, opacity: 1 }, animate: {
|
|
1574
|
+
x: Math.cos(angle) * distance,
|
|
1575
|
+
y: Math.sin(angle) * distance,
|
|
1576
|
+
scale: [0, 1.5, 0],
|
|
1577
|
+
opacity: [1, 1, 0],
|
|
1578
|
+
}, transition: {
|
|
1579
|
+
duration: particle.duration * 0.5,
|
|
1580
|
+
ease: [0.25, 0.46, 0.45, 0.94],
|
|
1581
|
+
repeat: Infinity,
|
|
1582
|
+
repeatDelay: particle.duration * 0.5,
|
|
1583
|
+
delay: particle.delay,
|
|
1584
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1585
|
+
}
|
|
1586
|
+
return (jsx(motion.div, { style: {
|
|
1587
|
+
position: 'absolute',
|
|
1588
|
+
left: particle.x,
|
|
1589
|
+
width: particle.size,
|
|
1590
|
+
height: particle.size,
|
|
1591
|
+
color: particle.color,
|
|
1592
|
+
pointerEvents: 'none',
|
|
1593
|
+
filter: isLantern ? 'drop-shadow(0 0 6px #FF0000)' : undefined,
|
|
1594
|
+
}, initial: { y: viewportHeight + 50, opacity: 0, scale: 0 }, animate: {
|
|
1595
|
+
y: -50,
|
|
1596
|
+
x: [0, drift / 2, -drift / 2, 0],
|
|
1597
|
+
opacity: [0, 1, 1, 0],
|
|
1598
|
+
scale: [0, 1, 1.1, 1, 0],
|
|
1599
|
+
}, transition: {
|
|
1600
|
+
duration: particle.duration,
|
|
1601
|
+
ease: 'easeOut',
|
|
1602
|
+
repeat: Infinity,
|
|
1603
|
+
delay: particle.delay,
|
|
1604
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1605
|
+
};
|
|
1606
|
+
/**
|
|
1607
|
+
* Generate particles for Chinese New Year effect
|
|
1608
|
+
*/
|
|
1609
|
+
function generateChineseNewYearParticles(count, viewport, colors) {
|
|
1610
|
+
const effectColors = colors && colors.length > 0 ? colors : CHINESENEWYEAR_CONFIG.colors;
|
|
1611
|
+
const physics = CHINESENEWYEAR_CONFIG.physics;
|
|
1612
|
+
const particles = [];
|
|
1613
|
+
const particleTypes = CHINESENEWYEAR_CONFIG.particleTypes;
|
|
1614
|
+
for (let i = 0; i < count; i++) {
|
|
1615
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1616
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1617
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1618
|
+
// Position based on particle type
|
|
1619
|
+
const isFirework = type === 'firework';
|
|
1620
|
+
const x = isFirework
|
|
1621
|
+
? viewport.width * 0.2 + Math.random() * viewport.width * 0.6
|
|
1622
|
+
: Math.random() * viewport.width;
|
|
1623
|
+
const y = isFirework
|
|
1624
|
+
? viewport.height * 0.2 + Math.random() * viewport.height * 0.4
|
|
1625
|
+
: viewport.height + 50 + Math.random() * 80;
|
|
1626
|
+
particles.push({
|
|
1627
|
+
id: `chinesenewyear-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1628
|
+
x,
|
|
1629
|
+
y,
|
|
1630
|
+
size,
|
|
1631
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1632
|
+
type,
|
|
1633
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1634
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1635
|
+
custom: {
|
|
1636
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1637
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1638
|
+
angle: Math.random() * Math.PI * 2,
|
|
1639
|
+
distance: 80 + Math.random() * 120,
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return particles;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Chinese New Year Effect Component
|
|
1647
|
+
* Renders rising red lanterns and golden fireworks
|
|
1648
|
+
* Red/gold color palette
|
|
1649
|
+
*/
|
|
1650
|
+
const ChineseNewYearEffect = ({ particles, viewport }) => {
|
|
1651
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(ChineseNewYearParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Holi effect configuration
|
|
1656
|
+
*/
|
|
1657
|
+
const HOLI_CONFIG = {
|
|
1658
|
+
baseParticleCount: 50,
|
|
1659
|
+
colors: ['#FF1493', '#00FF00', '#FFFF00', '#FF4500', '#9400D3', '#00BFFF'],
|
|
1660
|
+
particleTypes: ['color-splash'],
|
|
1661
|
+
animationType: 'scatter',
|
|
1662
|
+
physics: {
|
|
1663
|
+
speed: 4,
|
|
1664
|
+
drift: 150,
|
|
1665
|
+
rotation: 180,
|
|
1666
|
+
scale: [0.4, 2.0],
|
|
1667
|
+
},
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* Individual color splash particle with scatter animation
|
|
1671
|
+
*/
|
|
1672
|
+
const HoliParticle = ({ particle }) => {
|
|
1673
|
+
const { angle, distance, rotation } = particle.custom;
|
|
1674
|
+
return (jsx(motion.div, { style: {
|
|
1675
|
+
position: 'absolute',
|
|
1676
|
+
left: particle.x,
|
|
1677
|
+
top: particle.y,
|
|
1678
|
+
width: particle.size,
|
|
1679
|
+
height: particle.size,
|
|
1680
|
+
color: particle.color,
|
|
1681
|
+
pointerEvents: 'none',
|
|
1682
|
+
filter: `drop-shadow(0 0 10px ${particle.color})`,
|
|
1683
|
+
}, initial: { scale: 0, opacity: 0.8 }, animate: {
|
|
1684
|
+
x: Math.cos(angle) * distance,
|
|
1685
|
+
y: Math.sin(angle) * distance + 50,
|
|
1686
|
+
scale: [0, 2, 0],
|
|
1687
|
+
opacity: [0.8, 0.6, 0],
|
|
1688
|
+
rotate: rotation,
|
|
1689
|
+
}, transition: {
|
|
1690
|
+
duration: particle.duration * 0.7,
|
|
1691
|
+
ease: 'easeOut',
|
|
1692
|
+
repeat: Infinity,
|
|
1693
|
+
repeatDelay: particle.duration * 0.3,
|
|
1694
|
+
delay: particle.delay,
|
|
1695
|
+
}, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS['color-splash'] } }));
|
|
1696
|
+
};
|
|
1697
|
+
/**
|
|
1698
|
+
* Generate particles for Holi effect
|
|
1699
|
+
*/
|
|
1700
|
+
function generateHoliParticles(count, viewport, colors) {
|
|
1701
|
+
const effectColors = colors && colors.length > 0 ? colors : HOLI_CONFIG.colors;
|
|
1702
|
+
const physics = HOLI_CONFIG.physics;
|
|
1703
|
+
const particles = [];
|
|
1704
|
+
for (let i = 0; i < count; i++) {
|
|
1705
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1706
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1707
|
+
particles.push({
|
|
1708
|
+
id: `holi-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1709
|
+
x: viewport.width * 0.3 + Math.random() * viewport.width * 0.4,
|
|
1710
|
+
y: viewport.height * 0.3 + Math.random() * viewport.height * 0.4,
|
|
1711
|
+
size,
|
|
1712
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1713
|
+
type: 'color-splash',
|
|
1714
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1715
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1716
|
+
custom: {
|
|
1717
|
+
angle: Math.random() * Math.PI * 2,
|
|
1718
|
+
distance: 50 + Math.random() * 100,
|
|
1719
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
return particles;
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Holi Effect Component
|
|
1727
|
+
* Renders scattered color powder bursts
|
|
1728
|
+
* Vibrant multi-color palette
|
|
1729
|
+
*/
|
|
1730
|
+
const HoliEffect = ({ particles }) => {
|
|
1731
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(HoliParticle, { particle: particle }, particle.id))) }));
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Eid effect configuration
|
|
1736
|
+
*/
|
|
1737
|
+
const EID_CONFIG = {
|
|
1738
|
+
baseParticleCount: 35,
|
|
1739
|
+
colors: ['#FFD700', '#C0C0C0', '#228B22', '#FFFFFF'],
|
|
1740
|
+
particleTypes: ['moon', 'star'],
|
|
1741
|
+
animationType: 'float',
|
|
1742
|
+
physics: {
|
|
1743
|
+
speed: 0.8,
|
|
1744
|
+
drift: 10,
|
|
1745
|
+
rotation: 5,
|
|
1746
|
+
scale: [0.5, 1.5],
|
|
1747
|
+
},
|
|
1748
|
+
};
|
|
1749
|
+
/**
|
|
1750
|
+
* Individual moon/star particle with gentle floating animation
|
|
1751
|
+
*/
|
|
1752
|
+
const EidParticle = ({ particle }) => {
|
|
1753
|
+
const { drift, rotation } = particle.custom;
|
|
1754
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1755
|
+
// Stars twinkle, moons glow
|
|
1756
|
+
const isStar = particle.type === 'star';
|
|
1757
|
+
const isMoon = particle.type === 'moon';
|
|
1758
|
+
return (jsx(motion.div, { style: {
|
|
1759
|
+
position: 'absolute',
|
|
1760
|
+
left: particle.x,
|
|
1761
|
+
top: particle.y,
|
|
1762
|
+
width: particle.size,
|
|
1763
|
+
height: particle.size,
|
|
1764
|
+
color: particle.color,
|
|
1765
|
+
pointerEvents: 'none',
|
|
1766
|
+
filter: isMoon
|
|
1767
|
+
? 'drop-shadow(0 0 8px #FFD700)'
|
|
1768
|
+
: isStar
|
|
1769
|
+
? 'drop-shadow(0 0 4px #FFFFFF)'
|
|
1770
|
+
: undefined,
|
|
1771
|
+
}, initial: { opacity: 0, scale: 0.8 }, animate: {
|
|
1772
|
+
x: [0, drift, -drift, 0],
|
|
1773
|
+
y: [0, -15, 8, 0],
|
|
1774
|
+
opacity: isStar ? [0, 1, 0.5, 1, 0] : [0, 1, 1, 0],
|
|
1775
|
+
scale: isStar ? [0.8, 1, 0.9, 1.1, 0.8] : [0.8, 1, 1.05, 1, 0.8],
|
|
1776
|
+
rotate: [0, rotation / 4, -rotation / 4, 0],
|
|
1777
|
+
}, transition: {
|
|
1778
|
+
duration: particle.duration,
|
|
1779
|
+
ease: 'easeInOut',
|
|
1780
|
+
repeat: Infinity,
|
|
1781
|
+
delay: particle.delay,
|
|
1782
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1783
|
+
};
|
|
1784
|
+
/**
|
|
1785
|
+
* Generate particles for Eid effect
|
|
1786
|
+
*/
|
|
1787
|
+
function generateEidParticles(count, viewport, colors) {
|
|
1788
|
+
const effectColors = colors && colors.length > 0 ? colors : EID_CONFIG.colors;
|
|
1789
|
+
const physics = EID_CONFIG.physics;
|
|
1790
|
+
const particles = [];
|
|
1791
|
+
const particleTypes = EID_CONFIG.particleTypes;
|
|
1792
|
+
for (let i = 0; i < count; i++) {
|
|
1793
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1794
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1795
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1796
|
+
particles.push({
|
|
1797
|
+
id: `eid-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1798
|
+
x: Math.random() * viewport.width,
|
|
1799
|
+
y: Math.random() * viewport.height,
|
|
1800
|
+
size,
|
|
1801
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1802
|
+
type,
|
|
1803
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1804
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1805
|
+
custom: {
|
|
1806
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1807
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
return particles;
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Eid Effect Component
|
|
1815
|
+
* Renders floating crescent moons and twinkling stars
|
|
1816
|
+
* Gold/silver/green color palette
|
|
1817
|
+
*/
|
|
1818
|
+
const EidEffect = ({ particles }) => {
|
|
1819
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(EidParticle, { particle: particle }, particle.id))) }));
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* St. Patrick's effect configuration
|
|
1824
|
+
*/
|
|
1825
|
+
const STPATRICKS_CONFIG = {
|
|
1826
|
+
baseParticleCount: 40,
|
|
1827
|
+
colors: ['#228B22', '#32CD32', '#00FF00', '#FFD700'],
|
|
1828
|
+
particleTypes: ['shamrock', 'rainbow'],
|
|
1829
|
+
animationType: 'fall',
|
|
1830
|
+
physics: {
|
|
1831
|
+
speed: 2,
|
|
1832
|
+
drift: 40,
|
|
1833
|
+
rotation: 180,
|
|
1834
|
+
scale: [0.6, 1.3],
|
|
1835
|
+
},
|
|
1836
|
+
};
|
|
1837
|
+
/**
|
|
1838
|
+
* Individual shamrock/rainbow particle with spinning fall animation
|
|
1839
|
+
*/
|
|
1840
|
+
const StPatricksParticle = ({ particle, viewportHeight }) => {
|
|
1841
|
+
const { drift, rotation } = particle.custom;
|
|
1842
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1843
|
+
// Shamrocks spin more, rainbows drift gently
|
|
1844
|
+
const isShamrock = particle.type === 'shamrock';
|
|
1845
|
+
return (jsx(motion.div, { style: {
|
|
1846
|
+
position: 'absolute',
|
|
1847
|
+
left: particle.x,
|
|
1848
|
+
width: particle.size,
|
|
1849
|
+
height: particle.size,
|
|
1850
|
+
color: particle.color,
|
|
1851
|
+
pointerEvents: 'none',
|
|
1852
|
+
}, initial: { y: -50, opacity: 0, rotate: 0 }, animate: {
|
|
1853
|
+
y: viewportHeight + 50,
|
|
1854
|
+
x: isShamrock ? [0, drift * 0.5, -drift * 0.3, drift] : drift,
|
|
1855
|
+
opacity: [0, 1, 1, 0],
|
|
1856
|
+
rotate: isShamrock ? rotation * 2 : rotation * 0.5,
|
|
1857
|
+
}, transition: {
|
|
1858
|
+
duration: particle.duration,
|
|
1859
|
+
ease: 'linear',
|
|
1860
|
+
repeat: Infinity,
|
|
1861
|
+
delay: particle.delay,
|
|
1862
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1863
|
+
};
|
|
1864
|
+
/**
|
|
1865
|
+
* Generate particles for St. Patrick's effect
|
|
1866
|
+
*/
|
|
1867
|
+
function generateStPatricksParticles(count, viewport, colors) {
|
|
1868
|
+
const effectColors = colors && colors.length > 0 ? colors : STPATRICKS_CONFIG.colors;
|
|
1869
|
+
const physics = STPATRICKS_CONFIG.physics;
|
|
1870
|
+
const particles = [];
|
|
1871
|
+
for (let i = 0; i < count; i++) {
|
|
1872
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1873
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1874
|
+
// More shamrocks than rainbows
|
|
1875
|
+
const type = Math.random() < 0.8 ? 'shamrock' : 'rainbow';
|
|
1876
|
+
particles.push({
|
|
1877
|
+
id: `stpatricks-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1878
|
+
x: Math.random() * viewport.width,
|
|
1879
|
+
y: -50 - Math.random() * 80,
|
|
1880
|
+
size,
|
|
1881
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1882
|
+
type,
|
|
1883
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1884
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1885
|
+
custom: {
|
|
1886
|
+
drift: physics.drift * (Math.random() - 0.5) * 2,
|
|
1887
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1888
|
+
},
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
return particles;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* St. Patrick's Effect Component
|
|
1895
|
+
* Renders falling shamrocks with spin
|
|
1896
|
+
* Green color palette with gold accents
|
|
1897
|
+
*/
|
|
1898
|
+
const StPatricksEffect = ({ particles, viewport }) => {
|
|
1899
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(StPatricksParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Independence Day effect configuration
|
|
1904
|
+
*/
|
|
1905
|
+
const INDEPENDENCE_CONFIG = {
|
|
1906
|
+
baseParticleCount: 45,
|
|
1907
|
+
colors: ['#FF0000', '#FFFFFF', '#0000FF'], // Default US colors, customizable
|
|
1908
|
+
particleTypes: ['firework', 'spark', 'confetti'],
|
|
1909
|
+
animationType: 'explode',
|
|
1910
|
+
physics: {
|
|
1911
|
+
speed: 5,
|
|
1912
|
+
drift: 120,
|
|
1913
|
+
rotation: 360,
|
|
1914
|
+
scale: [0.3, 1.5],
|
|
1915
|
+
},
|
|
1916
|
+
};
|
|
1917
|
+
/**
|
|
1918
|
+
* Individual firework/spark/confetti particle with explode animation
|
|
1919
|
+
*/
|
|
1920
|
+
const IndependenceParticle = ({ particle }) => {
|
|
1921
|
+
const { angle, distance, rotation } = particle.custom;
|
|
1922
|
+
const svgString = PARTICLE_SVGS[particle.type];
|
|
1923
|
+
// Fireworks have bigger explosions, confetti falls after burst
|
|
1924
|
+
const isFirework = particle.type === 'firework';
|
|
1925
|
+
const isConfetti = particle.type === 'confetti';
|
|
1926
|
+
return (jsx(motion.div, { style: {
|
|
1927
|
+
position: 'absolute',
|
|
1928
|
+
left: particle.x,
|
|
1929
|
+
top: particle.y,
|
|
1930
|
+
width: particle.size,
|
|
1931
|
+
height: particle.size,
|
|
1932
|
+
color: particle.color,
|
|
1933
|
+
pointerEvents: 'none',
|
|
1934
|
+
filter: isFirework ? `drop-shadow(0 0 6px ${particle.color})` : undefined,
|
|
1935
|
+
}, initial: { scale: 0, opacity: 1 }, animate: {
|
|
1936
|
+
x: Math.cos(angle) * distance * (isFirework ? 1.2 : 1),
|
|
1937
|
+
y: isConfetti
|
|
1938
|
+
? [Math.sin(angle) * distance * 0.5, Math.sin(angle) * distance + 100]
|
|
1939
|
+
: Math.sin(angle) * distance,
|
|
1940
|
+
scale: isFirework ? [0, 1.8, 0] : [0, 1.5, 0],
|
|
1941
|
+
opacity: [1, 1, 0],
|
|
1942
|
+
rotate: isConfetti ? rotation * 2 : rotation,
|
|
1943
|
+
}, transition: {
|
|
1944
|
+
duration: particle.duration * (isConfetti ? 0.7 : 0.5),
|
|
1945
|
+
ease: isConfetti ? 'easeIn' : [0.25, 0.46, 0.45, 0.94],
|
|
1946
|
+
repeat: Infinity,
|
|
1947
|
+
repeatDelay: particle.duration * (isConfetti ? 0.3 : 0.5),
|
|
1948
|
+
delay: particle.delay,
|
|
1949
|
+
}, dangerouslySetInnerHTML: { __html: svgString } }));
|
|
1950
|
+
};
|
|
1951
|
+
/**
|
|
1952
|
+
* Generate particles for Independence Day effect
|
|
1953
|
+
*/
|
|
1954
|
+
function generateIndependenceParticles(count, viewport, colors) {
|
|
1955
|
+
const effectColors = colors && colors.length > 0 ? colors : INDEPENDENCE_CONFIG.colors;
|
|
1956
|
+
const physics = INDEPENDENCE_CONFIG.physics;
|
|
1957
|
+
const particles = [];
|
|
1958
|
+
const particleTypes = INDEPENDENCE_CONFIG.particleTypes;
|
|
1959
|
+
for (let i = 0; i < count; i++) {
|
|
1960
|
+
const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
|
|
1961
|
+
const baseDelay = i * (1 / count) * 3;
|
|
1962
|
+
const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
|
|
1963
|
+
particles.push({
|
|
1964
|
+
id: `independence-${i}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1965
|
+
x: viewport.width * 0.2 + Math.random() * viewport.width * 0.6,
|
|
1966
|
+
y: viewport.height * 0.2 + Math.random() * viewport.height * 0.4,
|
|
1967
|
+
size,
|
|
1968
|
+
color: effectColors[Math.floor(Math.random() * effectColors.length)],
|
|
1969
|
+
type,
|
|
1970
|
+
delay: baseDelay + Math.random() * 0.5,
|
|
1971
|
+
duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
|
|
1972
|
+
custom: {
|
|
1973
|
+
angle: Math.random() * Math.PI * 2,
|
|
1974
|
+
distance: 100 + Math.random() * 150,
|
|
1975
|
+
rotation: physics.rotation * (Math.random() - 0.5) * 2,
|
|
1976
|
+
},
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
return particles;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Independence Day Effect Component
|
|
1983
|
+
* Renders customizable color fireworks and patriotic confetti
|
|
1984
|
+
* Default US colors (red, white, blue), customizable via colors prop
|
|
1985
|
+
*/
|
|
1986
|
+
const IndependenceEffect = ({ particles }) => {
|
|
1987
|
+
return (jsx(Fragment, { children: particles.map((particle) => (jsx(IndependenceParticle, { particle: particle }, particle.id))) }));
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
// Individual Festival Effects
|
|
1991
|
+
// Each effect provides dedicated components and particle generators
|
|
1992
|
+
// Christmas - Falling snowflakes
|
|
1993
|
+
const EFFECT_GENERATORS = {
|
|
1994
|
+
christmas: generateChristmasParticles,
|
|
1995
|
+
newyear: generateNewYearParticles,
|
|
1996
|
+
valentine: generateValentineParticles,
|
|
1997
|
+
easter: generateEasterParticles,
|
|
1998
|
+
halloween: generateHalloweenParticles,
|
|
1999
|
+
thanksgiving: generateThanksgivingParticles,
|
|
2000
|
+
diwali: generateDiwaliParticles,
|
|
2001
|
+
chinesenewyear: generateChineseNewYearParticles,
|
|
2002
|
+
holi: generateHoliParticles,
|
|
2003
|
+
eid: generateEidParticles,
|
|
2004
|
+
stpatricks: generateStPatricksParticles,
|
|
2005
|
+
independence: generateIndependenceParticles,
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
export { ANIMATION_VARIANTS, CHINESENEWYEAR_CONFIG, CHRISTMAS_CONFIG, ChineseNewYearEffect, ChristmasEffect, DIWALI_CONFIG, DiwaliEffect, EASTER_CONFIG, EFFECT_GENERATORS, EID_CONFIG, EasterEffect, EidEffect, FESTIVAL_CONFIGS, FESTIVAL_TYPES, FestivalRegistry, FestiveEffects, HALLOWEEN_CONFIG, HOLI_CONFIG, HalloweenEffect, HoliEffect, INDEPENDENCE_CONFIG, INTENSITY_CONFIG, IndependenceEffect, NEWYEAR_CONFIG, NewYearEffect, PARTICLE_SVGS, Particle, STPATRICKS_CONFIG, StPatricksEffect, THANKSGIVING_CONFIG, ThanksgivingEffect, VALENTINE_CONFIG, ValentineEffect, calculateParticleCount, createAnimationCustomData, createFestivalEffect, defaultRegistry, explodeVariants, fallVariants, floatVariants, generateChineseNewYearParticles, generateChristmasParticles, generateDiwaliParticles, generateEasterParticles, generateEidParticles, generateHalloweenParticles, generateHoliParticles, generateIndependenceParticles, generateNewYearParticles, generateParticles, generateStPatricksParticles, generateThanksgivingParticles, generateValentineParticles, getAnimationPropsForType, getExplodeTransition, getFallTransition, getFloatTransition, getParticleCount, getRiseTransition, getScatterTransition, getTransitionForType, riseVariants, scatterVariants, useReducedMotion };
|
|
2009
|
+
//# sourceMappingURL=index.esm.js.map
|