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,1477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guy Fawkes Night Theme for Domma Celebrations
|
|
3
|
+
* (Bonfire Night - November 5th, UK)
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Spectacular firework displays with multiple colors
|
|
7
|
+
* - Animated bonfires with crackling flames and embers
|
|
8
|
+
* - Rocket trails shooting upward before exploding
|
|
9
|
+
* - Catherine wheels spinning with colorful sparks
|
|
10
|
+
* - Roman candles shooting multiple bursts
|
|
11
|
+
* - Guy Fawkes effigy silhouette
|
|
12
|
+
* - Floating embers and ash particles
|
|
13
|
+
* - Dark night sky with dramatic pyrotechnics
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Import firework functions from Christmas theme (reusable!)
|
|
17
|
+
import christmas from './christmas.js';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
name: 'guy-fawkes',
|
|
21
|
+
displayName: 'Guy Fawkes Night',
|
|
22
|
+
emoji: '🎆',
|
|
23
|
+
|
|
24
|
+
// Intensity configurations
|
|
25
|
+
intensityConfig: {
|
|
26
|
+
light: {
|
|
27
|
+
count: 60,
|
|
28
|
+
initialParticleRatio: 0.25, // Start with 25% of particles (15), build up to full count
|
|
29
|
+
speedRange: [0.5, 1.5],
|
|
30
|
+
sizeRange: [2, 4],
|
|
31
|
+
bonfires: 1,
|
|
32
|
+
fireworkChance: 0.0009, // ~1 firework every 18 seconds
|
|
33
|
+
rocketChance: 0.0007,
|
|
34
|
+
burstChance: 0.0012
|
|
35
|
+
},
|
|
36
|
+
medium: {
|
|
37
|
+
count: 120,
|
|
38
|
+
initialParticleRatio: 0.25, // Start with 25% of particles (30), build up to full count
|
|
39
|
+
speedRange: [0.5, 2.0],
|
|
40
|
+
sizeRange: [2, 5],
|
|
41
|
+
bonfires: 2,
|
|
42
|
+
fireworkChance: 0.0011, // ~1 firework every 15 seconds
|
|
43
|
+
rocketChance: 0.0009,
|
|
44
|
+
burstChance: 0.0015
|
|
45
|
+
},
|
|
46
|
+
heavy: {
|
|
47
|
+
count: 200,
|
|
48
|
+
initialParticleRatio: 0.25, // Start with 25% of particles (50), build up to full count
|
|
49
|
+
speedRange: [0.5, 2.5],
|
|
50
|
+
sizeRange: [3, 6],
|
|
51
|
+
bonfires: 3,
|
|
52
|
+
fireworkChance: 0.0014, // ~1 firework every 12 seconds
|
|
53
|
+
rocketChance: 0.0012,
|
|
54
|
+
burstChance: 0.0018
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
particles: ['ember', 'firework', 'spark', 'rocket', 'burst', 'trail'],
|
|
59
|
+
decorations: ['bonfire', 'guy-effigy', 'catherine-wheel', 'roman-candle', 'sparkler-bundle', 'moon'],
|
|
60
|
+
colors: {
|
|
61
|
+
primary: '#ff4500', // Orange-red flames
|
|
62
|
+
secondary: '#ffd700', // Gold sparks
|
|
63
|
+
accent: '#8b0000', // Deep red
|
|
64
|
+
fire: ['#ff4500', '#ffa500', '#ffff00', '#ff0000'], // Flame colors
|
|
65
|
+
firework: ['#ff0000', '#ffff00', '#00ff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff']
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Lightning effect properties
|
|
69
|
+
lightningChance: 0.0005, // 0.05% chance per frame to strike
|
|
70
|
+
lightningDuration: 200, // Lightning flash lasts 200ms
|
|
71
|
+
lightningTimer: 0,
|
|
72
|
+
lightningActive: false,
|
|
73
|
+
lightningColor: '#fefefe', // Bright white/blue flash
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Draw a procedurally generated lightning bolt
|
|
77
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
78
|
+
* @param {number} x1 - Start X
|
|
79
|
+
* @param {number} y1 - Start Y
|
|
80
|
+
* @param {number} x2 - End X
|
|
81
|
+
* @param {number} y2 - End Y
|
|
82
|
+
* @param {number} segments - Number of segments for the bolt (controls detail)
|
|
83
|
+
* @param {number} displacement - Max displacement for forks
|
|
84
|
+
* @param {number} roughness - How jagged the line is
|
|
85
|
+
* @param {number} branchChance - Probability of branching
|
|
86
|
+
* @param {number} lineWidth - Base line width
|
|
87
|
+
* @param {string} color - Color of the lightning
|
|
88
|
+
*/
|
|
89
|
+
drawLightning(ctx, x1, y1, x2, y2, segments, displacement, roughness, branchChance, lineWidth, color) {
|
|
90
|
+
if (segments < 1) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const midpointX = (x1 + x2) / 2;
|
|
95
|
+
const midpointY = (y1 + y2) / 2;
|
|
96
|
+
|
|
97
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
98
|
+
const perpendicularAngle = angle + Math.PI / 2;
|
|
99
|
+
|
|
100
|
+
const offset = (Math.random() - 0.5) * displacement;
|
|
101
|
+
const newMidpointX = midpointX + Math.cos(perpendicularAngle) * offset;
|
|
102
|
+
const newMidpointY = midpointY + Math.sin(perpendicularAngle) * offset;
|
|
103
|
+
|
|
104
|
+
const newSegments = segments - 1;
|
|
105
|
+
const newDisplacement = displacement * roughness;
|
|
106
|
+
const newLineWidth = lineWidth * 0.8;
|
|
107
|
+
|
|
108
|
+
ctx.strokeStyle = color;
|
|
109
|
+
ctx.lineWidth = lineWidth;
|
|
110
|
+
ctx.lineCap = 'round';
|
|
111
|
+
ctx.lineJoin = 'round';
|
|
112
|
+
|
|
113
|
+
// Draw main segment
|
|
114
|
+
ctx.beginPath();
|
|
115
|
+
ctx.moveTo(x1, y1);
|
|
116
|
+
ctx.lineTo(newMidpointX, newMidpointY);
|
|
117
|
+
ctx.lineTo(x2, y2);
|
|
118
|
+
ctx.stroke();
|
|
119
|
+
|
|
120
|
+
// Recursive call for sub-segments
|
|
121
|
+
if (newSegments > 0) {
|
|
122
|
+
this.drawLightning(ctx, x1, y1, newMidpointX, newMidpointY, newSegments, newDisplacement, roughness, branchChance, newLineWidth, color);
|
|
123
|
+
this.drawLightning(ctx, newMidpointX, newMidpointY, x2, y2, newSegments, newDisplacement, roughness, branchChance, newLineWidth, color);
|
|
124
|
+
|
|
125
|
+
// Create a fork
|
|
126
|
+
if (Math.random() < branchChance && newSegments > 1) {
|
|
127
|
+
const forkLength = Math.random() * displacement * 0.5;
|
|
128
|
+
const forkAngle = (Math.random() - 0.5) * Math.PI / 3; // Max 60 degree deviation
|
|
129
|
+
const forkX = newMidpointX + Math.cos(angle + forkAngle) * forkLength;
|
|
130
|
+
const forkY = newMidpointY + Math.sin(angle + forkAngle) * forkLength;
|
|
131
|
+
this.drawLightning(ctx, newMidpointX, newMidpointY, forkX, forkY, newSegments - 1, newDisplacement * 0.5, roughness, branchChance * 0.5, newLineWidth * 0.6, color);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create rising ember particle that will explode
|
|
138
|
+
*/
|
|
139
|
+
createEmber(canvasWidth, canvasHeight, config) {
|
|
140
|
+
// Ensure config has required properties with defaults
|
|
141
|
+
const sizeRange = config?.sizeRange || [2, 4];
|
|
142
|
+
const speedRange = config?.speedRange || [0.5, 1.5];
|
|
143
|
+
|
|
144
|
+
const emberColors = this.colors.firework;
|
|
145
|
+
const startX = Math.random() * canvasWidth;
|
|
146
|
+
const startY = canvasHeight - 50; // Start near bottom
|
|
147
|
+
const targetY = 100 + Math.random() * 300; // Explode at random height
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
type: 'ember',
|
|
151
|
+
x: startX,
|
|
152
|
+
y: startY,
|
|
153
|
+
vx: (Math.random() - 0.5) * 1.5, // Horizontal drift
|
|
154
|
+
vy: -2 - Math.random() * 2, // Upward velocity
|
|
155
|
+
targetY: targetY,
|
|
156
|
+
size: sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]),
|
|
157
|
+
speed: speedRange[0] + Math.random() * (speedRange[1] - speedRange[0]),
|
|
158
|
+
opacity: 1,
|
|
159
|
+
color: emberColors[Math.floor(Math.random() * emberColors.length)],
|
|
160
|
+
trailColor: emberColors[Math.floor(Math.random() * emberColors.length)],
|
|
161
|
+
glowPhase: Math.random() * Math.PI * 2,
|
|
162
|
+
exploded: false,
|
|
163
|
+
explosionParticles: [],
|
|
164
|
+
active: true
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create bonfire decoration
|
|
170
|
+
*/
|
|
171
|
+
createBonfire(canvasWidth, canvasHeight, options = {}) {
|
|
172
|
+
return {
|
|
173
|
+
type: 'bonfire',
|
|
174
|
+
x: options.x !== undefined ? options.x : Math.random() * canvasWidth * 0.6 + canvasWidth * 0.2,
|
|
175
|
+
y: options.y !== undefined ? options.y : canvasHeight - 60,
|
|
176
|
+
size: 30 + Math.random() * 20,
|
|
177
|
+
opacity: 1,
|
|
178
|
+
flames: [],
|
|
179
|
+
logs: [],
|
|
180
|
+
embers: [],
|
|
181
|
+
time: 0,
|
|
182
|
+
cracklePhase: 0,
|
|
183
|
+
active: true,
|
|
184
|
+
static: true
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create Guy Fawkes effigy (on bonfire)
|
|
190
|
+
*/
|
|
191
|
+
createGuyEffigy(canvasWidth, canvasHeight, bonfireX, bonfireY) {
|
|
192
|
+
return {
|
|
193
|
+
type: 'guy-effigy',
|
|
194
|
+
x: bonfireX,
|
|
195
|
+
y: bonfireY - 50,
|
|
196
|
+
size: 20 + Math.random() * 10,
|
|
197
|
+
opacity: 0.8,
|
|
198
|
+
burning: false,
|
|
199
|
+
burnProgress: 0,
|
|
200
|
+
active: true,
|
|
201
|
+
static: true
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create Catherine wheel (spinning firework)
|
|
207
|
+
*/
|
|
208
|
+
createCatherineWheel(canvasWidth, canvasHeight, options = {}) {
|
|
209
|
+
return {
|
|
210
|
+
type: 'catherine-wheel',
|
|
211
|
+
x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
|
|
212
|
+
y: options.y !== undefined ? options.y : 100 + Math.random() * 200,
|
|
213
|
+
size: 15 + Math.random() * 10,
|
|
214
|
+
opacity: 1,
|
|
215
|
+
rotation: 0,
|
|
216
|
+
rotationSpeed: 0.05 + Math.random() * 0.1,
|
|
217
|
+
sparks: [], // Keep this for internal spark management
|
|
218
|
+
sparkTimer: 0, // New: timer for emitting sparks
|
|
219
|
+
sparkInterval: 50, // New: emit sparks every 50ms
|
|
220
|
+
time: 0,
|
|
221
|
+
duration: 5000 + Math.random() * 3000,
|
|
222
|
+
active: true,
|
|
223
|
+
static: true
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Create Roman candle (shoots bursts upward)
|
|
229
|
+
*/
|
|
230
|
+
createRomanCandle(canvasWidth, canvasHeight, options = {}) {
|
|
231
|
+
return {
|
|
232
|
+
type: 'roman-candle',
|
|
233
|
+
x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
|
|
234
|
+
y: options.y !== undefined ? options.y : canvasHeight - 30,
|
|
235
|
+
size: 8 + Math.random() * 4,
|
|
236
|
+
opacity: 1,
|
|
237
|
+
shots: [],
|
|
238
|
+
shotInterval: 800 + Math.random() * 400,
|
|
239
|
+
lastShotTime: 0,
|
|
240
|
+
shotCount: 0,
|
|
241
|
+
maxShots: 5 + Math.floor(Math.random() * 5),
|
|
242
|
+
time: 0,
|
|
243
|
+
active: true,
|
|
244
|
+
static: true
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create rocket particle (shoots up then explodes)
|
|
250
|
+
*/
|
|
251
|
+
createRocket(canvasWidth, canvasHeight, config) {
|
|
252
|
+
const sizeRange = config?.sizeRange || [2, 4];
|
|
253
|
+
const startX = 50 + Math.random() * (canvasWidth - 100);
|
|
254
|
+
const targetY = 80 + Math.random() * 150; // Explode in upper third
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
type: 'rocket',
|
|
258
|
+
x: startX,
|
|
259
|
+
y: canvasHeight - 20,
|
|
260
|
+
vx: (Math.random() - 0.5) * 0.5, // Slight drift
|
|
261
|
+
vy: -3 - Math.random() * 2, // Fast upward
|
|
262
|
+
targetY: targetY,
|
|
263
|
+
size: sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]),
|
|
264
|
+
opacity: 1,
|
|
265
|
+
color: this.colors.firework[Math.floor(Math.random() * this.colors.firework.length)],
|
|
266
|
+
trail: [], // Trail particles behind rocket
|
|
267
|
+
exploded: false,
|
|
268
|
+
rotation: Math.random() * Math.PI * 2,
|
|
269
|
+
rotationSpeed: (Math.random() - 0.5) * 0.1,
|
|
270
|
+
active: true
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Create burst particle (explosion fragment)
|
|
276
|
+
*/
|
|
277
|
+
createBurst(canvasWidth, canvasHeight, config, origin) {
|
|
278
|
+
const sizeRange = config?.sizeRange || [1, 3];
|
|
279
|
+
const angle = Math.random() * Math.PI * 2;
|
|
280
|
+
const speed = 1 + Math.random() * 4;
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
type: 'burst',
|
|
284
|
+
x: origin?.x || canvasWidth / 2,
|
|
285
|
+
y: origin?.y || canvasHeight / 2,
|
|
286
|
+
vx: Math.cos(angle) * speed,
|
|
287
|
+
vy: Math.sin(angle) * speed,
|
|
288
|
+
size: sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]),
|
|
289
|
+
opacity: 1,
|
|
290
|
+
color: this.colors.firework[Math.floor(Math.random() * this.colors.firework.length)],
|
|
291
|
+
life: 1.0, // Fades out over time
|
|
292
|
+
fadeRate: 0.015 + Math.random() * 0.015,
|
|
293
|
+
gravity: 0.05,
|
|
294
|
+
sparkle: Math.random() < 0.3, // 30% chance to sparkle
|
|
295
|
+
active: true
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create trail particle (follows rockets)
|
|
301
|
+
*/
|
|
302
|
+
createTrail(canvasWidth, canvasHeight, config, origin) {
|
|
303
|
+
const sizeRange = config?.sizeRange || [1, 2];
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
type: 'trail',
|
|
307
|
+
x: origin?.x || canvasWidth / 2,
|
|
308
|
+
y: origin?.y || canvasHeight / 2,
|
|
309
|
+
vx: (Math.random() - 0.5) * 0.3,
|
|
310
|
+
vy: (Math.random() - 0.5) * 0.3,
|
|
311
|
+
size: sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]),
|
|
312
|
+
opacity: 0.8,
|
|
313
|
+
color: origin?.color || '#ffd700',
|
|
314
|
+
life: 1.0,
|
|
315
|
+
fadeRate: 0.03 + Math.random() * 0.02,
|
|
316
|
+
active: true
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create spark particle (small glowing bits)
|
|
322
|
+
*/
|
|
323
|
+
createSpark(canvasWidth, canvasHeight, config, origin) {
|
|
324
|
+
const sizeRange = config?.sizeRange || [0.5, 1.5];
|
|
325
|
+
const angle = Math.random() * Math.PI * 2;
|
|
326
|
+
const speed = 0.5 + Math.random() * 2;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
type: 'spark',
|
|
330
|
+
x: origin?.x || Math.random() * canvasWidth,
|
|
331
|
+
y: origin?.y || Math.random() * canvasHeight,
|
|
332
|
+
vx: Math.cos(angle) * speed,
|
|
333
|
+
vy: Math.sin(angle) * speed,
|
|
334
|
+
size: sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]),
|
|
335
|
+
opacity: 1,
|
|
336
|
+
color: this.colors.secondary, // Gold sparks
|
|
337
|
+
life: 1.0,
|
|
338
|
+
fadeRate: 0.02 + Math.random() * 0.02,
|
|
339
|
+
active: true
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create falling particle (embers)
|
|
345
|
+
*/
|
|
346
|
+
createFallingParticle(canvasWidth, canvasHeight, config) {
|
|
347
|
+
return this.createEmber(canvasWidth, canvasHeight, config);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create initial static decorations (bonfires, Guy effigy, etc.)
|
|
352
|
+
*/
|
|
353
|
+
createInitialDecorations(canvasWidth, canvasHeight, config) {
|
|
354
|
+
const decorations = [];
|
|
355
|
+
|
|
356
|
+
// Create bonfires
|
|
357
|
+
const bonfireCount = config.bonfires || 2;
|
|
358
|
+
for (let i = 0; i < bonfireCount; i++) {
|
|
359
|
+
const bonfireX = (canvasWidth / (bonfireCount + 1)) * (i + 1);
|
|
360
|
+
const bonfireY = canvasHeight - 60;
|
|
361
|
+
|
|
362
|
+
decorations.push(this.createBonfire(canvasWidth, canvasHeight, {
|
|
363
|
+
x: bonfireX,
|
|
364
|
+
y: bonfireY
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
// Guy effigy on top of bonfire
|
|
368
|
+
decorations.push(this.createGuyEffigy(canvasWidth, canvasHeight, bonfireX, bonfireY));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Catherine wheels mounted on sides
|
|
372
|
+
const wheelCount = Math.min(bonfireCount, 2);
|
|
373
|
+
for (let i = 0; i < wheelCount; i++) {
|
|
374
|
+
const side = i % 2 === 0 ? 'left' : 'right';
|
|
375
|
+
decorations.push(this.createCatherineWheel(canvasWidth, canvasHeight, {
|
|
376
|
+
x: side === 'left' ? 100 : canvasWidth - 100,
|
|
377
|
+
y: 150 + i * 100
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Roman candles at ground level
|
|
382
|
+
const candleCount = Math.min(bonfireCount, 2);
|
|
383
|
+
for (let i = 0; i < candleCount; i++) {
|
|
384
|
+
decorations.push(this.createRomanCandle(canvasWidth, canvasHeight, {
|
|
385
|
+
x: 150 + i * ((canvasWidth - 300) / (candleCount)),
|
|
386
|
+
y: canvasHeight - 30
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Sparkler bundles (decorative)
|
|
391
|
+
const sparklerCount = Math.min(bonfireCount + 1, 3);
|
|
392
|
+
for (let i = 0; i < sparklerCount; i++) {
|
|
393
|
+
decorations.push({
|
|
394
|
+
type: 'sparkler-bundle',
|
|
395
|
+
x: 80 + i * ((canvasWidth - 160) / (sparklerCount - 1)),
|
|
396
|
+
y: canvasHeight - 20,
|
|
397
|
+
size: 12 + Math.random() * 4,
|
|
398
|
+
opacity: 0.85,
|
|
399
|
+
sparklePhase: Math.random() * Math.PI * 2,
|
|
400
|
+
active: true,
|
|
401
|
+
static: true
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Soft red moon (top right corner)
|
|
406
|
+
decorations.push({
|
|
407
|
+
type: 'moon',
|
|
408
|
+
x: canvasWidth - 120,
|
|
409
|
+
y: 100,
|
|
410
|
+
size: 60 + Math.random() * 20,
|
|
411
|
+
opacity: 0.75,
|
|
412
|
+
glowPhase: Math.random() * Math.PI * 2,
|
|
413
|
+
active: true,
|
|
414
|
+
static: true
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return decorations;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Spawn special Guy Fawkes particles
|
|
422
|
+
*/
|
|
423
|
+
spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight, config) {
|
|
424
|
+
const choice = Math.random();
|
|
425
|
+
|
|
426
|
+
// Fireworks (uses Christmas firework code!)
|
|
427
|
+
if (choice < config.fireworkChance) {
|
|
428
|
+
// Random explosion height between 20% and 70% of screen height (always visible)
|
|
429
|
+
const targetY = canvasHeight * (0.2 + Math.random() * 0.5);
|
|
430
|
+
const startY = canvasHeight;
|
|
431
|
+
const distance = startY - targetY;
|
|
432
|
+
|
|
433
|
+
// Calculate velocity to reach target in reasonable time
|
|
434
|
+
const flightTime = 40 + Math.random() * 20; // 40-60 frames (~0.7-1 second)
|
|
435
|
+
const vy = -(distance / flightTime);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
type: 'firework',
|
|
439
|
+
x: Math.random() * canvasWidth,
|
|
440
|
+
y: startY,
|
|
441
|
+
targetY: targetY, // Explosion height
|
|
442
|
+
vx: (Math.random() - 0.5) * 3, // Slight horizontal drift
|
|
443
|
+
vy: vy, // Calculated to reach target
|
|
444
|
+
size: 2 + Math.random() * 2,
|
|
445
|
+
opacity: 1,
|
|
446
|
+
active: true,
|
|
447
|
+
static: false,
|
|
448
|
+
time: 0,
|
|
449
|
+
exploded: false,
|
|
450
|
+
explosionTime: flightTime, // Time to reach target
|
|
451
|
+
color: this.colors.firework[Math.floor(Math.random() * this.colors.firework.length)]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Bonfire (rare, max 3)
|
|
456
|
+
if (choice < 0.0003) {
|
|
457
|
+
const bonfireCount = specialParticles.filter(p => p.type === 'bonfire').length;
|
|
458
|
+
if (bonfireCount < config.bonfires) {
|
|
459
|
+
return this.createBonfire(canvasWidth, canvasHeight);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Catherine wheel (rare)
|
|
464
|
+
if (choice < 0.0005) {
|
|
465
|
+
return this.createCatherineWheel(canvasWidth, canvasHeight);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Roman candle (rare)
|
|
469
|
+
if (choice < 0.0008) {
|
|
470
|
+
return this.createRomanCandle(canvasWidth, canvasHeight);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return null;
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Update special particles (fireworks, sparks, etc.)
|
|
478
|
+
*/
|
|
479
|
+
updateSpecialParticles(specialParticles, deltaTime) {
|
|
480
|
+
// Update lightning effect
|
|
481
|
+
if (this.lightningActive) {
|
|
482
|
+
this.lightningTimer += deltaTime;
|
|
483
|
+
if (this.lightningTimer >= this.lightningDuration) {
|
|
484
|
+
this.lightningActive = false;
|
|
485
|
+
this.lightningTimer = 0;
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
if (Math.random() < this.lightningChance) {
|
|
489
|
+
this.lightningActive = true;
|
|
490
|
+
this.lightningTimer = 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
for (const particle of specialParticles) {
|
|
495
|
+
// Increment time for animated particles
|
|
496
|
+
if (particle.time !== undefined) {
|
|
497
|
+
particle.time++;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Check firework explosion
|
|
501
|
+
if (particle.type === 'firework' && !particle.exploded) {
|
|
502
|
+
// Explode when reached target height OR after flight time
|
|
503
|
+
const reachedTarget = particle.targetY && particle.y <= particle.targetY;
|
|
504
|
+
const timeExpired = particle.time >= particle.explosionTime;
|
|
505
|
+
|
|
506
|
+
if (reachedTarget || timeExpired) {
|
|
507
|
+
particle.exploded = true;
|
|
508
|
+
this.explodeFirework(particle, specialParticles);
|
|
509
|
+
particle.active = false; // Remove the firework itself
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Fade out sparks
|
|
514
|
+
if (particle.type === 'spark') {
|
|
515
|
+
particle.opacity -= 0.015;
|
|
516
|
+
particle.vy += 0.15; // Gravity
|
|
517
|
+
if (particle.opacity <= 0) {
|
|
518
|
+
particle.active = false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Update Catherine Wheel
|
|
523
|
+
if (particle.type === 'catherine-wheel') {
|
|
524
|
+
particle.rotation += particle.rotationSpeed * (deltaTime / 16); // Normalize rotation speed
|
|
525
|
+
particle.time += deltaTime;
|
|
526
|
+
|
|
527
|
+
// Deactivate after duration
|
|
528
|
+
if (particle.time > particle.duration) {
|
|
529
|
+
particle.active = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Emit new sparks
|
|
533
|
+
particle.sparkTimer += deltaTime;
|
|
534
|
+
if (particle.sparkTimer >= particle.sparkInterval) {
|
|
535
|
+
particle.sparkTimer = 0;
|
|
536
|
+
// Emit multiple sparks per interval
|
|
537
|
+
const numSparks = 2 + Math.floor(Math.random() * 3); // 2-4 sparks
|
|
538
|
+
for (let i = 0; i < numSparks; i++) {
|
|
539
|
+
const angle = Math.random() * Math.PI * 2; // Random direction
|
|
540
|
+
const speed = 1 + Math.random() * 3;
|
|
541
|
+
particle.sparks.push({
|
|
542
|
+
x: 0, // Relative to wheel center
|
|
543
|
+
y: 0, // Relative to wheel center
|
|
544
|
+
vx: Math.cos(angle) * speed,
|
|
545
|
+
vy: Math.sin(angle) * speed,
|
|
546
|
+
size: 1 + Math.random() * 1.5,
|
|
547
|
+
opacity: 1,
|
|
548
|
+
life: 1.0,
|
|
549
|
+
fadeRate: 0.05 + Math.random() * 0.03,
|
|
550
|
+
color: this.colors.firework[Math.floor(Math.random() * this.colors.firework.length)]
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Update existing sparks
|
|
556
|
+
particle.sparks = particle.sparks.filter(spark => {
|
|
557
|
+
spark.x += spark.vx;
|
|
558
|
+
spark.y += spark.vy;
|
|
559
|
+
spark.life -= spark.fadeRate;
|
|
560
|
+
spark.opacity = Math.max(0, spark.life);
|
|
561
|
+
return spark.life > 0;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Draw floating ember
|
|
569
|
+
*/
|
|
570
|
+
drawEmber(ctx, particle, time) {
|
|
571
|
+
// Safety check: validate particle properties
|
|
572
|
+
if (!isFinite(particle.x) || !isFinite(particle.y) || !isFinite(particle.size) ||
|
|
573
|
+
!isFinite(particle.vx) || !isFinite(particle.vy)) {
|
|
574
|
+
console.warn('[Guy Fawkes] Invalid ember particle values, deactivating:', particle);
|
|
575
|
+
particle.active = false;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Update physics - rising motion
|
|
580
|
+
if (!particle.exploded) {
|
|
581
|
+
particle.x += particle.vx;
|
|
582
|
+
particle.y += particle.vy;
|
|
583
|
+
particle.vy += 0.02; // Slight gravity
|
|
584
|
+
|
|
585
|
+
// Validate after update
|
|
586
|
+
if (!isFinite(particle.x) || !isFinite(particle.y)) {
|
|
587
|
+
console.warn('[Guy Fawkes] Ember particle became non-finite after update, deactivating');
|
|
588
|
+
particle.active = false;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Check if reached target height
|
|
593
|
+
if (particle.y <= particle.targetY) {
|
|
594
|
+
particle.exploded = true;
|
|
595
|
+
// Create DRAMATIC explosion particles (MORE and BIGGER!)
|
|
596
|
+
const explosionCount = 50 + Math.floor(Math.random() * 40); // 50-90 particles
|
|
597
|
+
for (let i = 0; i < explosionCount; i++) {
|
|
598
|
+
const angle = (i / explosionCount) * Math.PI * 2;
|
|
599
|
+
const speed = 2 + Math.random() * 5; // Faster spread
|
|
600
|
+
const isTracer = i % 5 === 0; // Every 5th particle is a bright tracer
|
|
601
|
+
particle.explosionParticles.push({
|
|
602
|
+
x: particle.x,
|
|
603
|
+
y: particle.y,
|
|
604
|
+
vx: Math.cos(angle) * speed,
|
|
605
|
+
vy: Math.sin(angle) * speed,
|
|
606
|
+
size: isTracer ? 2 + Math.random() * 2 : 0.8 + Math.random() * 2, // Bigger particles
|
|
607
|
+
color: isTracer ? '#ffffff' : this.colors.firework[Math.floor(Math.random() * this.colors.firework.length)],
|
|
608
|
+
opacity: 1,
|
|
609
|
+
life: 1.0,
|
|
610
|
+
trail: [] // Add trail for tracer particles
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const x = particle.x;
|
|
617
|
+
const y = particle.y;
|
|
618
|
+
const size = particle.size;
|
|
619
|
+
|
|
620
|
+
ctx.save();
|
|
621
|
+
|
|
622
|
+
if (!particle.exploded) {
|
|
623
|
+
// Rising ember with trail
|
|
624
|
+
const glowIntensity = 0.8 + (Math.sin(time * 0.01 + particle.glowPhase) + 1) * 0.2;
|
|
625
|
+
|
|
626
|
+
// Trail
|
|
627
|
+
ctx.globalAlpha = 0.3;
|
|
628
|
+
ctx.strokeStyle = particle.trailColor;
|
|
629
|
+
ctx.lineWidth = size * 0.8;
|
|
630
|
+
ctx.lineCap = 'round';
|
|
631
|
+
ctx.beginPath();
|
|
632
|
+
ctx.moveTo(x, y);
|
|
633
|
+
ctx.lineTo(x - particle.vx * 5, y - particle.vy * 5);
|
|
634
|
+
ctx.stroke();
|
|
635
|
+
|
|
636
|
+
// Glow
|
|
637
|
+
const gradient = ctx.createRadialGradient(x, y, 0, x, y, size * 4);
|
|
638
|
+
gradient.addColorStop(0, particle.color);
|
|
639
|
+
gradient.addColorStop(0.5, `${particle.color}80`);
|
|
640
|
+
gradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
641
|
+
ctx.globalAlpha = glowIntensity;
|
|
642
|
+
ctx.fillStyle = gradient;
|
|
643
|
+
ctx.fillRect(x - size * 4, y - size * 4, size * 8, size * 8);
|
|
644
|
+
|
|
645
|
+
// Core
|
|
646
|
+
ctx.globalAlpha = 1;
|
|
647
|
+
ctx.fillStyle = particle.color;
|
|
648
|
+
ctx.shadowColor = particle.color;
|
|
649
|
+
ctx.shadowBlur = size * 2;
|
|
650
|
+
ctx.beginPath();
|
|
651
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
652
|
+
ctx.fill();
|
|
653
|
+
} else {
|
|
654
|
+
// Draw explosion particles
|
|
655
|
+
particle.explosionParticles = particle.explosionParticles.filter(p => {
|
|
656
|
+
p.x += p.vx;
|
|
657
|
+
p.y += p.vy;
|
|
658
|
+
p.vy += 0.05; // Gravity on explosion particles
|
|
659
|
+
p.life -= 0.015;
|
|
660
|
+
p.opacity = p.life;
|
|
661
|
+
|
|
662
|
+
if (p.life <= 0) return false;
|
|
663
|
+
|
|
664
|
+
// Draw explosion particle
|
|
665
|
+
ctx.globalAlpha = p.opacity;
|
|
666
|
+
ctx.fillStyle = p.color;
|
|
667
|
+
ctx.shadowColor = p.color;
|
|
668
|
+
ctx.shadowBlur = p.size * 3;
|
|
669
|
+
ctx.beginPath();
|
|
670
|
+
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
671
|
+
ctx.fill();
|
|
672
|
+
|
|
673
|
+
return true;
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Deactivate when all explosion particles are gone
|
|
677
|
+
if (particle.explosionParticles.length === 0) {
|
|
678
|
+
particle.active = false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
ctx.restore();
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Draw bonfire with animated flames
|
|
687
|
+
*/
|
|
688
|
+
drawBonfire(ctx, particle, time) {
|
|
689
|
+
const x = particle.x;
|
|
690
|
+
const y = particle.y;
|
|
691
|
+
const size = particle.size;
|
|
692
|
+
|
|
693
|
+
ctx.save();
|
|
694
|
+
ctx.translate(x, y);
|
|
695
|
+
|
|
696
|
+
// Logs (wood pile)
|
|
697
|
+
ctx.fillStyle = '#8B4513';
|
|
698
|
+
ctx.strokeStyle = '#654321';
|
|
699
|
+
ctx.lineWidth = 2;
|
|
700
|
+
|
|
701
|
+
// Bottom log
|
|
702
|
+
ctx.fillRect(-size * 0.8, size * 0.3, size * 1.6, size * 0.3);
|
|
703
|
+
ctx.strokeRect(-size * 0.8, size * 0.3, size * 1.6, size * 0.3);
|
|
704
|
+
|
|
705
|
+
// Middle logs (crossed)
|
|
706
|
+
ctx.save();
|
|
707
|
+
ctx.rotate(-0.3);
|
|
708
|
+
ctx.fillRect(-size * 0.7, size * 0.1, size * 1.4, size * 0.25);
|
|
709
|
+
ctx.strokeRect(-size * 0.7, size * 0.1, size * 1.4, size * 0.25);
|
|
710
|
+
ctx.restore();
|
|
711
|
+
|
|
712
|
+
ctx.save();
|
|
713
|
+
ctx.rotate(0.3);
|
|
714
|
+
ctx.fillRect(-size * 0.7, size * 0.1, size * 1.4, size * 0.25);
|
|
715
|
+
ctx.strokeRect(-size * 0.7, size * 0.1, size * 1.4, size * 0.25);
|
|
716
|
+
ctx.restore();
|
|
717
|
+
|
|
718
|
+
// Animated flames (BIGGER and MORE DRAMATIC!)
|
|
719
|
+
const flameCount = 12; // More flames for fuller effect
|
|
720
|
+
for (let i = 0; i < flameCount; i++) {
|
|
721
|
+
const flameX = -size * 0.6 + (i / flameCount) * size * 1.2;
|
|
722
|
+
// Taller, more dynamic flames
|
|
723
|
+
const flameHeight = size * (1.2 + Math.sin(time * 0.006 + i) * 0.5);
|
|
724
|
+
const flameWidth = size * (0.2 + Math.sin(time * 0.01 + i * 0.5) * 0.12);
|
|
725
|
+
|
|
726
|
+
// More dramatic flame gradient with white-hot core
|
|
727
|
+
const flameGradient = ctx.createLinearGradient(flameX, 0, flameX, -flameHeight);
|
|
728
|
+
flameGradient.addColorStop(0, '#ffffff'); // White-hot base
|
|
729
|
+
flameGradient.addColorStop(0.15, '#ffff00'); // Yellow
|
|
730
|
+
flameGradient.addColorStop(0.4, '#ffa500'); // Orange
|
|
731
|
+
flameGradient.addColorStop(0.7, '#ff4500'); // Red-orange
|
|
732
|
+
flameGradient.addColorStop(0.85, '#ff0000'); // Red
|
|
733
|
+
flameGradient.addColorStop(1, 'rgba(139, 0, 0, 0)'); // Dark red fade
|
|
734
|
+
|
|
735
|
+
ctx.fillStyle = flameGradient;
|
|
736
|
+
ctx.globalAlpha = 0.85;
|
|
737
|
+
ctx.shadowColor = '#ff6600';
|
|
738
|
+
ctx.shadowBlur = size * 0.3;
|
|
739
|
+
|
|
740
|
+
ctx.beginPath();
|
|
741
|
+
ctx.moveTo(flameX, 0);
|
|
742
|
+
// More exaggerated, flickering shape
|
|
743
|
+
ctx.bezierCurveTo(
|
|
744
|
+
flameX - flameWidth, -flameHeight * 0.4,
|
|
745
|
+
flameX - flameWidth * 0.6, -flameHeight * 0.7,
|
|
746
|
+
flameX, -flameHeight
|
|
747
|
+
);
|
|
748
|
+
ctx.bezierCurveTo(
|
|
749
|
+
flameX + flameWidth * 0.6, -flameHeight * 0.7,
|
|
750
|
+
flameX + flameWidth, -flameHeight * 0.4,
|
|
751
|
+
flameX, 0
|
|
752
|
+
);
|
|
753
|
+
ctx.closePath();
|
|
754
|
+
ctx.fill();
|
|
755
|
+
}
|
|
756
|
+
ctx.shadowBlur = 0;
|
|
757
|
+
ctx.globalAlpha = 1;
|
|
758
|
+
|
|
759
|
+
// Glow
|
|
760
|
+
const glowGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size * 2);
|
|
761
|
+
glowGradient.addColorStop(0, 'rgba(255, 165, 0, 0.4)');
|
|
762
|
+
glowGradient.addColorStop(0.5, 'rgba(255, 69, 0, 0.2)');
|
|
763
|
+
glowGradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
764
|
+
ctx.fillStyle = glowGradient;
|
|
765
|
+
ctx.fillRect(-size * 2, -size * 2, size * 4, size * 2);
|
|
766
|
+
|
|
767
|
+
// Sparks/embers rising from fire
|
|
768
|
+
for (let i = 0; i < 5; i++) {
|
|
769
|
+
const sparkX = (Math.random() - 0.5) * size * 0.8;
|
|
770
|
+
const sparkY = -size * Math.random() * 1.5;
|
|
771
|
+
const sparkSize = 1 + Math.random() * 2;
|
|
772
|
+
ctx.fillStyle = Math.random() < 0.5 ? '#ffa500' : '#ffff00';
|
|
773
|
+
ctx.globalAlpha = Math.random() * 0.8;
|
|
774
|
+
ctx.beginPath();
|
|
775
|
+
ctx.arc(sparkX, sparkY, sparkSize, 0, Math.PI * 2);
|
|
776
|
+
ctx.fill();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
ctx.restore();
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Draw Guy Fawkes effigy
|
|
784
|
+
*/
|
|
785
|
+
drawGuyEffigy(ctx, particle, time) {
|
|
786
|
+
const x = particle.x;
|
|
787
|
+
const y = particle.y;
|
|
788
|
+
const size = particle.size;
|
|
789
|
+
|
|
790
|
+
ctx.save();
|
|
791
|
+
ctx.globalAlpha = particle.opacity * (1 - particle.burnProgress);
|
|
792
|
+
ctx.translate(x, y);
|
|
793
|
+
|
|
794
|
+
// Body (straw/cloth)
|
|
795
|
+
ctx.fillStyle = '#8B7355';
|
|
796
|
+
ctx.fillRect(-size * 0.4, 0, size * 0.8, size * 1.5);
|
|
797
|
+
|
|
798
|
+
// Arms
|
|
799
|
+
ctx.fillRect(-size * 0.9, size * 0.3, size * 0.5, size * 0.2);
|
|
800
|
+
ctx.fillRect(size * 0.4, size * 0.3, size * 0.5, size * 0.2);
|
|
801
|
+
|
|
802
|
+
// Head (sack/mask)
|
|
803
|
+
ctx.fillStyle = '#D2B48C';
|
|
804
|
+
ctx.beginPath();
|
|
805
|
+
ctx.arc(0, -size * 0.3, size * 0.5, 0, Math.PI * 2);
|
|
806
|
+
ctx.fill();
|
|
807
|
+
|
|
808
|
+
// Hat
|
|
809
|
+
ctx.fillStyle = '#4a4a4a';
|
|
810
|
+
ctx.fillRect(-size * 0.6, -size * 0.8, size * 1.2, size * 0.2);
|
|
811
|
+
ctx.fillRect(-size * 0.4, -size * 1.2, size * 0.8, size * 0.4);
|
|
812
|
+
|
|
813
|
+
// Face (scary mask)
|
|
814
|
+
ctx.fillStyle = '#000000';
|
|
815
|
+
// Eyes
|
|
816
|
+
ctx.fillRect(-size * 0.25, -size * 0.4, size * 0.15, size * 0.2);
|
|
817
|
+
ctx.fillRect(size * 0.1, -size * 0.4, size * 0.15, size * 0.2);
|
|
818
|
+
|
|
819
|
+
// Mouth (grin)
|
|
820
|
+
ctx.beginPath();
|
|
821
|
+
ctx.arc(0, -size * 0.1, size * 0.25, 0, Math.PI);
|
|
822
|
+
ctx.fill();
|
|
823
|
+
|
|
824
|
+
// Burning effect if burning
|
|
825
|
+
if (particle.burning) {
|
|
826
|
+
ctx.globalAlpha = 1;
|
|
827
|
+
const fireGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size * 2);
|
|
828
|
+
fireGradient.addColorStop(0, 'rgba(255, 255, 0, 0.8)');
|
|
829
|
+
fireGradient.addColorStop(0.5, 'rgba(255, 69, 0, 0.5)');
|
|
830
|
+
fireGradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
|
|
831
|
+
ctx.fillStyle = fireGradient;
|
|
832
|
+
ctx.fillRect(-size * 2, -size * 2, size * 4, size * 4);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
ctx.restore();
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Draw Catherine wheel (spinning firework) - IMPROVED
|
|
840
|
+
*/
|
|
841
|
+
drawCatherineWheel(ctx, particle, time) {
|
|
842
|
+
const x = particle.x;
|
|
843
|
+
const y = particle.y;
|
|
844
|
+
const size = particle.size;
|
|
845
|
+
|
|
846
|
+
ctx.save();
|
|
847
|
+
ctx.translate(x, y);
|
|
848
|
+
ctx.rotate(particle.rotation);
|
|
849
|
+
|
|
850
|
+
// Center pin/mount
|
|
851
|
+
ctx.fillStyle = '#654321'; // Dark wood/metal color
|
|
852
|
+
ctx.beginPath();
|
|
853
|
+
ctx.arc(0, 0, size * 0.1, 0, Math.PI * 2);
|
|
854
|
+
ctx.fill();
|
|
855
|
+
|
|
856
|
+
// Wheel structure (concentric circles, more defined)
|
|
857
|
+
ctx.strokeStyle = '#8B4513'; // Brownish for the cardboard/wood wheel
|
|
858
|
+
ctx.lineWidth = size * 0.15;
|
|
859
|
+
ctx.beginPath();
|
|
860
|
+
ctx.arc(0, 0, size * 0.6, 0, Math.PI * 2);
|
|
861
|
+
ctx.stroke();
|
|
862
|
+
|
|
863
|
+
ctx.strokeStyle = '#cc6600'; // Inner ring for color
|
|
864
|
+
ctx.lineWidth = size * 0.1;
|
|
865
|
+
ctx.beginPath();
|
|
866
|
+
ctx.arc(0, 0, size * 0.4, 0, Math.PI * 2);
|
|
867
|
+
ctx.stroke();
|
|
868
|
+
|
|
869
|
+
// Outer firework tubes/spokes
|
|
870
|
+
const tubeCount = 8;
|
|
871
|
+
for (let i = 0; i < tubeCount; i++) {
|
|
872
|
+
const angle = (i / tubeCount) * Math.PI * 2;
|
|
873
|
+
const tubeX = Math.cos(angle) * size * 0.7;
|
|
874
|
+
const tubeY = Math.sin(angle) * size * 0.7;
|
|
875
|
+
|
|
876
|
+
ctx.fillStyle = '#444444'; // Dark grey for tubes
|
|
877
|
+
ctx.strokeStyle = '#222222';
|
|
878
|
+
ctx.lineWidth = 1;
|
|
879
|
+
ctx.beginPath();
|
|
880
|
+
ctx.arc(tubeX, tubeY, size * 0.15, 0, Math.PI * 2);
|
|
881
|
+
ctx.fill();
|
|
882
|
+
ctx.stroke();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Draw emitted sparks
|
|
886
|
+
particle.sparks.forEach(spark => {
|
|
887
|
+
ctx.save();
|
|
888
|
+
ctx.translate(spark.x, spark.y);
|
|
889
|
+
|
|
890
|
+
// Spark glow
|
|
891
|
+
const glowGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, spark.size * 5);
|
|
892
|
+
glowGradient.addColorStop(0, spark.color);
|
|
893
|
+
glowGradient.addColorStop(0.5, `${spark.color}80`);
|
|
894
|
+
glowGradient.addColorStop(1, 'rgba(255, 69, 0, 0)'); // Fade to transparent orange/red
|
|
895
|
+
ctx.fillStyle = glowGradient;
|
|
896
|
+
ctx.globalAlpha = spark.opacity * 0.6;
|
|
897
|
+
ctx.fillRect(-spark.size * 5, -spark.size * 5, spark.size * 10, spark.size * 10);
|
|
898
|
+
|
|
899
|
+
// Spark core
|
|
900
|
+
ctx.globalAlpha = spark.opacity;
|
|
901
|
+
ctx.fillStyle = spark.color;
|
|
902
|
+
ctx.shadowColor = spark.color;
|
|
903
|
+
ctx.shadowBlur = spark.size * 3;
|
|
904
|
+
ctx.beginPath();
|
|
905
|
+
ctx.arc(0, 0, spark.size, 0, Math.PI * 2);
|
|
906
|
+
ctx.fill();
|
|
907
|
+
|
|
908
|
+
ctx.restore();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// Pulsing inner glow from the active wheel
|
|
912
|
+
const innerGlowIntensity = 0.6 + Math.sin(time * 0.01) * 0.4;
|
|
913
|
+
ctx.globalAlpha = innerGlowIntensity * particle.opacity;
|
|
914
|
+
const innerGlow = ctx.createRadialGradient(0, 0, 0, 0, 0, size);
|
|
915
|
+
innerGlow.addColorStop(0, 'rgba(255, 200, 0, 0.7)');
|
|
916
|
+
innerGlow.addColorStop(0.5, 'rgba(255, 100, 0, 0.4)');
|
|
917
|
+
innerGlow.addColorStop(1, 'rgba(255, 0, 0, 0)');
|
|
918
|
+
ctx.fillStyle = innerGlow;
|
|
919
|
+
ctx.beginPath();
|
|
920
|
+
ctx.arc(0, 0, size, 0, Math.PI * 2);
|
|
921
|
+
ctx.fill();
|
|
922
|
+
|
|
923
|
+
ctx.shadowBlur = 0;
|
|
924
|
+
ctx.restore();
|
|
925
|
+
},
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Draw Roman candle
|
|
929
|
+
*/
|
|
930
|
+
drawRomanCandle(ctx, particle, time) {
|
|
931
|
+
const x = particle.x;
|
|
932
|
+
const y = particle.y;
|
|
933
|
+
const size = particle.size;
|
|
934
|
+
|
|
935
|
+
ctx.save();
|
|
936
|
+
ctx.translate(x, y);
|
|
937
|
+
|
|
938
|
+
// Candle body (tube)
|
|
939
|
+
ctx.fillStyle = '#c00';
|
|
940
|
+
ctx.strokeStyle = '#800';
|
|
941
|
+
ctx.lineWidth = 2;
|
|
942
|
+
ctx.fillRect(-size * 0.4, 0, size * 0.8, size * 2);
|
|
943
|
+
ctx.strokeRect(-size * 0.4, 0, size * 0.8, size * 2);
|
|
944
|
+
|
|
945
|
+
// Gold label
|
|
946
|
+
ctx.fillStyle = '#ffd700';
|
|
947
|
+
ctx.fillRect(-size * 0.3, size * 0.8, size * 0.6, size * 0.5);
|
|
948
|
+
|
|
949
|
+
// Fuse/top
|
|
950
|
+
ctx.fillStyle = '#654321';
|
|
951
|
+
ctx.fillRect(-size * 0.2, -size * 0.3, size * 0.4, size * 0.3);
|
|
952
|
+
|
|
953
|
+
// Spark at top when firing
|
|
954
|
+
if (particle.shotCount < particle.maxShots) {
|
|
955
|
+
const sparkIntensity = Math.sin(time * 0.05) * 0.5 + 0.5;
|
|
956
|
+
ctx.fillStyle = `rgba(255, 255, 0, ${sparkIntensity})`;
|
|
957
|
+
ctx.shadowColor = '#ffff00';
|
|
958
|
+
ctx.shadowBlur = 15;
|
|
959
|
+
ctx.beginPath();
|
|
960
|
+
ctx.arc(0, -size * 0.2, size * 0.3, 0, Math.PI * 2);
|
|
961
|
+
ctx.fill();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
ctx.shadowBlur = 0;
|
|
965
|
+
ctx.restore();
|
|
966
|
+
|
|
967
|
+
// Draw shots in flight
|
|
968
|
+
particle.shots.forEach(shot => {
|
|
969
|
+
ctx.save();
|
|
970
|
+
ctx.globalAlpha = shot.opacity;
|
|
971
|
+
|
|
972
|
+
// Trail
|
|
973
|
+
ctx.strokeStyle = shot.color;
|
|
974
|
+
ctx.lineWidth = 3;
|
|
975
|
+
ctx.shadowColor = shot.color;
|
|
976
|
+
ctx.shadowBlur = 10;
|
|
977
|
+
ctx.beginPath();
|
|
978
|
+
ctx.moveTo(shot.x, shot.y);
|
|
979
|
+
ctx.lineTo(shot.x, shot.y + 10);
|
|
980
|
+
ctx.stroke();
|
|
981
|
+
|
|
982
|
+
// Core
|
|
983
|
+
ctx.fillStyle = shot.color;
|
|
984
|
+
ctx.beginPath();
|
|
985
|
+
ctx.arc(shot.x, shot.y, shot.size, 0, Math.PI * 2);
|
|
986
|
+
ctx.fill();
|
|
987
|
+
|
|
988
|
+
ctx.shadowBlur = 0;
|
|
989
|
+
ctx.restore();
|
|
990
|
+
});
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Draw sparkler bundle (decorative handheld fireworks)
|
|
995
|
+
*/
|
|
996
|
+
drawSparklerBundle(ctx, particle, time) {
|
|
997
|
+
const x = particle.x;
|
|
998
|
+
const y = particle.y;
|
|
999
|
+
const size = particle.size;
|
|
1000
|
+
|
|
1001
|
+
ctx.save();
|
|
1002
|
+
ctx.globalAlpha = particle.opacity;
|
|
1003
|
+
ctx.translate(x, y);
|
|
1004
|
+
|
|
1005
|
+
// Bundle of sparklers (5 sticks)
|
|
1006
|
+
const sparklerCount = 5;
|
|
1007
|
+
const spread = size * 0.15;
|
|
1008
|
+
|
|
1009
|
+
for (let i = 0; i < sparklerCount; i++) {
|
|
1010
|
+
const offsetX = (i - 2) * spread;
|
|
1011
|
+
const tilt = (i - 2) * 0.05;
|
|
1012
|
+
|
|
1013
|
+
ctx.save();
|
|
1014
|
+
ctx.translate(offsetX, 0);
|
|
1015
|
+
ctx.rotate(tilt);
|
|
1016
|
+
|
|
1017
|
+
// Stick (metallic wire)
|
|
1018
|
+
ctx.strokeStyle = '#888888';
|
|
1019
|
+
ctx.lineWidth = size * 0.08;
|
|
1020
|
+
ctx.lineCap = 'round';
|
|
1021
|
+
ctx.beginPath();
|
|
1022
|
+
ctx.moveTo(0, 0);
|
|
1023
|
+
ctx.lineTo(0, -size * 1.8);
|
|
1024
|
+
ctx.stroke();
|
|
1025
|
+
|
|
1026
|
+
// Sparkler tip (lit)
|
|
1027
|
+
const sparkIntensity = Math.sin(time * 0.01 + particle.sparklePhase + i * 0.5) * 0.5 + 0.5;
|
|
1028
|
+
const tipGradient = ctx.createRadialGradient(0, -size * 1.8, 0, 0, -size * 1.8, size * 0.5);
|
|
1029
|
+
tipGradient.addColorStop(0, `rgba(255, 255, 255, ${sparkIntensity})`);
|
|
1030
|
+
tipGradient.addColorStop(0.4, `rgba(255, 215, 0, ${sparkIntensity * 0.8})`);
|
|
1031
|
+
tipGradient.addColorStop(0.7, `rgba(255, 140, 0, ${sparkIntensity * 0.5})`);
|
|
1032
|
+
tipGradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
1033
|
+
|
|
1034
|
+
ctx.fillStyle = tipGradient;
|
|
1035
|
+
ctx.beginPath();
|
|
1036
|
+
ctx.arc(0, -size * 1.8, size * 0.5, 0, Math.PI * 2);
|
|
1037
|
+
ctx.fill();
|
|
1038
|
+
|
|
1039
|
+
// Tiny sparks flying off
|
|
1040
|
+
const sparkCount = 3;
|
|
1041
|
+
for (let j = 0; j < sparkCount; j++) {
|
|
1042
|
+
const sparkAngle = Math.random() * Math.PI * 2;
|
|
1043
|
+
const sparkDist = size * (0.6 + Math.random() * 0.4);
|
|
1044
|
+
const sparkX = Math.cos(sparkAngle) * sparkDist;
|
|
1045
|
+
const sparkY = -size * 1.8 + Math.sin(sparkAngle) * sparkDist;
|
|
1046
|
+
const sparkSize = size * (0.05 + Math.random() * 0.08);
|
|
1047
|
+
|
|
1048
|
+
ctx.fillStyle = Math.random() < 0.5 ? '#ffffff' : '#ffd700';
|
|
1049
|
+
ctx.shadowColor = ctx.fillStyle;
|
|
1050
|
+
ctx.shadowBlur = size * 0.3;
|
|
1051
|
+
ctx.globalAlpha = particle.opacity * Math.random() * 0.8;
|
|
1052
|
+
|
|
1053
|
+
ctx.beginPath();
|
|
1054
|
+
ctx.arc(sparkX, sparkY, sparkSize, 0, Math.PI * 2);
|
|
1055
|
+
ctx.fill();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
ctx.restore();
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Handle/holder at bottom
|
|
1062
|
+
ctx.fillStyle = '#654321';
|
|
1063
|
+
ctx.fillRect(-size * 0.5, -size * 0.2, size, size * 0.4);
|
|
1064
|
+
|
|
1065
|
+
ctx.shadowBlur = 0;
|
|
1066
|
+
ctx.restore();
|
|
1067
|
+
},
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Draw firework (reuses Christmas firework code)
|
|
1071
|
+
*/
|
|
1072
|
+
drawFirework(ctx, particle) {
|
|
1073
|
+
return christmas.drawFirework(ctx, particle);
|
|
1074
|
+
},
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Explode firework (reuses Christmas explosion code)
|
|
1078
|
+
*/
|
|
1079
|
+
explodeFirework(particle, specialParticles) {
|
|
1080
|
+
// Use themed colors instead of Christmas colors
|
|
1081
|
+
const sparkCount = 60 + Math.random() * 60;
|
|
1082
|
+
const colors = this.colors.firework;
|
|
1083
|
+
|
|
1084
|
+
for (let i = 0; i < sparkCount; i++) {
|
|
1085
|
+
const angle = Math.random() * Math.PI * 2;
|
|
1086
|
+
const speed = Math.random() * 6 + 3;
|
|
1087
|
+
specialParticles.push({
|
|
1088
|
+
type: 'spark',
|
|
1089
|
+
x: particle.x,
|
|
1090
|
+
y: particle.y,
|
|
1091
|
+
vx: Math.cos(angle) * speed,
|
|
1092
|
+
vy: Math.sin(angle) * speed,
|
|
1093
|
+
size: 2 + Math.random() * 2.5,
|
|
1094
|
+
opacity: 1,
|
|
1095
|
+
active: true,
|
|
1096
|
+
static: false,
|
|
1097
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
1098
|
+
trail: []
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Draw soft red moon (Bonfire Night atmosphere)
|
|
1105
|
+
*/
|
|
1106
|
+
drawMoon(ctx, particle, time) {
|
|
1107
|
+
const x = particle.x;
|
|
1108
|
+
const y = particle.y;
|
|
1109
|
+
const size = particle.size;
|
|
1110
|
+
|
|
1111
|
+
ctx.save();
|
|
1112
|
+
ctx.globalAlpha = particle.opacity;
|
|
1113
|
+
ctx.translate(x, y);
|
|
1114
|
+
|
|
1115
|
+
// Moon body (soft red/orange tint)
|
|
1116
|
+
const moonGradient = ctx.createRadialGradient(-size * 0.2, -size * 0.2, 0, 0, 0, size);
|
|
1117
|
+
moonGradient.addColorStop(0, '#ffb6a0'); // Soft peachy center
|
|
1118
|
+
moonGradient.addColorStop(0.5, '#ff8c66'); // Warm orange-red
|
|
1119
|
+
moonGradient.addColorStop(1, '#cc6644'); // Deeper red edge
|
|
1120
|
+
ctx.fillStyle = moonGradient;
|
|
1121
|
+
ctx.beginPath();
|
|
1122
|
+
ctx.arc(0, 0, size, 0, Math.PI * 2);
|
|
1123
|
+
ctx.fill();
|
|
1124
|
+
|
|
1125
|
+
// Craters (darker red)
|
|
1126
|
+
ctx.fillStyle = 'rgba(120, 50, 40, 0.25)';
|
|
1127
|
+
ctx.beginPath();
|
|
1128
|
+
ctx.arc(-size * 0.3, -size * 0.2, size * 0.15, 0, Math.PI * 2);
|
|
1129
|
+
ctx.fill();
|
|
1130
|
+
ctx.beginPath();
|
|
1131
|
+
ctx.arc(size * 0.25, size * 0.1, size * 0.2, 0, Math.PI * 2);
|
|
1132
|
+
ctx.fill();
|
|
1133
|
+
ctx.beginPath();
|
|
1134
|
+
ctx.arc(size * 0.1, -size * 0.4, size * 0.12, 0, Math.PI * 2);
|
|
1135
|
+
ctx.fill();
|
|
1136
|
+
|
|
1137
|
+
// Soft smoke wisps drifting across moon surface
|
|
1138
|
+
ctx.strokeStyle = 'rgba(100, 60, 50, 0.15)';
|
|
1139
|
+
ctx.lineWidth = size * 0.08;
|
|
1140
|
+
ctx.lineCap = 'round';
|
|
1141
|
+
const smokeWave = Math.sin(time * 0.001 + particle.glowPhase) * size * 0.3;
|
|
1142
|
+
for (let i = 0; i < 3; i++) {
|
|
1143
|
+
ctx.beginPath();
|
|
1144
|
+
const yPos = -size * 0.4 + i * size * 0.4;
|
|
1145
|
+
ctx.moveTo(-size * 0.8, yPos + smokeWave * (i % 2 === 0 ? 1 : -1));
|
|
1146
|
+
ctx.bezierCurveTo(
|
|
1147
|
+
-size * 0.3, yPos + smokeWave * 0.5,
|
|
1148
|
+
size * 0.3, yPos - smokeWave * 0.5,
|
|
1149
|
+
size * 0.8, yPos + smokeWave * (i % 2 === 0 ? -1 : 1)
|
|
1150
|
+
);
|
|
1151
|
+
ctx.stroke();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Soft red glow (pulsing gently)
|
|
1155
|
+
const glowIntensity = 0.25 + Math.sin(time * 0.0015 + particle.glowPhase) * 0.1;
|
|
1156
|
+
ctx.globalAlpha = glowIntensity;
|
|
1157
|
+
const glowGradient = ctx.createRadialGradient(0, 0, size * 0.8, 0, 0, size * 2.0);
|
|
1158
|
+
glowGradient.addColorStop(0, 'rgba(255, 140, 100, 0.6)');
|
|
1159
|
+
glowGradient.addColorStop(0.5, 'rgba(255, 100, 80, 0.3)');
|
|
1160
|
+
glowGradient.addColorStop(1, 'rgba(255, 80, 60, 0)');
|
|
1161
|
+
ctx.fillStyle = glowGradient;
|
|
1162
|
+
ctx.beginPath();
|
|
1163
|
+
ctx.arc(0, 0, size * 2.0, 0, Math.PI * 2);
|
|
1164
|
+
ctx.fill();
|
|
1165
|
+
|
|
1166
|
+
ctx.restore();
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Draw rocket particle (shoots up with trail, then explodes)
|
|
1171
|
+
*/
|
|
1172
|
+
drawRocket(ctx, particle, time) {
|
|
1173
|
+
ctx.save();
|
|
1174
|
+
|
|
1175
|
+
if (!particle.exploded) {
|
|
1176
|
+
// Update physics - upward flight
|
|
1177
|
+
particle.x += particle.vx;
|
|
1178
|
+
particle.y += particle.vy;
|
|
1179
|
+
particle.rotation += particle.rotationSpeed;
|
|
1180
|
+
|
|
1181
|
+
// Check if reached target explosion height
|
|
1182
|
+
if (particle.y <= particle.targetY) {
|
|
1183
|
+
particle.exploded = true;
|
|
1184
|
+
|
|
1185
|
+
// Create MASSIVE explosion - more burst particles than embers!
|
|
1186
|
+
const explosionCount = 80 + Math.floor(Math.random() * 60); // 80-140 particles!
|
|
1187
|
+
particle.explosionParticles = [];
|
|
1188
|
+
|
|
1189
|
+
for (let i = 0; i < explosionCount; i++) {
|
|
1190
|
+
const angle = (i / explosionCount) * Math.PI * 2;
|
|
1191
|
+
const speed = 2 + Math.random() * 6; // Fast explosive spread
|
|
1192
|
+
const isTracer = i % 6 === 0; // Every 6th particle is bright tracer
|
|
1193
|
+
|
|
1194
|
+
particle.explosionParticles.push({
|
|
1195
|
+
x: particle.x,
|
|
1196
|
+
y: particle.y,
|
|
1197
|
+
vx: Math.cos(angle) * speed,
|
|
1198
|
+
vy: Math.sin(angle) * speed,
|
|
1199
|
+
size: isTracer ? 2.5 + Math.random() * 2 : 1 + Math.random() * 2,
|
|
1200
|
+
color: isTracer ? '#ffffff' : this.colors.firework[Math.floor(Math.random() * this.colors.firework.length)],
|
|
1201
|
+
opacity: 1,
|
|
1202
|
+
life: 1.0,
|
|
1203
|
+
gravity: 0.05,
|
|
1204
|
+
sparkle: Math.random() < 0.3
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const x = particle.x;
|
|
1210
|
+
const y = particle.y;
|
|
1211
|
+
const size = particle.size;
|
|
1212
|
+
|
|
1213
|
+
// Draw rocket trail
|
|
1214
|
+
const trailLength = 15;
|
|
1215
|
+
ctx.globalAlpha = 0.6;
|
|
1216
|
+
const trailGradient = ctx.createLinearGradient(x, y, x - particle.vx * trailLength, y - particle.vy * trailLength);
|
|
1217
|
+
trailGradient.addColorStop(0, particle.color);
|
|
1218
|
+
trailGradient.addColorStop(0.5, `${particle.color}80`);
|
|
1219
|
+
trailGradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
1220
|
+
ctx.strokeStyle = trailGradient;
|
|
1221
|
+
ctx.lineWidth = size * 1.5;
|
|
1222
|
+
ctx.lineCap = 'round';
|
|
1223
|
+
ctx.beginPath();
|
|
1224
|
+
ctx.moveTo(x, y);
|
|
1225
|
+
ctx.lineTo(x - particle.vx * trailLength, y - particle.vy * trailLength);
|
|
1226
|
+
ctx.stroke();
|
|
1227
|
+
|
|
1228
|
+
// Rocket glow
|
|
1229
|
+
ctx.globalAlpha = 0.8;
|
|
1230
|
+
const glowGradient = ctx.createRadialGradient(x, y, 0, x, y, size * 5);
|
|
1231
|
+
glowGradient.addColorStop(0, particle.color);
|
|
1232
|
+
glowGradient.addColorStop(0.5, `${particle.color}60`);
|
|
1233
|
+
glowGradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
1234
|
+
ctx.fillStyle = glowGradient;
|
|
1235
|
+
ctx.fillRect(x - size * 5, y - size * 5, size * 10, size * 10);
|
|
1236
|
+
|
|
1237
|
+
// Rocket core (bright white-hot center)
|
|
1238
|
+
ctx.globalAlpha = 1;
|
|
1239
|
+
ctx.fillStyle = '#ffffff';
|
|
1240
|
+
ctx.shadowColor = particle.color;
|
|
1241
|
+
ctx.shadowBlur = size * 3;
|
|
1242
|
+
ctx.beginPath();
|
|
1243
|
+
ctx.arc(x, y, size * 1.2, 0, Math.PI * 2);
|
|
1244
|
+
ctx.fill();
|
|
1245
|
+
|
|
1246
|
+
// Outer colored ring
|
|
1247
|
+
ctx.fillStyle = particle.color;
|
|
1248
|
+
ctx.shadowBlur = size * 2;
|
|
1249
|
+
ctx.beginPath();
|
|
1250
|
+
ctx.arc(x, y, size * 0.8, 0, Math.PI * 2);
|
|
1251
|
+
ctx.fill();
|
|
1252
|
+
} else {
|
|
1253
|
+
// Draw explosion particles (similar to ember explosions)
|
|
1254
|
+
particle.explosionParticles = particle.explosionParticles.filter(p => {
|
|
1255
|
+
p.x += p.vx;
|
|
1256
|
+
p.y += p.vy;
|
|
1257
|
+
p.vy += p.gravity; // Gravity pulls down
|
|
1258
|
+
p.life -= 0.012; // Faster fade than embers for dramatic effect
|
|
1259
|
+
p.opacity = p.life;
|
|
1260
|
+
|
|
1261
|
+
if (p.life <= 0) return false;
|
|
1262
|
+
|
|
1263
|
+
// Draw explosion particle with glow
|
|
1264
|
+
ctx.globalAlpha = p.opacity * 0.6;
|
|
1265
|
+
const glowGradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 4);
|
|
1266
|
+
glowGradient.addColorStop(0, p.color);
|
|
1267
|
+
glowGradient.addColorStop(0.5, `${p.color}60`);
|
|
1268
|
+
glowGradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
1269
|
+
ctx.fillStyle = glowGradient;
|
|
1270
|
+
ctx.fillRect(p.x - p.size * 4, p.y - p.size * 4, p.size * 8, p.size * 8);
|
|
1271
|
+
|
|
1272
|
+
// Core
|
|
1273
|
+
ctx.globalAlpha = p.opacity;
|
|
1274
|
+
ctx.fillStyle = p.color;
|
|
1275
|
+
ctx.shadowColor = p.color;
|
|
1276
|
+
ctx.shadowBlur = p.size * 3;
|
|
1277
|
+
|
|
1278
|
+
// Sparkle effect
|
|
1279
|
+
if (p.sparkle && Math.random() < 0.3) {
|
|
1280
|
+
ctx.shadowBlur = p.size * 5;
|
|
1281
|
+
ctx.fillStyle = '#ffffff';
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
ctx.beginPath();
|
|
1285
|
+
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
1286
|
+
ctx.fill();
|
|
1287
|
+
|
|
1288
|
+
return true;
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// Deactivate rocket when all explosion particles are gone
|
|
1292
|
+
if (particle.explosionParticles.length === 0) {
|
|
1293
|
+
particle.active = false;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
ctx.shadowBlur = 0;
|
|
1298
|
+
ctx.restore();
|
|
1299
|
+
},
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Draw burst particle (explosion fragment)
|
|
1303
|
+
*/
|
|
1304
|
+
drawBurst(ctx, particle) {
|
|
1305
|
+
const x = particle.x;
|
|
1306
|
+
const y = particle.y;
|
|
1307
|
+
const size = particle.size;
|
|
1308
|
+
|
|
1309
|
+
ctx.save();
|
|
1310
|
+
|
|
1311
|
+
// Update physics
|
|
1312
|
+
particle.x += particle.vx;
|
|
1313
|
+
particle.y += particle.vy;
|
|
1314
|
+
particle.vy += particle.gravity; // Gravity pulls down
|
|
1315
|
+
particle.life -= particle.fadeRate;
|
|
1316
|
+
particle.opacity = Math.max(0, particle.life);
|
|
1317
|
+
|
|
1318
|
+
// Deactivate when faded out
|
|
1319
|
+
if (particle.life <= 0) {
|
|
1320
|
+
particle.active = false;
|
|
1321
|
+
ctx.restore();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Glow effect
|
|
1326
|
+
ctx.globalAlpha = particle.opacity * 0.6;
|
|
1327
|
+
const glowGradient = ctx.createRadialGradient(x, y, 0, x, y, size * 4);
|
|
1328
|
+
glowGradient.addColorStop(0, particle.color);
|
|
1329
|
+
glowGradient.addColorStop(0.5, `${particle.color}60`);
|
|
1330
|
+
glowGradient.addColorStop(1, 'rgba(255, 69, 0, 0)');
|
|
1331
|
+
ctx.fillStyle = glowGradient;
|
|
1332
|
+
ctx.fillRect(x - size * 4, y - size * 4, size * 8, size * 8);
|
|
1333
|
+
|
|
1334
|
+
// Core particle
|
|
1335
|
+
ctx.globalAlpha = particle.opacity;
|
|
1336
|
+
ctx.fillStyle = particle.color;
|
|
1337
|
+
ctx.shadowColor = particle.color;
|
|
1338
|
+
ctx.shadowBlur = size * 2;
|
|
1339
|
+
|
|
1340
|
+
// Sparkle effect (randomly brighter)
|
|
1341
|
+
if (particle.sparkle && Math.random() < 0.3) {
|
|
1342
|
+
ctx.shadowBlur = size * 4;
|
|
1343
|
+
ctx.fillStyle = '#ffffff';
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
ctx.beginPath();
|
|
1347
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
1348
|
+
ctx.fill();
|
|
1349
|
+
|
|
1350
|
+
ctx.shadowBlur = 0;
|
|
1351
|
+
ctx.restore();
|
|
1352
|
+
},
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Draw trail particle (follows rockets)
|
|
1356
|
+
*/
|
|
1357
|
+
drawTrail(ctx, particle) {
|
|
1358
|
+
const x = particle.x;
|
|
1359
|
+
const y = particle.y;
|
|
1360
|
+
const size = particle.size;
|
|
1361
|
+
|
|
1362
|
+
ctx.save();
|
|
1363
|
+
|
|
1364
|
+
// Update physics
|
|
1365
|
+
particle.x += particle.vx;
|
|
1366
|
+
particle.y += particle.vy;
|
|
1367
|
+
particle.life -= particle.fadeRate;
|
|
1368
|
+
particle.opacity = Math.max(0, particle.life * 0.8);
|
|
1369
|
+
|
|
1370
|
+
// Deactivate when faded out
|
|
1371
|
+
if (particle.life <= 0) {
|
|
1372
|
+
particle.active = false;
|
|
1373
|
+
ctx.restore();
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Glow
|
|
1378
|
+
ctx.globalAlpha = particle.opacity * 0.4;
|
|
1379
|
+
const glowGradient = ctx.createRadialGradient(x, y, 0, x, y, size * 3);
|
|
1380
|
+
glowGradient.addColorStop(0, particle.color);
|
|
1381
|
+
glowGradient.addColorStop(0.6, `${particle.color}40`);
|
|
1382
|
+
glowGradient.addColorStop(1, 'rgba(255, 215, 0, 0)');
|
|
1383
|
+
ctx.fillStyle = glowGradient;
|
|
1384
|
+
ctx.fillRect(x - size * 3, y - size * 3, size * 6, size * 6);
|
|
1385
|
+
|
|
1386
|
+
// Core
|
|
1387
|
+
ctx.globalAlpha = particle.opacity;
|
|
1388
|
+
ctx.fillStyle = particle.color;
|
|
1389
|
+
ctx.shadowColor = particle.color;
|
|
1390
|
+
ctx.shadowBlur = size * 1.5;
|
|
1391
|
+
ctx.beginPath();
|
|
1392
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
1393
|
+
ctx.fill();
|
|
1394
|
+
|
|
1395
|
+
ctx.shadowBlur = 0;
|
|
1396
|
+
ctx.restore();
|
|
1397
|
+
},
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Draw spark particle (small glowing bits)
|
|
1401
|
+
*/
|
|
1402
|
+
drawSpark(ctx, particle) {
|
|
1403
|
+
const x = particle.x;
|
|
1404
|
+
const y = particle.y;
|
|
1405
|
+
const size = particle.size;
|
|
1406
|
+
|
|
1407
|
+
ctx.save();
|
|
1408
|
+
|
|
1409
|
+
// Update physics
|
|
1410
|
+
particle.x += particle.vx;
|
|
1411
|
+
particle.y += particle.vy;
|
|
1412
|
+
particle.life -= particle.fadeRate;
|
|
1413
|
+
particle.opacity = Math.max(0, particle.life);
|
|
1414
|
+
|
|
1415
|
+
// Deactivate when faded out
|
|
1416
|
+
if (particle.life <= 0) {
|
|
1417
|
+
particle.active = false;
|
|
1418
|
+
ctx.restore();
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Glow
|
|
1423
|
+
ctx.globalAlpha = particle.opacity * 0.5;
|
|
1424
|
+
const glowGradient = ctx.createRadialGradient(x, y, 0, x, y, size * 3);
|
|
1425
|
+
glowGradient.addColorStop(0, particle.color);
|
|
1426
|
+
glowGradient.addColorStop(0.5, `${particle.color}60`);
|
|
1427
|
+
glowGradient.addColorStop(1, 'rgba(255, 215, 0, 0)');
|
|
1428
|
+
ctx.fillStyle = glowGradient;
|
|
1429
|
+
ctx.fillRect(x - size * 3, y - size * 3, size * 6, size * 6);
|
|
1430
|
+
|
|
1431
|
+
// Core (very small, bright gold)
|
|
1432
|
+
ctx.globalAlpha = particle.opacity;
|
|
1433
|
+
ctx.fillStyle = particle.color;
|
|
1434
|
+
ctx.shadowColor = particle.color;
|
|
1435
|
+
ctx.shadowBlur = size * 2;
|
|
1436
|
+
ctx.beginPath();
|
|
1437
|
+
ctx.arc(x, y, size * 0.8, 0, Math.PI * 2);
|
|
1438
|
+
ctx.fill();
|
|
1439
|
+
|
|
1440
|
+
ctx.shadowBlur = 0;
|
|
1441
|
+
ctx.restore();
|
|
1442
|
+
},
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Draw global theme effects (e.g., lightning)
|
|
1446
|
+
*/
|
|
1447
|
+
drawGlobalEffects(ctx, currentTime, canvasWidth, canvasHeight) {
|
|
1448
|
+
if (this.lightningActive) {
|
|
1449
|
+
ctx.save();
|
|
1450
|
+
ctx.globalAlpha = 0.8 + Math.sin(this.lightningTimer * 0.01) * 0.2; // Pulsing effect during flash
|
|
1451
|
+
ctx.shadowColor = this.lightningColor;
|
|
1452
|
+
ctx.shadowBlur = 20;
|
|
1453
|
+
|
|
1454
|
+
// Draw main lightning bolt from top-center to random bottom position
|
|
1455
|
+
const startX = canvasWidth * (0.4 + Math.random() * 0.2); // Top middle
|
|
1456
|
+
const startY = 0;
|
|
1457
|
+
const endX = canvasWidth * Math.random();
|
|
1458
|
+
const endY = canvasHeight;
|
|
1459
|
+
|
|
1460
|
+
this.drawLightning(
|
|
1461
|
+
ctx,
|
|
1462
|
+
startX,
|
|
1463
|
+
startY,
|
|
1464
|
+
endX,
|
|
1465
|
+
endY,
|
|
1466
|
+
5 + Math.floor(Math.random() * 3), // 5-7 segments
|
|
1467
|
+
50 + Math.random() * 50, // 50-100 displacement
|
|
1468
|
+
0.7, // Roughness
|
|
1469
|
+
0.3, // Branch chance
|
|
1470
|
+
3 + Math.random() * 2, // Line width
|
|
1471
|
+
this.lightningColor
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
ctx.restore();
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
};
|