domma-cms 0.1.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/LICENSE +21 -0
- package/README.md +469 -0
- package/admin/css/admin.css +1123 -0
- package/admin/index.html +72 -0
- package/admin/js/api.js +210 -0
- package/admin/js/app.js +270 -0
- package/admin/js/config/sidebar-config.js +107 -0
- package/admin/js/lib/card.js +63 -0
- package/admin/js/lib/image-editor.js +869 -0
- package/admin/js/lib/markdown-toolbar.js +421 -0
- package/admin/js/templates/dashboard.html +50 -0
- package/admin/js/templates/documentation.html +237 -0
- package/admin/js/templates/layouts.html +11 -0
- package/admin/js/templates/login.html +58 -0
- package/admin/js/templates/media.html +16 -0
- package/admin/js/templates/navigation.html +50 -0
- package/admin/js/templates/page-editor.html +126 -0
- package/admin/js/templates/pages.html +18 -0
- package/admin/js/templates/plugins.html +12 -0
- package/admin/js/templates/settings.html +190 -0
- package/admin/js/templates/tutorials.html +233 -0
- package/admin/js/templates/user-editor.html +12 -0
- package/admin/js/templates/users.html +10 -0
- package/admin/js/views/dashboard.js +48 -0
- package/admin/js/views/documentation.js +12 -0
- package/admin/js/views/index.js +33 -0
- package/admin/js/views/layouts.js +49 -0
- package/admin/js/views/login.js +254 -0
- package/admin/js/views/media.js +240 -0
- package/admin/js/views/navigation.js +152 -0
- package/admin/js/views/page-editor.js +479 -0
- package/admin/js/views/pages.js +64 -0
- package/admin/js/views/plugins.js +100 -0
- package/admin/js/views/settings.js +64 -0
- package/admin/js/views/tutorials.js +12 -0
- package/admin/js/views/user-editor.js +88 -0
- package/admin/js/views/users.js +73 -0
- package/bin/cli.js +334 -0
- package/config/auth.json +20 -0
- package/config/content.json +10 -0
- package/config/navigation.json +63 -0
- package/config/plugins.json +47 -0
- package/config/presets.json +34 -0
- package/config/server.json +6 -0
- package/config/site.json +33 -0
- package/package.json +67 -0
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
- package/plugins/back-to-top/config.js +10 -0
- package/plugins/back-to-top/plugin.js +24 -0
- package/plugins/back-to-top/plugin.json +36 -0
- package/plugins/back-to-top/public/inject-body.html +105 -0
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
- package/plugins/cookie-consent/config.js +30 -0
- package/plugins/cookie-consent/plugin.js +24 -0
- package/plugins/cookie-consent/plugin.json +36 -0
- package/plugins/cookie-consent/public/inject-body.html +69 -0
- package/plugins/custom-css/admin/templates/custom-css.html +17 -0
- package/plugins/custom-css/admin/views/custom-css.js +35 -0
- package/plugins/custom-css/config.js +1 -0
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +63 -0
- package/plugins/custom-css/plugin.json +32 -0
- package/plugins/custom-css/public/inject-head.html +1 -0
- package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
- package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
- package/plugins/domma-effects/config.js +9 -0
- package/plugins/domma-effects/plugin.js +22 -0
- package/plugins/domma-effects/plugin.json +36 -0
- package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
- package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
- package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
- package/plugins/domma-effects/public/celebrations/index.js +535 -0
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
- package/plugins/domma-effects/public/inject-body.html +268 -0
- package/plugins/example-analytics/admin/templates/analytics.html +10 -0
- package/plugins/example-analytics/admin/views/analytics.js +51 -0
- package/plugins/example-analytics/config.js +6 -0
- package/plugins/example-analytics/plugin.js +58 -0
- package/plugins/example-analytics/plugin.json +27 -0
- package/plugins/example-analytics/public/inject-body.html +13 -0
- package/plugins/example-analytics/public/inject-head.html +1 -0
- package/plugins/example-analytics/stats.json +1 -0
- package/plugins/form-builder/admin/templates/form-editor.html +158 -0
- package/plugins/form-builder/admin/templates/form-settings.html +29 -0
- package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
- package/plugins/form-builder/admin/templates/forms-list.html +17 -0
- package/plugins/form-builder/admin/views/form-editor.js +817 -0
- package/plugins/form-builder/admin/views/form-settings.js +38 -0
- package/plugins/form-builder/admin/views/form-submissions.js +295 -0
- package/plugins/form-builder/admin/views/forms-list.js +164 -0
- package/plugins/form-builder/config.js +9 -0
- package/plugins/form-builder/data/forms/contact-details.json +63 -0
- package/plugins/form-builder/data/forms/contact.json +52 -0
- package/plugins/form-builder/data/submissions/contact-details.json +1 -0
- package/plugins/form-builder/data/submissions/contact.json +14 -0
- package/plugins/form-builder/email.js +103 -0
- package/plugins/form-builder/plugin.js +454 -0
- package/plugins/form-builder/plugin.json +56 -0
- package/plugins/form-builder/public/inject-body.html +270 -0
- package/plugins/form-builder/public/inject-head.html +42 -0
- package/public/css/site.css +189 -0
- package/public/js/site.js +109 -0
- package/scripts/copy-domma.js +48 -0
- package/scripts/fresh.js +41 -0
- package/scripts/reset.js +124 -0
- package/scripts/seed.js +666 -0
- package/scripts/setup.js +263 -0
- package/server/config.js +56 -0
- package/server/middleware/auth.js +97 -0
- package/server/routes/api/auth.js +116 -0
- package/server/routes/api/layouts.js +25 -0
- package/server/routes/api/media.js +93 -0
- package/server/routes/api/navigation.js +37 -0
- package/server/routes/api/pages.js +118 -0
- package/server/routes/api/plugins.js +46 -0
- package/server/routes/api/settings.js +25 -0
- package/server/routes/api/users.js +110 -0
- package/server/routes/public.js +108 -0
- package/server/server.js +169 -0
- package/server/services/content.js +298 -0
- package/server/services/images.js +334 -0
- package/server/services/markdown.js +297 -0
- package/server/services/plugins.js +246 -0
- package/server/services/renderer.js +80 -0
- package/server/services/users.js +212 -0
- package/server/templates/page.html +78 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domma Celebrations - Main Module
|
|
3
|
+
*
|
|
4
|
+
* Year-round visual celebration effects system supporting 8 celebration themes.
|
|
5
|
+
* Auto-detects current celebration based on date or allows manual theme selection.
|
|
6
|
+
*
|
|
7
|
+
* @module celebrations
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CanvasManager } from './core/canvas.js';
|
|
11
|
+
import { PhysicsEngine, updateParticlePhysics, updateMovingParticle } from './core/physics.js';
|
|
12
|
+
import { createParticle } from './core/particles.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Main Celebrations Effect class
|
|
16
|
+
*/
|
|
17
|
+
export class CelebrationsEffect {
|
|
18
|
+
// Theme date ranges: [[startMonth, startDay], [endMonth, endDay]]
|
|
19
|
+
// 5-day lead-up, ends at midnight on celebration day
|
|
20
|
+
static themes = {
|
|
21
|
+
christmas: {
|
|
22
|
+
module: './themes/christmas.js',
|
|
23
|
+
dates: [[12, 1], [1, 1]], // All December + New Year's Day
|
|
24
|
+
displayName: 'Christmas',
|
|
25
|
+
emoji: '🎄'
|
|
26
|
+
},
|
|
27
|
+
halloween: {
|
|
28
|
+
module: './themes/halloween.js',
|
|
29
|
+
dates: [[10, 26], [10, 31]], // Oct 26-31 (midnight)
|
|
30
|
+
displayName: 'Halloween',
|
|
31
|
+
emoji: '🎃'
|
|
32
|
+
},
|
|
33
|
+
valentines: {
|
|
34
|
+
module: './themes/valentines.js',
|
|
35
|
+
dates: [[2, 9], [2, 14]], // Feb 9-14 (midnight)
|
|
36
|
+
displayName: 'Valentine\'s Day',
|
|
37
|
+
emoji: '💕'
|
|
38
|
+
},
|
|
39
|
+
'guy-fawkes': {
|
|
40
|
+
module: './themes/guy-fawkes.js',
|
|
41
|
+
dates: [[11, 1], [11, 5]], // Nov 1-5 (midnight)
|
|
42
|
+
displayName: 'Guy Fawkes Night',
|
|
43
|
+
emoji: '🎆'
|
|
44
|
+
},
|
|
45
|
+
'st-patricks': {
|
|
46
|
+
module: './themes/st-patricks.js',
|
|
47
|
+
dates: [[3, 12], [3, 17]], // Mar 12-17 (midnight)
|
|
48
|
+
displayName: 'St Patrick\'s Day',
|
|
49
|
+
emoji: '☘️'
|
|
50
|
+
},
|
|
51
|
+
'st-andrews': {
|
|
52
|
+
module: './themes/st-andrews.js',
|
|
53
|
+
dates: [[11, 25], [11, 30]], // Nov 25-30 (midnight)
|
|
54
|
+
displayName: 'St Andrew\'s Day',
|
|
55
|
+
emoji: '🏴'
|
|
56
|
+
},
|
|
57
|
+
'st-davids': {
|
|
58
|
+
module: './themes/st-davids.js',
|
|
59
|
+
dates: [[2, 24], [3, 1]], // Feb 24 - Mar 1 (midnight)
|
|
60
|
+
displayName: 'St David\'s Day',
|
|
61
|
+
emoji: '🏴'
|
|
62
|
+
},
|
|
63
|
+
'st-georges': {
|
|
64
|
+
module: './themes/st-georges.js',
|
|
65
|
+
dates: [[4, 18], [4, 23]], // Apr 18-23 (midnight)
|
|
66
|
+
displayName: 'St George\'s Day',
|
|
67
|
+
emoji: '🏴'
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if current date is within a celebration period
|
|
73
|
+
*/
|
|
74
|
+
static isDateInRange(startMonth, startDay, endMonth, endDay) {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const month = now.getMonth() + 1; // 1-12
|
|
77
|
+
const day = now.getDate();
|
|
78
|
+
|
|
79
|
+
// Handle year wrapping (e.g., Dec 1 - Jan 1)
|
|
80
|
+
if (endMonth < startMonth) {
|
|
81
|
+
return (
|
|
82
|
+
(month === startMonth && day >= startDay) ||
|
|
83
|
+
(month > startMonth) ||
|
|
84
|
+
(month < startMonth && month <= endMonth) ||
|
|
85
|
+
(month === endMonth && day <= endDay)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Normal date range
|
|
90
|
+
return (
|
|
91
|
+
(month > startMonth || (month === startMonth && day >= startDay)) &&
|
|
92
|
+
(month < endMonth || (month === endMonth && day <= endDay))
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Auto-detect current theme based on date
|
|
98
|
+
* @returns {string|null} Theme name or null if no celebration is active
|
|
99
|
+
*/
|
|
100
|
+
static getCurrentTheme() {
|
|
101
|
+
for (const [themeName, themeData] of Object.entries(CelebrationsEffect.themes)) {
|
|
102
|
+
const [[startMonth, startDay], [endMonth, endDay]] = themeData.dates;
|
|
103
|
+
if (CelebrationsEffect.isDateInRange(startMonth, startDay, endMonth, endDay)) {
|
|
104
|
+
return themeName;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if any celebration is currently active
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
static isCelebrationSeason() {
|
|
115
|
+
return CelebrationsEffect.getCurrentTheme() !== null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get all available themes
|
|
120
|
+
* @returns {Object} Themes object
|
|
121
|
+
*/
|
|
122
|
+
static getThemes() {
|
|
123
|
+
return CelebrationsEffect.themes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a new celebrations effect
|
|
128
|
+
* @param {Object} options - Configuration options
|
|
129
|
+
* @param {string} options.theme - Theme name (or 'auto' for auto-detection)
|
|
130
|
+
* @param {string} options.intensity - Intensity level ('light', 'medium', 'heavy')
|
|
131
|
+
* @param {boolean} options.enabled - Whether to start enabled
|
|
132
|
+
* @param {number} options.zIndex - Canvas z-index
|
|
133
|
+
*/
|
|
134
|
+
constructor(options = {}) {
|
|
135
|
+
this.options = {
|
|
136
|
+
theme: options.theme || 'auto',
|
|
137
|
+
intensity: options.intensity || 'medium',
|
|
138
|
+
enabled: options.enabled !== undefined ? options.enabled : true,
|
|
139
|
+
zIndex: options.zIndex || 999
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
this.canvasManager = null;
|
|
143
|
+
this.physicsEngine = null;
|
|
144
|
+
this.themeModule = null;
|
|
145
|
+
this.currentTheme = null;
|
|
146
|
+
|
|
147
|
+
this.particles = [];
|
|
148
|
+
this.specialParticles = [];
|
|
149
|
+
|
|
150
|
+
this.animationFrame = null;
|
|
151
|
+
this.lastTime = 0;
|
|
152
|
+
this.running = false;
|
|
153
|
+
this.initialized = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Initialize the celebrations effect
|
|
158
|
+
*/
|
|
159
|
+
async init() {
|
|
160
|
+
if (this.initialized) {
|
|
161
|
+
console.warn('[Celebrations] Already initialized');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log('[Celebrations] Initializing...');
|
|
166
|
+
|
|
167
|
+
// Determine theme
|
|
168
|
+
let themeName = this.options.theme;
|
|
169
|
+
if (themeName === 'auto') {
|
|
170
|
+
themeName = CelebrationsEffect.getCurrentTheme();
|
|
171
|
+
if (!themeName) {
|
|
172
|
+
console.log('[Celebrations] No active celebration detected');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate theme
|
|
178
|
+
if (!CelebrationsEffect.themes[themeName]) {
|
|
179
|
+
console.error(`[Celebrations] Unknown theme: ${themeName}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.currentTheme = themeName;
|
|
184
|
+
console.log(`[Celebrations] Loading theme: ${themeName}`);
|
|
185
|
+
|
|
186
|
+
// Load theme module
|
|
187
|
+
try {
|
|
188
|
+
const themeData = CelebrationsEffect.themes[themeName];
|
|
189
|
+
const module = await import(themeData.module);
|
|
190
|
+
this.themeModule = module.default;
|
|
191
|
+
console.log(`[Celebrations] Theme loaded: ${this.themeModule.displayName}`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`[Celebrations] Failed to load theme module: ${error.message}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Initialize canvas
|
|
198
|
+
this.canvasManager = new CanvasManager({
|
|
199
|
+
canvasId: 'celebrations-canvas',
|
|
200
|
+
zIndex: this.options.zIndex
|
|
201
|
+
});
|
|
202
|
+
this.canvasManager.create();
|
|
203
|
+
|
|
204
|
+
// Initialize physics
|
|
205
|
+
this.physicsEngine = new PhysicsEngine({
|
|
206
|
+
gustInterval: [5000, 15000],
|
|
207
|
+
gustStrength: [-2, 2]
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Setup resize handler
|
|
211
|
+
window.addEventListener('resize', () => {
|
|
212
|
+
this.canvasManager.resize();
|
|
213
|
+
this.resetParticles();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Initialize particles
|
|
217
|
+
this.resetParticles();
|
|
218
|
+
|
|
219
|
+
this.initialized = true;
|
|
220
|
+
console.log('[Celebrations] Initialized successfully');
|
|
221
|
+
|
|
222
|
+
// Auto-start if enabled
|
|
223
|
+
if (this.options.enabled) {
|
|
224
|
+
this.start();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reset particles based on current intensity
|
|
230
|
+
*/
|
|
231
|
+
resetParticles() {
|
|
232
|
+
const config = this.themeModule.intensityConfig[this.options.intensity];
|
|
233
|
+
const width = this.canvasManager.canvas.width;
|
|
234
|
+
const height = this.canvasManager.canvas.height;
|
|
235
|
+
|
|
236
|
+
// Clear existing particles
|
|
237
|
+
this.particles = [];
|
|
238
|
+
this.specialParticles = [];
|
|
239
|
+
|
|
240
|
+
// Determine initial particle count (some themes may want to start with fewer particles)
|
|
241
|
+
const initialRatio = config.initialParticleRatio !== undefined ? config.initialParticleRatio : 1.0;
|
|
242
|
+
const initialCount = Math.floor(config.count * initialRatio);
|
|
243
|
+
|
|
244
|
+
// Create falling particles (theme-specific method or generic)
|
|
245
|
+
if (this.themeModule.createFallingParticle) {
|
|
246
|
+
for (let i = 0; i < initialCount; i++) {
|
|
247
|
+
const particle = this.themeModule.createFallingParticle(width, height, config);
|
|
248
|
+
particle.y = Math.random() * height; // Spread across screen initially
|
|
249
|
+
this.particles.push(particle);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
// Fallback to generic particles
|
|
253
|
+
for (let i = 0; i < initialCount; i++) {
|
|
254
|
+
const particle = createParticle(config, width, height);
|
|
255
|
+
particle.y = Math.random() * height;
|
|
256
|
+
this.particles.push(particle);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Create initial static decorations (theme-specific)
|
|
261
|
+
if (this.themeModule.createInitialDecorations) {
|
|
262
|
+
const decorations = this.themeModule.createInitialDecorations(width, height, config);
|
|
263
|
+
this.specialParticles.push(...decorations);
|
|
264
|
+
console.log(`[Celebrations] Created ${decorations.length} decorations`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(`[Celebrations] Created ${initialCount}/${config.count} particles (${this.options.intensity}, ${Math.round(initialRatio * 100)}% initial)`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Start the animation
|
|
272
|
+
*/
|
|
273
|
+
start() {
|
|
274
|
+
if (!this.initialized) {
|
|
275
|
+
console.warn('[Celebrations] Cannot start - not initialized');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (this.running) {
|
|
280
|
+
console.warn('[Celebrations] Already running');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log('[Celebrations] Starting animation');
|
|
285
|
+
this.running = true;
|
|
286
|
+
this.lastTime = performance.now();
|
|
287
|
+
this.animate();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Pause the animation
|
|
292
|
+
*/
|
|
293
|
+
pause() {
|
|
294
|
+
if (!this.running) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log('[Celebrations] Pausing animation');
|
|
299
|
+
this.running = false;
|
|
300
|
+
|
|
301
|
+
if (this.animationFrame) {
|
|
302
|
+
cancelAnimationFrame(this.animationFrame);
|
|
303
|
+
this.animationFrame = null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Resume the animation
|
|
309
|
+
*/
|
|
310
|
+
resume() {
|
|
311
|
+
if (this.running) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.start();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Destroy the effect
|
|
320
|
+
*/
|
|
321
|
+
destroy() {
|
|
322
|
+
console.log('[Celebrations] Destroying effect');
|
|
323
|
+
this.pause();
|
|
324
|
+
|
|
325
|
+
if (this.canvasManager && this.canvasManager.canvas) {
|
|
326
|
+
this.canvasManager.canvas.remove();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.particles = [];
|
|
330
|
+
this.specialParticles = [];
|
|
331
|
+
this.canvasManager = null;
|
|
332
|
+
this.physicsEngine = null;
|
|
333
|
+
this.themeModule = null;
|
|
334
|
+
this.initialized = false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Change intensity
|
|
339
|
+
*/
|
|
340
|
+
setIntensity(intensity) {
|
|
341
|
+
if (!['light', 'medium', 'heavy'].includes(intensity)) {
|
|
342
|
+
console.error(`[Celebrations] Invalid intensity: ${intensity}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(`[Celebrations] Changing intensity to: ${intensity}`);
|
|
347
|
+
this.options.intensity = intensity;
|
|
348
|
+
this.resetParticles();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Change theme
|
|
353
|
+
*/
|
|
354
|
+
async setTheme(themeName) {
|
|
355
|
+
console.log(`[Celebrations] Changing theme to: ${themeName}`);
|
|
356
|
+
|
|
357
|
+
const wasRunning = this.running;
|
|
358
|
+
this.pause();
|
|
359
|
+
|
|
360
|
+
// Wait for current animation frame to complete before clearing
|
|
361
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
362
|
+
|
|
363
|
+
this.initialized = false;
|
|
364
|
+
|
|
365
|
+
// Clear canvas and particles from previous theme
|
|
366
|
+
if (this.canvasManager) {
|
|
367
|
+
this.canvasManager.clear();
|
|
368
|
+
}
|
|
369
|
+
this.particles = [];
|
|
370
|
+
this.specialParticles = [];
|
|
371
|
+
|
|
372
|
+
// Clear again to be absolutely sure
|
|
373
|
+
if (this.canvasManager) {
|
|
374
|
+
this.canvasManager.clear();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.options.theme = themeName;
|
|
378
|
+
await this.init();
|
|
379
|
+
|
|
380
|
+
if (wasRunning) {
|
|
381
|
+
this.start();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Main animation loop
|
|
387
|
+
*/
|
|
388
|
+
animate() {
|
|
389
|
+
if (!this.running) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const currentTime = performance.now();
|
|
394
|
+
const deltaTime = currentTime - this.lastTime;
|
|
395
|
+
this.lastTime = currentTime;
|
|
396
|
+
|
|
397
|
+
// Clear canvas
|
|
398
|
+
this.canvasManager.clear();
|
|
399
|
+
|
|
400
|
+
const ctx = this.canvasManager.ctx;
|
|
401
|
+
const width = this.canvasManager.canvas.width;
|
|
402
|
+
const height = this.canvasManager.canvas.height;
|
|
403
|
+
const config = this.themeModule.intensityConfig[this.options.intensity];
|
|
404
|
+
|
|
405
|
+
// Update wind
|
|
406
|
+
this.physicsEngine.updateWind(currentTime);
|
|
407
|
+
const windForce = this.physicsEngine.getWindForce();
|
|
408
|
+
|
|
409
|
+
// Gradually add particles until reaching target count (for themes with initialParticleRatio < 1)
|
|
410
|
+
if (this.particles.length < config.count && Math.random() < 0.05) {
|
|
411
|
+
if (this.themeModule.createFallingParticle) {
|
|
412
|
+
const newParticle = this.themeModule.createFallingParticle(width, height, config);
|
|
413
|
+
// For new particles added during animation, start them off-screen
|
|
414
|
+
if (newParticle.y === undefined || newParticle.y < 0) {
|
|
415
|
+
newParticle.y = -20;
|
|
416
|
+
}
|
|
417
|
+
this.particles.push(newParticle);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Update and render particles
|
|
422
|
+
this.particles.forEach((particle, index) => {
|
|
423
|
+
// Update physics
|
|
424
|
+
updateParticlePhysics(particle, deltaTime, windForce);
|
|
425
|
+
|
|
426
|
+
// Recycle particle if off-screen
|
|
427
|
+
if (particle.y > height + 50) {
|
|
428
|
+
particle.y = -20;
|
|
429
|
+
particle.x = Math.random() * width;
|
|
430
|
+
}
|
|
431
|
+
if (particle.x < -50) {
|
|
432
|
+
particle.x = width + 50;
|
|
433
|
+
}
|
|
434
|
+
if (particle.x > width + 50) {
|
|
435
|
+
particle.x = -50;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Render particle (theme-specific)
|
|
439
|
+
// For now, render as generic snowflake-like particle
|
|
440
|
+
// Theme modules should provide custom rendering
|
|
441
|
+
if (this.themeModule.drawSnowflake && particle.type === 'snowflake') {
|
|
442
|
+
this.themeModule.drawSnowflake(ctx, particle);
|
|
443
|
+
} else if (this.themeModule['draw' + this.capitalizeFirst(particle.type)]) {
|
|
444
|
+
const drawMethod = 'draw' + this.capitalizeFirst(particle.type);
|
|
445
|
+
this.themeModule[drawMethod](ctx, particle, currentTime);
|
|
446
|
+
} else {
|
|
447
|
+
// Fallback generic rendering
|
|
448
|
+
ctx.save();
|
|
449
|
+
ctx.globalAlpha = particle.opacity;
|
|
450
|
+
ctx.fillStyle = this.themeModule.colors?.primary || '#ffffff';
|
|
451
|
+
ctx.beginPath();
|
|
452
|
+
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
|
453
|
+
ctx.fill();
|
|
454
|
+
ctx.restore();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Theme-specific special particle updates (firework explosions, etc.)
|
|
459
|
+
if (this.themeModule.updateSpecialParticles) {
|
|
460
|
+
this.themeModule.updateSpecialParticles(this.specialParticles, deltaTime, width, height);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Update and render special particles (decorations)
|
|
464
|
+
this.specialParticles = this.specialParticles.filter(particle => {
|
|
465
|
+
if (!particle.active) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Update moving particles
|
|
470
|
+
if (!particle.static) {
|
|
471
|
+
updateMovingParticle(particle, deltaTime, currentTime);
|
|
472
|
+
|
|
473
|
+
// Remove if off-screen (wider tolerance for large particles like trains)
|
|
474
|
+
const offscreenMargin = particle.type === 'train' ? 600 : 200;
|
|
475
|
+
if (particle.x < -offscreenMargin || particle.x > width + offscreenMargin) {
|
|
476
|
+
particle.active = false;
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Safety check: validate particle properties before rendering
|
|
482
|
+
if (!isFinite(particle.x) || !isFinite(particle.y) || (particle.size && !isFinite(particle.size))) {
|
|
483
|
+
console.warn('[Celebrations] Invalid particle values, removing:', particle.type);
|
|
484
|
+
particle.active = false;
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Render (theme-specific)
|
|
489
|
+
const typeName = this.capitalizeFirst(particle.type);
|
|
490
|
+
const drawMethod = 'draw' + typeName;
|
|
491
|
+
if (this.themeModule[drawMethod]) {
|
|
492
|
+
this.themeModule[drawMethod](ctx, particle, currentTime);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return true;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Spawn special particles
|
|
499
|
+
if (this.themeModule.spawnSpecialParticle) {
|
|
500
|
+
const newParticle = this.themeModule.spawnSpecialParticle(
|
|
501
|
+
this.specialParticles,
|
|
502
|
+
width,
|
|
503
|
+
height,
|
|
504
|
+
config
|
|
505
|
+
);
|
|
506
|
+
if (newParticle) {
|
|
507
|
+
this.specialParticles.push(newParticle);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Call theme-specific global drawing (e.g., lightning)
|
|
512
|
+
if (this.themeModule.drawGlobalEffects) {
|
|
513
|
+
this.themeModule.drawGlobalEffects(ctx, currentTime, width, height);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Continue animation
|
|
517
|
+
this.animationFrame = requestAnimationFrame(() => this.animate());
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Helper: Capitalize first letter and letters after hyphens
|
|
522
|
+
* Converts hyphenated names to camelCase: 'sparkler-bundle' → 'SparklerBundle'
|
|
523
|
+
*/
|
|
524
|
+
capitalizeFirst(str) {
|
|
525
|
+
if (!str) return '';
|
|
526
|
+
// First, capitalize letters after hyphens and remove hyphens
|
|
527
|
+
// 'sparkler-bundle' → 'sparklerBundle'
|
|
528
|
+
const camelCase = str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
|
529
|
+
// Then capitalize the first letter: 'sparklerBundle' → 'SparklerBundle'
|
|
530
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Export for direct use
|
|
535
|
+
export default CelebrationsEffect;
|