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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +469 -0
  3. package/admin/css/admin.css +1123 -0
  4. package/admin/index.html +72 -0
  5. package/admin/js/api.js +210 -0
  6. package/admin/js/app.js +270 -0
  7. package/admin/js/config/sidebar-config.js +107 -0
  8. package/admin/js/lib/card.js +63 -0
  9. package/admin/js/lib/image-editor.js +869 -0
  10. package/admin/js/lib/markdown-toolbar.js +421 -0
  11. package/admin/js/templates/dashboard.html +50 -0
  12. package/admin/js/templates/documentation.html +237 -0
  13. package/admin/js/templates/layouts.html +11 -0
  14. package/admin/js/templates/login.html +58 -0
  15. package/admin/js/templates/media.html +16 -0
  16. package/admin/js/templates/navigation.html +50 -0
  17. package/admin/js/templates/page-editor.html +126 -0
  18. package/admin/js/templates/pages.html +18 -0
  19. package/admin/js/templates/plugins.html +12 -0
  20. package/admin/js/templates/settings.html +190 -0
  21. package/admin/js/templates/tutorials.html +233 -0
  22. package/admin/js/templates/user-editor.html +12 -0
  23. package/admin/js/templates/users.html +10 -0
  24. package/admin/js/views/dashboard.js +48 -0
  25. package/admin/js/views/documentation.js +12 -0
  26. package/admin/js/views/index.js +33 -0
  27. package/admin/js/views/layouts.js +49 -0
  28. package/admin/js/views/login.js +254 -0
  29. package/admin/js/views/media.js +240 -0
  30. package/admin/js/views/navigation.js +152 -0
  31. package/admin/js/views/page-editor.js +479 -0
  32. package/admin/js/views/pages.js +64 -0
  33. package/admin/js/views/plugins.js +100 -0
  34. package/admin/js/views/settings.js +64 -0
  35. package/admin/js/views/tutorials.js +12 -0
  36. package/admin/js/views/user-editor.js +88 -0
  37. package/admin/js/views/users.js +73 -0
  38. package/bin/cli.js +334 -0
  39. package/config/auth.json +20 -0
  40. package/config/content.json +10 -0
  41. package/config/navigation.json +63 -0
  42. package/config/plugins.json +47 -0
  43. package/config/presets.json +34 -0
  44. package/config/server.json +6 -0
  45. package/config/site.json +33 -0
  46. package/package.json +67 -0
  47. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
  48. package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
  49. package/plugins/back-to-top/config.js +10 -0
  50. package/plugins/back-to-top/plugin.js +24 -0
  51. package/plugins/back-to-top/plugin.json +36 -0
  52. package/plugins/back-to-top/public/inject-body.html +105 -0
  53. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
  54. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
  55. package/plugins/cookie-consent/config.js +30 -0
  56. package/plugins/cookie-consent/plugin.js +24 -0
  57. package/plugins/cookie-consent/plugin.json +36 -0
  58. package/plugins/cookie-consent/public/inject-body.html +69 -0
  59. package/plugins/custom-css/admin/templates/custom-css.html +17 -0
  60. package/plugins/custom-css/admin/views/custom-css.js +35 -0
  61. package/plugins/custom-css/config.js +1 -0
  62. package/plugins/custom-css/data/custom.css +0 -0
  63. package/plugins/custom-css/plugin.js +63 -0
  64. package/plugins/custom-css/plugin.json +32 -0
  65. package/plugins/custom-css/public/inject-head.html +1 -0
  66. package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
  67. package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
  68. package/plugins/domma-effects/config.js +9 -0
  69. package/plugins/domma-effects/plugin.js +22 -0
  70. package/plugins/domma-effects/plugin.json +36 -0
  71. package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
  72. package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
  73. package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
  74. package/plugins/domma-effects/public/celebrations/index.js +535 -0
  75. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
  76. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
  77. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
  78. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
  79. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
  80. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
  81. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
  82. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
  83. package/plugins/domma-effects/public/inject-body.html +268 -0
  84. package/plugins/example-analytics/admin/templates/analytics.html +10 -0
  85. package/plugins/example-analytics/admin/views/analytics.js +51 -0
  86. package/plugins/example-analytics/config.js +6 -0
  87. package/plugins/example-analytics/plugin.js +58 -0
  88. package/plugins/example-analytics/plugin.json +27 -0
  89. package/plugins/example-analytics/public/inject-body.html +13 -0
  90. package/plugins/example-analytics/public/inject-head.html +1 -0
  91. package/plugins/example-analytics/stats.json +1 -0
  92. package/plugins/form-builder/admin/templates/form-editor.html +158 -0
  93. package/plugins/form-builder/admin/templates/form-settings.html +29 -0
  94. package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
  95. package/plugins/form-builder/admin/templates/forms-list.html +17 -0
  96. package/plugins/form-builder/admin/views/form-editor.js +817 -0
  97. package/plugins/form-builder/admin/views/form-settings.js +38 -0
  98. package/plugins/form-builder/admin/views/form-submissions.js +295 -0
  99. package/plugins/form-builder/admin/views/forms-list.js +164 -0
  100. package/plugins/form-builder/config.js +9 -0
  101. package/plugins/form-builder/data/forms/contact-details.json +63 -0
  102. package/plugins/form-builder/data/forms/contact.json +52 -0
  103. package/plugins/form-builder/data/submissions/contact-details.json +1 -0
  104. package/plugins/form-builder/data/submissions/contact.json +14 -0
  105. package/plugins/form-builder/email.js +103 -0
  106. package/plugins/form-builder/plugin.js +454 -0
  107. package/plugins/form-builder/plugin.json +56 -0
  108. package/plugins/form-builder/public/inject-body.html +270 -0
  109. package/plugins/form-builder/public/inject-head.html +42 -0
  110. package/public/css/site.css +189 -0
  111. package/public/js/site.js +109 -0
  112. package/scripts/copy-domma.js +48 -0
  113. package/scripts/fresh.js +41 -0
  114. package/scripts/reset.js +124 -0
  115. package/scripts/seed.js +666 -0
  116. package/scripts/setup.js +263 -0
  117. package/server/config.js +56 -0
  118. package/server/middleware/auth.js +97 -0
  119. package/server/routes/api/auth.js +116 -0
  120. package/server/routes/api/layouts.js +25 -0
  121. package/server/routes/api/media.js +93 -0
  122. package/server/routes/api/navigation.js +37 -0
  123. package/server/routes/api/pages.js +118 -0
  124. package/server/routes/api/plugins.js +46 -0
  125. package/server/routes/api/settings.js +25 -0
  126. package/server/routes/api/users.js +110 -0
  127. package/server/routes/public.js +108 -0
  128. package/server/server.js +169 -0
  129. package/server/services/content.js +298 -0
  130. package/server/services/images.js +334 -0
  131. package/server/services/markdown.js +297 -0
  132. package/server/services/plugins.js +246 -0
  133. package/server/services/renderer.js +80 -0
  134. package/server/services/users.js +212 -0
  135. package/server/templates/page.html +78 -0
@@ -0,0 +1,1837 @@
1
+ /**
2
+ * Halloween Theme for Domma Celebrations
3
+ *
4
+ * Features:
5
+ * - Flying bats with wing animation
6
+ * - Floating ghosts with wavy motion
7
+ *
8
+ * - Pumpkins (both floating and static jack-o-lanterns)
9
+ * - Gravestones scattered on ground
10
+ * - Haunted castle with towers and turrets
11
+ * - Bubbling cauldron with smoke/steam
12
+ * - Spiders dangling from webs
13
+ * - Dark, spooky atmosphere with orange and purple accents
14
+ * - Wind effects for eerie atmosphere
15
+ */
16
+
17
+ export default {
18
+ name: 'halloween',
19
+ displayName: 'Halloween',
20
+ emoji: '🎃',
21
+
22
+ // Intensity configurations
23
+ intensityConfig: {
24
+ light: {
25
+ count: 30, // Fewer bats/ghosts for subtlety
26
+ speedRange: [0.3, 1.0],
27
+ sizeRange: [2, 5],
28
+ gravestones: 3,
29
+ jackOLanterns: 2,
30
+ spiders: 2,
31
+ scarecrows: 1,
32
+ twinklingStars: 8
33
+ },
34
+ medium: {
35
+ count: 60,
36
+ speedRange: [0.5, 1.5],
37
+ sizeRange: [2, 6],
38
+ gravestones: 5,
39
+ jackOLanterns: 4,
40
+ spiders: 4,
41
+ scarecrows: 2,
42
+ twinklingStars: 15
43
+ },
44
+ heavy: {
45
+ count: 100,
46
+ speedRange: [0.7, 2.0],
47
+ sizeRange: [3, 8],
48
+ gravestones: 8,
49
+ jackOLanterns: 6,
50
+ spiders: 6,
51
+ scarecrows: 3,
52
+ twinklingStars: 25
53
+ }
54
+ },
55
+
56
+ particles: ['bat', 'ghost', 'pumpkin'],
57
+ decorations: ['gravestone', 'jack-o-lantern', 'haunted-house', 'cauldron', 'spider', 'scarecrow', 'twinkling-star'],
58
+ colors: {
59
+ primary: '#ff6600', // Pumpkin orange
60
+ secondary: '#1a1a2e', // Dark night blue
61
+ accent: '#8b0000', // Deep blood red
62
+ ghost: '#f0f0f0', // Ghostly white
63
+ purple: '#6a0dad' // Spooky purple
64
+ },
65
+
66
+ // Lightning effect properties
67
+ lightningChance: 0.0005, // 0.05% chance per frame to strike
68
+ lightningDuration: 200, // Lightning flash lasts 200ms
69
+ lightningTimer: 0,
70
+ lightningActive: false,
71
+ lightningColor: '#fefefe', // Bright white/blue flash
72
+
73
+ /**
74
+ * Create bat particle
75
+ */
76
+ createBat(canvasWidth, canvasHeight, config) {
77
+ const depth = Math.random();
78
+ let size, speed, opacity;
79
+
80
+ if (depth < 0.33) {
81
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 1.5;
82
+ speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]) * 1.3;
83
+ opacity = 0.9 + Math.random() * 0.1;
84
+ } else if (depth < 0.66) {
85
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]);
86
+ speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.sizeRange[0]);
87
+ opacity = 0.6 + Math.random() * 0.2;
88
+ } else {
89
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.7;
90
+ speed = config.speedRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.7;
91
+ opacity = 0.4 + Math.random() * 0.2;
92
+ }
93
+
94
+ const fromLeft = Math.random() < 0.5;
95
+
96
+ return {
97
+ type: 'bat',
98
+ x: fromLeft ? -20 : canvasWidth + 20,
99
+ y: Math.random() * canvasHeight * 0.7, // Upper 70% of screen
100
+ baseY: Math.random() * canvasHeight * 0.7,
101
+ vx: fromLeft ? speed * 2 : -speed * 2,
102
+ vy: 0,
103
+ size: size,
104
+ speed: speed,
105
+ opacity: opacity,
106
+ waveAmplitude: 10 + Math.random() * 20,
107
+ waveFrequency: 0.002 + Math.random() * 0.003,
108
+ waveOffset: Math.random() * Math.PI * 2,
109
+ time: 0,
110
+ rotation: 0,
111
+ active: true,
112
+ depth: depth
113
+ };
114
+ },
115
+
116
+ /**
117
+ * Create ghost particle
118
+ */
119
+ createGhost(canvasWidth, canvasHeight, config) {
120
+ const depth = Math.random();
121
+ let size, speed, opacity;
122
+
123
+ if (depth < 0.33) {
124
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 1.8;
125
+ speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.sizeRange[0]) * 0.5;
126
+ opacity = 0.7 + Math.random() * 0.2;
127
+ } else if (depth < 0.66) {
128
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 1.2;
129
+ speed = config.speedRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.5;
130
+ opacity = 0.5 + Math.random() * 0.15;
131
+ } else {
132
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]);
133
+ speed = config.speedRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.5;
134
+ opacity = 0.3 + Math.random() * 0.15;
135
+ }
136
+
137
+ return {
138
+ type: 'ghost',
139
+ x: Math.random() * canvasWidth,
140
+ y: -50, // Start above viewport
141
+ size: size,
142
+ speed: speed,
143
+ opacity: opacity,
144
+ windOffset: Math.random() * Math.PI * 2,
145
+ windSpeed: 0.01 + Math.random() * 0.02,
146
+ rotation: 0,
147
+ rotationSpeed: (Math.random() - 0.5) * 0.005,
148
+ time: Math.random() * 1000,
149
+ active: true,
150
+ depth: depth
151
+ };
152
+ },
153
+
154
+ /**
155
+ * Create floating pumpkin particle
156
+ */
157
+ createPumpkin(canvasWidth, canvasHeight, config) {
158
+ return {
159
+ type: 'pumpkin',
160
+ x: Math.random() * canvasWidth,
161
+ y: Math.random() * canvasHeight,
162
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
163
+ vx: (Math.random() - 0.5) * 0.2, // Gentle horizontal drift
164
+ speed: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.sizeRange[0])) * 0.2, // Slight downward/floating speed
165
+ opacity: 0.8 + Math.random() * 0.2,
166
+ rotation: Math.random() * Math.PI * 2,
167
+ rotationSpeed: (Math.random() - 0.5) * 0.008,
168
+ windOffset: Math.random() * Math.PI * 2,
169
+ windSpeed: 0.008 + Math.random() * 0.015, // Slower wind
170
+ glowPhase: Math.random() * Math.PI * 2,
171
+ floatPhase: Math.random() * Math.PI * 2, // For bobbing motion
172
+ floatSpeed: 0.001 + Math.random() * 0.002,
173
+ active: true
174
+ };
175
+ },
176
+
177
+ /**
178
+ * Create static gravestone decoration
179
+ */
180
+ createGravestone(canvasWidth, canvasHeight, options = {}) {
181
+ const shapes = ['rounded', 'cross', 'obelisk'];
182
+ return {
183
+ type: 'gravestone',
184
+ x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
185
+ y: options.y !== undefined ? options.y : canvasHeight - 30 - Math.random() * 20,
186
+ size: 15 + Math.random() * 10,
187
+ opacity: 0.7 + Math.random() * 0.2,
188
+ shape: shapes[Math.floor(Math.random() * shapes.length)],
189
+ rotation: (Math.random() - 0.5) * 0.1, // Slight tilt
190
+ active: true,
191
+ static: true
192
+ };
193
+ },
194
+
195
+ /**
196
+ * Create static jack-o-lantern
197
+ */
198
+ createJackOLantern(canvasWidth, canvasHeight, options = {}) {
199
+ const faces = ['happy', 'scary', 'wicked'];
200
+ return {
201
+ type: 'jack-o-lantern',
202
+ x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
203
+ y: options.y !== undefined ? options.y : canvasHeight - 25 - Math.random() * 15,
204
+ size: 12 + Math.random() * 8,
205
+ opacity: 0.9 + Math.random() * 0.1,
206
+ face: faces[Math.floor(Math.random() * faces.length)],
207
+ glowPhase: Math.random() * Math.PI * 2,
208
+ active: true,
209
+ static: true
210
+ };
211
+ },
212
+
213
+ /**
214
+ * Create dangling spider
215
+ */
216
+ createSpider(canvasWidth, canvasHeight, options = {}) {
217
+ return {
218
+ type: 'spider',
219
+ x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
220
+ y: options.y !== undefined ? options.y : Math.random() * canvasHeight * 0.3, // Upper portion
221
+ baseY: options.y !== undefined ? options.y : Math.random() * canvasHeight * 0.3,
222
+ size: 5 + Math.random() * 5,
223
+ opacity: 0.8 + Math.random() * 0.2,
224
+ threadLength: 30 + Math.random() * 50,
225
+ swingPhase: Math.random() * Math.PI * 2,
226
+ swingAmplitude: 20 + Math.random() * 30,
227
+ swingSpeed: 0.001 + Math.random() * 0.002,
228
+ time: 0,
229
+ active: true,
230
+ static: false
231
+ };
232
+ },
233
+
234
+ /**
235
+ * Create falling particle (mix of pumpkins and spiders)
236
+ */
237
+ createFallingParticle(canvasWidth, canvasHeight, config) {
238
+ const choice = Math.random();
239
+
240
+ // 70% pumpkins, 30% spiders
241
+ if (choice < 0.7) {
242
+ return this.createPumpkin(canvasWidth, canvasHeight, config);
243
+ } else {
244
+ // Create small spider particle
245
+ return {
246
+ type: 'spider-small',
247
+ x: Math.random() * canvasWidth,
248
+ y: -20,
249
+ size: 1 + Math.random() * 2,
250
+ speed: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.sizeRange[0]) * 0.5,
251
+ opacity: 0.6 + Math.random() * 0.3,
252
+ rotation: Math.random() * Math.PI * 2,
253
+ rotationSpeed: (Math.random() - 0.5) * 0.02,
254
+ windOffset: Math.random() * Math.PI * 2,
255
+ windSpeed: 0.01 + Math.random() * 0.02,
256
+ active: true
257
+ };
258
+ }
259
+ },
260
+
261
+ /**
262
+ * Create initial static decorations
263
+ */
264
+ createInitialDecorations(canvasWidth, canvasHeight, config) {
265
+ const decorations = [];
266
+
267
+ // Haunted castle silhouette (left side)
268
+ decorations.push({
269
+ type: 'haunted-house',
270
+ x: 80,
271
+ y: canvasHeight - 120,
272
+ size: 80,
273
+ opacity: 0.85,
274
+ active: true,
275
+ static: true
276
+ });
277
+
278
+ // Full moon (right side)
279
+ decorations.push({
280
+ type: 'moon',
281
+ x: canvasWidth - 120,
282
+ y: 100,
283
+ size: 60,
284
+ opacity: 0.9,
285
+ glowPhase: 0,
286
+ active: true,
287
+ static: true
288
+ // Removed Batman properties
289
+ });
290
+
291
+ // Create gravestones
292
+ const gravestoneCount = config.gravestones || 5;
293
+ for (let i = 0; i < gravestoneCount; i++) {
294
+ decorations.push({
295
+ type: 'gravestone',
296
+ x: (canvasWidth / (gravestoneCount + 1)) * (i + 1) + (Math.random() - 0.5) * 40,
297
+ y: canvasHeight - 30 - Math.random() * 10,
298
+ size: 12 + Math.random() * 8,
299
+ opacity: 0.7 + Math.random() * 0.2,
300
+ variant: Math.floor(Math.random() * 3), // Different gravestone shapes
301
+ active: true,
302
+ static: true
303
+ });
304
+ }
305
+
306
+ // Create jack-o-lanterns
307
+ const lanternCount = config.jackOLanterns || 4;
308
+ for (let i = 0; i < lanternCount; i++) {
309
+ decorations.push({
310
+ type: 'jack-o-lantern',
311
+ x: (canvasWidth / (lanternCount + 1)) * (i + 1) + (Math.random() - 0.5) * 30,
312
+ y: canvasHeight - 15,
313
+ size: 10 + Math.random() * 6,
314
+ opacity: 0.9,
315
+ glowPhase: Math.random() * Math.PI * 2,
316
+ active: true,
317
+ static: true
318
+ });
319
+ }
320
+
321
+ // Create dangling spiders
322
+ const spiderCount = config.spiders || 4;
323
+ for (let i = 0; i < spiderCount; i++) {
324
+ decorations.push({
325
+ type: 'spider',
326
+ x: (canvasWidth / (spiderCount + 1)) * (i + 1),
327
+ y: 0,
328
+ targetY: 50 + Math.random() * 100,
329
+ currentY: 0,
330
+ size: 3 + Math.random() * 3,
331
+ opacity: 0.8,
332
+ swingPhase: Math.random() * Math.PI * 2,
333
+ swingAmplitude: 20 + Math.random() * 30,
334
+ swingSpeed: 0.02 + Math.random() * 0.02,
335
+ active: true,
336
+ static: true
337
+ });
338
+ }
339
+
340
+ // Create sinister scarecrows
341
+ const scarecrowCount = config.scarecrows || 2;
342
+ for (let i = 0; i < scarecrowCount; i++) {
343
+ decorations.push({
344
+ type: 'scarecrow',
345
+ x: 120 + (i / (scarecrowCount - 1 || 1)) * (canvasWidth - 240),
346
+ y: canvasHeight - 60,
347
+ size: 30 + Math.random() * 10,
348
+ opacity: 0.9,
349
+ swayPhase: Math.random() * Math.PI * 2,
350
+ swaySpeed: 0.015 + Math.random() * 0.015,
351
+ glowPhase: Math.random() * Math.PI * 2,
352
+ glowSpeed: 0.02,
353
+ active: true,
354
+ static: true
355
+ });
356
+ }
357
+
358
+ // Create twinkling stars scattered across the night sky
359
+ const starCount = config.twinklingStars || 15;
360
+ for (let i = 0; i < starCount; i++) {
361
+ decorations.push(this.createTwinklingStar(canvasWidth, canvasHeight));
362
+ }
363
+
364
+ return decorations;
365
+ },
366
+
367
+ /**
368
+ * Create twinkling star particle
369
+ */
370
+ createTwinklingStar(canvasWidth, canvasHeight) {
371
+ return {
372
+ type: 'twinkling-star',
373
+ x: Math.random() * canvasWidth,
374
+ y: Math.random() * (canvasHeight * 0.5), // Top half of screen only
375
+ size: 1 + Math.random() * 2.5,
376
+ opacity: 0.5 + Math.random() * 0.3,
377
+ twinklePhase: Math.random() * Math.PI * 2,
378
+ twinkleSpeed: 0.004 + Math.random() * 0.003,
379
+ colorVariant: Math.floor(Math.random() * 3), // 0=pale white, 1=purple tint, 2=dim orange
380
+ active: true,
381
+ static: true
382
+ };
383
+ },
384
+
385
+ /**
386
+ * Draw a procedurally generated lightning bolt
387
+ * @param {CanvasRenderingContext2D} ctx
388
+ * @param {number} x1 - Start X
389
+ * @param {number} y1 - Start Y
390
+ * @param {number} x2 - End X
391
+ * @param {number} y2 - End Y
392
+ * @param {number} segments - Number of segments for the bolt (controls detail)
393
+ * @param {number} displacement - Max displacement for forks
394
+ * @param {number} roughness - How jagged the line is
395
+ * @param {number} branchChance - Probability of branching
396
+ * @param {number} lineWidth - Base line width
397
+ * @param {string} color - Color of the lightning
398
+ */
399
+ drawLightning(ctx, x1, y1, x2, y2, segments, displacement, roughness, branchChance, lineWidth, color) {
400
+ if (segments < 1) {
401
+ return;
402
+ }
403
+
404
+ const midpointX = (x1 + x2) / 2;
405
+ const midpointY = (y1 + y2) / 2;
406
+
407
+ const angle = Math.atan2(y2 - y1, x2 - x1);
408
+ const perpendicularAngle = angle + Math.PI / 2;
409
+
410
+ const offset = (Math.random() - 0.5) * displacement;
411
+ const newMidpointX = midpointX + Math.cos(perpendicularAngle) * offset;
412
+ const newMidpointY = midpointY + Math.sin(perpendicularAngle) * offset;
413
+
414
+ const newSegments = segments - 1;
415
+ const newDisplacement = displacement * roughness;
416
+ const newLineWidth = lineWidth * 0.8;
417
+
418
+ ctx.strokeStyle = color;
419
+ ctx.lineWidth = lineWidth;
420
+ ctx.lineCap = 'round';
421
+ ctx.lineJoin = 'round';
422
+
423
+ // Draw main segment
424
+ ctx.beginPath();
425
+ ctx.moveTo(x1, y1);
426
+ ctx.lineTo(newMidpointX, newMidpointY);
427
+ ctx.lineTo(x2, y2);
428
+ ctx.stroke();
429
+
430
+ // Recursive call for sub-segments
431
+ if (newSegments > 0) {
432
+ this.drawLightning(ctx, x1, y1, newMidpointX, newMidpointY, newSegments, newDisplacement, roughness, branchChance, newLineWidth, color);
433
+ this.drawLightning(ctx, newMidpointX, newMidpointY, x2, y2, newSegments, newDisplacement, roughness, branchChance, newLineWidth, color);
434
+
435
+ // Create a fork
436
+ if (Math.random() < branchChance && newSegments > 1) {
437
+ const forkLength = Math.random() * displacement * 0.5;
438
+ const forkAngle = (Math.random() - 0.5) * Math.PI / 3; // Max 60 degree deviation
439
+ const forkX = newMidpointX + Math.cos(angle + forkAngle) * forkLength;
440
+ const forkY = newMidpointY + Math.sin(angle + forkAngle) * forkLength;
441
+ this.drawLightning(ctx, newMidpointX, newMidpointY, forkX, forkY, newSegments - 1, newDisplacement * 0.5, roughness, branchChance * 0.5, newLineWidth * 0.6, color);
442
+ }
443
+ }
444
+ },
445
+
446
+ /**
447
+ * Spawn special Halloween particles
448
+ */
449
+ spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight) {
450
+ const choice = Math.random();
451
+ const config = this.intensityConfig.medium; // Use medium config for spawning
452
+
453
+ // Bat (15% chance)
454
+ if (choice < 0.15) {
455
+ return this.createBat(canvasWidth, canvasHeight, config);
456
+ }
457
+
458
+ // Witch (1% chance, max 1 flying)
459
+ if (choice < 0.16) {
460
+ if (specialParticles.some(p => p.type === 'witch')) {
461
+ return null;
462
+ }
463
+ const fromLeft = Math.random() < 0.5;
464
+ return {
465
+ type: 'witch',
466
+ x: fromLeft ? -100 : canvasWidth + 100,
467
+ y: Math.random() * (canvasHeight * 0.4) + 50,
468
+ baseY: Math.random() * (canvasHeight * 0.4) + 50,
469
+ vx: fromLeft ? 3 + Math.random() * 2 : -(3 + Math.random() * 2),
470
+ size: 15 + Math.random() * 8,
471
+ opacity: 0.85,
472
+ waveAmplitude: 15 + Math.random() * 20,
473
+ waveFrequency: 0.002 + Math.random() * 0.002,
474
+ waveOffset: Math.random() * Math.PI * 2,
475
+ time: 0,
476
+ active: true,
477
+ static: false
478
+ };
479
+ }
480
+
481
+ // Floating pumpkin (2% chance)
482
+ if (choice < 0.18) {
483
+ return {
484
+ type: 'floating-pumpkin',
485
+ x: Math.random() * canvasWidth,
486
+ y: -50,
487
+ size: 8 + Math.random() * 6,
488
+ vx: (Math.random() - 0.5) * 0.5, // Initial horizontal velocity
489
+ vy: 0.3 + Math.random() * 0.5, // Initial vertical velocity
490
+ opacity: 0.9,
491
+ rotation: Math.random() * Math.PI * 2,
492
+ rotationSpeed: (Math.random() - 0.5) * 0.01,
493
+ glowPhase: Math.random() * Math.PI * 2,
494
+ windOffset: Math.random() * Math.PI * 2,
495
+ windSpeed: 0.015,
496
+ active: true
497
+ };
498
+ }
499
+
500
+ // Haunted castle (rare, max 1)
501
+ if (choice < 0.2102) {
502
+ if (specialParticles.some(p => p.type === 'haunted-house')) {
503
+ return null;
504
+ }
505
+ return {
506
+ type: 'haunted-house',
507
+ x: Math.random() * canvasWidth * 0.6 + canvasWidth * 0.2, // Center-ish
508
+ y: canvasHeight - 100,
509
+ size: 40 + Math.random() * 20,
510
+ opacity: 0.8,
511
+ windowFlicker: 0,
512
+ active: true,
513
+ static: true
514
+ };
515
+ }
516
+
517
+ // Cauldron (rare, max 1)
518
+ if (choice < 0.2105) {
519
+ if (specialParticles.some(p => p.type === 'cauldron')) {
520
+ return null;
521
+ }
522
+ return {
523
+ type: 'cauldron',
524
+ x: Math.random() * canvasWidth * 0.6 + canvasWidth * 0.2,
525
+ y: canvasHeight - 40,
526
+ size: 25 + Math.random() * 10,
527
+ opacity: 1,
528
+ bubbles: [],
529
+ smoke: [],
530
+ time: 0,
531
+ active: true,
532
+ static: true
533
+ };
534
+ }
535
+
536
+ return null;
537
+ },
538
+
539
+ /**
540
+ * Update special Halloween particles (moon, witch, floating pumpkin)
541
+ */
542
+ updateSpecialParticles(specialParticles, deltaTime, canvasWidth, canvasHeight) {
543
+ // Update lightning effect
544
+ if (this.lightningActive) {
545
+ this.lightningTimer += deltaTime;
546
+ if (this.lightningTimer >= this.lightningDuration) {
547
+ this.lightningActive = false;
548
+ this.lightningTimer = 0;
549
+ }
550
+ } else {
551
+ if (Math.random() < this.lightningChance) {
552
+ this.lightningActive = true;
553
+ this.lightningTimer = 0;
554
+ }
555
+ }
556
+
557
+ specialParticles.forEach(particle => {
558
+ // Increment time for animated particles (if they have a time property)
559
+ if (particle.time !== undefined) {
560
+ particle.time += deltaTime;
561
+ }
562
+
563
+ switch (particle.type) {
564
+ case 'moon':
565
+ // Batman logo logic removed
566
+ break;
567
+
568
+ case 'witch':
569
+ // Witch flies across the screen then deactivates
570
+ if ((particle.vx > 0 && particle.x > canvasWidth + 100) || (particle.vx < 0 && particle.x < -100)) {
571
+ particle.active = false;
572
+ }
573
+ break;
574
+
575
+ case 'floating-pumpkin':
576
+ // Floating pumpkins also eventually go off-screen
577
+ if (particle.y > canvasHeight + 50) {
578
+ particle.active = false;
579
+ }
580
+ break;
581
+
582
+ case 'cauldron':
583
+ // Manage bubbles
584
+ if (particle.time % 100 < 20) { // Spawn new bubbles periodically
585
+ particle.bubbles.push({
586
+ x: (Math.random() - 0.5) * particle.size * 0.5,
587
+ y: particle.size * 0.3 + Math.random() * particle.size * 0.2, // From potion surface
588
+ size: particle.size * (0.05 + Math.random() * 0.1),
589
+ vy: -0.5 - Math.random() * 0.5,
590
+ opacity: 1,
591
+ life: 1.0,
592
+ fadeRate: 0.01 + Math.random() * 0.005
593
+ });
594
+ }
595
+ particle.bubbles = particle.bubbles.filter(bubble => {
596
+ bubble.y += bubble.vy;
597
+ bubble.vy *= 0.98; // Slow down ascent
598
+ bubble.opacity -= bubble.fadeRate;
599
+ return bubble.opacity > 0 && bubble.y > -particle.size; // Remove if too high or faded
600
+ });
601
+
602
+ // Manage smoke/steam
603
+ if (particle.time % 200 < 30) { // Emit smoke periodically
604
+ particle.smoke.push({
605
+ x: (Math.random() - 0.5) * particle.size * 0.3,
606
+ y: -particle.size * 0.5 + Math.random() * particle.size * 0.1,
607
+ size: particle.size * (0.2 + Math.random() * 0.3),
608
+ vx: (Math.random() - 0.5) * 0.1,
609
+ vy: -0.2 - Math.random() * 0.2,
610
+ opacity: 0.5,
611
+ life: 1.0,
612
+ fadeRate: 0.005 + Math.random() * 0.003
613
+ });
614
+ }
615
+ particle.smoke = particle.smoke.filter(smoke => {
616
+ smoke.x += smoke.vx;
617
+ smoke.y += smoke.vy;
618
+ smoke.size *= 1.02; // Expand
619
+ smoke.opacity -= smoke.fadeRate;
620
+ return smoke.opacity > 0;
621
+ });
622
+ break;
623
+
624
+ // Add other special particle update logic here if needed
625
+ }
626
+ });
627
+ },
628
+
629
+ /**
630
+ * Draw bat with wing animation
631
+ */
632
+ drawBat(ctx, particle) {
633
+ const x = particle.x;
634
+ const y = particle.y;
635
+ const size = particle.size;
636
+ const dir = particle.vx > 0 ? 1 : -1;
637
+ const time = particle.time;
638
+
639
+ ctx.save();
640
+ ctx.globalAlpha = particle.opacity;
641
+ ctx.translate(x, y);
642
+ if (dir === -1) {
643
+ ctx.scale(-1, 1);
644
+ }
645
+
646
+ // Wing flap animation
647
+ const flapCycle = time * 0.01;
648
+ const wingAngle = Math.sin(flapCycle) * (Math.PI / 4);
649
+
650
+ // Body
651
+ ctx.fillStyle = '#1a1a1a';
652
+ ctx.beginPath();
653
+ ctx.ellipse(0, 0, size * 0.4, size * 0.6, 0, 0, Math.PI * 2);
654
+ ctx.fill();
655
+
656
+ // Head
657
+ ctx.beginPath();
658
+ ctx.arc(size * 0.3, -size * 0.4, size * 0.35, 0, Math.PI * 2);
659
+ ctx.fill();
660
+
661
+ // Ears
662
+ ctx.beginPath();
663
+ ctx.moveTo(size * 0.1, -size * 0.7);
664
+ ctx.lineTo(size * 0.25, -size * 1.1);
665
+ ctx.lineTo(size * 0.4, -size * 0.7);
666
+ ctx.fill();
667
+ ctx.beginPath();
668
+ ctx.moveTo(size * 0.2, -size * 0.7);
669
+ ctx.lineTo(size * 0.35, -size * 1.0);
670
+ ctx.lineTo(size * 0.5, -size * 0.7);
671
+ ctx.fill();
672
+
673
+ // Eyes (red glow)
674
+ ctx.fillStyle = '#ff0000';
675
+ ctx.beginPath();
676
+ ctx.arc(size * 0.2, -size * 0.45, size * 0.08, 0, Math.PI * 2);
677
+ ctx.fill();
678
+ ctx.beginPath();
679
+ ctx.arc(size * 0.4, -size * 0.45, size * 0.08, 0, Math.PI * 2);
680
+ ctx.fill();
681
+
682
+ // Eye glow
683
+ ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
684
+ ctx.beginPath();
685
+ ctx.arc(size * 0.2, -size * 0.45, size * 0.15, 0, Math.PI * 2);
686
+ ctx.fill();
687
+ ctx.beginPath();
688
+ ctx.arc(size * 0.4, -size * 0.45, size * 0.15, 0, Math.PI * 2);
689
+ ctx.fill();
690
+
691
+ // Wings
692
+ ctx.fillStyle = '#2a2a2a';
693
+ ctx.strokeStyle = '#1a1a1a';
694
+ ctx.lineWidth = 1;
695
+
696
+ // Left wing
697
+ ctx.save();
698
+ ctx.translate(-size * 0.3, 0);
699
+ ctx.rotate(-wingAngle);
700
+ ctx.beginPath();
701
+ ctx.moveTo(0, 0);
702
+ ctx.quadraticCurveTo(-size * 0.8, -size * 0.5, -size * 1.2, 0);
703
+ ctx.quadraticCurveTo(-size * 0.9, size * 0.3, -size * 0.6, size * 0.4);
704
+ ctx.quadraticCurveTo(-size * 0.3, size * 0.2, 0, 0);
705
+ ctx.closePath();
706
+ ctx.fill();
707
+ ctx.stroke();
708
+ ctx.restore();
709
+
710
+ // Right wing
711
+ ctx.save();
712
+ ctx.translate(size * 0.3, 0);
713
+ ctx.rotate(wingAngle);
714
+ ctx.beginPath();
715
+ ctx.moveTo(0, 0);
716
+ ctx.quadraticCurveTo(size * 0.8, -size * 0.5, size * 1.2, 0);
717
+ ctx.quadraticCurveTo(size * 0.9, size * 0.3, size * 0.6, size * 0.4);
718
+ ctx.quadraticCurveTo(size * 0.3, size * 0.2, 0, 0);
719
+ ctx.closePath();
720
+ ctx.fill();
721
+ ctx.stroke();
722
+ ctx.restore();
723
+
724
+ ctx.restore();
725
+ },
726
+
727
+ /**
728
+ * Draw floating ghost with wavy motion
729
+ */
730
+ drawGhost(ctx, particle) {
731
+ const x = particle.x;
732
+ const y = particle.y;
733
+ const size = particle.size;
734
+ const time = particle.time;
735
+
736
+ ctx.save();
737
+ ctx.globalAlpha = particle.opacity;
738
+ ctx.translate(x, y);
739
+
740
+ // Ghost body (wavy bottom)
741
+ ctx.fillStyle = '#f0f0f0';
742
+ ctx.shadowColor = 'rgba(240, 240, 240, 0.5)';
743
+ ctx.shadowBlur = 15;
744
+
745
+ ctx.beginPath();
746
+ ctx.arc(0, -size * 0.5, size * 0.6, Math.PI, 0);
747
+
748
+ // Wavy bottom edge
749
+ const waveCount = 5;
750
+ for (let i = 0; i <= waveCount; i++) {
751
+ const waveX = -size * 0.6 + (i / waveCount) * size * 1.2;
752
+ const waveY = Math.sin(time * 0.005 + i) * size * 0.15;
753
+ if (i === 0) {
754
+ ctx.lineTo(waveX, waveY);
755
+ } else {
756
+ ctx.quadraticCurveTo(
757
+ waveX - size * 0.12,
758
+ waveY - size * 0.1,
759
+ waveX,
760
+ waveY
761
+ );
762
+ }
763
+ }
764
+ ctx.closePath();
765
+ ctx.fill();
766
+ ctx.shadowBlur = 0;
767
+
768
+ // Eyes
769
+ ctx.fillStyle = '#000000';
770
+ ctx.beginPath();
771
+ ctx.ellipse(-size * 0.2, -size * 0.6, size * 0.12, size * 0.18, -0.2, 0, Math.PI * 2);
772
+ ctx.fill();
773
+ ctx.beginPath();
774
+ ctx.ellipse(size * 0.2, -size * 0.6, size * 0.12, size * 0.18, 0.2, 0, Math.PI * 2);
775
+ ctx.fill();
776
+
777
+ // Mouth (spooky O shape)
778
+ ctx.beginPath();
779
+ ctx.ellipse(0, -size * 0.3, size * 0.15, size * 0.2, 0, 0, Math.PI * 2);
780
+ ctx.fill();
781
+
782
+ ctx.restore();
783
+ },
784
+
785
+ /**
786
+ * Draw floating pumpkin
787
+ */
788
+ drawPumpkin(ctx, particle, time) {
789
+ const x = particle.x;
790
+ const y = particle.y;
791
+ const size = particle.size;
792
+
793
+ ctx.save();
794
+ ctx.globalAlpha = particle.opacity;
795
+ ctx.translate(x, y);
796
+ ctx.rotate(particle.rotation);
797
+
798
+ // Pumpkin body
799
+ ctx.fillStyle = '#ff6600';
800
+ ctx.strokeStyle = '#cc5200';
801
+ ctx.lineWidth = size * 0.08;
802
+
803
+ // Draw pumpkin segments
804
+ const segments = 5;
805
+ ctx.beginPath();
806
+ for (let i = 0; i < segments; i++) {
807
+ const angle = (i / segments) * Math.PI * 2 - Math.PI / 2;
808
+ const nextAngle = ((i + 1) / segments) * Math.PI * 2 - Math.PI / 2;
809
+ const radiusX = size * 0.9;
810
+ const radiusY = size;
811
+
812
+ if (i === 0) {
813
+ ctx.moveTo(Math.cos(angle) * radiusX, Math.sin(angle) * radiusY);
814
+ }
815
+ ctx.quadraticCurveTo(
816
+ Math.cos((angle + nextAngle) / 2) * radiusX * 0.8,
817
+ Math.sin((angle + nextAngle) / 2) * radiusY * 0.8,
818
+ Math.cos(nextAngle) * radiusX,
819
+ Math.sin(nextAngle) * radiusY
820
+ );
821
+ }
822
+ ctx.closePath();
823
+ ctx.fill();
824
+ ctx.stroke();
825
+
826
+ // Stem
827
+ ctx.fillStyle = '#8B4513';
828
+ ctx.fillRect(-size * 0.2, -size * 1.1, size * 0.4, size * 0.2);
829
+
830
+ // Inner glow (pulsing)
831
+ const glowIntensity = (Math.sin(time * 0.002 + particle.glowPhase) + 1) * 0.5;
832
+ ctx.globalAlpha = particle.opacity * glowIntensity * 0.7;
833
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size);
834
+ gradient.addColorStop(0, '#ffaa00');
835
+ gradient.addColorStop(1, 'rgba(255, 170, 0, 0)');
836
+ ctx.fillStyle = gradient;
837
+ ctx.fillRect(-size, -size, size * 2, size * 2);
838
+
839
+ ctx.restore();
840
+ },
841
+
842
+ /**
843
+ * Draw gravestone
844
+ */
845
+ drawGravestone(ctx, particle) {
846
+ const x = particle.x;
847
+ const y = particle.y;
848
+ const size = particle.size;
849
+
850
+ ctx.save();
851
+ ctx.globalAlpha = particle.opacity;
852
+ ctx.translate(x, y);
853
+ ctx.rotate(particle.rotation);
854
+
855
+ ctx.fillStyle = '#555555';
856
+ ctx.strokeStyle = '#333333';
857
+ ctx.lineWidth = 1;
858
+
859
+ if (particle.shape === 'rounded') {
860
+ // Rounded top gravestone
861
+ ctx.beginPath();
862
+ ctx.moveTo(-size * 0.5, size * 0.8);
863
+ ctx.lineTo(-size * 0.5, -size * 0.3);
864
+ ctx.arc(0, -size * 0.3, size * 0.5, Math.PI, 0);
865
+ ctx.lineTo(size * 0.5, size * 0.8);
866
+ ctx.closePath();
867
+ ctx.fill();
868
+ ctx.stroke();
869
+ } else if (particle.shape === 'cross') {
870
+ // Cross-shaped gravestone
871
+ ctx.fillRect(-size * 0.15, -size, size * 0.3, size * 1.8);
872
+ ctx.fillRect(-size * 0.5, -size * 0.3, size, size * 0.3);
873
+ } else {
874
+ // Obelisk
875
+ ctx.beginPath();
876
+ ctx.moveTo(-size * 0.4, size * 0.8);
877
+ ctx.lineTo(-size * 0.5, -size * 0.5);
878
+ ctx.lineTo(0, -size);
879
+ ctx.lineTo(size * 0.5, -size * 0.5);
880
+ ctx.lineTo(size * 0.4, size * 0.8);
881
+ ctx.closePath();
882
+ ctx.fill();
883
+ ctx.stroke();
884
+ }
885
+
886
+ // Moss/aging texture
887
+ ctx.fillStyle = 'rgba(0, 100, 0, 0.3)';
888
+ ctx.fillRect(-size * 0.4, size * 0.5, size * 0.8, size * 0.3);
889
+
890
+ // RIP text
891
+ ctx.fillStyle = '#222222';
892
+ ctx.font = `${size * 0.4}px serif`;
893
+ ctx.textAlign = 'center';
894
+ ctx.textBaseline = 'middle';
895
+ ctx.fillText('RIP', 0, 0);
896
+
897
+ ctx.restore();
898
+ },
899
+
900
+ /**
901
+ * Draw sinister jack-o-lantern with spikey mouth and yellow glow
902
+ */
903
+ drawJackOLantern(ctx, particle, time) {
904
+ const x = particle.x;
905
+ const y = particle.y;
906
+ const size = particle.size;
907
+
908
+ ctx.save();
909
+ ctx.globalAlpha = particle.opacity;
910
+ ctx.translate(x, y);
911
+
912
+ // Pumpkin body
913
+ ctx.fillStyle = '#ff8800';
914
+ ctx.beginPath();
915
+ ctx.ellipse(0, 0, size * 1.2, size, 0, 0, Math.PI * 2);
916
+ ctx.fill();
917
+
918
+ // Vertical ridges
919
+ ctx.strokeStyle = '#cc6600';
920
+ ctx.lineWidth = size * 0.08;
921
+ for (let i = -2; i <= 2; i++) {
922
+ ctx.beginPath();
923
+ ctx.moveTo(i * size * 0.35, -size * 0.8);
924
+ ctx.quadraticCurveTo(i * size * 0.3, 0, i * size * 0.35, size * 0.8);
925
+ ctx.stroke();
926
+ }
927
+
928
+ // Stem
929
+ ctx.fillStyle = '#8B4513';
930
+ ctx.fillRect(-size * 0.2, -size * 1.1, size * 0.4, size * 0.2);
931
+
932
+ // Inner glow (pulsing) - now just an internal light source
933
+ const flicker = 0.8 + Math.sin(time * 0.02 + particle.glowPhase) * 0.2; // Flicker effect
934
+ const glowIntensity = 0.5 + flicker * 0.5;
935
+ const glowGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size * 1.5);
936
+ glowGradient.addColorStop(0, `rgba(255, 255, 100, ${glowIntensity})`); // Bright yellow center
937
+ glowGradient.addColorStop(0.5, `rgba(255, 200, 0, ${glowIntensity * 0.7})`); // Orange-yellow transition
938
+ glowGradient.addColorStop(1, 'rgba(255, 150, 0, 0)'); // Fades to transparent orange
939
+ ctx.fillStyle = glowGradient;
940
+ ctx.fillRect(-size * 1.5, -size * 1.5, size * 3, size * 3);
941
+
942
+ // Face (carved out - black with enhanced glow)
943
+ ctx.fillStyle = '#000000';
944
+ ctx.shadowColor = `rgba(255, 180, 0, ${flicker})`; // Shadow color matches glow
945
+ ctx.shadowBlur = size * 0.5; // Increased blur for more glow
946
+
947
+ // Sinister Triangular Eyes
948
+ ctx.beginPath();
949
+ ctx.moveTo(-size * 0.6, -size * 0.4);
950
+ ctx.lineTo(-size * 0.2, -size * 0.3);
951
+ ctx.lineTo(-size * 0.5, -size * 0.1);
952
+ ctx.closePath();
953
+ ctx.fill();
954
+
955
+ ctx.beginPath();
956
+ ctx.moveTo(size * 0.6, -size * 0.4);
957
+ ctx.lineTo(size * 0.2, -size * 0.3);
958
+ ctx.lineTo(size * 0.5, -size * 0.1);
959
+ ctx.closePath();
960
+ ctx.fill();
961
+
962
+ // Spikey Mouth
963
+ ctx.beginPath();
964
+ ctx.moveTo(-size * 0.7, size * 0.2);
965
+ ctx.lineTo(-size * 0.5, size * 0.4);
966
+ ctx.lineTo(-size * 0.3, size * 0.25);
967
+ ctx.lineTo(0, size * 0.45);
968
+ ctx.lineTo(size * 0.3, size * 0.25);
969
+ ctx.lineTo(size * 0.5, size * 0.4);
970
+ ctx.lineTo(size * 0.7, size * 0.2);
971
+ ctx.lineTo(size * 0.5, size * 0.1);
972
+ ctx.lineTo(0, size * 0.25);
973
+ ctx.lineTo(-size * 0.5, size * 0.1);
974
+ ctx.closePath();
975
+ ctx.fill();
976
+
977
+ ctx.shadowBlur = 0;
978
+ ctx.restore();
979
+ },
980
+
981
+ /**
982
+ * Draw haunted castle with towers, turrets, and battlements
983
+ */
984
+ drawHauntedHouse(ctx, particle, time) {
985
+ const x = particle.x;
986
+ const y = particle.y;
987
+ const size = particle.size;
988
+
989
+ ctx.save();
990
+ ctx.globalAlpha = particle.opacity;
991
+ ctx.translate(x, y);
992
+
993
+ const castleColor = '#1a1a2e';
994
+ const darkOutline = '#0a0a0a';
995
+ const flickerIntensity = 0.5 + Math.random() * 0.5;
996
+
997
+ ctx.fillStyle = castleColor;
998
+ ctx.strokeStyle = darkOutline;
999
+ ctx.lineWidth = 2;
1000
+
1001
+ // Left tower (tall, circular with conical roof)
1002
+ ctx.fillRect(-size * 1.2, -size * 1.4, size * 0.5, size * 2.0);
1003
+ // Battlements on left tower
1004
+ for (let i = 0; i < 3; i++) {
1005
+ ctx.fillRect(-size * 1.2 + i * size * 0.2, -size * 1.5, size * 0.12, size * 0.1);
1006
+ }
1007
+ // Conical roof
1008
+ ctx.beginPath();
1009
+ ctx.moveTo(-size * 1.3, -size * 1.5);
1010
+ ctx.lineTo(-size * 0.95, -size * 2.0);
1011
+ ctx.lineTo(-size * 0.6, -size * 1.5);
1012
+ ctx.closePath();
1013
+ ctx.fill();
1014
+ ctx.stroke();
1015
+
1016
+ // Right tower (tall, circular with conical roof)
1017
+ ctx.fillRect(size * 0.7, -size * 1.3, size * 0.5, size * 1.9);
1018
+ // Battlements on right tower
1019
+ for (let i = 0; i < 3; i++) {
1020
+ ctx.fillRect(size * 0.7 + i * size * 0.2, -size * 1.4, size * 0.12, size * 0.1);
1021
+ }
1022
+ // Conical roof
1023
+ ctx.beginPath();
1024
+ ctx.moveTo(size * 0.6, -size * 1.4);
1025
+ ctx.lineTo(size * 0.95, -size * 1.9);
1026
+ ctx.lineTo(size * 1.3, -size * 1.4);
1027
+ ctx.closePath();
1028
+ ctx.fill();
1029
+ ctx.stroke();
1030
+
1031
+ // Central keep (main castle body)
1032
+ ctx.fillRect(-size * 0.7, -size * 0.9, size * 1.4, size * 1.5);
1033
+ ctx.strokeRect(-size * 0.7, -size * 0.9, size * 1.4, size * 1.5);
1034
+
1035
+ // Central keep battlements (crenellations)
1036
+ for (let i = 0; i < 7; i++) {
1037
+ if (i % 2 === 0) {
1038
+ ctx.fillRect(-size * 0.7 + i * size * 0.2, -size * 1.0, size * 0.2, size * 0.1);
1039
+ }
1040
+ }
1041
+
1042
+ // Small turrets on central keep corners
1043
+ // Left turret
1044
+ ctx.fillRect(-size * 0.8, -size * 1.1, size * 0.3, size * 0.5);
1045
+ ctx.beginPath();
1046
+ ctx.moveTo(-size * 0.85, -size * 1.1);
1047
+ ctx.lineTo(-size * 0.65, -size * 1.35);
1048
+ ctx.lineTo(-size * 0.45, -size * 1.1);
1049
+ ctx.closePath();
1050
+ ctx.fill();
1051
+
1052
+ // Right turret
1053
+ ctx.fillRect(size * 0.5, -size * 1.1, size * 0.3, size * 0.5);
1054
+ ctx.beginPath();
1055
+ ctx.moveTo(size * 0.45, -size * 1.1);
1056
+ ctx.lineTo(size * 0.65, -size * 1.35);
1057
+ ctx.lineTo(size * 0.85, -size * 1.1);
1058
+ ctx.closePath();
1059
+ ctx.fill();
1060
+
1061
+ // Windows with flickering light
1062
+ ctx.fillStyle = `rgba(255, 200, 100, ${flickerIntensity})`;
1063
+ ctx.shadowColor = '#ffcc66';
1064
+ ctx.shadowBlur = 10;
1065
+
1066
+ // Left tower windows (3 vertically)
1067
+ ctx.fillRect(-size * 1.05, -size * 1.2, size * 0.2, size * 0.25);
1068
+ ctx.fillRect(-size * 1.05, -size * 0.8, size * 0.2, size * 0.25);
1069
+ ctx.fillRect(-size * 1.05, -size * 0.4, size * 0.2, size * 0.25);
1070
+
1071
+ // Right tower windows (3 vertically)
1072
+ ctx.fillRect(size * 0.85, -size * 1.1, size * 0.2, size * 0.25);
1073
+ ctx.fillRect(size * 0.85, -size * 0.7, size * 0.2, size * 0.25);
1074
+ ctx.fillRect(size * 0.85, -size * 0.3, size * 0.2, size * 0.25);
1075
+
1076
+ // Central keep windows (2 rows)
1077
+ ctx.fillRect(-size * 0.5, -size * 0.6, size * 0.25, size * 0.3);
1078
+ ctx.fillRect(-size * 0.1, -size * 0.6, size * 0.25, size * 0.3);
1079
+ ctx.fillRect(size * 0.3, -size * 0.6, size * 0.25, size * 0.3);
1080
+ ctx.fillRect(-size * 0.3, -size * 0.15, size * 0.25, size * 0.3);
1081
+ ctx.fillRect(size * 0.1, -size * 0.15, size * 0.25, size * 0.3);
1082
+
1083
+ ctx.shadowBlur = 0;
1084
+
1085
+ // Grand entrance archway
1086
+ ctx.fillStyle = darkOutline;
1087
+ ctx.beginPath();
1088
+ ctx.arc(0, size * 0.3, size * 0.25, Math.PI, 0, true);
1089
+ ctx.lineTo(size * 0.25, size * 0.6);
1090
+ ctx.lineTo(-size * 0.25, size * 0.6);
1091
+ ctx.closePath();
1092
+ ctx.fill();
1093
+
1094
+ // Portcullis gate (vertical bars)
1095
+ ctx.strokeStyle = '#333333';
1096
+ ctx.lineWidth = 1.5;
1097
+ for (let i = -2; i <= 2; i++) {
1098
+ ctx.beginPath();
1099
+ ctx.moveTo(i * size * 0.1, size * 0.3);
1100
+ ctx.lineTo(i * size * 0.1, size * 0.6);
1101
+ ctx.stroke();
1102
+ }
1103
+
1104
+ ctx.restore();
1105
+ },
1106
+
1107
+ /**
1108
+ * Draw bubbling cauldron
1109
+ */
1110
+ drawCauldron(ctx, particle, time) {
1111
+ const x = particle.x;
1112
+ const y = particle.y;
1113
+ const size = particle.size;
1114
+
1115
+ ctx.save();
1116
+ ctx.translate(x, y);
1117
+
1118
+ // Cauldron body
1119
+ ctx.fillStyle = '#222222';
1120
+ ctx.strokeStyle = '#111111';
1121
+ ctx.lineWidth = 2;
1122
+
1123
+ ctx.beginPath();
1124
+ ctx.arc(0, 0, size * 0.8, 0, Math.PI);
1125
+ ctx.lineTo(-size * 0.8, size * 0.5);
1126
+ ctx.quadraticCurveTo(0, size * 0.8, size * 0.8, size * 0.5);
1127
+ ctx.closePath();
1128
+ ctx.fill();
1129
+ ctx.stroke();
1130
+
1131
+ // Legs
1132
+ ctx.strokeStyle = '#1a1a1a';
1133
+ ctx.lineWidth = 3;
1134
+ [-size * 0.5, 0, size * 0.5].forEach(legX => {
1135
+ ctx.beginPath();
1136
+ ctx.moveTo(legX, size * 0.5);
1137
+ ctx.lineTo(legX, size * 0.9);
1138
+ ctx.stroke();
1139
+ });
1140
+
1141
+ // Bubbling potion (green)
1142
+ ctx.fillStyle = '#00ff00';
1143
+ ctx.shadowColor = '#00ff00';
1144
+ ctx.shadowBlur = 15;
1145
+ ctx.beginPath();
1146
+ ctx.ellipse(0, 0, size * 0.7, size * 0.3, 0, 0, Math.PI * 2);
1147
+ ctx.fill();
1148
+ ctx.shadowBlur = 0;
1149
+
1150
+ // Bubbles
1151
+ particle.bubbles.forEach(bubble => {
1152
+ ctx.globalAlpha = bubble.opacity;
1153
+ ctx.fillStyle = '#88ff88';
1154
+ ctx.beginPath();
1155
+ ctx.arc(bubble.x, bubble.y, bubble.size, 0, Math.PI * 2);
1156
+ ctx.fill();
1157
+ });
1158
+ ctx.globalAlpha = 1;
1159
+
1160
+ // Smoke/steam
1161
+ particle.smoke.forEach(smoke => {
1162
+ ctx.globalAlpha = smoke.opacity;
1163
+ ctx.fillStyle = '#aaaaaa';
1164
+ ctx.beginPath();
1165
+ ctx.arc(smoke.x, smoke.y, smoke.size, 0, Math.PI * 2);
1166
+ ctx.fill();
1167
+ });
1168
+
1169
+ ctx.restore();
1170
+ },
1171
+
1172
+ /**
1173
+ * Draw spider dangling from web
1174
+ */
1175
+ drawSpider(ctx, particle, time) {
1176
+ const x = particle.x;
1177
+ const y = particle.y;
1178
+ const size = particle.size;
1179
+
1180
+ // Calculate swing position
1181
+ const swingOffset = Math.sin(time * particle.swingSpeed + particle.swingPhase) * particle.swingAmplitude;
1182
+
1183
+ ctx.save();
1184
+ ctx.globalAlpha = particle.opacity;
1185
+
1186
+ // Thread
1187
+ ctx.strokeStyle = '#cccccc';
1188
+ ctx.lineWidth = 1;
1189
+ ctx.setLineDash([2, 2]);
1190
+ ctx.beginPath();
1191
+ ctx.moveTo(x, y);
1192
+ ctx.lineTo(x + swingOffset, y + particle.threadLength);
1193
+ ctx.stroke();
1194
+ ctx.setLineDash([]);
1195
+
1196
+ // Spider body
1197
+ ctx.translate(x + swingOffset, y + particle.threadLength);
1198
+ ctx.fillStyle = '#000000';
1199
+
1200
+ // Abdomen
1201
+ ctx.beginPath();
1202
+ ctx.ellipse(0, 0, size * 0.5, size * 0.7, 0, 0, Math.PI * 2);
1203
+ ctx.fill();
1204
+
1205
+ // Head
1206
+ ctx.beginPath();
1207
+ ctx.arc(0, -size * 0.5, size * 0.3, 0, Math.PI * 2);
1208
+ ctx.fill();
1209
+
1210
+ // Legs (8 legs)
1211
+ ctx.strokeStyle = '#1a1a1a';
1212
+ ctx.lineWidth = size * 0.1;
1213
+ const legAngle = Math.sin(time * 0.005) * 0.2;
1214
+ for (let i = 0; i < 4; i++) {
1215
+ const angle = (i / 4) * Math.PI - Math.PI / 2;
1216
+ const legLength = size * 1.5;
1217
+
1218
+ // Left legs
1219
+ ctx.beginPath();
1220
+ ctx.moveTo(-size * 0.3, -size * 0.2 + i * size * 0.2);
1221
+ ctx.quadraticCurveTo(
1222
+ -size - Math.sin(angle + legAngle) * size * 0.3,
1223
+ -size * 0.2 + i * size * 0.2 - size * 0.5,
1224
+ -size * 1.2,
1225
+ -size * 0.2 + i * size * 0.2
1226
+ );
1227
+ ctx.stroke();
1228
+
1229
+ // Right legs
1230
+ ctx.beginPath();
1231
+ ctx.moveTo(size * 0.3, -size * 0.2 + i * size * 0.2);
1232
+ ctx.quadraticCurveTo(
1233
+ size + Math.sin(angle + legAngle) * size * 0.3,
1234
+ -size * 0.2 + i * size * 0.2 - size * 0.5,
1235
+ size * 1.2,
1236
+ -size * 0.2 + i * size * 0.2
1237
+ );
1238
+ ctx.stroke();
1239
+ }
1240
+
1241
+ // Eyes (8 eyes)
1242
+ ctx.fillStyle = '#ff0000';
1243
+ for (let i = 0; i < 4; i++) {
1244
+ ctx.beginPath();
1245
+ ctx.arc(-size * 0.15 + (i % 2) * size * 0.3, -size * 0.55 + Math.floor(i / 2) * size * 0.1, size * 0.05, 0, Math.PI * 2);
1246
+ ctx.fill();
1247
+ }
1248
+
1249
+ ctx.restore();
1250
+ },
1251
+
1252
+ /**
1253
+ * Draw small spider particle (falling)
1254
+ */
1255
+ drawSpiderSmall(ctx, particle) {
1256
+ ctx.save();
1257
+ ctx.globalAlpha = particle.opacity;
1258
+ ctx.translate(particle.x, particle.y);
1259
+ ctx.rotate(particle.rotation);
1260
+
1261
+ // Tiny spider body
1262
+ ctx.fillStyle = '#1a1a1a';
1263
+ ctx.beginPath();
1264
+ ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
1265
+ ctx.fill();
1266
+
1267
+ // Tiny legs
1268
+ ctx.strokeStyle = '#1a1a1a';
1269
+ ctx.lineWidth = particle.size * 0.3;
1270
+ for (let i = 0; i < 4; i++) {
1271
+ const angle = (i / 4) * Math.PI - Math.PI / 2;
1272
+ // Left leg
1273
+ ctx.beginPath();
1274
+ ctx.moveTo(0, 0);
1275
+ ctx.lineTo(Math.cos(angle) * particle.size * 2, Math.sin(angle) * particle.size * 2);
1276
+ ctx.stroke();
1277
+ // Right leg
1278
+ ctx.beginPath();
1279
+ ctx.moveTo(0, 0);
1280
+ ctx.lineTo(-Math.cos(angle) * particle.size * 2, Math.sin(angle) * particle.size * 2);
1281
+ ctx.stroke();
1282
+ }
1283
+
1284
+ ctx.restore();
1285
+ },
1286
+
1287
+ /**
1288
+ * Draw flying witch
1289
+ */
1290
+ drawWitch(ctx, particle, time) {
1291
+ const x = particle.x;
1292
+ const y = particle.y + Math.sin(time * particle.waveFrequency + particle.waveOffset) * particle.waveAmplitude;
1293
+ const size = particle.size;
1294
+ const dir = particle.vx > 0 ? 1 : -1;
1295
+
1296
+ ctx.save();
1297
+ ctx.globalAlpha = particle.opacity;
1298
+ ctx.translate(x, y);
1299
+ if (dir === -1) {
1300
+ ctx.scale(-1, 1);
1301
+ }
1302
+
1303
+ // Broomstick (horizontal, pointing backward relative to direction of travel)
1304
+ ctx.strokeStyle = '#8B4513';
1305
+ ctx.lineWidth = size * 0.12;
1306
+ ctx.lineCap = 'round';
1307
+ ctx.beginPath();
1308
+ ctx.moveTo(size * 0.2, size * 0.6); // Start point near witch's body
1309
+ ctx.lineTo(-size * 1.5, size * 0.6); // End point extending backward
1310
+ ctx.stroke();
1311
+
1312
+ // Bristles (at back of broom)
1313
+ ctx.strokeStyle = '#D2691E';
1314
+ ctx.lineWidth = size * 0.06;
1315
+ for (let i = 0; i < 8; i++) {
1316
+ const angle = (Math.PI / 3) * ((i / 7) - 0.5);
1317
+ ctx.beginPath();
1318
+ ctx.moveTo(-size * 1.5, size * 0.6); // Bristles start at the back of the broom
1319
+ ctx.lineTo(
1320
+ -size * 1.5 + Math.cos(angle) * size * 0.4,
1321
+ size * 0.6 + Math.sin(angle) * size * 0.4
1322
+ );
1323
+ ctx.stroke();
1324
+ }
1325
+
1326
+ // Flowing black cloak (side profile)
1327
+ ctx.fillStyle = '#1a1a1a';
1328
+ ctx.strokeStyle = '#0a0a0a';
1329
+ ctx.lineWidth = 1;
1330
+ ctx.beginPath();
1331
+ ctx.moveTo(size * 0.2, size * 0.6); // Bottom of cloak on broom
1332
+ ctx.quadraticCurveTo(-size * 0.5, size * 0.4, -size * 0.8, size * 0.1);
1333
+ ctx.lineTo(-size * 0.7, -size * 0.3);
1334
+ ctx.quadraticCurveTo(-size * 0.3, -size * 0.5, 0, -size * 0.6);
1335
+ ctx.lineTo(size * 0.3, -size * 0.4);
1336
+ ctx.lineTo(size * 0.3, size * 0.5);
1337
+ ctx.closePath();
1338
+ ctx.fill();
1339
+ ctx.stroke();
1340
+
1341
+ // Head (side profile, greenish skin)
1342
+ ctx.fillStyle = '#8B9355';
1343
+ ctx.beginPath();
1344
+ ctx.ellipse(size * 0.25, -size * 0.6, size * 0.3, size * 0.35, 0, 0, Math.PI * 2);
1345
+ ctx.fill();
1346
+
1347
+ // Crooked nose (prominent hooked nose)
1348
+ ctx.fillStyle = '#7a8245';
1349
+ ctx.beginPath();
1350
+ ctx.moveTo(size * 0.45, -size * 0.6);
1351
+ ctx.quadraticCurveTo(size * 0.7, -size * 0.65, size * 0.72, -size * 0.5);
1352
+ ctx.lineTo(size * 0.6, -size * 0.52);
1353
+ ctx.quadraticCurveTo(size * 0.58, -size * 0.58, size * 0.4, -size * 0.55);
1354
+ ctx.closePath();
1355
+ ctx.fill();
1356
+
1357
+ // Pointed witch hat (side profile)
1358
+ ctx.fillStyle = '#2C1810';
1359
+ // Hat cone (tilted forward)
1360
+ ctx.beginPath();
1361
+ ctx.moveTo(size * 0.05, -size * 0.85);
1362
+ ctx.lineTo(size * 0.4, -size * 1.6);
1363
+ ctx.lineTo(size * 0.5, -size * 0.8);
1364
+ ctx.closePath();
1365
+ ctx.fill();
1366
+ // Hat brim
1367
+ ctx.beginPath();
1368
+ ctx.ellipse(size * 0.25, -size * 0.85, size * 0.5, size * 0.12, 0, 0, Math.PI * 2);
1369
+ ctx.fill();
1370
+
1371
+ // Eye (single glowing eye, profile view)
1372
+ ctx.fillStyle = '#ff6600';
1373
+ ctx.shadowColor = '#ff6600';
1374
+ ctx.shadowBlur = size * 0.3;
1375
+ ctx.beginPath();
1376
+ ctx.arc(size * 0.35, -size * 0.65, size * 0.08, 0, Math.PI * 2);
1377
+ ctx.fill();
1378
+ ctx.shadowBlur = 0;
1379
+
1380
+ // Hands gripping broom
1381
+ ctx.fillStyle = '#8B9355';
1382
+ ctx.beginPath();
1383
+ ctx.arc(size * 0.1, size * 0.5, size * 0.12, 0, Math.PI * 2);
1384
+ ctx.fill();
1385
+ ctx.beginPath();
1386
+ ctx.arc(-size * 0.3, size * 0.55, size * 0.1, 0, Math.PI * 2);
1387
+ ctx.fill();
1388
+
1389
+ // Legs dangling
1390
+ ctx.strokeStyle = '#1a1a1a';
1391
+ ctx.lineWidth = size * 0.1;
1392
+ ctx.beginPath();
1393
+ ctx.moveTo(size * 0.15, size * 0.6);
1394
+ ctx.lineTo(size * 0.1, size * 1.0);
1395
+ ctx.stroke();
1396
+ ctx.beginPath();
1397
+ ctx.moveTo(size * 0.05, size * 0.6);
1398
+ ctx.lineTo(size * 0.2, size * 0.95);
1399
+ ctx.stroke();
1400
+
1401
+ ctx.restore();
1402
+ },
1403
+
1404
+ /**
1405
+ * Draw floating pumpkin
1406
+ */
1407
+ drawFloatingPumpkin(ctx, particle) {
1408
+ const x = particle.x;
1409
+ const y = particle.y;
1410
+ const size = particle.size;
1411
+
1412
+ ctx.save();
1413
+ ctx.globalAlpha = particle.opacity;
1414
+ ctx.translate(x, y);
1415
+ ctx.rotate(particle.rotation);
1416
+
1417
+ // Pumpkin body
1418
+ ctx.fillStyle = '#ff6600';
1419
+ ctx.strokeStyle = '#cc5200';
1420
+ ctx.lineWidth = size * 0.1;
1421
+
1422
+ ctx.beginPath();
1423
+ ctx.ellipse(0, 0, size, size * 0.9, 0, 0, Math.PI * 2);
1424
+ ctx.fill();
1425
+ ctx.stroke();
1426
+
1427
+ // Vertical lines
1428
+ for (let i = -2; i <= 2; i++) {
1429
+ ctx.beginPath();
1430
+ ctx.moveTo(i * size * 0.25, -size * 0.8);
1431
+ ctx.quadraticCurveTo(i * size * 0.3, 0, i * size * 0.25, size * 0.8);
1432
+ ctx.stroke();
1433
+ }
1434
+
1435
+ // Stem
1436
+ ctx.fillStyle = '#228B22';
1437
+ ctx.beginPath();
1438
+ ctx.moveTo(-size * 0.15, -size * 0.8);
1439
+ ctx.lineTo(-size * 0.1, -size * 1.1);
1440
+ ctx.lineTo(size * 0.1, -size * 1.1);
1441
+ ctx.lineTo(size * 0.15, -size * 0.8);
1442
+ ctx.closePath();
1443
+ ctx.fill();
1444
+
1445
+ // Jack-o-lantern face (glowing)
1446
+ const glow = 0.5 + Math.sin(particle.glowPhase + Date.now() * 0.005) * 0.5;
1447
+ ctx.fillStyle = `rgba(255, 200, 0, ${glow})`;
1448
+
1449
+ // Eyes (triangles)
1450
+ ctx.beginPath();
1451
+ ctx.moveTo(-size * 0.4, -size * 0.3);
1452
+ ctx.lineTo(-size * 0.2, -size * 0.1);
1453
+ ctx.lineTo(-size * 0.6, -size * 0.1);
1454
+ ctx.closePath();
1455
+ ctx.fill();
1456
+
1457
+ ctx.beginPath();
1458
+ ctx.moveTo(size * 0.4, -size * 0.3);
1459
+ ctx.lineTo(size * 0.6, -size * 0.1);
1460
+ ctx.lineTo(size * 0.2, -size * 0.1);
1461
+ ctx.closePath();
1462
+ ctx.fill();
1463
+
1464
+ // Mouth (zigzag)
1465
+ ctx.beginPath();
1466
+ ctx.moveTo(-size * 0.5, size * 0.2);
1467
+ for (let i = -4; i <= 4; i++) {
1468
+ ctx.lineTo(i * size * 0.12, size * 0.2 + (i % 2 === 0 ? size * 0.15 : 0));
1469
+ }
1470
+ ctx.lineTo(size * 0.5, size * 0.2);
1471
+ ctx.closePath();
1472
+ ctx.fill();
1473
+
1474
+ ctx.restore();
1475
+ },
1476
+
1477
+ /**
1478
+ * Draw full moon with eerie glow
1479
+ */
1480
+ drawMoon(ctx, particle, time) {
1481
+ const x = particle.x;
1482
+ const y = particle.y;
1483
+ const size = particle.size;
1484
+
1485
+ ctx.save();
1486
+ ctx.globalAlpha = particle.opacity;
1487
+ ctx.translate(x, y);
1488
+
1489
+ // Glow effect
1490
+ const glow = 0.8 + Math.sin(time * 0.001 + particle.glowPhase) * 0.2;
1491
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size * 1.5);
1492
+ gradient.addColorStop(0, `rgba(255, 255, 220, ${glow})`);
1493
+ gradient.addColorStop(0.4, `rgba(255, 255, 200, ${glow * 0.5})`);
1494
+ gradient.addColorStop(0.7, `rgba(255, 220, 150, ${glow * 0.2})`);
1495
+ gradient.addColorStop(1, 'rgba(255, 200, 100, 0)');
1496
+ ctx.fillStyle = gradient;
1497
+ ctx.fillRect(-size * 1.5, -size * 1.5, size * 3, size * 3);
1498
+
1499
+ // Moon body
1500
+ ctx.fillStyle = '#ffffdc';
1501
+ ctx.shadowColor = '#ffffaa';
1502
+ ctx.shadowBlur = size * 0.3;
1503
+ ctx.beginPath();
1504
+ ctx.arc(0, 0, size, 0, Math.PI * 2);
1505
+ ctx.fill();
1506
+
1507
+ // Moon craters (darker spots)
1508
+ ctx.shadowBlur = 0;
1509
+ ctx.fillStyle = 'rgba(200, 200, 170, 0.4)';
1510
+ ctx.beginPath();
1511
+ ctx.arc(-size * 0.3, -size * 0.2, size * 0.2, 0, Math.PI * 2);
1512
+ ctx.fill();
1513
+ ctx.beginPath();
1514
+ ctx.arc(size * 0.4, size * 0.1, size * 0.15, 0, Math.PI * 2);
1515
+ ctx.fill();
1516
+ ctx.beginPath();
1517
+ ctx.arc(size * 0.1, size * 0.4, size * 0.18, 0, Math.PI * 2);
1518
+ ctx.fill();
1519
+ ctx.beginPath();
1520
+ ctx.arc(-size * 0.2, size * 0.5, size * 0.12, 0, Math.PI * 2);
1521
+ ctx.fill();
1522
+
1523
+ // No Batman logo drawing
1524
+ ctx.restore();
1525
+ },
1526
+
1527
+ /**
1528
+ * Draw sinister scarecrow (creepy field guardian)
1529
+ */
1530
+ drawScarecrow(ctx, particle, time) {
1531
+ const x = particle.x;
1532
+ const y = particle.y;
1533
+ const size = particle.size;
1534
+ const sway = Math.sin(time * particle.swaySpeed + particle.swayPhase) * size * 0.12; // Increased amplitude
1535
+ const bounce = Math.abs(Math.sin(time * particle.swaySpeed * 2 + particle.swayPhase)) * size * 0.03; // Subtle vertical bounce
1536
+
1537
+ ctx.save();
1538
+ ctx.globalAlpha = particle.opacity;
1539
+ ctx.translate(x + sway, y - bounce);
1540
+
1541
+ // Wooden pole/stake (weathered wood)
1542
+ const poleGradient = ctx.createLinearGradient(-size * 0.05, 0, size * 0.05, 0);
1543
+ poleGradient.addColorStop(0, '#5a4632');
1544
+ poleGradient.addColorStop(0.5, '#6b5644');
1545
+ poleGradient.addColorStop(1, '#4a3622');
1546
+ ctx.fillStyle = poleGradient;
1547
+ ctx.fillRect(-size * 0.05, 0, size * 0.1, size * 1.8);
1548
+ ctx.strokeStyle = '#3a2612';
1549
+ ctx.lineWidth = 1;
1550
+ ctx.strokeRect(-size * 0.05, 0, size * 0.1, size * 1.8);
1551
+
1552
+ // Cross beam (arms)
1553
+ ctx.fillRect(-size * 0.8, size * 0.5, size * 1.6, size * 0.08);
1554
+ ctx.strokeRect(-size * 0.8, size * 0.5, size * 1.6, size * 0.08);
1555
+
1556
+ // Ragged burlap body (tattered shirt)
1557
+ ctx.fillStyle = '#8B7355';
1558
+ ctx.strokeStyle = '#654321';
1559
+ ctx.lineWidth = 1.5;
1560
+ ctx.beginPath();
1561
+ ctx.moveTo(-size * 0.4, size * 0.5);
1562
+ ctx.lineTo(-size * 0.45, size * 1.2);
1563
+ ctx.lineTo(-size * 0.25, size * 1.4);
1564
+ ctx.lineTo(size * 0.25, size * 1.4);
1565
+ ctx.lineTo(size * 0.45, size * 1.2);
1566
+ ctx.lineTo(size * 0.4, size * 0.5);
1567
+ ctx.closePath();
1568
+ ctx.fill();
1569
+ ctx.stroke();
1570
+
1571
+ // Tattered patches (darker)
1572
+ ctx.fillStyle = '#6b5c4d';
1573
+ ctx.fillRect(-size * 0.3, size * 0.7, size * 0.2, size * 0.15);
1574
+ ctx.fillRect(size * 0.1, size * 1.0, size * 0.18, size * 0.2);
1575
+
1576
+ // Ragged sleeves on cross beam
1577
+ ctx.fillStyle = '#8B7355';
1578
+ ctx.strokeStyle = '#654321';
1579
+ // Left sleeve
1580
+ ctx.beginPath();
1581
+ ctx.moveTo(-size * 0.8, size * 0.5);
1582
+ ctx.lineTo(-size * 0.75, size * 0.7);
1583
+ ctx.lineTo(-size * 0.85, size * 0.75);
1584
+ ctx.lineTo(-size * 0.9, size * 0.6);
1585
+ ctx.closePath();
1586
+ ctx.fill();
1587
+ ctx.stroke();
1588
+ // Right sleeve
1589
+ ctx.beginPath();
1590
+ ctx.moveTo(size * 0.8, size * 0.5);
1591
+ ctx.lineTo(size * 0.75, size * 0.7);
1592
+ ctx.lineTo(size * 0.85, size * 0.75);
1593
+ ctx.lineTo(size * 0.9, size * 0.6);
1594
+ ctx.closePath();
1595
+ ctx.fill();
1596
+ ctx.stroke();
1597
+
1598
+ // Creepy pumpkin head (or burlap sack head)
1599
+ const usesPumpkin = Math.random() < 0.6; // 60% pumpkin, 40% burlap
1600
+ if (usesPumpkin) {
1601
+ // Pumpkin head (orange with carved face)
1602
+ const headGradient = ctx.createRadialGradient(-size * 0.1, -size * 0.05, 0, 0, 0, size * 0.35);
1603
+ headGradient.addColorStop(0, '#ff8c00');
1604
+ headGradient.addColorStop(0.7, '#ff6600');
1605
+ headGradient.addColorStop(1, '#cc5500');
1606
+ ctx.fillStyle = headGradient;
1607
+ ctx.beginPath();
1608
+ ctx.ellipse(0, size * 0.2, size * 0.35, size * 0.3, 0, 0, Math.PI * 2);
1609
+ ctx.fill();
1610
+ ctx.strokeStyle = '#8B4513';
1611
+ ctx.lineWidth = 2;
1612
+ ctx.stroke();
1613
+
1614
+ // Pumpkin ridges (vertical lines)
1615
+ ctx.strokeStyle = '#cc5500';
1616
+ ctx.lineWidth = 1;
1617
+ for (let i = -2; i <= 2; i++) {
1618
+ ctx.beginPath();
1619
+ ctx.moveTo(i * size * 0.12, size * 0.05);
1620
+ ctx.quadraticCurveTo(i * size * 0.1, size * 0.2, i * size * 0.12, size * 0.45);
1621
+ ctx.stroke();
1622
+ }
1623
+
1624
+ // Stem
1625
+ ctx.fillStyle = '#228B22';
1626
+ ctx.strokeStyle = '#1a6b1a';
1627
+ ctx.fillRect(-size * 0.04, size * 0.02, size * 0.08, size * 0.1);
1628
+ ctx.strokeRect(-size * 0.04, size * 0.02, size * 0.08, size * 0.1);
1629
+ } else {
1630
+ // Burlap sack head
1631
+ ctx.fillStyle = '#d2b48c';
1632
+ ctx.strokeStyle = '#8b7355';
1633
+ ctx.lineWidth = 2;
1634
+ ctx.beginPath();
1635
+ ctx.ellipse(0, size * 0.2, size * 0.32, size * 0.28, 0, 0, Math.PI * 2);
1636
+ ctx.fill();
1637
+ ctx.stroke();
1638
+
1639
+ // Burlap texture (crosshatch)
1640
+ ctx.strokeStyle = '#a0826d';
1641
+ ctx.lineWidth = 0.5;
1642
+ for (let i = -3; i <= 3; i++) {
1643
+ ctx.beginPath();
1644
+ ctx.moveTo(i * size * 0.1, size * 0.05);
1645
+ ctx.lineTo(i * size * 0.1, size * 0.42);
1646
+ ctx.stroke();
1647
+ }
1648
+ }
1649
+
1650
+ // Evil glowing eyes
1651
+ const glowIntensity = 0.6 + Math.sin(time * particle.glowSpeed + particle.glowPhase) * 0.4;
1652
+ ctx.shadowColor = '#ff0000';
1653
+ ctx.shadowBlur = size * 0.2;
1654
+ ctx.fillStyle = `rgba(255, 0, 0, ${glowIntensity})`;
1655
+ // Left eye
1656
+ ctx.beginPath();
1657
+ ctx.moveTo(-size * 0.18, size * 0.15);
1658
+ ctx.lineTo(-size * 0.12, size * 0.2);
1659
+ ctx.lineTo(-size * 0.18, size * 0.25);
1660
+ ctx.closePath();
1661
+ ctx.fill();
1662
+ // Right eye
1663
+ ctx.beginPath();
1664
+ ctx.moveTo(size * 0.18, size * 0.15);
1665
+ ctx.lineTo(size * 0.12, size * 0.2);
1666
+ ctx.lineTo(size * 0.18, size * 0.25);
1667
+ ctx.closePath();
1668
+ ctx.fill();
1669
+
1670
+ // Jagged mouth (sinister grin)
1671
+ ctx.strokeStyle = '#000000';
1672
+ ctx.lineWidth = size * 0.03;
1673
+ ctx.beginPath();
1674
+ ctx.moveTo(-size * 0.2, size * 0.35);
1675
+ for (let i = -3; i <= 3; i++) {
1676
+ const yOff = i % 2 === 0 ? 0 : size * 0.04;
1677
+ ctx.lineTo(i * size * 0.07, size * 0.35 + yOff);
1678
+ }
1679
+ ctx.stroke();
1680
+
1681
+ ctx.shadowBlur = 0;
1682
+
1683
+ // Tattered wide-brim hat
1684
+ ctx.fillStyle = '#2a2a2a';
1685
+ ctx.strokeStyle = '#1a1a1a';
1686
+ ctx.lineWidth = 2;
1687
+ // Brim
1688
+ ctx.beginPath();
1689
+ ctx.ellipse(0, size * 0.05, size * 0.45, size * 0.08, 0, 0, Math.PI * 2);
1690
+ ctx.fill();
1691
+ ctx.stroke();
1692
+ // Crown (cone shape)
1693
+ ctx.beginPath();
1694
+ ctx.moveTo(-size * 0.2, size * 0.05);
1695
+ ctx.lineTo(-size * 0.15, -size * 0.2);
1696
+ ctx.lineTo(size * 0.15, -size * 0.2);
1697
+ ctx.lineTo(size * 0.2, size * 0.05);
1698
+ ctx.closePath();
1699
+ ctx.fill();
1700
+ ctx.stroke();
1701
+ // Hat band
1702
+ ctx.fillStyle = '#8B4513';
1703
+ ctx.fillRect(-size * 0.2, size * 0.03, size * 0.4, size * 0.05);
1704
+
1705
+ // Tattered hat edges (tears/rips)
1706
+ ctx.strokeStyle = '#1a1a1a';
1707
+ ctx.lineWidth = 1;
1708
+ for (let i = 0; i < 5; i++) {
1709
+ const angle = (i / 5) * Math.PI * 2;
1710
+ const brimX = Math.cos(angle) * size * 0.45;
1711
+ const brimY = size * 0.05 + Math.sin(angle) * size * 0.08;
1712
+ ctx.beginPath();
1713
+ ctx.moveTo(brimX, brimY);
1714
+ ctx.lineTo(brimX + Math.cos(angle) * size * 0.08, brimY + Math.sin(angle) * size * 0.08);
1715
+ ctx.stroke();
1716
+ }
1717
+
1718
+ // Crows perched on shoulders (rare, ominous)
1719
+ if (Math.random() < 0.3) {
1720
+ const crowX = Math.random() < 0.5 ? -size * 0.6 : size * 0.6;
1721
+ ctx.fillStyle = '#000000';
1722
+ ctx.beginPath();
1723
+ // Simple crow silhouette
1724
+ ctx.arc(crowX, size * 0.5, size * 0.08, 0, Math.PI * 2);
1725
+ ctx.fill();
1726
+ // Beak
1727
+ ctx.fillStyle = '#ffa500';
1728
+ ctx.beginPath();
1729
+ ctx.moveTo(crowX + size * 0.08, size * 0.5);
1730
+ ctx.lineTo(crowX + size * 0.15, size * 0.5);
1731
+ ctx.lineTo(crowX + size * 0.1, size * 0.52);
1732
+ ctx.closePath();
1733
+ ctx.fill();
1734
+ }
1735
+
1736
+ ctx.restore();
1737
+ },
1738
+
1739
+ /**
1740
+ * Draw twinkling star
1741
+ */
1742
+ drawTwinklingStar(ctx, particle, time) {
1743
+ const x = particle.x;
1744
+ const y = particle.y;
1745
+ const size = particle.size;
1746
+
1747
+ // Calculate twinkle intensity
1748
+ const twinkleIntensity = 0.4 + Math.sin(time * particle.twinkleSpeed + particle.twinklePhase) * 0.6;
1749
+
1750
+ // Select color based on variant (Halloween themed)
1751
+ let starColor, glowColor;
1752
+ switch (particle.colorVariant) {
1753
+ case 0: // Pale white (most common)
1754
+ starColor = `rgba(255, 255, 255, ${twinkleIntensity})`;
1755
+ glowColor = `rgba(255, 255, 255, ${twinkleIntensity * 0.3})`;
1756
+ break;
1757
+ case 1: // Purple tint (eerie)
1758
+ starColor = `rgba(200, 150, 255, ${twinkleIntensity})`;
1759
+ glowColor = `rgba(150, 100, 255, ${twinkleIntensity * 0.3})`;
1760
+ break;
1761
+ case 2: // Dim orange (haunting)
1762
+ starColor = `rgba(255, 180, 100, ${twinkleIntensity})`;
1763
+ glowColor = `rgba(255, 150, 50, ${twinkleIntensity * 0.3})`;
1764
+ break;
1765
+ }
1766
+
1767
+ ctx.save();
1768
+ ctx.translate(x, y);
1769
+
1770
+ // Outer glow
1771
+ ctx.shadowColor = glowColor;
1772
+ ctx.shadowBlur = size * 4 * twinkleIntensity;
1773
+ ctx.fillStyle = starColor;
1774
+
1775
+ // Draw 4-pointed star
1776
+ ctx.beginPath();
1777
+ for (let i = 0; i < 4; i++) {
1778
+ const angle = (i / 4) * Math.PI - Math.PI / 2;
1779
+ const outerX = Math.cos(angle) * size;
1780
+ const outerY = Math.sin(angle) * size;
1781
+ const innerAngle = angle + Math.PI / 4;
1782
+ const innerX = Math.cos(innerAngle) * (size * 0.3);
1783
+ const innerY = Math.sin(innerAngle) * (size * 0.3);
1784
+
1785
+ if (i === 0) {
1786
+ ctx.moveTo(outerX, outerY);
1787
+ } else {
1788
+ ctx.lineTo(outerX, outerY);
1789
+ }
1790
+ ctx.lineTo(innerX, innerY);
1791
+ }
1792
+ ctx.closePath();
1793
+ ctx.fill();
1794
+
1795
+ // Bright center
1796
+ ctx.shadowBlur = size * 2 * twinkleIntensity;
1797
+ ctx.beginPath();
1798
+ ctx.arc(0, 0, size * 0.3, 0, Math.PI * 2);
1799
+ ctx.fill();
1800
+
1801
+ ctx.restore();
1802
+ },
1803
+
1804
+ /**
1805
+ * Draw global theme effects (e.g., lightning)
1806
+ */
1807
+ drawGlobalEffects(ctx, currentTime, canvasWidth, canvasHeight) {
1808
+ if (this.lightningActive) {
1809
+ ctx.save();
1810
+ ctx.globalAlpha = 0.8 + Math.sin(this.lightningTimer * 0.01) * 0.2; // Pulsing effect during flash
1811
+ ctx.shadowColor = this.lightningColor;
1812
+ ctx.shadowBlur = 20;
1813
+
1814
+ // Draw main lightning bolt from top-center to random bottom position
1815
+ const startX = canvasWidth * (0.4 + Math.random() * 0.2); // Top middle
1816
+ const startY = 0;
1817
+ const endX = canvasWidth * Math.random();
1818
+ const endY = canvasHeight;
1819
+
1820
+ this.drawLightning(
1821
+ ctx,
1822
+ startX,
1823
+ startY,
1824
+ endX,
1825
+ endY,
1826
+ 5 + Math.floor(Math.random() * 3), // 5-7 segments
1827
+ 50 + Math.random() * 50, // 50-100 displacement
1828
+ 0.7, // Roughness
1829
+ 0.3, // Branch chance
1830
+ 3 + Math.random() * 2, // Line width
1831
+ this.lightningColor
1832
+ );
1833
+
1834
+ ctx.restore();
1835
+ }
1836
+ }
1837
+ };