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,1754 @@
1
+ /**
2
+ * St George's Day Theme for Domma Celebrations
3
+ * (April 23rd, English Celebration)
4
+ *
5
+ * Features:
6
+ * - Rose petals (red and white - English colors)
7
+ * - English roses blooming
8
+ * - St George's Cross (red cross on white)
9
+ * - Knight silhouette (St George)
10
+ * - Dragon (medieval European style)
11
+ * - Shield and sword imagery
12
+ * - Red and white color scheme with gold accents
13
+ */
14
+
15
+ export default {
16
+ name: 'st-georges',
17
+ displayName: 'St George\'s Day',
18
+ emoji: '🏴󠁧󠁢󠁥󠁮󠁧󠁿',
19
+
20
+ // Intensity configurations
21
+ intensityConfig: {
22
+ light: {
23
+ count: 40,
24
+ speedRange: [0.3, 1.0],
25
+ sizeRange: [2, 4],
26
+ roses: 3,
27
+ knightChance: 0.0002,
28
+ twinklingStars: 10
29
+ },
30
+ medium: {
31
+ count: 80,
32
+ speedRange: [0.4, 1.3],
33
+ sizeRange: [2, 5],
34
+ roses: 5,
35
+ knightChance: 0.0004,
36
+ twinklingStars: 18
37
+ },
38
+ heavy: {
39
+ count: 120,
40
+ speedRange: [0.5, 1.6],
41
+ sizeRange: [3, 6],
42
+ roses: 8,
43
+ knightChance: 0.0006,
44
+ twinklingStars: 25
45
+ }
46
+ },
47
+
48
+ particles: ['rose-petal', 'tudor-rose', 'oak-leaf', 'sparkle'],
49
+ decorations: ['english-rose', 'st-georges-cross', 'knight', 'dragon', 'shield', 'castle','twinkling-star'],
50
+ colors: {
51
+ primary: '#C8102E', // English red
52
+ secondary: '#FFFFFF', // White
53
+ tertiary: '#012169', // English blue (from Union Jack)
54
+ accent: '#FFD700', // Gold
55
+ greenStem: '#228B22' // Rose stem
56
+ },
57
+
58
+ /**
59
+ * Create rose petal particle (simple red/white petal)
60
+ */
61
+ createRosePetal(canvasWidth, canvasHeight, config) {
62
+ // 70% red petals, 30% white petals (English colors)
63
+ const isRed = Math.random() < 0.7;
64
+ const petalColors = isRed
65
+ ? ['#C8102E', '#DC143C', '#B91C1C', '#991B1B'] // Red shades
66
+ : ['#FFFFFF', '#FFF5F5', '#FECACA', '#FEE2E2']; // White to pink shades
67
+
68
+ return {
69
+ type: 'rose-petal',
70
+ x: -30, // Start from left edge
71
+ y: Math.random() * canvasHeight, // Random height
72
+ vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
73
+ size: (config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0])) * 1.5, // Larger petals
74
+ speed: (Math.random() - 0.5) * 0.2, // Gentle vertical bobbing
75
+ opacity: 0.75 + Math.random() * 0.25,
76
+ windOffset: Math.random() * Math.PI * 2,
77
+ windSpeed: 0.015 + Math.random() * 0.02,
78
+ rotation: Math.random() * Math.PI * 2,
79
+ rotationSpeed: (Math.random() - 0.5) * 0.03,
80
+ color: petalColors[Math.floor(Math.random() * petalColors.length)],
81
+ flutter: Math.random() * Math.PI * 2,
82
+ flutterSpeed: 0.02 + Math.random() * 0.02,
83
+ active: true
84
+ };
85
+ },
86
+
87
+ /**
88
+ * Create Tudor Rose particle (red and white layered rose - full flower)
89
+ */
90
+ createTudorRose(canvasWidth, canvasHeight, config) {
91
+ return {
92
+ type: 'tudor-rose',
93
+ x: -30, // Start from left edge
94
+ y: Math.random() * canvasHeight, // Random height
95
+ vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
96
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
97
+ speed: (Math.random() - 0.5) * 0.15, // Gentle vertical bobbing
98
+ opacity: 0.8 + Math.random() * 0.2,
99
+ windOffset: Math.random() * Math.PI * 2,
100
+ windSpeed: 0.015 + Math.random() * 0.02,
101
+ rotation: Math.random() * Math.PI * 2,
102
+ rotationSpeed: (Math.random() - 0.5) * 0.02,
103
+ petalCount: 5, // Tudor rose has 5 petals
104
+ active: true
105
+ };
106
+ },
107
+
108
+ /**
109
+ * Create Oak Leaf particle (English oak - national tree)
110
+ */
111
+ createOakLeaf(canvasWidth, canvasHeight, config) {
112
+ const colorChoice = Math.random();
113
+ let color;
114
+ if (colorChoice < 0.6) {
115
+ color = '#2d5016'; // 60% dark green
116
+ } else if (colorChoice < 0.85) {
117
+ color = '#6b8e23'; // 25% olive green
118
+ } else {
119
+ color = '#8b4513'; // 15% autumn brown
120
+ }
121
+
122
+ return {
123
+ type: 'oak-leaf',
124
+ x: -30, // Start from left edge
125
+ y: Math.random() * canvasHeight, // Random height
126
+ vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift
127
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
128
+ speed: (Math.random() - 0.5) * 0.2, // Gentle vertical bobbing
129
+ opacity: 0.75 + Math.random() * 0.25,
130
+ windOffset: Math.random() * Math.PI * 2,
131
+ windSpeed: 0.02 + Math.random() * 0.03,
132
+ rotation: Math.random() * Math.PI * 2,
133
+ rotationSpeed: (Math.random() - 0.5) * 0.04,
134
+ color: color,
135
+ flutter: Math.random() * Math.PI * 2,
136
+ flutterSpeed: 0.03 + Math.random() * 0.02,
137
+ active: true
138
+ };
139
+ },
140
+
141
+ /**
142
+ * Create sparkle particle (English celebration sparkles)
143
+ */
144
+ createSparkle(canvasWidth, canvasHeight, config) {
145
+ const colors = ['#FFD700', '#FFFFFF', '#C8102E']; // Gold, white, red
146
+ return {
147
+ type: 'sparkle',
148
+ x: -20, // Start from left edge
149
+ y: Math.random() * canvasHeight, // Random height
150
+ vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift
151
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.6,
152
+ vy: (Math.random() - 0.5) * 0.2, // Minimal random vertical movement
153
+ opacity: 0.6 + Math.random() * 0.4,
154
+ rotation: Math.random() * Math.PI * 2,
155
+ rotationSpeed: (Math.random() - 0.5) * 0.04,
156
+ color: colors[Math.floor(Math.random() * colors.length)],
157
+ twinklePhase: Math.random() * Math.PI * 2,
158
+ windOffset: Math.random() * Math.PI * 2,
159
+ windSpeed: 0.015 + Math.random() * 0.02,
160
+ active: true,
161
+ static: false
162
+ };
163
+ },
164
+
165
+ /**
166
+ * Create drifting particle (randomly picks type)
167
+ * Note: St. George's Day particles drift horizontally (left-to-right), not vertically
168
+ */
169
+ createFallingParticle(canvasWidth, canvasHeight, config) {
170
+ const choice = Math.random();
171
+
172
+ // 60% rose petals, 20% full Tudor roses, 15% sparkles, 5% oak leaves
173
+ if (choice < 0.6) {
174
+ return this.createRosePetal(canvasWidth, canvasHeight, config);
175
+ } else if (choice < 0.8) {
176
+ return this.createTudorRose(canvasWidth, canvasHeight, config);
177
+ } else if (choice < 0.95) {
178
+ return this.createSparkle(canvasWidth, canvasHeight, config);
179
+ } else {
180
+ return this.createOakLeaf(canvasWidth, canvasHeight, config);
181
+ }
182
+ },
183
+
184
+ /**
185
+ * Create English rose decoration (blooming flower)
186
+ */
187
+ createEnglishRose(canvasWidth, canvasHeight, options = {}) {
188
+ return {
189
+ type: 'english-rose',
190
+ x: options.x !== undefined ? options.x : Math.random() * canvasWidth * 0.8 + canvasWidth * 0.1,
191
+ y: options.y !== undefined ? options.y : canvasHeight - 60 - Math.random() * 100,
192
+ size: 15 + Math.random() * 10,
193
+ opacity: 1,
194
+ color: Math.random() < 0.7 ? '#C8102E' : '#FFFFFF', // 70% red, 30% white
195
+ bloomPhase: Math.random() * Math.PI * 2,
196
+ petalCount: 8 + Math.floor(Math.random() * 5),
197
+ active: true,
198
+ static: true
199
+ };
200
+ },
201
+
202
+ /**
203
+ * Create St George's Cross flag decoration
204
+ */
205
+ createStGeorgesCross(canvasWidth, canvasHeight, options = {}) {
206
+ return {
207
+ type: 'st-georges-cross',
208
+ x: options.x !== undefined ? options.x : canvasWidth * 0.1 + Math.random() * canvasWidth * 0.8,
209
+ y: options.y !== undefined ? options.y : 50 + Math.random() * 100,
210
+ size: 40 + Math.random() * 20,
211
+ opacity: 1,
212
+ waveOffset: Math.random() * Math.PI * 2,
213
+ waveSpeed: 0.001 + Math.random() * 0.001,
214
+ active: true,
215
+ static: true
216
+ };
217
+ },
218
+
219
+ /**
220
+ * Create knight (St George) decoration
221
+ */
222
+ createKnight(canvasWidth, canvasHeight) {
223
+ const fromLeft = Math.random() < 0.5;
224
+ return {
225
+ type: 'knight',
226
+ x: fromLeft ? -80 : canvasWidth + 80,
227
+ y: canvasHeight - 50,
228
+ baseY: canvasHeight - 50,
229
+ vx: fromLeft ? 0.8 + Math.random() * 0.4 : -(0.8 + Math.random() * 0.4),
230
+ size: 20 + Math.random() * 10,
231
+ opacity: 1,
232
+ marchPhase: Math.random() * Math.PI * 2,
233
+ marchSpeed: 0.015,
234
+ active: true,
235
+ static: false
236
+ };
237
+ },
238
+
239
+ /**
240
+ * Create dragon decoration (medieval European style)
241
+ */
242
+ createDragon(canvasWidth, canvasHeight, options = {}) {
243
+ const fromLeft = Math.random() < 0.5;
244
+ return {
245
+ type: 'dragon',
246
+ x: fromLeft ? -100 : canvasWidth + 100,
247
+ y: canvasHeight * 0.3 + Math.random() * canvasHeight * 0.2,
248
+ baseY: canvasHeight * 0.3 + Math.random() * canvasHeight * 0.2,
249
+ vx: fromLeft ? 1.5 + Math.random() * 0.5 : -(1.5 + Math.random() * 0.5),
250
+ size: 30 + Math.random() * 15,
251
+ opacity: 1,
252
+ wingPhase: Math.random() * Math.PI * 2,
253
+ wingSpeed: 0.012,
254
+ breatheFirePhase: Math.random() * Math.PI * 2,
255
+ active: true,
256
+ static: false
257
+ };
258
+ },
259
+
260
+ /**
261
+ * Create shield decoration (with St George's Cross)
262
+ */
263
+ createShield(canvasWidth, canvasHeight, options = {}) {
264
+ return {
265
+ type: 'shield',
266
+ x: options.x !== undefined ? options.x : canvasWidth * 0.2 + Math.random() * canvasWidth * 0.6,
267
+ y: options.y !== undefined ? options.y : canvasHeight - 80 - Math.random() * 150,
268
+ size: 20 + Math.random() * 10,
269
+ opacity: 1,
270
+ rotation: -Math.PI / 12 + Math.random() * (Math.PI / 6),
271
+ glintPhase: Math.random() * Math.PI * 2,
272
+ active: true,
273
+ static: true
274
+ };
275
+ },
276
+
277
+ /**
278
+ * Create initial static decorations
279
+ */
280
+ createInitialDecorations(canvasWidth, canvasHeight, config) {
281
+ const decorations = [];
282
+
283
+ // Castle (left side)
284
+ decorations.push({
285
+ type: 'castle',
286
+ x: 150,
287
+ y: canvasHeight - 100,
288
+ size: 80 + Math.random() * 20,
289
+ opacity: 1,
290
+ active: true,
291
+ static: true
292
+ });
293
+
294
+ // Static dragon (perched on castle or ground, right side)
295
+ decorations.push({
296
+ type: 'dragon',
297
+ x: canvasWidth - 200,
298
+ y: canvasHeight - 60,
299
+ baseY: canvasHeight - 60,
300
+ vx: 0, // Static, not moving
301
+ size: 25 + Math.random() * 10,
302
+ opacity: 1,
303
+ time: 0,
304
+ wingPhase: Math.random() * Math.PI * 2,
305
+ breathePhase: Math.random() * Math.PI * 2,
306
+ active: true,
307
+ static: true
308
+ });
309
+
310
+ // Knight on steed (center-left, facing dragon)
311
+ decorations.push({
312
+ type: 'knight',
313
+ x: canvasWidth * 0.4,
314
+ y: canvasHeight - 45,
315
+ baseY: canvasHeight - 45,
316
+ vx: 0, // Static, mounted and ready
317
+ size: 20 + Math.random() * 5,
318
+ opacity: 1,
319
+ time: 0,
320
+ legPhase: 0, // Standing still
321
+ weaponRaised: true, // Lance raised toward dragon
322
+ active: true,
323
+ static: true
324
+ });
325
+
326
+ // St George's Cross flags on castle
327
+ decorations.push(this.createStGeorgesCross(canvasWidth, canvasHeight, {
328
+ x: 150,
329
+ y: canvasHeight - 180,
330
+ size: 30
331
+ }));
332
+
333
+ // Shields displayed (decorative)
334
+ decorations.push(this.createShield(canvasWidth, canvasHeight, {
335
+ x: canvasWidth * 0.15,
336
+ y: canvasHeight - 60
337
+ }));
338
+
339
+ // Create twinkling stars
340
+ const starCount = config.twinklingStars || 18;
341
+ for (let i = 0; i < starCount; i++) {
342
+ decorations.push(this.createTwinklingStar(canvasWidth, canvasHeight));
343
+ }
344
+
345
+ return decorations;
346
+ },
347
+
348
+ /**
349
+ * Create twinkling star particle
350
+ */
351
+ createTwinklingStar(canvasWidth, canvasHeight) {
352
+ return {
353
+ type: 'twinkling-star',
354
+ x: Math.random() * canvasWidth,
355
+ y: Math.random() * (canvasHeight * 0.5),
356
+ size: 1 + Math.random() * 2,
357
+ opacity: 0.6 + Math.random() * 0.3,
358
+ twinklePhase: Math.random() * Math.PI * 2,
359
+ twinkleSpeed: 0.003 + Math.random() * 0.003,
360
+ active: true,
361
+ static: true
362
+ };
363
+ },
364
+
365
+ /**
366
+ * Spawn special St George's particles
367
+ */
368
+ spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight, config) {
369
+ const choice = Math.random();
370
+
371
+ // Knight (very rare, max 1)
372
+ if (choice < config.knightChance) {
373
+ if (specialParticles.some(p => p.type === 'knight')) {
374
+ return null;
375
+ }
376
+ return this.createKnight(canvasWidth, canvasHeight);
377
+ }
378
+
379
+ // Dragon (very rare, max 1)
380
+ if (choice < config.knightChance * 1.2) {
381
+ if (specialParticles.some(p => p.type === 'dragon')) {
382
+ return null;
383
+ }
384
+ return this.createDragon(canvasWidth, canvasHeight);
385
+ }
386
+
387
+ // English rose (rare)
388
+ if (choice < 0.0005) {
389
+ const roseCount = specialParticles.filter(p => p.type === 'english-rose').length;
390
+ if (roseCount < config.roses) {
391
+ return this.createEnglishRose(canvasWidth, canvasHeight);
392
+ }
393
+ }
394
+
395
+ // St George's Cross flag (rare, max 2)
396
+ if (choice < 0.0003) {
397
+ const flagCount = specialParticles.filter(p => p.type === 'st-georges-cross').length;
398
+ if (flagCount < 2) {
399
+ return this.createStGeorgesCross(canvasWidth, canvasHeight);
400
+ }
401
+ }
402
+
403
+ // Shield (rare, max 3)
404
+ if (choice < 0.0004) {
405
+ const shieldCount = specialParticles.filter(p => p.type === 'shield').length;
406
+ if (shieldCount < 3) {
407
+ return this.createShield(canvasWidth, canvasHeight);
408
+ }
409
+ }
410
+
411
+ return null;
412
+ },
413
+
414
+ /**
415
+ * Draw rose petal (simple red/white petal)
416
+ */
417
+ drawRosePetal(ctx, particle, time) {
418
+ const x = particle.x;
419
+ const y = particle.y;
420
+ const size = particle.size;
421
+
422
+ // Flutter effect (petal curling as it drifts)
423
+ const flutter = Math.sin(time * particle.flutterSpeed + particle.flutter) * 0.3;
424
+
425
+ ctx.save();
426
+ ctx.globalAlpha = particle.opacity;
427
+ ctx.translate(x, y);
428
+ ctx.rotate(particle.rotation + flutter);
429
+
430
+ // Determine if this is a white or red petal based on color
431
+ const isWhite = particle.color.includes('FFF') || particle.color.includes('fff');
432
+
433
+ // Petal shape (heart-like rose petal)
434
+ ctx.fillStyle = particle.color;
435
+ ctx.strokeStyle = isWhite ? '#FFE6E6' : '#8B0000';
436
+ ctx.lineWidth = size * 0.08;
437
+
438
+ ctx.beginPath();
439
+ // Top curves (two rounded lobes)
440
+ ctx.moveTo(0, -size * 0.4);
441
+ ctx.bezierCurveTo(
442
+ size * 0.6, -size * 0.8,
443
+ size * 0.8, -size * 0.2,
444
+ size * 0.4, size * 0.2
445
+ );
446
+ // Bottom point
447
+ ctx.lineTo(0, size * 1.0);
448
+ ctx.lineTo(-size * 0.4, size * 0.2);
449
+ // Left curve
450
+ ctx.bezierCurveTo(
451
+ -size * 0.8, -size * 0.2,
452
+ -size * 0.6, -size * 0.8,
453
+ 0, -size * 0.4
454
+ );
455
+ ctx.closePath();
456
+ ctx.fill();
457
+ ctx.stroke();
458
+
459
+ // Central vein
460
+ ctx.strokeStyle = isWhite ? 'rgba(255, 182, 193, 0.5)' : 'rgba(139, 0, 0, 0.4)';
461
+ ctx.lineWidth = size * 0.06;
462
+ ctx.beginPath();
463
+ ctx.moveTo(0, -size * 0.3);
464
+ ctx.lineTo(0, size * 0.9);
465
+ ctx.stroke();
466
+
467
+ ctx.restore();
468
+ },
469
+
470
+ /**
471
+ * Draw English rose (blooming flower)
472
+ */
473
+ drawEnglishRose(ctx, particle, time) {
474
+ const x = particle.x;
475
+ const y = particle.y;
476
+ const size = particle.size;
477
+ const bloom = 0.8 + (Math.sin(time * 0.002 + particle.bloomPhase) + 1) * 0.1;
478
+
479
+ ctx.save();
480
+ ctx.translate(x, y);
481
+
482
+ // Stem
483
+ ctx.strokeStyle = this.colors.greenStem;
484
+ ctx.lineWidth = size * 0.12;
485
+ ctx.beginPath();
486
+ ctx.moveTo(0, 0);
487
+ ctx.quadraticCurveTo(
488
+ -size * 0.2, size * 0.8,
489
+ 0, size * 1.5
490
+ );
491
+ ctx.stroke();
492
+
493
+ // Leaves
494
+ ctx.fillStyle = '#2D5016';
495
+ [-1, 1].forEach(side => {
496
+ ctx.save();
497
+ ctx.translate(side * size * 0.15, size * 0.8);
498
+ ctx.rotate(side * Math.PI / 6);
499
+ ctx.beginPath();
500
+ ctx.ellipse(0, 0, size * 0.3, size * 0.15, 0, 0, Math.PI * 2);
501
+ ctx.fill();
502
+ ctx.restore();
503
+ });
504
+
505
+ // Rose petals (layered circular pattern)
506
+ ctx.fillStyle = particle.color;
507
+ ctx.strokeStyle = particle.color === '#FFFFFF' ? '#FFE6E6' : '#8B0000';
508
+ ctx.lineWidth = 1;
509
+
510
+ // Outer layer
511
+ for (let i = 0; i < particle.petalCount; i++) {
512
+ const angle = (i / particle.petalCount) * Math.PI * 2;
513
+ ctx.save();
514
+ ctx.rotate(angle);
515
+ ctx.translate(0, -size * 0.4 * bloom);
516
+
517
+ ctx.beginPath();
518
+ ctx.moveTo(0, size * 0.25);
519
+ ctx.bezierCurveTo(
520
+ -size * 0.25, -size * 0.1,
521
+ -size * 0.15, -size * 0.35,
522
+ 0, -size * 0.25
523
+ );
524
+ ctx.bezierCurveTo(
525
+ size * 0.15, -size * 0.35,
526
+ size * 0.25, -size * 0.1,
527
+ 0, size * 0.25
528
+ );
529
+ ctx.closePath();
530
+ ctx.fill();
531
+ ctx.stroke();
532
+
533
+ ctx.restore();
534
+ }
535
+
536
+ // Inner layer (offset rotation)
537
+ ctx.globalAlpha = 0.9;
538
+ for (let i = 0; i < particle.petalCount - 2; i++) {
539
+ const angle = (i / (particle.petalCount - 2)) * Math.PI * 2 + (Math.PI / particle.petalCount);
540
+ ctx.save();
541
+ ctx.rotate(angle);
542
+ ctx.translate(0, -size * 0.25 * bloom);
543
+
544
+ ctx.beginPath();
545
+ ctx.moveTo(0, size * 0.15);
546
+ ctx.bezierCurveTo(
547
+ -size * 0.15, -size * 0.05,
548
+ -size * 0.1, -size * 0.2,
549
+ 0, -size * 0.15
550
+ );
551
+ ctx.bezierCurveTo(
552
+ size * 0.1, -size * 0.2,
553
+ size * 0.15, -size * 0.05,
554
+ 0, size * 0.15
555
+ );
556
+ ctx.closePath();
557
+ ctx.fill();
558
+
559
+ ctx.restore();
560
+ }
561
+
562
+ // Center (yellow stamens)
563
+ ctx.globalAlpha = 1;
564
+ ctx.fillStyle = '#FFD700';
565
+ ctx.beginPath();
566
+ ctx.arc(0, 0, size * 0.15 * bloom, 0, Math.PI * 2);
567
+ ctx.fill();
568
+
569
+ // Stamen dots
570
+ ctx.fillStyle = '#FFA500';
571
+ for (let i = 0; i < 8; i++) {
572
+ const angle = (i / 8) * Math.PI * 2;
573
+ const distance = size * 0.08 * bloom;
574
+ ctx.beginPath();
575
+ ctx.arc(
576
+ Math.cos(angle) * distance,
577
+ Math.sin(angle) * distance,
578
+ size * 0.03,
579
+ 0,
580
+ Math.PI * 2
581
+ );
582
+ ctx.fill();
583
+ }
584
+
585
+ ctx.restore();
586
+ },
587
+
588
+ /**
589
+ * Draw St George's Cross flag (redesigned)
590
+ */
591
+ drawStGeorgesCross(ctx, particle, time) {
592
+ const x = particle.x;
593
+ const y = particle.y;
594
+ const size = particle.size;
595
+ const waveOffset = time * particle.waveSpeed + particle.waveOffset;
596
+
597
+ ctx.save();
598
+ ctx.translate(x, y);
599
+
600
+ // ===== FLAGPOLE (wooden with grain detail) =====
601
+ const poleGradient = ctx.createLinearGradient(-size * 0.64, 0, -size * 0.56, 0);
602
+ poleGradient.addColorStop(0, '#5a4a3a');
603
+ poleGradient.addColorStop(0.5, '#8B4513');
604
+ poleGradient.addColorStop(1, '#6a5343');
605
+ ctx.fillStyle = poleGradient;
606
+ ctx.fillRect(-size * 0.64, -size * 0.35, size * 0.08, size * 1.9);
607
+
608
+ // Pole edge highlight
609
+ ctx.strokeStyle = '#9a6333';
610
+ ctx.lineWidth = 1;
611
+ ctx.beginPath();
612
+ ctx.moveTo(-size * 0.64, -size * 0.35);
613
+ ctx.lineTo(-size * 0.64, size * 1.55);
614
+ ctx.stroke();
615
+
616
+ // Pole shadow
617
+ ctx.strokeStyle = '#3a2a1a';
618
+ ctx.beginPath();
619
+ ctx.moveTo(-size * 0.56, -size * 0.35);
620
+ ctx.lineTo(-size * 0.56, size * 1.55);
621
+ ctx.stroke();
622
+
623
+ // Gold finial (decorative top)
624
+ ctx.fillStyle = '#FFD700';
625
+ ctx.beginPath();
626
+ ctx.arc(-size * 0.6, -size * 0.4, size * 0.06, 0, Math.PI * 2);
627
+ ctx.fill();
628
+ ctx.strokeStyle = '#DAA520';
629
+ ctx.lineWidth = 1.5;
630
+ ctx.stroke();
631
+
632
+ // ===== FLAG FABRIC (smooth wave, proper cross) =====
633
+ const flagWidth = size * 1.5;
634
+ const flagHeight = size * 1.0;
635
+ const verticalStripes = 12; // More stripes for smoother wave
636
+
637
+ // Draw flag in vertical strips with wave displacement
638
+ for (let i = 0; i < verticalStripes; i++) {
639
+ const stripX = -size * 0.5 + (i / verticalStripes) * flagWidth;
640
+ const nextStripX = -size * 0.5 + ((i + 1) / verticalStripes) * flagWidth;
641
+ const stripWidth = nextStripX - stripX;
642
+
643
+ // Wave calculation for this strip
644
+ const wave = Math.sin(waveOffset + i * 0.4) * size * 0.12;
645
+ const nextWave = Math.sin(waveOffset + (i + 1) * 0.4) * size * 0.12;
646
+
647
+ // Calculate if this strip is in the cross area
648
+ const stripCenter = stripX + stripWidth / 2 + size * 0.5;
649
+ const normalizedX = stripCenter / flagWidth; // 0 to 1
650
+
651
+ const isVerticalBar = normalizedX > 0.4 && normalizedX < 0.6;
652
+
653
+ ctx.save();
654
+
655
+ // ===== WHITE BACKGROUND STRIP =====
656
+ ctx.fillStyle = '#FFFFFF';
657
+ ctx.beginPath();
658
+ ctx.moveTo(stripX, -flagHeight / 2 + wave);
659
+ ctx.lineTo(nextStripX, -flagHeight / 2 + nextWave);
660
+ ctx.lineTo(nextStripX, flagHeight / 2 + nextWave);
661
+ ctx.lineTo(stripX, flagHeight / 2 + wave);
662
+ ctx.closePath();
663
+ ctx.fill();
664
+
665
+ // ===== RED CROSS STRIPS =====
666
+ ctx.fillStyle = '#C8102E';
667
+
668
+ // Vertical bar of cross (center strip)
669
+ if (isVerticalBar) {
670
+ ctx.beginPath();
671
+ ctx.moveTo(stripX, -flagHeight / 2 + wave);
672
+ ctx.lineTo(nextStripX, -flagHeight / 2 + nextWave);
673
+ ctx.lineTo(nextStripX, flagHeight / 2 + nextWave);
674
+ ctx.lineTo(stripX, flagHeight / 2 + wave);
675
+ ctx.closePath();
676
+ ctx.fill();
677
+ }
678
+
679
+ // Horizontal bar of cross (middle section of each strip)
680
+ ctx.beginPath();
681
+ ctx.moveTo(stripX, -flagHeight * 0.15 + wave);
682
+ ctx.lineTo(nextStripX, -flagHeight * 0.15 + nextWave);
683
+ ctx.lineTo(nextStripX, flagHeight * 0.15 + nextWave);
684
+ ctx.lineTo(stripX, flagHeight * 0.15 + wave);
685
+ ctx.closePath();
686
+ ctx.fill();
687
+
688
+ // ===== FABRIC SHADING (alternating for depth) =====
689
+ const shade = Math.sin(waveOffset + i * 0.4) * 0.15;
690
+ if (shade > 0) {
691
+ ctx.fillStyle = `rgba(255, 255, 255, ${shade * 0.3})`;
692
+ } else {
693
+ ctx.fillStyle = `rgba(0, 0, 0, ${-shade * 0.2})`;
694
+ }
695
+ ctx.beginPath();
696
+ ctx.moveTo(stripX, -flagHeight / 2 + wave);
697
+ ctx.lineTo(nextStripX, -flagHeight / 2 + nextWave);
698
+ ctx.lineTo(nextStripX, flagHeight / 2 + nextWave);
699
+ ctx.lineTo(stripX, flagHeight / 2 + wave);
700
+ ctx.closePath();
701
+ ctx.fill();
702
+
703
+ // Strip edge line (fabric texture)
704
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
705
+ ctx.lineWidth = 0.5;
706
+ ctx.beginPath();
707
+ ctx.moveTo(nextStripX, -flagHeight / 2 + nextWave);
708
+ ctx.lineTo(nextStripX, flagHeight / 2 + nextWave);
709
+ ctx.stroke();
710
+
711
+ ctx.restore();
712
+ }
713
+
714
+ // ===== FLAG BORDER (subtle outline) =====
715
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)';
716
+ ctx.lineWidth = 1.5;
717
+ ctx.beginPath();
718
+ // Top edge
719
+ ctx.moveTo(-size * 0.5, -flagHeight / 2 + Math.sin(waveOffset) * size * 0.12);
720
+ for (let i = 1; i <= verticalStripes; i++) {
721
+ const stripX = -size * 0.5 + (i / verticalStripes) * flagWidth;
722
+ const wave = Math.sin(waveOffset + i * 0.4) * size * 0.12;
723
+ ctx.lineTo(stripX, -flagHeight / 2 + wave);
724
+ }
725
+ // Right edge
726
+ ctx.lineTo(
727
+ -size * 0.5 + flagWidth,
728
+ flagHeight / 2 + Math.sin(waveOffset + verticalStripes * 0.4) * size * 0.12
729
+ );
730
+ // Bottom edge
731
+ for (let i = verticalStripes; i >= 0; i--) {
732
+ const stripX = -size * 0.5 + (i / verticalStripes) * flagWidth;
733
+ const wave = Math.sin(waveOffset + i * 0.4) * size * 0.12;
734
+ ctx.lineTo(stripX, flagHeight / 2 + wave);
735
+ }
736
+ // Left edge (back to pole)
737
+ ctx.closePath();
738
+ ctx.stroke();
739
+
740
+ // ===== HOIST ATTACHMENT (flag grommets) =====
741
+ ctx.fillStyle = '#A9A9A9'; // Metal grommets
742
+ for (let i = 0; i < 3; i++) {
743
+ const yPos = -flagHeight / 2 + (i / 2) * flagHeight;
744
+ const wave = Math.sin(waveOffset) * size * 0.12;
745
+ ctx.beginPath();
746
+ ctx.arc(-size * 0.48, yPos + wave, size * 0.03, 0, Math.PI * 2);
747
+ ctx.fill();
748
+ ctx.strokeStyle = '#696969';
749
+ ctx.lineWidth = 1;
750
+ ctx.stroke();
751
+ }
752
+
753
+ ctx.restore();
754
+ },
755
+
756
+ /**
757
+ * Draw knight (St George)
758
+ */
759
+ drawKnight(ctx, particle, time) {
760
+ const x = particle.x;
761
+ const y = particle.y;
762
+ const size = particle.size;
763
+ const dir = particle.vx > 0 ? 1 : -1;
764
+ const marchPhase = Math.sin(time * particle.marchSpeed + particle.marchPhase) * (Math.PI / 8);
765
+
766
+ ctx.save();
767
+ ctx.translate(x, y);
768
+ if (dir === -1) {
769
+ ctx.scale(-1, 1);
770
+ }
771
+
772
+ // Legs (marching)
773
+ ctx.strokeStyle = '#C0C0C0'; // Silver armor
774
+ ctx.lineWidth = size * 0.15;
775
+ ctx.beginPath();
776
+ ctx.moveTo(-size * 0.15, size * 0.5);
777
+ ctx.lineTo(-size * 0.2, size * 1.2 + Math.sin(marchPhase) * size * 0.15);
778
+ ctx.stroke();
779
+ ctx.beginPath();
780
+ ctx.moveTo(size * 0.15, size * 0.5);
781
+ ctx.lineTo(size * 0.2, size * 1.2 + Math.sin(marchPhase + Math.PI) * size * 0.15);
782
+ ctx.stroke();
783
+
784
+ // Boots
785
+ ctx.fillStyle = '#2F4F4F';
786
+ ctx.fillRect(-size * 0.25, size * 1.15, size * 0.2, size * 0.2);
787
+ ctx.fillRect(size * 0.05, size * 1.15, size * 0.2, size * 0.2);
788
+
789
+ // Body (chainmail/armor)
790
+ ctx.fillStyle = '#C0C0C0';
791
+ ctx.fillRect(-size * 0.4, 0, size * 0.8, size * 0.6);
792
+
793
+ // Surcoat (white with red cross)
794
+ ctx.fillStyle = '#FFFFFF';
795
+ ctx.fillRect(-size * 0.35, size * 0.05, size * 0.7, size * 0.5);
796
+
797
+ // Red cross on surcoat
798
+ ctx.fillStyle = '#C8102E';
799
+ ctx.fillRect(-size * 0.1, size * 0.05, size * 0.2, size * 0.5); // Vertical
800
+ ctx.fillRect(-size * 0.35, size * 0.2, size * 0.7, size * 0.2); // Horizontal
801
+
802
+ // Shield (left side)
803
+ ctx.save();
804
+ ctx.translate(-size * 0.5, size * 0.3);
805
+ ctx.rotate(-Math.PI / 6);
806
+
807
+ // Shield shape
808
+ ctx.fillStyle = '#FFFFFF';
809
+ ctx.strokeStyle = '#FFD700';
810
+ ctx.lineWidth = 2;
811
+ ctx.beginPath();
812
+ ctx.moveTo(0, -size * 0.4);
813
+ ctx.lineTo(size * 0.25, -size * 0.3);
814
+ ctx.lineTo(size * 0.25, size * 0.2);
815
+ ctx.lineTo(0, size * 0.4);
816
+ ctx.lineTo(-size * 0.25, size * 0.2);
817
+ ctx.lineTo(-size * 0.25, -size * 0.3);
818
+ ctx.closePath();
819
+ ctx.fill();
820
+ ctx.stroke();
821
+
822
+ // Red cross on shield
823
+ ctx.fillStyle = '#C8102E';
824
+ ctx.fillRect(-size * 0.05, -size * 0.3, size * 0.1, size * 0.6);
825
+ ctx.fillRect(-size * 0.2, -size * 0.05, size * 0.4, size * 0.1);
826
+
827
+ ctx.restore();
828
+
829
+ // Sword (right side, raised)
830
+ ctx.strokeStyle = '#708090';
831
+ ctx.lineWidth = size * 0.1;
832
+ ctx.beginPath();
833
+ ctx.moveTo(size * 0.4, -size * 0.2);
834
+ ctx.lineTo(size * 0.6, -size * 0.8);
835
+ ctx.stroke();
836
+
837
+ // Sword crossguard
838
+ ctx.lineWidth = size * 0.15;
839
+ ctx.beginPath();
840
+ ctx.moveTo(size * 0.3, -size * 0.3);
841
+ ctx.lineTo(size * 0.5, -size * 0.3);
842
+ ctx.stroke();
843
+
844
+ // Sword pommel
845
+ ctx.fillStyle = '#FFD700';
846
+ ctx.beginPath();
847
+ ctx.arc(size * 0.4, -size * 0.15, size * 0.08, 0, Math.PI * 2);
848
+ ctx.fill();
849
+
850
+ // Helmet
851
+ ctx.fillStyle = '#708090';
852
+ ctx.beginPath();
853
+ ctx.arc(0, -size * 0.3, size * 0.35, 0, Math.PI * 2);
854
+ ctx.fill();
855
+
856
+ // Visor slot
857
+ ctx.fillStyle = '#000000';
858
+ ctx.fillRect(-size * 0.25, -size * 0.35, size * 0.5, size * 0.1);
859
+
860
+ // Plume (red)
861
+ ctx.fillStyle = '#C8102E';
862
+ ctx.beginPath();
863
+ ctx.moveTo(0, -size * 0.65);
864
+ ctx.bezierCurveTo(
865
+ -size * 0.15, -size * 0.85,
866
+ -size * 0.1, -size * 1.0,
867
+ 0, -size * 0.95
868
+ );
869
+ ctx.bezierCurveTo(
870
+ size * 0.1, -size * 1.0,
871
+ size * 0.15, -size * 0.85,
872
+ 0, -size * 0.65
873
+ );
874
+ ctx.closePath();
875
+ ctx.fill();
876
+
877
+ ctx.restore();
878
+ },
879
+
880
+ /**
881
+ * Draw dragon (medieval European style)
882
+ */
883
+ drawDragon(ctx, particle, time) {
884
+ const x = particle.x;
885
+ const y = particle.y;
886
+ const size = particle.size;
887
+ const dir = particle.vx > 0 ? 1 : -1;
888
+ const wingAngle = Math.sin(time * particle.wingSpeed + particle.wingPhase) * (Math.PI / 3);
889
+
890
+ ctx.save();
891
+ ctx.translate(x, y);
892
+ if (dir === -1) {
893
+ ctx.scale(-1, 1);
894
+ }
895
+
896
+ // Wings (bat-like, dark green)
897
+ ctx.fillStyle = '#2F4F2F';
898
+ ctx.strokeStyle = '#1C3020';
899
+ ctx.lineWidth = 2;
900
+
901
+ // Left wing
902
+ ctx.save();
903
+ ctx.translate(-size * 0.3, -size * 0.2);
904
+ ctx.rotate(wingAngle);
905
+ ctx.beginPath();
906
+ ctx.moveTo(0, 0);
907
+ ctx.bezierCurveTo(
908
+ -size * 0.4, -size * 0.3,
909
+ -size * 0.6, -size * 0.2,
910
+ -size * 0.7, 0
911
+ );
912
+ ctx.bezierCurveTo(
913
+ -size * 0.6, size * 0.1,
914
+ -size * 0.4, size * 0.05,
915
+ 0, 0
916
+ );
917
+ ctx.closePath();
918
+ ctx.fill();
919
+ ctx.stroke();
920
+ ctx.restore();
921
+
922
+ // Right wing
923
+ ctx.save();
924
+ ctx.translate(size * 0.3, -size * 0.2);
925
+ ctx.rotate(-wingAngle);
926
+ ctx.beginPath();
927
+ ctx.moveTo(0, 0);
928
+ ctx.bezierCurveTo(
929
+ size * 0.4, -size * 0.3,
930
+ size * 0.6, -size * 0.2,
931
+ size * 0.7, 0
932
+ );
933
+ ctx.bezierCurveTo(
934
+ size * 0.6, size * 0.1,
935
+ size * 0.4, size * 0.05,
936
+ 0, 0
937
+ );
938
+ ctx.closePath();
939
+ ctx.fill();
940
+ ctx.stroke();
941
+ ctx.restore();
942
+
943
+ // Body (serpentine, dark green)
944
+ ctx.fillStyle = '#556B2F';
945
+ ctx.strokeStyle = '#2F4F2F';
946
+ ctx.lineWidth = 2;
947
+
948
+ // Main body (oval)
949
+ ctx.beginPath();
950
+ ctx.ellipse(0, 0, size * 0.5, size * 0.3, 0, 0, Math.PI * 2);
951
+ ctx.fill();
952
+ ctx.stroke();
953
+
954
+ // Tail (curved)
955
+ ctx.strokeStyle = '#556B2F';
956
+ ctx.lineWidth = size * 0.2;
957
+ ctx.lineCap = 'round';
958
+ ctx.beginPath();
959
+ ctx.moveTo(-size * 0.4, 0);
960
+ ctx.quadraticCurveTo(
961
+ -size * 0.8, size * 0.2,
962
+ -size * 1.2, 0
963
+ );
964
+ ctx.stroke();
965
+
966
+ // Tail spikes
967
+ ctx.strokeStyle = '#8B0000';
968
+ ctx.lineWidth = 2;
969
+ for (let i = 0; i < 3; i++) {
970
+ const tx = -size * 0.6 - i * size * 0.2;
971
+ const ty = size * 0.1;
972
+ ctx.beginPath();
973
+ ctx.moveTo(tx, ty);
974
+ ctx.lineTo(tx, ty - size * 0.15);
975
+ ctx.stroke();
976
+ }
977
+
978
+ // Neck
979
+ ctx.fillStyle = '#556B2F';
980
+ ctx.beginPath();
981
+ ctx.moveTo(size * 0.3, -size * 0.1);
982
+ ctx.lineTo(size * 0.5, -size * 0.4);
983
+ ctx.lineTo(size * 0.5, -size * 0.2);
984
+ ctx.closePath();
985
+ ctx.fill();
986
+
987
+ // Head (reptilian)
988
+ ctx.fillStyle = '#6B8E23';
989
+ ctx.beginPath();
990
+ ctx.ellipse(size * 0.65, -size * 0.35, size * 0.25, size * 0.2, -Math.PI / 6, 0, Math.PI * 2);
991
+ ctx.fill();
992
+ ctx.stroke();
993
+
994
+ // Snout
995
+ ctx.beginPath();
996
+ ctx.moveTo(size * 0.75, -size * 0.35);
997
+ ctx.lineTo(size * 0.95, -size * 0.35);
998
+ ctx.lineTo(size * 0.85, -size * 0.25);
999
+ ctx.closePath();
1000
+ ctx.fill();
1001
+
1002
+ // Horns
1003
+ ctx.strokeStyle = '#8B4513';
1004
+ ctx.lineWidth = 3;
1005
+ [size * 0.55, size * 0.65].forEach(hx => {
1006
+ ctx.beginPath();
1007
+ ctx.moveTo(hx, -size * 0.5);
1008
+ ctx.lineTo(hx, -size * 0.7);
1009
+ ctx.stroke();
1010
+ });
1011
+
1012
+ // Eye (red, glowing)
1013
+ ctx.fillStyle = '#FF0000';
1014
+ ctx.shadowBlur = 8;
1015
+ ctx.shadowColor = '#FF0000';
1016
+ ctx.beginPath();
1017
+ ctx.arc(size * 0.7, -size * 0.4, size * 0.05, 0, Math.PI * 2);
1018
+ ctx.fill();
1019
+ ctx.shadowBlur = 0;
1020
+
1021
+ // Fire breath (occasional)
1022
+ if (Math.sin(time * 0.005 + particle.breatheFirePhase) > 0.7) {
1023
+ const fireGradient = ctx.createLinearGradient(size * 0.95, -size * 0.35, size * 1.5, -size * 0.35);
1024
+ fireGradient.addColorStop(0, 'rgba(255, 100, 0, 0.8)');
1025
+ fireGradient.addColorStop(0.5, 'rgba(255, 200, 0, 0.5)');
1026
+ fireGradient.addColorStop(1, 'rgba(255, 255, 0, 0)');
1027
+
1028
+ ctx.fillStyle = fireGradient;
1029
+ ctx.beginPath();
1030
+ ctx.moveTo(size * 0.95, -size * 0.35);
1031
+ ctx.lineTo(size * 1.5, -size * 0.45);
1032
+ ctx.lineTo(size * 1.5, -size * 0.25);
1033
+ ctx.closePath();
1034
+ ctx.fill();
1035
+
1036
+ // Fire particles
1037
+ for (let i = 0; i < 5; i++) {
1038
+ const fx = size * 1.0 + Math.random() * size * 0.5;
1039
+ const fy = -size * 0.35 + (Math.random() - 0.5) * size * 0.3;
1040
+ ctx.fillStyle = `rgba(255, ${100 + Math.random() * 100}, 0, ${Math.random() * 0.8})`;
1041
+ ctx.beginPath();
1042
+ ctx.arc(fx, fy, size * 0.05, 0, Math.PI * 2);
1043
+ ctx.fill();
1044
+ }
1045
+ }
1046
+
1047
+ // Belly scales
1048
+ ctx.fillStyle = '#9ACD32';
1049
+ for (let i = 0; i < 3; i++) {
1050
+ ctx.beginPath();
1051
+ ctx.arc(-size * 0.2 + i * size * 0.2, size * 0.1, size * 0.08, 0, Math.PI * 2);
1052
+ ctx.fill();
1053
+ }
1054
+
1055
+ ctx.restore();
1056
+ },
1057
+
1058
+ /**
1059
+ * Draw shield with St George's Cross
1060
+ */
1061
+ drawShield(ctx, particle, time) {
1062
+ const x = particle.x;
1063
+ const y = particle.y;
1064
+ const size = particle.size;
1065
+ const glint = (Math.sin(time * 0.003 + particle.glintPhase) + 1) * 0.5;
1066
+
1067
+ ctx.save();
1068
+ ctx.translate(x, y);
1069
+ ctx.rotate(particle.rotation);
1070
+
1071
+ // Shield outline (kite shield shape)
1072
+ ctx.fillStyle = '#FFFFFF';
1073
+ ctx.strokeStyle = '#FFD700';
1074
+ ctx.lineWidth = 2;
1075
+
1076
+ ctx.beginPath();
1077
+ ctx.moveTo(0, -size * 0.7);
1078
+ ctx.lineTo(size * 0.5, -size * 0.5);
1079
+ ctx.lineTo(size * 0.5, size * 0.3);
1080
+ ctx.lineTo(0, size * 0.7);
1081
+ ctx.lineTo(-size * 0.5, size * 0.3);
1082
+ ctx.lineTo(-size * 0.5, -size * 0.5);
1083
+ ctx.closePath();
1084
+ ctx.fill();
1085
+ ctx.stroke();
1086
+
1087
+ // St George's Cross
1088
+ ctx.fillStyle = '#C8102E';
1089
+ // Vertical bar
1090
+ ctx.fillRect(-size * 0.1, -size * 0.6, size * 0.2, size * 1.2);
1091
+ // Horizontal bar
1092
+ ctx.fillRect(-size * 0.45, -size * 0.1, size * 0.9, size * 0.2);
1093
+
1094
+ // Metallic edge highlights
1095
+ ctx.strokeStyle = `rgba(255, 215, 0, ${glint * 0.8})`;
1096
+ ctx.lineWidth = 3;
1097
+ ctx.beginPath();
1098
+ ctx.moveTo(0, -size * 0.7);
1099
+ ctx.lineTo(size * 0.5, -size * 0.5);
1100
+ ctx.lineTo(size * 0.5, size * 0.3);
1101
+ ctx.stroke();
1102
+
1103
+ // Central boss (decorative)
1104
+ ctx.fillStyle = '#FFD700';
1105
+ ctx.strokeStyle = '#DAA520';
1106
+ ctx.lineWidth = 1;
1107
+ ctx.beginPath();
1108
+ ctx.arc(0, 0, size * 0.15, 0, Math.PI * 2);
1109
+ ctx.fill();
1110
+ ctx.stroke();
1111
+
1112
+ // Glint on boss
1113
+ ctx.fillStyle = `rgba(255, 255, 255, ${glint * 0.6})`;
1114
+ ctx.beginPath();
1115
+ ctx.arc(-size * 0.05, -size * 0.05, size * 0.06, 0, Math.PI * 2);
1116
+ ctx.fill();
1117
+
1118
+ ctx.restore();
1119
+ },
1120
+
1121
+ /**
1122
+ * Draw English medieval castle (redesigned for visual impact)
1123
+ */
1124
+ drawCastle(ctx, particle) {
1125
+ const x = particle.x;
1126
+ const y = particle.y;
1127
+ const size = particle.size;
1128
+
1129
+ ctx.save();
1130
+ ctx.globalAlpha = particle.opacity;
1131
+ ctx.translate(x, y);
1132
+
1133
+ // Define stone colors
1134
+ const stoneBase = '#9a958f';
1135
+ const stoneDark = '#6a655f';
1136
+ const stoneShadow = '#4a4540';
1137
+ const stoneHighlight = '#b5b0aa';
1138
+
1139
+ // ===== MAIN KEEP (central rectangular tower) =====
1140
+ const keepWidth = size * 1.4;
1141
+ const keepHeight = size * 1.0;
1142
+
1143
+ // Keep shadow (depth)
1144
+ ctx.fillStyle = stoneShadow;
1145
+ ctx.fillRect(-keepWidth / 2 + size * 0.05, -keepHeight + size * 0.05, keepWidth, keepHeight);
1146
+
1147
+ // Keep base
1148
+ const keepGradient = ctx.createLinearGradient(-keepWidth / 2, -keepHeight, keepWidth / 2, -keepHeight);
1149
+ keepGradient.addColorStop(0, stoneDark);
1150
+ keepGradient.addColorStop(0.5, stoneBase);
1151
+ keepGradient.addColorStop(1, stoneDark);
1152
+ ctx.fillStyle = keepGradient;
1153
+ ctx.fillRect(-keepWidth / 2, -keepHeight, keepWidth, keepHeight);
1154
+
1155
+ // Keep outline
1156
+ ctx.strokeStyle = stoneShadow;
1157
+ ctx.lineWidth = 2;
1158
+ ctx.strokeRect(-keepWidth / 2, -keepHeight, keepWidth, keepHeight);
1159
+
1160
+ // Detailed stone blocks on keep
1161
+ ctx.strokeStyle = 'rgba(74, 69, 64, 0.4)';
1162
+ ctx.lineWidth = 1;
1163
+ const blockHeight = size * 0.12;
1164
+ const blockWidth = size * 0.18;
1165
+ for (let row = 0; row < 8; row++) {
1166
+ const yPos = -keepHeight + row * blockHeight;
1167
+ const offset = (row % 2) * (blockWidth / 2); // Staggered bricks
1168
+ for (let col = 0; col < 8; col++) {
1169
+ const xPos = -keepWidth / 2 + col * blockWidth + offset;
1170
+ ctx.strokeRect(xPos, yPos, blockWidth, blockHeight);
1171
+ }
1172
+ }
1173
+
1174
+ // Keep battlements (larger, more detailed)
1175
+ ctx.fillStyle = stoneBase;
1176
+ const battlementWidth = size * 0.15;
1177
+ const battlementHeight = size * 0.12;
1178
+ for (let i = 0; i < 9; i++) {
1179
+ if (i % 2 === 0) {
1180
+ const xPos = -keepWidth / 2 + i * (keepWidth / 8);
1181
+ // Merlon (raised part)
1182
+ ctx.fillRect(xPos, -keepHeight - battlementHeight, battlementWidth, battlementHeight);
1183
+ ctx.strokeRect(xPos, -keepHeight - battlementHeight, battlementWidth, battlementHeight);
1184
+ // Arrow slit in merlon
1185
+ ctx.fillStyle = stoneShadow;
1186
+ ctx.fillRect(xPos + battlementWidth / 2 - size * 0.015, -keepHeight - battlementHeight + size * 0.02, size * 0.03, battlementHeight - size * 0.04);
1187
+ ctx.fillStyle = stoneBase;
1188
+ }
1189
+ }
1190
+
1191
+ // ===== LEFT TOWER (round, larger) =====
1192
+ const towerRadius = size * 0.22;
1193
+ const towerHeight = size * 1.2;
1194
+ const leftTowerX = -size * 0.85;
1195
+ const leftTowerY = -towerHeight / 2;
1196
+
1197
+ // Tower shadow
1198
+ ctx.fillStyle = stoneShadow;
1199
+ ctx.beginPath();
1200
+ ctx.ellipse(leftTowerX + size * 0.03, leftTowerY + size * 0.03, towerRadius, towerHeight / 2, 0, 0, Math.PI * 2);
1201
+ ctx.fill();
1202
+
1203
+ // Tower body with gradient
1204
+ const leftTowerGradient = ctx.createRadialGradient(leftTowerX - towerRadius * 0.3, leftTowerY, 0, leftTowerX, leftTowerY, towerRadius);
1205
+ leftTowerGradient.addColorStop(0, stoneHighlight);
1206
+ leftTowerGradient.addColorStop(0.5, stoneBase);
1207
+ leftTowerGradient.addColorStop(1, stoneDark);
1208
+ ctx.fillStyle = leftTowerGradient;
1209
+ ctx.beginPath();
1210
+ ctx.ellipse(leftTowerX, leftTowerY, towerRadius, towerHeight / 2, 0, 0, Math.PI * 2);
1211
+ ctx.fill();
1212
+ ctx.strokeStyle = stoneShadow;
1213
+ ctx.lineWidth = 2;
1214
+ ctx.stroke();
1215
+
1216
+ // Tower battlements (conical top)
1217
+ ctx.fillStyle = stoneBase;
1218
+ for (let i = 0; i < 6; i++) {
1219
+ if (i % 2 === 0) {
1220
+ const angle = (i / 6) * Math.PI;
1221
+ const xOffset = Math.cos(angle) * towerRadius;
1222
+ ctx.fillRect(leftTowerX + xOffset - size * 0.06, -towerHeight - size * 0.1, size * 0.12, size * 0.1);
1223
+ ctx.strokeRect(leftTowerX + xOffset - size * 0.06, -towerHeight - size * 0.1, size * 0.12, size * 0.1);
1224
+ }
1225
+ }
1226
+
1227
+ // Tower windows (arrow slits)
1228
+ ctx.fillStyle = '#1a1510';
1229
+ for (let i = 0; i < 3; i++) {
1230
+ const yWin = leftTowerY - towerHeight * 0.2 + i * towerHeight * 0.25;
1231
+ ctx.fillRect(leftTowerX - size * 0.02, yWin, size * 0.04, size * 0.15);
1232
+ // Splay at bottom (wider inside)
1233
+ ctx.beginPath();
1234
+ ctx.moveTo(leftTowerX - size * 0.02, yWin + size * 0.15);
1235
+ ctx.lineTo(leftTowerX - size * 0.06, yWin + size * 0.18);
1236
+ ctx.lineTo(leftTowerX + size * 0.06, yWin + size * 0.18);
1237
+ ctx.lineTo(leftTowerX + size * 0.02, yWin + size * 0.15);
1238
+ ctx.fill();
1239
+ }
1240
+
1241
+ // ===== RIGHT TOWER (round, larger) =====
1242
+ const rightTowerX = size * 0.85;
1243
+
1244
+ // Tower shadow
1245
+ ctx.fillStyle = stoneShadow;
1246
+ ctx.beginPath();
1247
+ ctx.ellipse(rightTowerX + size * 0.03, leftTowerY + size * 0.03, towerRadius, towerHeight / 2, 0, 0, Math.PI * 2);
1248
+ ctx.fill();
1249
+
1250
+ // Tower body with gradient
1251
+ const rightTowerGradient = ctx.createRadialGradient(rightTowerX + towerRadius * 0.3, leftTowerY, 0, rightTowerX, leftTowerY, towerRadius);
1252
+ rightTowerGradient.addColorStop(0, stoneHighlight);
1253
+ rightTowerGradient.addColorStop(0.5, stoneBase);
1254
+ rightTowerGradient.addColorStop(1, stoneDark);
1255
+ ctx.fillStyle = rightTowerGradient;
1256
+ ctx.beginPath();
1257
+ ctx.ellipse(rightTowerX, leftTowerY, towerRadius, towerHeight / 2, 0, 0, Math.PI * 2);
1258
+ ctx.fill();
1259
+ ctx.strokeStyle = stoneShadow;
1260
+ ctx.lineWidth = 2;
1261
+ ctx.stroke();
1262
+
1263
+ // Tower battlements
1264
+ ctx.fillStyle = stoneBase;
1265
+ for (let i = 0; i < 6; i++) {
1266
+ if (i % 2 === 0) {
1267
+ const angle = (i / 6) * Math.PI;
1268
+ const xOffset = Math.cos(angle) * towerRadius;
1269
+ ctx.fillRect(rightTowerX + xOffset - size * 0.06, -towerHeight - size * 0.1, size * 0.12, size * 0.1);
1270
+ ctx.strokeRect(rightTowerX + xOffset - size * 0.06, -towerHeight - size * 0.1, size * 0.12, size * 0.1);
1271
+ }
1272
+ }
1273
+
1274
+ // Tower windows
1275
+ ctx.fillStyle = '#1a1510';
1276
+ for (let i = 0; i < 3; i++) {
1277
+ const yWin = leftTowerY - towerHeight * 0.2 + i * towerHeight * 0.25;
1278
+ ctx.fillRect(rightTowerX - size * 0.02, yWin, size * 0.04, size * 0.15);
1279
+ ctx.beginPath();
1280
+ ctx.moveTo(rightTowerX - size * 0.02, yWin + size * 0.15);
1281
+ ctx.lineTo(rightTowerX - size * 0.06, yWin + size * 0.18);
1282
+ ctx.lineTo(rightTowerX + size * 0.06, yWin + size * 0.18);
1283
+ ctx.lineTo(rightTowerX + size * 0.02, yWin + size * 0.15);
1284
+ ctx.fill();
1285
+ }
1286
+
1287
+ // ===== GATEWAY (impressive arched entrance) =====
1288
+ const gateWidth = size * 0.35;
1289
+ const gateHeight = size * 0.5;
1290
+
1291
+ // Gateway arch recess (depth)
1292
+ ctx.fillStyle = stoneShadow;
1293
+ ctx.beginPath();
1294
+ ctx.moveTo(-gateWidth, -size * 0.05);
1295
+ ctx.lineTo(-gateWidth, -gateHeight);
1296
+ ctx.arc(0, -gateHeight, gateWidth, Math.PI, 0, false);
1297
+ ctx.lineTo(gateWidth, -size * 0.05);
1298
+ ctx.closePath();
1299
+ ctx.fill();
1300
+
1301
+ // Gateway arch surround (stonework)
1302
+ ctx.strokeStyle = stoneDark;
1303
+ ctx.lineWidth = size * 0.06;
1304
+ ctx.beginPath();
1305
+ ctx.arc(0, -gateHeight, gateWidth - size * 0.03, Math.PI, 0, false);
1306
+ ctx.stroke();
1307
+
1308
+ // Decorative voussoirs (arch stones)
1309
+ ctx.strokeStyle = 'rgba(74, 69, 64, 0.6)';
1310
+ ctx.lineWidth = 2;
1311
+ for (let i = 0; i < 9; i++) {
1312
+ const angle = Math.PI + (i / 8) * Math.PI;
1313
+ ctx.beginPath();
1314
+ ctx.moveTo(0, -gateHeight);
1315
+ ctx.lineTo(Math.cos(angle) * gateWidth, -gateHeight + Math.sin(angle) * gateWidth);
1316
+ ctx.stroke();
1317
+ }
1318
+
1319
+ // Gateway opening (dark)
1320
+ ctx.fillStyle = '#0a0a08';
1321
+ ctx.beginPath();
1322
+ ctx.moveTo(-gateWidth + size * 0.1, -size * 0.05);
1323
+ ctx.lineTo(-gateWidth + size * 0.1, -gateHeight + size * 0.05);
1324
+ ctx.arc(0, -gateHeight + size * 0.05, gateWidth - size * 0.1, Math.PI, 0, false);
1325
+ ctx.lineTo(gateWidth - size * 0.1, -size * 0.05);
1326
+ ctx.closePath();
1327
+ ctx.fill();
1328
+
1329
+ // Portcullis (heavy iron bars)
1330
+ ctx.strokeStyle = '#2a2520';
1331
+ ctx.lineWidth = 4;
1332
+ const barCount = 7;
1333
+ for (let i = 0; i < barCount; i++) {
1334
+ const xBar = -gateWidth + size * 0.15 + (i / (barCount - 1)) * (gateWidth * 2 - size * 0.3);
1335
+ ctx.beginPath();
1336
+ ctx.moveTo(xBar, -size * 0.05);
1337
+ ctx.lineTo(xBar, -gateHeight + size * 0.15);
1338
+ ctx.stroke();
1339
+ }
1340
+ // Horizontal bars
1341
+ ctx.lineWidth = 3;
1342
+ for (let i = 0; i < 4; i++) {
1343
+ const yBar = -size * 0.1 - i * size * 0.12;
1344
+ ctx.beginPath();
1345
+ ctx.moveTo(-gateWidth + size * 0.15, yBar);
1346
+ ctx.lineTo(gateWidth - size * 0.15, yBar);
1347
+ ctx.stroke();
1348
+ }
1349
+
1350
+ // ===== BUTTRESSES (structural supports) =====
1351
+ ctx.fillStyle = stoneDark;
1352
+ // Left buttress
1353
+ ctx.beginPath();
1354
+ ctx.moveTo(-keepWidth / 2, 0);
1355
+ ctx.lineTo(-keepWidth / 2 - size * 0.08, 0);
1356
+ ctx.lineTo(-keepWidth / 2 - size * 0.05, -keepHeight * 0.6);
1357
+ ctx.lineTo(-keepWidth / 2, -keepHeight * 0.6);
1358
+ ctx.closePath();
1359
+ ctx.fill();
1360
+ ctx.strokeStyle = stoneShadow;
1361
+ ctx.stroke();
1362
+
1363
+ // Right buttress
1364
+ ctx.beginPath();
1365
+ ctx.moveTo(keepWidth / 2, 0);
1366
+ ctx.lineTo(keepWidth / 2 + size * 0.08, 0);
1367
+ ctx.lineTo(keepWidth / 2 + size * 0.05, -keepHeight * 0.6);
1368
+ ctx.lineTo(keepWidth / 2, -keepHeight * 0.6);
1369
+ ctx.closePath();
1370
+ ctx.fill();
1371
+ ctx.stroke();
1372
+
1373
+ // ===== ST GEORGE'S CROSS FLAG =====
1374
+ ctx.save();
1375
+ ctx.translate(0, -towerHeight - size * 0.15);
1376
+
1377
+ // Flagpole (wooden)
1378
+ const poleGradient = ctx.createLinearGradient(-size * 0.02, 0, size * 0.02, 0);
1379
+ poleGradient.addColorStop(0, '#5a4a3a');
1380
+ poleGradient.addColorStop(0.5, '#8b7355');
1381
+ poleGradient.addColorStop(1, '#5a4a3a');
1382
+ ctx.fillStyle = poleGradient;
1383
+ ctx.fillRect(-size * 0.02, 0, size * 0.04, -size * 0.45);
1384
+ ctx.strokeStyle = '#3a2a1a';
1385
+ ctx.lineWidth = 1;
1386
+ ctx.strokeRect(-size * 0.02, 0, size * 0.04, -size * 0.45);
1387
+
1388
+ // Gold finial (decorative top)
1389
+ ctx.fillStyle = '#FFD700';
1390
+ ctx.beginPath();
1391
+ ctx.arc(0, -size * 0.45, size * 0.04, 0, Math.PI * 2);
1392
+ ctx.fill();
1393
+ ctx.strokeStyle = '#DAA520';
1394
+ ctx.stroke();
1395
+
1396
+ // Flag (white with red St George's cross)
1397
+ const flagWidth = size * 0.4;
1398
+ const flagHeight = size * 0.25;
1399
+ ctx.fillStyle = '#FFFFFF';
1400
+ ctx.fillRect(0, -size * 0.42, flagWidth, flagHeight);
1401
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
1402
+ ctx.strokeRect(0, -size * 0.42, flagWidth, flagHeight);
1403
+
1404
+ // Red cross (St George)
1405
+ ctx.fillStyle = '#C8102E';
1406
+ // Vertical bar
1407
+ ctx.fillRect(flagWidth * 0.4, -size * 0.42, flagWidth * 0.2, flagHeight);
1408
+ // Horizontal bar
1409
+ ctx.fillRect(0, -size * 0.42 + flagHeight * 0.4, flagWidth, flagHeight * 0.2);
1410
+
1411
+ ctx.restore();
1412
+
1413
+ ctx.restore();
1414
+ },
1415
+
1416
+ /**
1417
+ * Draw Tudor Rose (iconic English rose with 5 petals, red outer/white inner)
1418
+ */
1419
+ drawTudorRose(ctx, particle) {
1420
+ const x = particle.x;
1421
+ const y = particle.y;
1422
+ const size = particle.size;
1423
+
1424
+ ctx.save();
1425
+ ctx.globalAlpha = particle.opacity;
1426
+ ctx.translate(x, y);
1427
+ ctx.rotate(particle.rotation);
1428
+
1429
+ // Outer petals (red) - 5 petals
1430
+ ctx.fillStyle = '#C8102E';
1431
+ ctx.strokeStyle = '#8B0000';
1432
+ ctx.lineWidth = size * 0.05;
1433
+
1434
+ for (let i = 0; i < 5; i++) {
1435
+ const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
1436
+ ctx.save();
1437
+ ctx.rotate(angle);
1438
+
1439
+ // Heart-shaped petal
1440
+ ctx.beginPath();
1441
+ ctx.moveTo(0, 0);
1442
+ ctx.bezierCurveTo(-size * 0.5, -size * 0.4, -size * 0.6, -size * 0.8, 0, -size * 1.2);
1443
+ ctx.bezierCurveTo(size * 0.6, -size * 0.8, size * 0.5, -size * 0.4, 0, 0);
1444
+ ctx.closePath();
1445
+ ctx.fill();
1446
+ ctx.stroke();
1447
+
1448
+ ctx.restore();
1449
+ }
1450
+
1451
+ // Inner petals (white) - 5 petals, offset
1452
+ ctx.fillStyle = '#FFFFFF';
1453
+ ctx.strokeStyle = '#E0E0E0';
1454
+ for (let i = 0; i < 5; i++) {
1455
+ const angle = (i / 5) * Math.PI * 2 - Math.PI / 2 + Math.PI / 5;
1456
+ ctx.save();
1457
+ ctx.rotate(angle);
1458
+
1459
+ // Smaller heart-shaped petal
1460
+ ctx.beginPath();
1461
+ ctx.moveTo(0, 0);
1462
+ ctx.bezierCurveTo(-size * 0.3, -size * 0.25, -size * 0.35, -size * 0.5, 0, -size * 0.7);
1463
+ ctx.bezierCurveTo(size * 0.35, -size * 0.5, size * 0.3, -size * 0.25, 0, 0);
1464
+ ctx.closePath();
1465
+ ctx.fill();
1466
+ ctx.stroke();
1467
+
1468
+ ctx.restore();
1469
+ }
1470
+
1471
+ // Golden center
1472
+ ctx.fillStyle = '#FFD700';
1473
+ ctx.strokeStyle = '#DAA520';
1474
+ ctx.lineWidth = size * 0.05;
1475
+ ctx.beginPath();
1476
+ ctx.arc(0, 0, size * 0.2, 0, Math.PI * 2);
1477
+ ctx.fill();
1478
+ ctx.stroke();
1479
+
1480
+ // Stamens
1481
+ ctx.fillStyle = '#FFA500';
1482
+ for (let i = 0; i < 5; i++) {
1483
+ const angle = (i / 5) * Math.PI * 2;
1484
+ const dist = size * 0.12;
1485
+ ctx.beginPath();
1486
+ ctx.arc(Math.cos(angle) * dist, Math.sin(angle) * dist, size * 0.04, 0, Math.PI * 2);
1487
+ ctx.fill();
1488
+ }
1489
+
1490
+ ctx.restore();
1491
+ },
1492
+
1493
+ /**
1494
+ * Draw Oak Leaf (lobed English oak leaf)
1495
+ */
1496
+ drawOakLeaf(ctx, particle) {
1497
+ const x = particle.x;
1498
+ const y = particle.y;
1499
+ const size = particle.size;
1500
+
1501
+ ctx.save();
1502
+ ctx.globalAlpha = particle.opacity;
1503
+ ctx.translate(x, y);
1504
+ ctx.rotate(particle.rotation);
1505
+
1506
+ // Leaf color
1507
+ ctx.fillStyle = particle.color;
1508
+ ctx.strokeStyle = particle.color === '#8b4513' ? '#654321' : '#1a3a0a';
1509
+ ctx.lineWidth = size * 0.08;
1510
+
1511
+ // Oak leaf shape (characteristic lobes)
1512
+ ctx.beginPath();
1513
+ ctx.moveTo(0, size * 1.5); // Stem end
1514
+
1515
+ // Right side lobes (3 lobes)
1516
+ ctx.bezierCurveTo(size * 0.3, size * 1.2, size * 0.4, size * 1.0, size * 0.5, size * 0.8);
1517
+ ctx.bezierCurveTo(size * 0.7, size * 0.7, size * 0.8, size * 0.5, size * 0.6, size * 0.3);
1518
+ ctx.bezierCurveTo(size * 0.8, size * 0.2, size * 0.9, 0, size * 0.7, -size * 0.3);
1519
+ ctx.bezierCurveTo(size * 0.85, -size * 0.5, size * 0.8, -size * 0.8, size * 0.5, -size * 1.0);
1520
+
1521
+ // Tip
1522
+ ctx.bezierCurveTo(size * 0.3, -size * 1.2, 0, -size * 1.3, -size * 0.3, -size * 1.2);
1523
+
1524
+ // Left side lobes (3 lobes, mirror)
1525
+ ctx.bezierCurveTo(-size * 0.8, -size * 0.8, -size * 0.85, -size * 0.5, -size * 0.7, -size * 0.3);
1526
+ ctx.bezierCurveTo(-size * 0.9, 0, -size * 0.8, size * 0.2, -size * 0.6, size * 0.3);
1527
+ ctx.bezierCurveTo(-size * 0.8, size * 0.5, -size * 0.7, size * 0.7, -size * 0.5, size * 0.8);
1528
+ ctx.bezierCurveTo(-size * 0.4, size * 1.0, -size * 0.3, size * 1.2, 0, size * 1.5);
1529
+
1530
+ ctx.closePath();
1531
+ ctx.fill();
1532
+ ctx.stroke();
1533
+
1534
+ // Central vein
1535
+ ctx.strokeStyle = particle.color === '#8b4513' ? '#654321' : '#0a2505';
1536
+ ctx.lineWidth = size * 0.06;
1537
+ ctx.beginPath();
1538
+ ctx.moveTo(0, size * 1.4);
1539
+ ctx.lineTo(0, -size * 1.15);
1540
+ ctx.stroke();
1541
+
1542
+ // Side veins (branching)
1543
+ ctx.lineWidth = size * 0.03;
1544
+ for (let i = 0; i < 4; i++) {
1545
+ const yPos = size * 0.8 - i * size * 0.5;
1546
+ ctx.beginPath();
1547
+ ctx.moveTo(0, yPos);
1548
+ ctx.lineTo(size * 0.4, yPos - size * 0.15);
1549
+ ctx.stroke();
1550
+ ctx.beginPath();
1551
+ ctx.moveTo(0, yPos);
1552
+ ctx.lineTo(-size * 0.4, yPos - size * 0.15);
1553
+ ctx.stroke();
1554
+ }
1555
+
1556
+ ctx.restore();
1557
+ },
1558
+
1559
+ /**
1560
+ * Draw sparkle (English celebration sparkle)
1561
+ */
1562
+ drawSparkle(ctx, particle, time) {
1563
+ const x = particle.x;
1564
+ const y = particle.y;
1565
+ const size = particle.size;
1566
+ const twinkle = 0.6 + Math.sin(time * 0.004 + particle.twinklePhase) * 0.4;
1567
+
1568
+ ctx.save();
1569
+ ctx.globalAlpha = particle.opacity * twinkle;
1570
+ ctx.translate(x, y);
1571
+ ctx.rotate(particle.rotation);
1572
+
1573
+ // Draw 4-pointed sparkle
1574
+ ctx.fillStyle = particle.color;
1575
+ ctx.shadowColor = particle.color;
1576
+ ctx.shadowBlur = size * 2;
1577
+
1578
+ ctx.beginPath();
1579
+ ctx.moveTo(0, -size);
1580
+ ctx.lineTo(size * 0.3, -size * 0.3);
1581
+ ctx.lineTo(size, 0);
1582
+ ctx.lineTo(size * 0.3, size * 0.3);
1583
+ ctx.lineTo(0, size);
1584
+ ctx.lineTo(-size * 0.3, size * 0.3);
1585
+ ctx.lineTo(-size, 0);
1586
+ ctx.lineTo(-size * 0.3, -size * 0.3);
1587
+ ctx.closePath();
1588
+ ctx.fill();
1589
+
1590
+ ctx.restore();
1591
+ },
1592
+
1593
+ /**
1594
+ * Draw Royal Crown (St Edward's Crown style)
1595
+ */
1596
+ drawCrown(ctx, particle) {
1597
+ const x = particle.x;
1598
+ const y = particle.y;
1599
+ const size = particle.size;
1600
+ const glint = (Math.sin(particle.glint) + 1) * 0.5;
1601
+
1602
+ ctx.save();
1603
+ ctx.globalAlpha = particle.opacity;
1604
+ ctx.translate(x, y);
1605
+ ctx.rotate(particle.rotation);
1606
+
1607
+ // Crown base (golden)
1608
+ const crownGradient = ctx.createLinearGradient(-size, 0, size, 0);
1609
+ crownGradient.addColorStop(0, '#B8860B');
1610
+ crownGradient.addColorStop(0.5, '#FFD700');
1611
+ crownGradient.addColorStop(1, '#B8860B');
1612
+ ctx.fillStyle = crownGradient;
1613
+ ctx.strokeStyle = '#8B6914';
1614
+ ctx.lineWidth = size * 0.05;
1615
+
1616
+ // Crown band (base)
1617
+ ctx.beginPath();
1618
+ ctx.ellipse(0, size * 0.3, size * 0.8, size * 0.25, 0, 0, Math.PI * 2);
1619
+ ctx.fill();
1620
+ ctx.stroke();
1621
+
1622
+ // Lower band detail (jewels band)
1623
+ ctx.strokeStyle = '#8B0000';
1624
+ ctx.lineWidth = size * 0.08;
1625
+ ctx.beginPath();
1626
+ ctx.ellipse(0, size * 0.3, size * 0.7, size * 0.2, 0, 0, Math.PI * 2);
1627
+ ctx.stroke();
1628
+
1629
+ // Crown arches (5 points)
1630
+ ctx.fillStyle = crownGradient;
1631
+ ctx.strokeStyle = '#8B6914';
1632
+ ctx.lineWidth = size * 0.05;
1633
+
1634
+ for (let i = 0; i < 5; i++) {
1635
+ const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
1636
+ const baseX = Math.cos(angle) * size * 0.7;
1637
+ const baseY = size * 0.3 + Math.sin(angle) * size * 0.2;
1638
+
1639
+ // Fleur-de-lis point
1640
+ ctx.save();
1641
+ ctx.translate(baseX, baseY);
1642
+ ctx.rotate(angle + Math.PI / 2);
1643
+
1644
+ ctx.beginPath();
1645
+ // Center spike
1646
+ ctx.moveTo(0, -size * 0.8);
1647
+ ctx.lineTo(-size * 0.12, -size * 0.3);
1648
+ ctx.lineTo(size * 0.12, -size * 0.3);
1649
+ ctx.closePath();
1650
+ ctx.fill();
1651
+ ctx.stroke();
1652
+
1653
+ // Side curves
1654
+ ctx.beginPath();
1655
+ ctx.moveTo(-size * 0.12, -size * 0.3);
1656
+ ctx.bezierCurveTo(-size * 0.25, -size * 0.5, -size * 0.28, -size * 0.65, -size * 0.2, -size * 0.75);
1657
+ ctx.lineTo(-size * 0.15, -size * 0.65);
1658
+ ctx.bezierCurveTo(-size * 0.18, -size * 0.55, -size * 0.15, -size * 0.4, -size * 0.08, -size * 0.32);
1659
+ ctx.fill();
1660
+ ctx.stroke();
1661
+
1662
+ ctx.beginPath();
1663
+ ctx.moveTo(size * 0.12, -size * 0.3);
1664
+ ctx.bezierCurveTo(size * 0.25, -size * 0.5, size * 0.28, -size * 0.65, size * 0.2, -size * 0.75);
1665
+ ctx.lineTo(size * 0.15, -size * 0.65);
1666
+ ctx.bezierCurveTo(size * 0.18, -size * 0.55, size * 0.15, -size * 0.4, size * 0.08, -size * 0.32);
1667
+ ctx.fill();
1668
+ ctx.stroke();
1669
+
1670
+ ctx.restore();
1671
+ }
1672
+
1673
+ // Cross on top (St Edward's Crown has a cross)
1674
+ ctx.fillStyle = '#FFD700';
1675
+ ctx.strokeStyle = '#8B6914';
1676
+ ctx.lineWidth = size * 0.04;
1677
+ ctx.beginPath();
1678
+ // Vertical bar
1679
+ ctx.fillRect(-size * 0.05, -size * 0.9, size * 0.1, size * 0.25);
1680
+ ctx.strokeRect(-size * 0.05, -size * 0.9, size * 0.1, size * 0.25);
1681
+ // Horizontal bar
1682
+ ctx.fillRect(-size * 0.15, -size * 0.82, size * 0.3, size * 0.08);
1683
+ ctx.strokeRect(-size * 0.15, -size * 0.82, size * 0.3, size * 0.08);
1684
+
1685
+ // Jewels (red rubies, blue sapphires)
1686
+ const jewels = [
1687
+ { x: 0, y: size * 0.3, color: '#DC143C', size: 0.12 }, // Center ruby
1688
+ { x: size * 0.4, y: size * 0.3, color: '#0000CD', size: 0.08 }, // Right sapphire
1689
+ { x: -size * 0.4, y: size * 0.3, color: '#0000CD', size: 0.08 }, // Left sapphire
1690
+ ];
1691
+
1692
+ for (const jewel of jewels) {
1693
+ ctx.fillStyle = jewel.color;
1694
+ ctx.beginPath();
1695
+ ctx.arc(jewel.x, jewel.y, size * jewel.size, 0, Math.PI * 2);
1696
+ ctx.fill();
1697
+
1698
+ // Glint effect
1699
+ ctx.fillStyle = `rgba(255, 255, 255, ${glint * 0.7})`;
1700
+ ctx.beginPath();
1701
+ ctx.arc(jewel.x - size * jewel.size * 0.3, jewel.y - size * jewel.size * 0.3, size * jewel.size * 0.4, 0, Math.PI * 2);
1702
+ ctx.fill();
1703
+ }
1704
+
1705
+ ctx.restore();
1706
+ },
1707
+
1708
+ /**
1709
+ * Draw twinkling star
1710
+ */
1711
+ drawTwinklingStar(ctx, particle, time) {
1712
+ const x = particle.x;
1713
+ const y = particle.y;
1714
+ const size = particle.size;
1715
+
1716
+ const twinkleIntensity = 0.5 + Math.sin(time * particle.twinkleSpeed + particle.twinklePhase) * 0.5;
1717
+
1718
+ const starColor = `rgba(255, 220, 220, ${twinkleIntensity})`;
1719
+ const glowColor = `rgba(255, 180, 180, ${twinkleIntensity * 0.4})`;
1720
+
1721
+ ctx.save();
1722
+ ctx.translate(x, y);
1723
+
1724
+ ctx.shadowColor = glowColor;
1725
+ ctx.shadowBlur = size * 3 * twinkleIntensity;
1726
+ ctx.fillStyle = starColor;
1727
+
1728
+ ctx.beginPath();
1729
+ for (let i = 0; i < 4; i++) {
1730
+ const angle = (i * Math.PI) / 2;
1731
+ const outerX = Math.cos(angle) * size;
1732
+ const outerY = Math.sin(angle) * size;
1733
+ const innerAngle = angle + Math.PI / 4;
1734
+ const innerX = Math.cos(innerAngle) * (size * 0.3);
1735
+ const innerY = Math.sin(innerAngle) * (size * 0.3);
1736
+
1737
+ if (i === 0) {
1738
+ ctx.moveTo(outerX, outerY);
1739
+ } else {
1740
+ ctx.lineTo(outerX, outerY);
1741
+ }
1742
+ ctx.lineTo(innerX, innerY);
1743
+ }
1744
+ ctx.closePath();
1745
+ ctx.fill();
1746
+
1747
+ ctx.shadowBlur = size * 2 * twinkleIntensity;
1748
+ ctx.beginPath();
1749
+ ctx.arc(0, 0, size * 0.25, 0, Math.PI * 2);
1750
+ ctx.fill();
1751
+
1752
+ ctx.restore();
1753
+ }
1754
+ };