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,1290 @@
1
+ /**
2
+ * St Patrick's Day Theme for Domma Celebrations
3
+ * (March 17th, Irish Celebration)
4
+ *
5
+ * Features:
6
+ * - Falling shamrocks (3-leaf clovers)
7
+ * - Golden coins floating
8
+ * - Rainbow with pot of gold
9
+ * - Leprechaun characters
10
+ * - Irish harps and Celtic knots
11
+ * - Green color scheme with gold accents
12
+ */
13
+
14
+ export default {
15
+ name: 'st-patricks',
16
+ displayName: 'St Patrick\'s Day',
17
+ emoji: '☘️',
18
+
19
+ // Intensity configurations
20
+ intensityConfig: {
21
+ light: {
22
+ count: 40,
23
+ speedRange: [0.4, 1.2],
24
+ sizeRange: [2, 4],
25
+ pots: 1,
26
+ leprechaunChance: 0.0003,
27
+ twinklingStars: 10
28
+ },
29
+ medium: {
30
+ count: 80,
31
+ speedRange: [0.5, 1.5],
32
+ sizeRange: [2, 5],
33
+ pots: 2,
34
+ leprechaunChance: 0.0005,
35
+ twinklingStars: 18
36
+ },
37
+ heavy: {
38
+ count: 120,
39
+ speedRange: [0.6, 1.8],
40
+ sizeRange: [3, 6],
41
+ pots: 3,
42
+ leprechaunChance: 0.0008,
43
+ twinklingStars: 25
44
+ }
45
+ },
46
+
47
+ particles: ['clover-petal', 'shamrock', 'gold-coin', 'sparkle'],
48
+ decorations: ['pot-of-gold', 'rainbow', 'leprechaun', 'harp', 'static-leprechaun', 'moon', 'twinkling-star'],
49
+ colors: {
50
+ primary: '#228B22', // Irish green
51
+ secondary: '#90EE90', // Light green
52
+ white: '#FFFFFF', // White (Irish flag)
53
+ accent: '#FFD700', // Gold
54
+ rainbow: ['#ff0000', '#ffa500', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#8b00ff']
55
+ },
56
+
57
+ /**
58
+ * Create clover petal particle (simple green heart-shaped petal)
59
+ */
60
+ createCloverPetal(canvasWidth, canvasHeight, config) {
61
+ const greenShades = ['#228B22', '#32CD32', '#90EE90', '#3CB371', '#2E8B57'];
62
+ return {
63
+ type: 'clover-petal',
64
+ x: -30, // Start from left edge
65
+ y: Math.random() * canvasHeight, // Random height
66
+ vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
67
+ size: (config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0])) * 1.5, // Larger petals
68
+ speed: (Math.random() - 0.5) * 0.2, // Gentle vertical bobbing
69
+ opacity: 0.75 + Math.random() * 0.25,
70
+ windOffset: Math.random() * Math.PI * 2,
71
+ windSpeed: 0.015 + Math.random() * 0.025,
72
+ rotation: Math.random() * Math.PI * 2,
73
+ rotationSpeed: (Math.random() - 0.5) * 0.03,
74
+ color: greenShades[Math.floor(Math.random() * greenShades.length)],
75
+ flutter: Math.random() * Math.PI * 2,
76
+ flutterSpeed: 0.02 + Math.random() * 0.02,
77
+ active: true
78
+ };
79
+ },
80
+
81
+ /**
82
+ * Create shamrock particle (full 3-leaf clover - rare)
83
+ */
84
+ createShamrock(canvasWidth, canvasHeight, config) {
85
+ const colorChoice = Math.random();
86
+ let color, strokeColor;
87
+ if (colorChoice < 0.5) {
88
+ color = '#228B22'; // 50% green
89
+ strokeColor = '#006400'; // Dark green
90
+ } else if (colorChoice < 0.75) {
91
+ color = '#FFFFFF'; // 25% white
92
+ strokeColor = '#d0d0d0'; // Light grey
93
+ } else {
94
+ color = '#FFD700'; // 25% gold
95
+ strokeColor = '#DAA520'; // Dark gold
96
+ }
97
+
98
+ return {
99
+ type: 'shamrock',
100
+ x: -30, // Start from left edge
101
+ y: Math.random() * canvasHeight, // Random height
102
+ vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
103
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
104
+ speed: (Math.random() - 0.5) * 0.15, // Gentle vertical bobbing
105
+ opacity: 0.7 + Math.random() * 0.3,
106
+ windOffset: Math.random() * Math.PI * 2,
107
+ windSpeed: 0.015 + Math.random() * 0.025,
108
+ rotation: Math.random() * Math.PI * 2,
109
+ rotationSpeed: (Math.random() - 0.5) * 0.02,
110
+ color: color,
111
+ strokeColor: strokeColor,
112
+ active: true
113
+ };
114
+ },
115
+
116
+ /**
117
+ * Create gold coin particle
118
+ */
119
+ createGoldCoin(canvasWidth, canvasHeight, config) {
120
+ return {
121
+ type: 'gold-coin',
122
+ x: -30, // Start from left edge
123
+ y: Math.random() * canvasHeight, // Random height
124
+ vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift
125
+ size: 2 + Math.random() * 3,
126
+ speed: (Math.random() - 0.5) * 0.1, // Gentle vertical bobbing
127
+ opacity: 0.9 + Math.random() * 0.1,
128
+ rotation: 0,
129
+ rotationSpeed: 0.05 + Math.random() * 0.1,
130
+ spinPhase: Math.random() * Math.PI * 2,
131
+ glintPhase: Math.random() * Math.PI * 2,
132
+ windOffset: Math.random() * Math.PI * 2,
133
+ windSpeed: 0.01 + Math.random() * 0.02,
134
+ active: true
135
+ };
136
+ },
137
+
138
+ /**
139
+ * Create pot of gold decoration
140
+ */
141
+ createPotOfGold(canvasWidth, canvasHeight, options = {}) {
142
+ return {
143
+ type: 'pot-of-gold',
144
+ x: options.x !== undefined ? options.x : Math.random() * canvasWidth * 0.6 + canvasWidth * 0.2,
145
+ y: options.y !== undefined ? options.y : canvasHeight - 40,
146
+ size: 20 + Math.random() * 10,
147
+ opacity: 1,
148
+ glintPhase: Math.random() * Math.PI * 2,
149
+ coins: [],
150
+ active: true,
151
+ static: true
152
+ };
153
+ },
154
+
155
+ /**
156
+ * Create rainbow decoration
157
+ */
158
+ createRainbow(canvasWidth, canvasHeight, potX, potY) {
159
+ const rainbowStartX = 20; // Fixed start near left edge
160
+ const rainbowEndX = potX; // End at pot position
161
+ const centerX = (rainbowStartX + potX) / 2;
162
+ const rainbowHeight = canvasHeight * 0.2;
163
+ const centerY = potY - (rainbowHeight / 2);
164
+
165
+ return {
166
+ type: 'rainbow',
167
+ x: centerX,
168
+ y: centerY,
169
+ startX: rainbowStartX,
170
+ endX: rainbowEndX,
171
+ endY: potY,
172
+ width: canvasWidth,
173
+ height: canvasHeight,
174
+ opacity: 0.7,
175
+ vx: 0,
176
+ vy: 0,
177
+ active: true,
178
+ static: true
179
+ };
180
+ },
181
+
182
+ /**
183
+ * Create sparkle particle (Irish sparkles)
184
+ */
185
+ createSparkle(canvasWidth, canvasHeight, config) {
186
+ const colors = ['#FFD700', '#FFFFFF', '#228B22']; // Gold, white, green
187
+ return {
188
+ type: 'sparkle',
189
+ x: -20, // Start from left edge
190
+ y: Math.random() * canvasHeight, // Random height
191
+ vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift
192
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.6,
193
+ vy: (Math.random() - 0.5) * 0.2, // Minimal random vertical movement
194
+ opacity: 0.6 + Math.random() * 0.4,
195
+ rotation: Math.random() * Math.PI * 2,
196
+ rotationSpeed: (Math.random() - 0.5) * 0.04,
197
+ color: colors[Math.floor(Math.random() * colors.length)],
198
+ twinklePhase: Math.random() * Math.PI * 2,
199
+ windOffset: Math.random() * Math.PI * 2,
200
+ windSpeed: 0.015 + Math.random() * 0.02,
201
+ active: true,
202
+ static: false
203
+ };
204
+ },
205
+
206
+ /**
207
+ * Create drifting particle (randomly picks type)
208
+ * Note: St. Patrick's Day particles drift horizontally (left-to-right), not vertically
209
+ */
210
+ createFallingParticle(canvasWidth, canvasHeight, config) {
211
+ const choice = Math.random();
212
+
213
+ // 60% clover petals, 20% full shamrocks, 15% gold coins, 5% sparkles
214
+ if (choice < 0.6) {
215
+ return this.createCloverPetal(canvasWidth, canvasHeight, config);
216
+ } else if (choice < 0.8) {
217
+ return this.createShamrock(canvasWidth, canvasHeight, config);
218
+ } else if (choice < 0.95) {
219
+ return this.createGoldCoin(canvasWidth, canvasHeight, config);
220
+ } else {
221
+ return this.createSparkle(canvasWidth, canvasHeight, config);
222
+ }
223
+ },
224
+
225
+ /**
226
+ * Create initial static decorations (pots of gold with rainbows)
227
+ */
228
+ createInitialDecorations(canvasWidth, canvasHeight, config) {
229
+ const decorations = [];
230
+
231
+ // Green moon (top right corner)
232
+ decorations.push({
233
+ type: 'moon',
234
+ x: canvasWidth - 120,
235
+ y: 100,
236
+ size: 60 + Math.random() * 20,
237
+ opacity: 0.85,
238
+ glowPhase: Math.random() * Math.PI * 2,
239
+ active: true,
240
+ static: true
241
+ });
242
+
243
+ // Create small corner rainbow with single pot of gold
244
+ const rainbowStartX = 20;
245
+ const rainbowEndX = 220; // Small 200px rainbow
246
+ const potY = canvasHeight - 40;
247
+
248
+ // Add single pot of gold at the right end of the rainbow
249
+ decorations.push(this.createPotOfGold(canvasWidth, canvasHeight, {
250
+ x: rainbowEndX,
251
+ y: potY
252
+ }));
253
+
254
+ // Add small corner rainbow
255
+ decorations.push(this.createRainbow(canvasWidth, canvasHeight, rainbowEndX, potY));
256
+
257
+ // Add 1-2 static leprechauns
258
+ const leprechaunCount = 1 + Math.floor(Math.random() * 2);
259
+ for (let i = 0; i < leprechaunCount; i++) {
260
+ const x = 60 + Math.random() * (canvasWidth - 120);
261
+ decorations.push({
262
+ type: 'static-leprechaun',
263
+ x: x,
264
+ y: canvasHeight - 35,
265
+ size: 12 + Math.random() * 4,
266
+ opacity: 1,
267
+ time: Math.random() * 1000,
268
+ wavePhase: Math.random() * Math.PI * 2,
269
+ active: true,
270
+ static: true
271
+ });
272
+ }
273
+
274
+ // Create twinkling stars
275
+ const starCount = config.twinklingStars || 18;
276
+ for (let i = 0; i < starCount; i++) {
277
+ decorations.push(this.createTwinklingStar(canvasWidth, canvasHeight));
278
+ }
279
+
280
+ return decorations;
281
+ },
282
+
283
+ /**
284
+ * Create twinkling star particle
285
+ */
286
+ createTwinklingStar(canvasWidth, canvasHeight) {
287
+ return {
288
+ type: 'twinkling-star',
289
+ x: Math.random() * canvasWidth,
290
+ y: Math.random() * (canvasHeight * 0.5),
291
+ size: 1 + Math.random() * 2,
292
+ opacity: 0.6 + Math.random() * 0.3,
293
+ twinklePhase: Math.random() * Math.PI * 2,
294
+ twinkleSpeed: 0.003 + Math.random() * 0.003,
295
+ active: true,
296
+ static: true
297
+ };
298
+ },
299
+
300
+ /**
301
+ * Spawn special St Patrick's particles
302
+ */
303
+ spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight, config) {
304
+ const choice = Math.random();
305
+
306
+ // Leprechaun (very rare, max 1)
307
+ if (choice < config.leprechaunChance) {
308
+ if (specialParticles.some(p => p.type === 'leprechaun')) {
309
+ return null;
310
+ }
311
+ const fromLeft = Math.random() < 0.5;
312
+
313
+ // Calculate vx for 3-second crossing
314
+ // Core physics: position updates by vx * normalizedDelta (≈1 per frame at 60fps)
315
+ // Per second: vx * 60 pixels
316
+ // For 3 seconds: vx = (canvasWidth + 100) / (3 * 60) = (canvasWidth + 100) / 180
317
+ const baseSpeed = (canvasWidth + 100) / 180;
318
+
319
+ const newLeprechaun = {
320
+ type: 'leprechaun',
321
+ x: fromLeft ? -50 : canvasWidth + 50,
322
+ y: canvasHeight - 35,
323
+ baseY: canvasHeight - 35,
324
+ vx: fromLeft ? baseSpeed : -baseSpeed,
325
+ vy: 0,
326
+ size: 12 + Math.random() * 6,
327
+ opacity: 1,
328
+ time: 0,
329
+ legPhase: Math.random() * Math.PI * 2,
330
+ active: true,
331
+ static: false
332
+ };
333
+ return newLeprechaun;
334
+ }
335
+
336
+ // Banshee (flying ghostly figure, rare, max 1)
337
+ if (choice < config.leprechaunChance + 0.0003) {
338
+ if (specialParticles.some(p => p.type === 'banshee')) {
339
+ return null;
340
+ }
341
+ const fromLeft = Math.random() < 0.5;
342
+ return {
343
+ type: 'banshee',
344
+ x: fromLeft ? -100 : canvasWidth + 100,
345
+ y: Math.random() * (canvasHeight * 0.4) + 50,
346
+ baseY: Math.random() * (canvasHeight * 0.4) + 50,
347
+ vx: fromLeft ? 2 + Math.random() * 1 : -(2 + Math.random() * 1),
348
+ size: 18 + Math.random() * 10,
349
+ opacity: 0.6 + Math.random() * 0.2,
350
+ waveAmplitude: 25 + Math.random() * 20,
351
+ waveFrequency: 0.0015 + Math.random() * 0.002,
352
+ waveOffset: Math.random() * Math.PI * 2,
353
+ time: 0,
354
+ wailPhase: Math.random() * Math.PI * 2,
355
+ active: true,
356
+ static: false
357
+ };
358
+ }
359
+
360
+ // Pot of gold + rainbow (rare)
361
+ if (choice < 0.001) {
362
+ const potCount = specialParticles.filter(p => p.type === 'pot-of-gold').length;
363
+ if (potCount < config.pots) {
364
+ const pot = this.createPotOfGold(canvasWidth, canvasHeight);
365
+ const rainbow = this.createRainbow(canvasWidth, canvasHeight, pot.x, pot.y);
366
+ specialParticles.push(rainbow);
367
+ return pot;
368
+ }
369
+ }
370
+
371
+ return null;
372
+ },
373
+
374
+ /**
375
+ * Update special particles (leprechauns, banshees)
376
+ */
377
+ updateSpecialParticles(specialParticles, deltaTime, canvasWidth, canvasHeight) {
378
+ specialParticles.forEach(particle => {
379
+ if (!particle.active) return;
380
+
381
+ if (particle.type === 'leprechaun' || particle.type === 'banshee') {
382
+ // Only update time for animation - position is handled by core updateMovingParticle()
383
+ particle.time += deltaTime;
384
+
385
+ // Remove if off-screen
386
+ if (particle.type === 'leprechaun') {
387
+ if ((particle.vx > 0 && particle.x > canvasWidth + particle.size * 2) ||
388
+ (particle.vx < 0 && particle.x < -particle.size * 2)) {
389
+ particle.active = false;
390
+ }
391
+ } else if (particle.type === 'banshee') {
392
+ if ((particle.vx > 0 && particle.x > canvasWidth + particle.size * 4) ||
393
+ (particle.vx < 0 && particle.x < -particle.size * 4)) {
394
+ particle.active = false;
395
+ }
396
+ }
397
+ }
398
+
399
+ // For static-leprechaun, only update internal time for idle animation
400
+ if (particle.type === 'static-leprechaun') {
401
+ particle.time += deltaTime;
402
+ }
403
+ });
404
+
405
+ return specialParticles.filter(p => p.active);
406
+ },
407
+
408
+ /**
409
+ * Draw clover petal (simple green heart-shaped petal)
410
+ */
411
+ drawCloverPetal(ctx, particle, time) {
412
+ const x = particle.x;
413
+ const y = particle.y;
414
+ const size = particle.size;
415
+
416
+ // Flutter effect (petal curling as it drifts)
417
+ const flutter = Math.sin(time * particle.flutterSpeed + particle.flutter) * 0.3;
418
+
419
+ ctx.save();
420
+ ctx.globalAlpha = particle.opacity;
421
+ ctx.translate(x, y);
422
+ ctx.rotate(particle.rotation + flutter);
423
+
424
+ // Clover petal is heart-shaped
425
+ ctx.fillStyle = particle.color;
426
+ ctx.strokeStyle = '#006400'; // Dark green edge
427
+ ctx.lineWidth = size * 0.08;
428
+
429
+ ctx.beginPath();
430
+ // Top curves (two rounded lobes forming a heart)
431
+ ctx.moveTo(0, -size * 0.3);
432
+ ctx.bezierCurveTo(
433
+ -size * 0.6, -size * 0.7,
434
+ -size * 0.8, -size * 0.3,
435
+ -size * 0.5, size * 0.2
436
+ );
437
+ // Bottom point
438
+ ctx.lineTo(0, size * 0.8);
439
+ ctx.lineTo(size * 0.5, size * 0.2);
440
+ // Right curve
441
+ ctx.bezierCurveTo(
442
+ size * 0.8, -size * 0.3,
443
+ size * 0.6, -size * 0.7,
444
+ 0, -size * 0.3
445
+ );
446
+ ctx.closePath();
447
+ ctx.fill();
448
+ ctx.stroke();
449
+
450
+ // Central vein
451
+ ctx.strokeStyle = 'rgba(0, 100, 0, 0.4)';
452
+ ctx.lineWidth = size * 0.06;
453
+ ctx.beginPath();
454
+ ctx.moveTo(0, -size * 0.2);
455
+ ctx.lineTo(0, size * 0.7);
456
+ ctx.stroke();
457
+
458
+ ctx.restore();
459
+ },
460
+
461
+ /**
462
+ * Draw sparkle (Irish celebration sparkle)
463
+ */
464
+ drawSparkle(ctx, particle, time) {
465
+ const x = particle.x;
466
+ const y = particle.y;
467
+ const size = particle.size;
468
+ const twinkle = 0.6 + Math.sin(time * 0.004 + particle.twinklePhase) * 0.4;
469
+
470
+ ctx.save();
471
+ ctx.globalAlpha = particle.opacity * twinkle;
472
+ ctx.translate(x, y);
473
+ ctx.rotate(particle.rotation);
474
+
475
+ // Draw 4-pointed sparkle
476
+ ctx.fillStyle = particle.color;
477
+ ctx.shadowColor = particle.color;
478
+ ctx.shadowBlur = size * 2;
479
+
480
+ ctx.beginPath();
481
+ ctx.moveTo(0, -size);
482
+ ctx.lineTo(size * 0.3, -size * 0.3);
483
+ ctx.lineTo(size, 0);
484
+ ctx.lineTo(size * 0.3, size * 0.3);
485
+ ctx.lineTo(0, size);
486
+ ctx.lineTo(-size * 0.3, size * 0.3);
487
+ ctx.lineTo(-size, 0);
488
+ ctx.lineTo(-size * 0.3, -size * 0.3);
489
+ ctx.closePath();
490
+ ctx.fill();
491
+
492
+ ctx.restore();
493
+ },
494
+
495
+ /**
496
+ * Draw shamrock (3-leaf clover) - Irish colors
497
+ */
498
+ drawShamrock(ctx, particle) {
499
+ const x = particle.x;
500
+ const y = particle.y;
501
+ const size = particle.size;
502
+
503
+ ctx.save();
504
+ ctx.globalAlpha = particle.opacity;
505
+ ctx.translate(x, y);
506
+ ctx.rotate(particle.rotation);
507
+
508
+ ctx.fillStyle = particle.color || '#228B22';
509
+ ctx.strokeStyle = particle.strokeColor || '#006400';
510
+ ctx.lineWidth = size * 0.1;
511
+
512
+ // Three heart-shaped leaves
513
+ for (let i = 0; i < 3; i++) {
514
+ const angle = (i / 3) * Math.PI * 2 - Math.PI / 2;
515
+ ctx.save();
516
+ ctx.rotate(angle);
517
+ ctx.translate(0, -size * 0.7);
518
+
519
+ // Heart-shaped leaf
520
+ ctx.beginPath();
521
+ ctx.moveTo(0, size * 0.3);
522
+ ctx.bezierCurveTo(-size * 0.5, -size * 0.2, -size * 0.3, -size * 0.6, 0, -size * 0.35);
523
+ ctx.bezierCurveTo(size * 0.3, -size * 0.6, size * 0.5, -size * 0.2, 0, size * 0.3);
524
+ ctx.closePath();
525
+ ctx.fill();
526
+ ctx.stroke();
527
+
528
+ ctx.restore();
529
+ }
530
+
531
+ // Stem
532
+ ctx.strokeStyle = '#8B4513';
533
+ ctx.lineWidth = size * 0.15;
534
+ ctx.beginPath();
535
+ ctx.moveTo(0, 0);
536
+ ctx.lineTo(0, size * 1.2);
537
+ ctx.stroke();
538
+
539
+ ctx.restore();
540
+ },
541
+
542
+ /**
543
+ * Draw spinning gold coin
544
+ */
545
+ drawGoldCoin(ctx, particle, time) {
546
+ const x = particle.x;
547
+ const y = particle.y;
548
+ const size = particle.size;
549
+
550
+ // 3D spin effect
551
+ const spinWidth = Math.abs(Math.cos(particle.rotation)) * size;
552
+
553
+ ctx.save();
554
+ ctx.globalAlpha = particle.opacity;
555
+ ctx.translate(x, y);
556
+
557
+ // Coin body
558
+ ctx.fillStyle = '#FFD700';
559
+ ctx.strokeStyle = '#DAA520';
560
+ ctx.lineWidth = size * 0.15;
561
+
562
+ ctx.beginPath();
563
+ ctx.ellipse(0, 0, spinWidth, size, 0, 0, Math.PI * 2);
564
+ ctx.fill();
565
+ ctx.stroke();
566
+
567
+ // Glint
568
+ if (spinWidth > size * 0.3) {
569
+ const glintIntensity = (Math.sin(time * 0.005 + particle.glintPhase) + 1) * 0.5;
570
+ ctx.fillStyle = `rgba(255, 255, 255, ${glintIntensity * 0.6})`;
571
+ ctx.beginPath();
572
+ ctx.ellipse(-spinWidth * 0.3, -size * 0.3, spinWidth * 0.3, size * 0.3, 0, 0, Math.PI * 2);
573
+ ctx.fill();
574
+ }
575
+
576
+ ctx.restore();
577
+ },
578
+
579
+ /**
580
+ * Draw pot of gold
581
+ */
582
+ drawPotOfGold(ctx, particle, time) {
583
+ const x = particle.x;
584
+ const y = particle.y;
585
+ const size = particle.size;
586
+
587
+ ctx.save();
588
+ ctx.translate(x, y);
589
+
590
+ // Pot (black cauldron)
591
+ ctx.fillStyle = '#1a1a1a';
592
+ ctx.strokeStyle = '#0a0a0a';
593
+ ctx.lineWidth = 2;
594
+
595
+ ctx.beginPath();
596
+ ctx.arc(0, 0, size * 0.7, 0, Math.PI);
597
+ ctx.lineTo(-size * 0.7, size * 0.4);
598
+ ctx.quadraticCurveTo(0, size * 0.6, size * 0.7, size * 0.4);
599
+ ctx.closePath();
600
+ ctx.fill();
601
+ ctx.stroke();
602
+
603
+ // Handle
604
+ ctx.strokeStyle = '#2a2a2a';
605
+ ctx.lineWidth = 3;
606
+ ctx.beginPath();
607
+ ctx.arc(0, -size * 0.2, size * 0.5, Math.PI, 0);
608
+ ctx.stroke();
609
+
610
+ // Gold coins overflowing
611
+ const coinPositions = [
612
+ {x: -size * 0.4, y: -size * 0.2, s: 0.8},
613
+ {x: size * 0.3, y: -size * 0.3, s: 0.9},
614
+ {x: -size * 0.1, y: -size * 0.5, s: 1.0},
615
+ {x: size * 0.5, y: -size * 0.1, s: 0.7},
616
+ {x: -size * 0.5, y: 0, s: 0.75},
617
+ {x: size * 0.1, y: -size * 0.6, s: 0.85},
618
+ {x: 0, y: -size * 0.3, s: 0.95}
619
+ ];
620
+
621
+ ctx.fillStyle = '#FFD700';
622
+ ctx.strokeStyle = '#DAA520';
623
+ ctx.lineWidth = 1;
624
+
625
+ coinPositions.forEach((coin, i) => {
626
+ const glintPhase = time * 0.002 + i * 0.5;
627
+ const glint = (Math.sin(glintPhase) + 1) * 0.5;
628
+
629
+ ctx.save();
630
+ ctx.translate(coin.x, coin.y);
631
+
632
+ // Coin
633
+ ctx.beginPath();
634
+ ctx.arc(0, 0, size * 0.2 * coin.s, 0, Math.PI * 2);
635
+ ctx.fill();
636
+ ctx.stroke();
637
+
638
+ // Glint
639
+ ctx.fillStyle = `rgba(255, 255, 255, ${glint * 0.6})`;
640
+ ctx.beginPath();
641
+ ctx.arc(-size * 0.08 * coin.s, -size * 0.08 * coin.s, size * 0.08 * coin.s, 0, Math.PI * 2);
642
+ ctx.fill();
643
+
644
+ ctx.restore();
645
+ });
646
+
647
+ // Glow
648
+ ctx.globalAlpha = 0.3;
649
+ const glowGradient = ctx.createRadialGradient(0, -size * 0.2, 0, 0, -size * 0.2, size * 1.5);
650
+ glowGradient.addColorStop(0, '#FFD700');
651
+ glowGradient.addColorStop(1, 'rgba(255, 215, 0, 0)');
652
+ ctx.fillStyle = glowGradient;
653
+ ctx.fillRect(-size * 1.5, -size * 1.5, size * 3, size * 1.5);
654
+
655
+ ctx.restore();
656
+ },
657
+
658
+ // Helper function to draw a small gold pot (similar to pot-of-gold but simpler)
659
+ drawSmallGoldPot(ctx, x, y, size) {
660
+ ctx.save();
661
+ ctx.translate(x, y);
662
+
663
+ // Pot (black cauldron)
664
+ ctx.fillStyle = '#1a1a1a';
665
+ ctx.strokeStyle = '#0a0a0a';
666
+ ctx.lineWidth = 1;
667
+
668
+ ctx.beginPath();
669
+ // Arc for the top of the pot
670
+ ctx.arc(0, -size * 0.4, size * 0.7, 0, Math.PI, true);
671
+ // Sides of the pot
672
+ ctx.lineTo(size * 0.7, size * 0.4);
673
+ ctx.quadraticCurveTo(0, size * 0.6, -size * 0.7, size * 0.4);
674
+ ctx.closePath();
675
+ ctx.fill();
676
+ ctx.stroke();
677
+
678
+ // Gold coins overflowing (simple blob)
679
+ ctx.fillStyle = '#FFD700';
680
+ ctx.beginPath();
681
+ ctx.arc(0, -size * 0.7, size * 0.5, 0, Math.PI * 2);
682
+ ctx.fill();
683
+
684
+ ctx.restore();
685
+ },
686
+
687
+
688
+ /**
689
+ * Draw rainbow
690
+ */
691
+ drawRainbow(ctx, particle) {
692
+ const startX = particle.startX;
693
+ const endX = particle.endX;
694
+ const endY = particle.endY;
695
+ const canvasHeight = particle.height;
696
+
697
+ ctx.save();
698
+ ctx.globalAlpha = 0.5;
699
+
700
+ const rainbowWidth = Math.abs(endX - startX);
701
+ const centerX = (startX + endX) / 2;
702
+ const rainbowHeight = canvasHeight * 0.2;
703
+
704
+ // Calculate radius and center Y for a circular arc
705
+ const radius = (rainbowWidth * rainbowWidth + 4 * rainbowHeight * rainbowHeight) / (8 * rainbowHeight);
706
+ const arcCenterY = endY + rainbowHeight - radius;
707
+
708
+ // Draw 7 rainbow bands from outer to inner
709
+ const colors = this.colors.rainbow;
710
+ const bandWidth = 5; // Smaller bands for the compact rainbow
711
+
712
+ for (let i = colors.length - 1; i >= 0; i--) {
713
+ ctx.strokeStyle = colors[i];
714
+ ctx.lineWidth = bandWidth;
715
+ ctx.beginPath();
716
+ ctx.arc(centerX, arcCenterY, radius + (colors.length - 1 - i) * bandWidth, Math.PI, 0);
717
+ ctx.stroke();
718
+ }
719
+
720
+ ctx.restore();
721
+
722
+ // Draw single small gold pot at the right end
723
+ const potSize = 20;
724
+ this.drawSmallGoldPot(ctx, endX, endY, potSize);
725
+ },
726
+
727
+ /**
728
+ * Draw leprechaun
729
+ */
730
+ drawLeprechaun(ctx, particle, time) {
731
+ const x = particle.x;
732
+ const y = particle.y;
733
+ const size = particle.size;
734
+ const dir = particle.vx > 0 ? 1 : -1;
735
+
736
+ ctx.save();
737
+ ctx.translate(x, y);
738
+ if (dir === -1) {
739
+ ctx.scale(-1, 1);
740
+ }
741
+
742
+ // Walking animation phases
743
+ const walkCycle = (particle.time * 0.05) % (Math.PI * 2); // Speed of walk based on particle's own time
744
+ const legSwing = Math.sin(walkCycle + particle.legPhase) * (Math.PI / 6); // Max swing angle
745
+ const armSwing = Math.sin(walkCycle + Math.PI + particle.legPhase) * (Math.PI / 8); // Arms swing opposite to legs
746
+
747
+ // Legs
748
+ ctx.strokeStyle = '#228B22'; // Green pants
749
+ ctx.lineWidth = size * 0.15;
750
+
751
+ // Right leg (front leg in current step)
752
+ ctx.beginPath();
753
+ ctx.moveTo(size * 0.2, size * 0.5);
754
+ ctx.lineTo(size * 0.2 + Math.sin(legSwing) * size * 0.3, size * 1.2 + Math.cos(legSwing) * size * 0.1);
755
+ ctx.stroke();
756
+
757
+ // Left leg (back leg in current step)
758
+ ctx.beginPath();
759
+ ctx.moveTo(-size * 0.2, size * 0.5);
760
+ ctx.lineTo(-size * 0.2 + Math.sin(legSwing + Math.PI) * size * 0.3, size * 1.2 + Math.cos(legSwing + Math.PI) * size * 0.1);
761
+ ctx.stroke();
762
+
763
+ // Shoes (buckles) - now animated with legs
764
+ ctx.fillStyle = '#000000';
765
+ ctx.fillRect(size * 0.2 + Math.sin(legSwing) * size * 0.3 - size * 0.15, size * 1.15 + Math.cos(legSwing) * size * 0.1, size * 0.3, size * 0.2);
766
+ ctx.fillRect(-size * 0.2 + Math.sin(legSwing + Math.PI) * size * 0.3 - size * 0.15, size * 1.15 + Math.cos(legSwing + Math.PI) * size * 0.1, size * 0.3, size * 0.2);
767
+
768
+ ctx.fillStyle = '#FFD700'; // Gold buckle
769
+ ctx.fillRect(size * 0.2 + Math.sin(legSwing) * size * 0.3 - size * 0.1, size * 1.2 + Math.cos(legSwing) * size * 0.1, size * 0.2, size * 0.1);
770
+ ctx.fillRect(-size * 0.2 + Math.sin(legSwing + Math.PI) * size * 0.3 - size * 0.1, size * 1.2 + Math.cos(legSwing + Math.PI) * size * 0.1, size * 0.2, size * 0.1);
771
+
772
+
773
+ // Body (green coat)
774
+ ctx.fillStyle = '#228B22';
775
+ ctx.fillRect(-size * 0.5, 0, size, size * 0.6);
776
+
777
+ // Belt
778
+ ctx.fillStyle = '#000000';
779
+ ctx.fillRect(-size * 0.5, size * 0.35, size, size * 0.1);
780
+ ctx.fillStyle = '#FFD700';
781
+ ctx.fillRect(-size * 0.15, size * 0.35, size * 0.3, size * 0.1);
782
+
783
+ // Arms (swinging)
784
+ ctx.fillStyle = '#228B22'; // Green coat
785
+ // Right arm
786
+ ctx.save();
787
+ ctx.translate(size * 0.4, size * 0.2);
788
+ ctx.rotate(armSwing);
789
+ ctx.fillRect(0, 0, size * 0.15, size * 0.4);
790
+ ctx.fillStyle = '#FFD7BA'; // Hand color
791
+ ctx.beginPath();
792
+ ctx.arc(size * 0.075, size * 0.4, size * 0.12, 0, Math.PI * 2);
793
+ ctx.fill();
794
+ ctx.restore();
795
+
796
+ // Left arm
797
+ ctx.save();
798
+ ctx.translate(-size * 0.4 - size * 0.15, size * 0.2); // Adjust for width
799
+ ctx.rotate(armSwing + Math.PI); // Opposite swing
800
+ ctx.fillRect(0, 0, size * 0.15, size * 0.4);
801
+ ctx.fillStyle = '#FFD7BA'; // Hand color
802
+ ctx.beginPath();
803
+ ctx.arc(size * 0.075, size * 0.4, size * 0.12, 0, Math.PI * 2);
804
+ ctx.fill();
805
+ ctx.restore();
806
+
807
+
808
+ // Head
809
+ ctx.fillStyle = '#FFD7BA';
810
+ ctx.beginPath();
811
+ ctx.arc(0, -size * 0.3, size * 0.4, 0, Math.PI * 2);
812
+ ctx.fill();
813
+
814
+ // Beard
815
+ ctx.fillStyle = '#ff6600';
816
+ ctx.beginPath();
817
+ ctx.moveTo(-size * 0.3, -size * 0.1);
818
+ ctx.quadraticCurveTo(-size * 0.4, size * 0.2, 0, size * 0.3);
819
+ ctx.quadraticCurveTo(size * 0.4, size * 0.2, size * 0.3, -size * 0.1);
820
+ ctx.closePath();
821
+ ctx.fill();
822
+
823
+ // Hat (green top hat)
824
+ ctx.fillStyle = '#228B22';
825
+ ctx.fillRect(-size * 0.5, -size * 0.8, size, size * 0.1);
826
+ ctx.fillRect(-size * 0.35, -size * 1.4, size * 0.7, size * 0.6);
827
+
828
+ // Hat buckle
829
+ ctx.fillStyle = '#FFD700';
830
+ ctx.fillRect(-size * 0.15, -size * 0.85, size * 0.3, size * 0.15);
831
+
832
+ // Eyes
833
+ ctx.fillStyle = '#000000';
834
+ ctx.beginPath();
835
+ ctx.arc(-size * 0.12, -size * 0.35, size * 0.06, 0, Math.PI * 2);
836
+ ctx.fill();
837
+ ctx.beginPath();
838
+ ctx.arc(size * 0.12, -size * 0.35, size * 0.06, 0, Math.PI * 2);
839
+ ctx.fill();
840
+
841
+ ctx.restore();
842
+ },
843
+
844
+ /**
845
+ * Draw banshee (flying ghostly figure)
846
+ */
847
+ drawBanshee(ctx, particle, time) {
848
+ const x = particle.x;
849
+ const y = particle.y + Math.sin(time * particle.waveFrequency + particle.waveOffset) * particle.waveAmplitude;
850
+ const size = particle.size;
851
+ const dir = particle.vx > 0 ? 1 : -1;
852
+
853
+ ctx.save();
854
+ ctx.globalAlpha = particle.opacity;
855
+ ctx.translate(x, y);
856
+ if (dir === -1) {
857
+ ctx.scale(-1, 1);
858
+ }
859
+
860
+ // Intense ghostly aura (larger, more ethereal)
861
+ const aura = ctx.createRadialGradient(0, -size * 0.5, 0, 0, -size * 0.5, size * 3);
862
+ aura.addColorStop(0, 'rgba(180, 255, 180, 0.4)');
863
+ aura.addColorStop(0.3, 'rgba(150, 255, 150, 0.2)');
864
+ aura.addColorStop(0.6, 'rgba(120, 255, 120, 0.1)');
865
+ aura.addColorStop(1, 'rgba(100, 255, 100, 0)');
866
+ ctx.fillStyle = aura;
867
+ ctx.fillRect(-size * 3, -size * 3, size * 6, size * 6);
868
+
869
+ // Flowing tattered shroud (multiple layers for depth)
870
+ const shroudWave = Math.sin(time * 0.008 + particle.wailPhase) * size * 0.25;
871
+
872
+ // Back layer (darker, more transparent)
873
+ ctx.fillStyle = 'rgba(200, 240, 200, 0.3)';
874
+ ctx.beginPath();
875
+ ctx.moveTo(-size * 0.8, -size * 1.2);
876
+ ctx.bezierCurveTo(
877
+ -size * 1.2 + shroudWave * 2, -size * 0.4,
878
+ -size * 1.0 + shroudWave * 1.5, size * 0.6,
879
+ -size * 0.6 + shroudWave, size * 1.8
880
+ );
881
+ ctx.bezierCurveTo(
882
+ -size * 0.3, size * 1.5,
883
+ size * 0.3, size * 1.5,
884
+ size * 0.6 - shroudWave, size * 1.8
885
+ );
886
+ ctx.bezierCurveTo(
887
+ size * 1.0 - shroudWave * 1.5, size * 0.6,
888
+ size * 1.2 - shroudWave * 2, -size * 0.4,
889
+ size * 0.8, -size * 1.2
890
+ );
891
+ ctx.closePath();
892
+ ctx.fill();
893
+
894
+ // Main shroud (brighter, more solid)
895
+ ctx.fillStyle = 'rgba(230, 255, 230, 0.85)';
896
+ ctx.strokeStyle = 'rgba(180, 255, 180, 0.6)';
897
+ ctx.lineWidth = size * 0.04;
898
+ ctx.beginPath();
899
+ ctx.moveTo(-size * 0.5, -size * 1.0);
900
+ for (let i = 0; i <= 12; i++) {
901
+ const t = i / 12;
902
+ const waveOffset = Math.sin(time * 0.012 + t * Math.PI * 3 + particle.wailPhase) * size * 0.3;
903
+ const posX = (t - 0.5) * size * 1.8 + waveOffset;
904
+ const posY = -size * 1.0 + t * size * 2.2;
905
+
906
+ if (i === 0) {
907
+ ctx.moveTo(posX, posY);
908
+ } else {
909
+ ctx.lineTo(posX, posY);
910
+ }
911
+ }
912
+ ctx.lineTo(-size * 0.5, -size * 1.0);
913
+ ctx.closePath();
914
+ ctx.fill();
915
+ ctx.stroke();
916
+
917
+ // Tattered edges (vertical streaks)
918
+ ctx.strokeStyle = 'rgba(180, 255, 180, 0.4)';
919
+ ctx.lineWidth = size * 0.02;
920
+ for (let i = 0; i < 8; i++) {
921
+ const xPos = -size * 0.7 + i * (size * 1.4 / 7);
922
+ const streakWave = Math.sin(time * 0.015 + i * 0.8) * size * 0.2;
923
+ ctx.beginPath();
924
+ ctx.moveTo(xPos, size * 0.4);
925
+ ctx.lineTo(xPos + streakWave, size * 1.2 + (i % 2) * size * 0.4);
926
+ ctx.stroke();
927
+ }
928
+
929
+ // Gaunt head and elongated face
930
+ const headTilt = Math.sin(time * 0.006) * 0.15;
931
+ ctx.save();
932
+ ctx.rotate(headTilt);
933
+
934
+ // Head (very pale, almost translucent)
935
+ ctx.fillStyle = 'rgba(240, 255, 240, 0.9)';
936
+ ctx.strokeStyle = 'rgba(180, 230, 180, 0.5)';
937
+ ctx.lineWidth = size * 0.03;
938
+ ctx.beginPath();
939
+ ctx.ellipse(0, -size * 0.8, size * 0.35, size * 0.55, 0, 0, Math.PI * 2);
940
+ ctx.fill();
941
+ ctx.stroke();
942
+
943
+ // Hollow, sunken eye sockets (dark green shadows)
944
+ ctx.fillStyle = 'rgba(50, 100, 50, 0.6)';
945
+ ctx.beginPath();
946
+ ctx.ellipse(-size * 0.15, -size * 0.85, size * 0.12, size * 0.08, 0, 0, Math.PI * 2);
947
+ ctx.ellipse(size * 0.15, -size * 0.85, size * 0.12, size * 0.08, 0, 0, Math.PI * 2);
948
+ ctx.fill();
949
+
950
+ // Intensely glowing eyes (pulsing bright green)
951
+ const eyePulse = 0.6 + Math.sin(time * 0.008 + particle.wailPhase) * 0.4;
952
+ ctx.fillStyle = `rgba(100, 255, 150, ${eyePulse})`;
953
+ ctx.shadowColor = '#64ff96';
954
+ ctx.shadowBlur = size * 0.6;
955
+ ctx.beginPath();
956
+ ctx.arc(-size * 0.15, -size * 0.85, size * 0.08, 0, Math.PI * 2);
957
+ ctx.arc(size * 0.15, -size * 0.85, size * 0.08, 0, Math.PI * 2);
958
+ ctx.fill();
959
+ ctx.shadowBlur = 0;
960
+
961
+ // Gaping mouth (wailing/screaming - O shape)
962
+ const mouthOpen = 0.2 + Math.sin(time * 0.01 + particle.wailPhase) * 0.1;
963
+ ctx.fillStyle = 'rgba(80, 150, 80, 0.7)';
964
+ ctx.strokeStyle = 'rgba(50, 100, 50, 0.8)';
965
+ ctx.lineWidth = size * 0.02;
966
+ ctx.beginPath();
967
+ ctx.ellipse(0, -size * 0.6, size * 0.15, size * (0.2 + mouthOpen), 0, 0, Math.PI * 2);
968
+ ctx.fill();
969
+ ctx.stroke();
970
+
971
+ ctx.restore();
972
+
973
+ // Wild, flowing hair (very long, chaotic strands)
974
+ ctx.strokeStyle = 'rgba(220, 255, 220, 0.7)';
975
+ for (let i = 0; i < 12; i++) {
976
+ const hairX = -size * 0.35 + (i / 11) * size * 0.7;
977
+ const hairWave1 = Math.sin(time * 0.01 + i * 0.6 + particle.wailPhase) * size * 0.5;
978
+ const hairWave2 = Math.cos(time * 0.008 + i * 0.4) * size * 0.3;
979
+
980
+ ctx.lineWidth = size * (0.04 + (i % 3) * 0.02);
981
+ ctx.beginPath();
982
+ ctx.moveTo(hairX, -size * 1.1);
983
+ ctx.bezierCurveTo(
984
+ hairX + hairWave1 * 0.3, -size * 0.5,
985
+ hairX + hairWave1 * 0.6 + hairWave2 * 0.5, size * 0.2,
986
+ hairX + hairWave1 + hairWave2, size * 1.0 + (i % 2) * size * 0.5
987
+ );
988
+ ctx.stroke();
989
+ }
990
+
991
+ // Spectral hands reaching forward
992
+ const handReach = Math.sin(time * 0.01 + particle.wailPhase) * size * 0.3;
993
+
994
+ // Left hand
995
+ ctx.fillStyle = 'rgba(230, 255, 230, 0.8)';
996
+ ctx.strokeStyle = 'rgba(180, 230, 180, 0.6)';
997
+ ctx.lineWidth = size * 0.02;
998
+ ctx.save();
999
+ ctx.translate(-size * 0.6, -size * 0.2);
1000
+ ctx.rotate(-0.3 + handReach * 0.002);
1001
+ ctx.beginPath();
1002
+ ctx.ellipse(0, 0, size * 0.15, size * 0.25, 0, 0, Math.PI * 2);
1003
+ ctx.fill();
1004
+ ctx.stroke();
1005
+ // Fingers (skeletal)
1006
+ for (let f = 0; f < 4; f++) {
1007
+ const fingerX = -size * 0.08 + f * size * 0.055;
1008
+ ctx.beginPath();
1009
+ ctx.moveTo(fingerX, size * 0.15);
1010
+ ctx.lineTo(fingerX, size * 0.35);
1011
+ ctx.stroke();
1012
+ }
1013
+ ctx.restore();
1014
+
1015
+ // Right hand
1016
+ ctx.save();
1017
+ ctx.translate(size * 0.6, -size * 0.2);
1018
+ ctx.rotate(0.3 - handReach * 0.002);
1019
+ ctx.beginPath();
1020
+ ctx.ellipse(0, 0, size * 0.15, size * 0.25, 0, 0, Math.PI * 2);
1021
+ ctx.fill();
1022
+ ctx.stroke();
1023
+ // Fingers
1024
+ for (let f = 0; f < 4; f++) {
1025
+ const fingerX = -size * 0.08 + f * size * 0.055;
1026
+ ctx.beginPath();
1027
+ ctx.moveTo(fingerX, size * 0.15);
1028
+ ctx.lineTo(fingerX, size * 0.35);
1029
+ ctx.stroke();
1030
+ }
1031
+ ctx.restore();
1032
+
1033
+ // Ethereal wisps trailing behind
1034
+ ctx.strokeStyle = 'rgba(180, 255, 180, 0.3)';
1035
+ ctx.lineWidth = size * 0.02;
1036
+ for (let i = 0; i < 6; i++) {
1037
+ const angle = (i / 6) * Math.PI * 2;
1038
+ const wispWave = Math.sin(time * 0.01 + i * 0.8) * size * 0.6;
1039
+ const wispLength = size * (1.5 + (i % 2) * 0.5);
1040
+ ctx.beginPath();
1041
+ ctx.moveTo(0, size * 0.4);
1042
+ ctx.bezierCurveTo(
1043
+ Math.cos(angle) * size * 0.8 + wispWave * 0.5, size * 1.0,
1044
+ Math.cos(angle) * size * 1.5 + wispWave, size * 1.8,
1045
+ Math.cos(angle) * wispLength + wispWave * 1.5, size * 2.5
1046
+ );
1047
+ ctx.stroke();
1048
+ }
1049
+
1050
+ ctx.restore();
1051
+ },
1052
+
1053
+ /**
1054
+ * Draw green moon
1055
+ */
1056
+ drawMoon(ctx, particle, time) {
1057
+ const x = particle.x;
1058
+ const y = particle.y;
1059
+ const size = particle.size;
1060
+
1061
+ ctx.save();
1062
+ ctx.globalAlpha = particle.opacity;
1063
+ ctx.translate(x, y);
1064
+
1065
+ // Moon body (Irish green tint)
1066
+ const moonGradient = ctx.createRadialGradient(-size * 0.2, -size * 0.2, 0, 0, 0, size);
1067
+ moonGradient.addColorStop(0, '#90EE90'); // Light green center
1068
+ moonGradient.addColorStop(0.5, '#7FD87F'); // Medium green
1069
+ moonGradient.addColorStop(1, '#5CB85C'); // Darker green edge
1070
+ ctx.fillStyle = moonGradient;
1071
+ ctx.beginPath();
1072
+ ctx.arc(0, 0, size, 0, Math.PI * 2);
1073
+ ctx.fill();
1074
+
1075
+ // Craters (darker green)
1076
+ ctx.fillStyle = 'rgba(60, 130, 60, 0.3)';
1077
+ ctx.beginPath();
1078
+ ctx.arc(-size * 0.3, -size * 0.2, size * 0.15, 0, Math.PI * 2);
1079
+ ctx.fill();
1080
+ ctx.beginPath();
1081
+ ctx.arc(size * 0.25, size * 0.1, size * 0.2, 0, Math.PI * 2);
1082
+ ctx.fill();
1083
+ ctx.beginPath();
1084
+ ctx.arc(size * 0.1, -size * 0.4, size * 0.12, 0, Math.PI * 2);
1085
+ ctx.fill();
1086
+
1087
+ // Shamrock silhouette on moon surface
1088
+ ctx.save();
1089
+ ctx.translate(size * 0.2, size * 0.35);
1090
+ ctx.scale(0.15, 0.15);
1091
+ ctx.fillStyle = 'rgba(34, 139, 34, 0.4)';
1092
+ // Three leaves
1093
+ for (let i = 0; i < 3; i++) {
1094
+ const angle = (i / 3) * Math.PI * 2 - Math.PI / 2;
1095
+ ctx.save();
1096
+ ctx.rotate(angle);
1097
+ ctx.translate(0, -size * 0.7);
1098
+ ctx.beginPath();
1099
+ ctx.moveTo(0, size * 0.3);
1100
+ ctx.bezierCurveTo(-size * 0.5, -size * 0.2, -size * 0.3, -size * 0.6, 0, -size * 0.35);
1101
+ ctx.bezierCurveTo(size * 0.3, -size * 0.6, size * 0.5, -size * 0.2, 0, size * 0.3);
1102
+ ctx.closePath();
1103
+ ctx.fill();
1104
+ ctx.restore();
1105
+ }
1106
+ ctx.restore();
1107
+
1108
+ // Green glow (pulsing)
1109
+ const glowIntensity = 0.3 + Math.sin(time * 0.002 + particle.glowPhase) * 0.15;
1110
+ ctx.globalAlpha = glowIntensity;
1111
+ const glowGradient = ctx.createRadialGradient(0, 0, size * 0.8, 0, 0, size * 1.8);
1112
+ glowGradient.addColorStop(0, 'rgba(144, 238, 144, 0.8)');
1113
+ glowGradient.addColorStop(0.5, 'rgba(144, 238, 144, 0.4)');
1114
+ glowGradient.addColorStop(1, 'rgba(144, 238, 144, 0)');
1115
+ ctx.fillStyle = glowGradient;
1116
+ ctx.beginPath();
1117
+ ctx.arc(0, 0, size * 1.8, 0, Math.PI * 2);
1118
+ ctx.fill();
1119
+
1120
+ ctx.restore();
1121
+ },
1122
+
1123
+ /**
1124
+ * Draw static leprechaun (standing still)
1125
+ */
1126
+ drawStaticLeprechaun(ctx, particle, time) {
1127
+ const x = particle.x;
1128
+ const y = particle.y;
1129
+ const size = particle.size;
1130
+
1131
+ ctx.save();
1132
+ ctx.translate(x, y);
1133
+
1134
+ // Subtle idle animations
1135
+ const idleBob = Math.sin(particle.time * 0.005 + particle.wavePhase) * size * 0.05; // Gentle up-down
1136
+ const idleSway = Math.sin(particle.time * 0.003 + particle.wavePhase * 0.5) * 0.05; // Gentle left-right sway
1137
+
1138
+ ctx.translate(0, idleBob);
1139
+ ctx.rotate(idleSway);
1140
+
1141
+ // Legs (still, but part of the overall bob/sway)
1142
+ ctx.strokeStyle = '#228B22';
1143
+ ctx.lineWidth = size * 0.15;
1144
+ ctx.beginPath();
1145
+ ctx.moveTo(-size * 0.2, size * 0.5);
1146
+ ctx.lineTo(-size * 0.25, size * 1.2);
1147
+ ctx.stroke();
1148
+ ctx.beginPath();
1149
+ ctx.moveTo(size * 0.2, size * 0.5);
1150
+ ctx.lineTo(size * 0.25, size * 1.2);
1151
+ ctx.stroke();
1152
+
1153
+ // Shoes (buckles)
1154
+ ctx.fillStyle = '#000000';
1155
+ ctx.fillRect(-size * 0.35, size * 1.15, size * 0.25, size * 0.15);
1156
+ ctx.fillRect(size * 0.1, size * 1.15, size * 0.25, size * 0.15);
1157
+
1158
+ ctx.fillStyle = '#FFD700';
1159
+ ctx.fillRect(-size * 0.32, size * 1.18, size * 0.15, size * 0.08);
1160
+ ctx.fillRect(size * 0.13, size * 1.18, size * 0.15, size * 0.08);
1161
+
1162
+ // Body (green coat)
1163
+ ctx.fillStyle = '#228B22';
1164
+ ctx.fillRect(-size * 0.5, 0, size, size * 0.6);
1165
+
1166
+ // Belt
1167
+ ctx.fillStyle = '#000000';
1168
+ ctx.fillRect(-size * 0.5, size * 0.35, size, size * 0.1);
1169
+ ctx.fillStyle = '#FFD700';
1170
+ ctx.fillRect(-size * 0.15, size * 0.35, size * 0.3, size * 0.1);
1171
+
1172
+ // Left arm (waving slightly - more natural)
1173
+ ctx.save();
1174
+ ctx.translate(-size * 0.5, size * 0.2);
1175
+ ctx.rotate(-0.3 + Math.sin(particle.time * 0.007 + particle.wavePhase * 1.2) * 0.1); // Waving
1176
+ ctx.fillStyle = '#228B22';
1177
+ ctx.fillRect(0, 0, size * 0.15, size * 0.4);
1178
+ // Hand
1179
+ ctx.fillStyle = '#FFD7BA';
1180
+ ctx.beginPath();
1181
+ ctx.arc(size * 0.075, size * 0.4, size * 0.12, 0, Math.PI * 2);
1182
+ ctx.fill();
1183
+ ctx.restore();
1184
+
1185
+ // Right arm (subtle movement)
1186
+ ctx.save();
1187
+ ctx.translate(size * 0.5, size * 0.2);
1188
+ ctx.rotate(0.3 - Math.sin(particle.time * 0.006 + particle.wavePhase * 0.8) * 0.05); // More subtle
1189
+ ctx.fillStyle = '#228B22';
1190
+ ctx.fillRect(-size * 0.15, 0, size * 0.15, size * 0.4);
1191
+ // Hand
1192
+ ctx.fillStyle = '#FFD7BA';
1193
+ ctx.beginPath();
1194
+ ctx.arc(-size * 0.075, size * 0.4, size * 0.12, 0, Math.PI * 2);
1195
+ ctx.fill();
1196
+ ctx.restore();
1197
+
1198
+ // Head (subtle nod)
1199
+ ctx.save();
1200
+ ctx.translate(0, -size * 0.3);
1201
+ ctx.rotate(Math.sin(particle.time * 0.004 + particle.wavePhase * 1.5) * 0.02);
1202
+ ctx.beginPath();
1203
+ ctx.arc(0, 0, size * 0.4, 0, Math.PI * 2);
1204
+ ctx.fill();
1205
+ ctx.restore();
1206
+
1207
+ // Beard (orange/red)
1208
+ ctx.fillStyle = '#ff6600';
1209
+ ctx.beginPath();
1210
+ ctx.moveTo(-size * 0.3, -size * 0.1);
1211
+ ctx.quadraticCurveTo(-size * 0.4, size * 0.2, 0, size * 0.3);
1212
+ ctx.quadraticCurveTo(size * 0.4, size * 0.2, size * 0.3, -size * 0.1);
1213
+ ctx.closePath();
1214
+ ctx.fill();
1215
+
1216
+ // Hat (green top hat)
1217
+ ctx.fillStyle = '#228B22';
1218
+ ctx.fillRect(-size * 0.5, -size * 0.8, size, size * 0.1);
1219
+ ctx.fillRect(-size * 0.35, -size * 1.4, size * 0.7, size * 0.6);
1220
+
1221
+ // Hat buckle
1222
+ ctx.fillStyle = '#FFD700';
1223
+ ctx.fillRect(-size * 0.15, -size * 0.85, size * 0.3, size * 0.15);
1224
+
1225
+ // Eyes
1226
+ ctx.fillStyle = '#000000';
1227
+ ctx.beginPath();
1228
+ ctx.arc(-size * 0.12, -size * 0.35, size * 0.06, 0, Math.PI * 2);
1229
+ ctx.fill();
1230
+ ctx.beginPath();
1231
+ ctx.arc(size * 0.12, -size * 0.35, size * 0.06, 0, Math.PI * 2);
1232
+ ctx.fill();
1233
+
1234
+ // Smile
1235
+ ctx.strokeStyle = '#000000';
1236
+ ctx.lineWidth = size * 0.04;
1237
+ ctx.beginPath();
1238
+ ctx.arc(0, -size * 0.2, size * 0.15, 0, Math.PI);
1239
+ ctx.stroke();
1240
+
1241
+ ctx.restore();
1242
+ },
1243
+
1244
+ /**
1245
+ * Draw twinkling star
1246
+ */
1247
+ drawTwinklingStar(ctx, particle, time) {
1248
+ const x = particle.x;
1249
+ const y = particle.y;
1250
+ const size = particle.size;
1251
+
1252
+ const twinkleIntensity = 0.5 + Math.sin(time * particle.twinkleSpeed + particle.twinklePhase) * 0.5;
1253
+
1254
+ const starColor = `rgba(220, 255, 220, ${twinkleIntensity})`;
1255
+ const glowColor = `rgba(180, 255, 180, ${twinkleIntensity * 0.4})`;
1256
+
1257
+ ctx.save();
1258
+ ctx.translate(x, y);
1259
+
1260
+ ctx.shadowColor = glowColor;
1261
+ ctx.shadowBlur = size * 3 * twinkleIntensity;
1262
+ ctx.fillStyle = starColor;
1263
+
1264
+ ctx.beginPath();
1265
+ for (let i = 0; i < 4; i++) {
1266
+ const angle = (i * Math.PI) / 2;
1267
+ const outerX = Math.cos(angle) * size;
1268
+ const outerY = Math.sin(angle) * size;
1269
+ const innerAngle = angle + Math.PI / 4;
1270
+ const innerX = Math.cos(innerAngle) * (size * 0.3);
1271
+ const innerY = Math.sin(innerAngle) * (size * 0.3);
1272
+
1273
+ if (i === 0) {
1274
+ ctx.moveTo(outerX, outerY);
1275
+ } else {
1276
+ ctx.lineTo(outerX, innerY);
1277
+ }
1278
+ ctx.lineTo(innerX, innerY);
1279
+ }
1280
+ ctx.closePath();
1281
+ ctx.fill();
1282
+
1283
+ ctx.shadowBlur = size * 2 * twinkleIntensity;
1284
+ ctx.beginPath();
1285
+ ctx.arc(0, 0, size * 0.25, 0, Math.PI * 2);
1286
+ ctx.fill();
1287
+
1288
+ ctx.restore();
1289
+ }
1290
+ };