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,1258 @@
1
+ /**
2
+ * St David's Day Theme for Domma Celebrations
3
+ * (March 1st, Welsh Celebration)
4
+ *
5
+ * Features:
6
+ * - Falling daffodils (Welsh national flower)
7
+ * - Leek shapes (Welsh symbol)
8
+ * - Welsh dragon
9
+ * - Red and white color scheme (Welsh flag colors)
10
+ * - Daffodil fields
11
+ * - Spring-themed particles
12
+ */
13
+
14
+ export default {
15
+ name: 'st-davids',
16
+ displayName: 'St David\'s Day',
17
+ emoji: '🌻',
18
+
19
+ // Intensity configurations
20
+ intensityConfig: {
21
+ light: {
22
+ count: 50,
23
+ speedRange: [0.4, 1.2],
24
+ sizeRange: [2, 4],
25
+ daffodilFields: 2,
26
+ dragonChance: 0.0002,
27
+ twinklingStars: 10
28
+ },
29
+ medium: {
30
+ count: 100,
31
+ speedRange: [0.5, 1.5],
32
+ sizeRange: [2, 5],
33
+ daffodilFields: 3,
34
+ dragonChance: 0.0004,
35
+ twinklingStars: 18
36
+ },
37
+ heavy: {
38
+ count: 150,
39
+ speedRange: [0.6, 1.8],
40
+ sizeRange: [3, 6],
41
+ daffodilFields: 4,
42
+ dragonChance: 0.0006,
43
+ twinklingStars: 25
44
+ }
45
+ },
46
+
47
+ particles: ['daffodil-petal', 'daffodil', 'spring-sparkle'],
48
+ decorations: ['daffodil-field', 'welsh-dragon', 'flag', 'harp', 'leek-bundle', 'twinkling-star'],
49
+ colors: {
50
+ primary: '#FFDD00', // Daffodil yellow
51
+ secondary: '#228B22', // Green (leeks)
52
+ accent: '#DC143C', // Welsh red
53
+ white: '#FFFFFF' // Welsh white
54
+ },
55
+
56
+ /**
57
+ * Create daffodil petal particle (simple yellow petal)
58
+ */
59
+ createDaffodilPetal(canvasWidth, canvasHeight, config) {
60
+ const yellowShades = ['#FFDD00', '#FFE135', '#FFED4E', '#FFF68F'];
61
+ return {
62
+ type: 'daffodil-petal',
63
+ x: -30, // Start from left edge
64
+ y: Math.random() * canvasHeight, // Random height
65
+ vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
66
+ size: (config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0])) * 1.5, // Larger petals
67
+ speed: (Math.random() - 0.5) * 0.2, // Gentle vertical bobbing
68
+ opacity: 0.75 + Math.random() * 0.25,
69
+ windOffset: Math.random() * Math.PI * 2,
70
+ windSpeed: 0.015 + Math.random() * 0.02,
71
+ rotation: Math.random() * Math.PI * 2,
72
+ rotationSpeed: (Math.random() - 0.5) * 0.03,
73
+ color: yellowShades[Math.floor(Math.random() * yellowShades.length)],
74
+ flutter: Math.random() * Math.PI * 2,
75
+ flutterSpeed: 0.02 + Math.random() * 0.02,
76
+ active: true
77
+ };
78
+ },
79
+
80
+ /**
81
+ * Create daffodil particle (full flower - rare)
82
+ */
83
+ createDaffodil(canvasWidth, canvasHeight, config) {
84
+ return {
85
+ type: 'daffodil',
86
+ x: -30, // Start from left edge
87
+ y: Math.random() * canvasHeight, // Random height
88
+ vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
89
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
90
+ speed: 0.1, // Minimal vertical movement
91
+ opacity: 0.8 + Math.random() * 0.2,
92
+ windOffset: Math.random() * Math.PI * 2,
93
+ windSpeed: 0.015 + Math.random() * 0.02,
94
+ rotation: Math.random() * Math.PI * 2,
95
+ rotationSpeed: (Math.random() - 0.5) * 0.02,
96
+ active: true
97
+ };
98
+ },
99
+
100
+ /**
101
+ * Create leek particle
102
+ */
103
+ createLeek(canvasWidth, canvasHeight, config) {
104
+ return {
105
+ type: 'leek',
106
+ x: -30, // Start from left edge
107
+ y: Math.random() * canvasHeight, // Random height
108
+ vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift (was vertical speed)
109
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 1.2,
110
+ speed: 0.15, // Minimal vertical movement (was downward falling)
111
+ opacity: 0.7 + Math.random() * 0.3,
112
+ rotation: Math.random() * Math.PI * 2,
113
+ rotationSpeed: (Math.random() - 0.5) * 0.03,
114
+ windOffset: Math.random() * Math.PI * 2,
115
+ windSpeed: 0.02 + Math.random() * 0.03,
116
+ active: true
117
+ };
118
+ },
119
+
120
+ /**
121
+ * Create spring sparkle particle (Welsh spring colors)
122
+ */
123
+ createSpringSparkle(canvasWidth, canvasHeight, config) {
124
+ const colors = ['#FFD700', '#FFFFFF', '#90EE90', '#98FB98']; // Gold, white, light green
125
+ return {
126
+ type: 'spring-sparkle',
127
+ x: -20, // Start from left edge
128
+ y: Math.random() * canvasHeight, // Random height
129
+ vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift (was vertical speed)
130
+ size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.6,
131
+ vy: (Math.random() - 0.5) * 0.2, // Minimal random vertical movement
132
+ opacity: 0.6 + Math.random() * 0.4,
133
+ rotation: Math.random() * Math.PI * 2,
134
+ rotationSpeed: (Math.random() - 0.5) * 0.04,
135
+ color: colors[Math.floor(Math.random() * colors.length)],
136
+ twinklePhase: Math.random() * Math.PI * 2,
137
+ windOffset: Math.random() * Math.PI * 2,
138
+ windSpeed: 0.015 + Math.random() * 0.02,
139
+ active: true,
140
+ static: false
141
+ };
142
+ },
143
+
144
+ /**
145
+ * Create drifting particle (randomly picks type)
146
+ * Note: St. David's Day particles drift horizontally (left-to-right), not vertically
147
+ */
148
+ createFallingParticle(canvasWidth, canvasHeight, config) {
149
+ const choice = Math.random();
150
+
151
+ // 60% daffodil petals, 20% full daffodils, 15% sparkles, 5% leeks
152
+ if (choice < 0.6) {
153
+ return this.createDaffodilPetal(canvasWidth, canvasHeight, config);
154
+ } else if (choice < 0.8) {
155
+ return this.createDaffodil(canvasWidth, canvasHeight, config);
156
+ } else if (choice < 0.95) {
157
+ return this.createSpringSparkle(canvasWidth, canvasHeight, config);
158
+ } else {
159
+ return this.createLeek(canvasWidth, canvasHeight, config);
160
+ }
161
+ },
162
+
163
+ /**
164
+ * Create daffodil field decoration
165
+ */
166
+ createDaffodilField(canvasWidth, canvasHeight, options = {}) {
167
+ const flowers = [];
168
+ const flowerCount = 15 + Math.floor(Math.random() * 10);
169
+
170
+ for (let i = 0; i < flowerCount; i++) {
171
+ flowers.push({
172
+ x: Math.random() * canvasWidth * 0.3,
173
+ y: Math.random() * 40,
174
+ size: 5 + Math.random() * 8,
175
+ swayPhase: Math.random() * Math.PI * 2
176
+ });
177
+ }
178
+
179
+ return {
180
+ type: 'daffodil-field',
181
+ x: options.x !== undefined ? options.x : Math.random() * canvasWidth * 0.7,
182
+ y: options.y !== undefined ? options.y : canvasHeight - 50,
183
+ flowers: flowers,
184
+ opacity: 0.9,
185
+ time: 0,
186
+ active: true,
187
+ static: true
188
+ };
189
+ },
190
+
191
+ /**
192
+ * Create initial static decorations (Welsh-themed)
193
+ */
194
+ createInitialDecorations(canvasWidth, canvasHeight, config) {
195
+ const decorations = [];
196
+
197
+ // Daffodil fields
198
+ const fieldCount = config.daffodilFields || 3;
199
+ for (let i = 0; i < fieldCount; i++) {
200
+ decorations.push(this.createDaffodilField(canvasWidth, canvasHeight, {
201
+ x: (canvasWidth / (fieldCount + 1)) * (i + 1),
202
+ y: canvasHeight - 50
203
+ }));
204
+ }
205
+
206
+ // Welsh dragon (Y Ddraig Goch - prominent red dragon, left side)
207
+ decorations.push({
208
+ type: 'welsh-dragon',
209
+ x: 150,
210
+ y: canvasHeight - 80,
211
+ baseY: canvasHeight - 80,
212
+ vx: 0, // Static display
213
+ size: 35 + Math.random() * 10,
214
+ opacity: 1,
215
+ time: 0,
216
+ wingPhase: Math.random() * Math.PI * 2,
217
+ breatheFirePhase: Math.random() * Math.PI * 2,
218
+ active: true,
219
+ static: true
220
+ });
221
+
222
+ // Welsh harp (traditional instrument, center-right)
223
+ decorations.push({
224
+ type: 'harp',
225
+ x: canvasWidth - 200,
226
+ y: canvasHeight - 90,
227
+ size: 40 + Math.random() * 10,
228
+ opacity: 1,
229
+ glintPhase: Math.random() * Math.PI * 2,
230
+ active: true,
231
+ static: true
232
+ });
233
+
234
+ // Leek bundle (national vegetable, decorative display)
235
+ decorations.push({
236
+ type: 'leek-bundle',
237
+ x: canvasWidth * 0.3,
238
+ y: canvasHeight - 40,
239
+ size: 25 + Math.random() * 5,
240
+ opacity: 1,
241
+ active: true,
242
+ static: true
243
+ });
244
+
245
+ // Welsh flag (Y Ddraig Goch on flag)
246
+ decorations.push({
247
+ type: 'flag',
248
+ x: canvasWidth - 100,
249
+ y: 80,
250
+ size: 50,
251
+ opacity: 1,
252
+ wavePhase: Math.random() * Math.PI * 2,
253
+ active: true,
254
+ static: true
255
+ });
256
+
257
+ // Create twinkling stars
258
+ const starCount = config.twinklingStars || 18;
259
+ for (let i = 0; i < starCount; i++) {
260
+ decorations.push(this.createTwinklingStar(canvasWidth, canvasHeight));
261
+ }
262
+
263
+ return decorations;
264
+ },
265
+
266
+ /**
267
+ * Create twinkling star particle
268
+ */
269
+ createTwinklingStar(canvasWidth, canvasHeight) {
270
+ return {
271
+ type: 'twinkling-star',
272
+ x: Math.random() * canvasWidth,
273
+ y: Math.random() * (canvasHeight * 0.5),
274
+ size: 1 + Math.random() * 2,
275
+ opacity: 0.6 + Math.random() * 0.3,
276
+ twinklePhase: Math.random() * Math.PI * 2,
277
+ twinkleSpeed: 0.003 + Math.random() * 0.003,
278
+ active: true,
279
+ static: true
280
+ };
281
+ },
282
+
283
+ /**
284
+ * Spawn special St David's particles
285
+ */
286
+ spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight, config) {
287
+ const choice = Math.random();
288
+
289
+ // Welsh dragon (very rare, max 1)
290
+ if (choice < config.dragonChance) {
291
+ if (specialParticles.some(p => p.type === 'welsh-dragon')) {
292
+ return null;
293
+ }
294
+ const fromLeft = Math.random() < 0.5;
295
+ return {
296
+ type: 'welsh-dragon',
297
+ x: fromLeft ? -100 : canvasWidth + 100,
298
+ y: Math.random() * canvasHeight * 0.4 + 50,
299
+ baseY: Math.random() * canvasHeight * 0.4 + 50,
300
+ vx: fromLeft ? 2 + Math.random() * 1 : -(2 + Math.random() * 1),
301
+ size: 25 + Math.random() * 15,
302
+ opacity: 1,
303
+ waveAmplitude: 25 + Math.random() * 20,
304
+ waveFrequency: 0.002 + Math.random() * 0.002,
305
+ waveOffset: Math.random() * Math.PI * 2,
306
+ time: 0,
307
+ wingPhase: Math.random() * Math.PI * 2,
308
+ breatheFirePhase: Math.random() * Math.PI * 2,
309
+ fireParticles: [],
310
+ active: true,
311
+ static: false
312
+ };
313
+ }
314
+
315
+ // Daffodil field (rare)
316
+ if (choice < 0.0004) {
317
+ const fieldCount = specialParticles.filter(p => p.type === 'daffodil-field').length;
318
+ if (fieldCount < config.daffodilFields) {
319
+ return this.createDaffodilField(canvasWidth, canvasHeight);
320
+ }
321
+ }
322
+
323
+ return null;
324
+ },
325
+
326
+ /**
327
+ * Draw daffodil petal (simple elliptical petal shape)
328
+ */
329
+ drawDaffodilPetal(ctx, particle, time) {
330
+ const x = particle.x;
331
+ const y = particle.y;
332
+ const size = particle.size;
333
+
334
+ // Flutter effect (petal curling as it drifts)
335
+ const flutter = Math.sin(time * particle.flutterSpeed + particle.flutter) * 0.3;
336
+
337
+ ctx.save();
338
+ ctx.globalAlpha = particle.opacity;
339
+ ctx.translate(x, y);
340
+ ctx.rotate(particle.rotation + flutter);
341
+
342
+ // Create petal shape (elongated ellipse)
343
+ ctx.fillStyle = particle.color;
344
+ ctx.strokeStyle = '#FFB700'; // Golden edge
345
+ ctx.lineWidth = size * 0.08;
346
+
347
+ ctx.beginPath();
348
+ // Petal is wider at one end (teardrop shape)
349
+ ctx.ellipse(0, 0, size * 0.6, size * 1.2, 0, 0, Math.PI * 2);
350
+ ctx.fill();
351
+ ctx.stroke();
352
+
353
+ // Add subtle detail line (petal vein)
354
+ ctx.strokeStyle = 'rgba(255, 200, 0, 0.4)';
355
+ ctx.lineWidth = size * 0.06;
356
+ ctx.beginPath();
357
+ ctx.moveTo(0, -size * 1.1);
358
+ ctx.lineTo(0, size * 1.1);
359
+ ctx.stroke();
360
+
361
+ ctx.restore();
362
+ },
363
+
364
+ /**
365
+ * Draw daffodil (full flower)
366
+ */
367
+ drawDaffodil(ctx, particle) {
368
+ const x = particle.x;
369
+ const y = particle.y;
370
+ const size = particle.size;
371
+
372
+ ctx.save();
373
+ ctx.globalAlpha = particle.opacity;
374
+ ctx.translate(x, y);
375
+ ctx.rotate(particle.rotation);
376
+
377
+ // Trumpet (center corona)
378
+ ctx.fillStyle = '#FFA500'; // Orange center
379
+ ctx.strokeStyle = '#FF8C00';
380
+ ctx.lineWidth = size * 0.08;
381
+ ctx.beginPath();
382
+ ctx.moveTo(-size * 0.25, 0);
383
+ ctx.lineTo(-size * 0.3, size * 0.3);
384
+ ctx.lineTo(size * 0.3, size * 0.3);
385
+ ctx.lineTo(size * 0.25, 0);
386
+ ctx.closePath();
387
+ ctx.fill();
388
+ ctx.stroke();
389
+
390
+ // Petals (6 petals around trumpet)
391
+ ctx.fillStyle = '#FFDD00';
392
+ ctx.strokeStyle = '#FFC700';
393
+ ctx.lineWidth = size * 0.06;
394
+
395
+ for (let i = 0; i < 6; i++) {
396
+ const angle = (i / 6) * Math.PI * 2 - Math.PI / 2;
397
+ ctx.save();
398
+ ctx.rotate(angle);
399
+ ctx.translate(0, -size * 0.7);
400
+
401
+ // Petal shape (pointed ellipse)
402
+ ctx.beginPath();
403
+ ctx.ellipse(0, 0, size * 0.4, size * 0.6, 0, 0, Math.PI * 2);
404
+ ctx.fill();
405
+ ctx.stroke();
406
+
407
+ ctx.restore();
408
+ }
409
+
410
+ // Stem
411
+ ctx.strokeStyle = '#228B22';
412
+ ctx.lineWidth = size * 0.12;
413
+ ctx.beginPath();
414
+ ctx.moveTo(0, size * 0.3);
415
+ ctx.lineTo(0, size * 1.5);
416
+ ctx.stroke();
417
+
418
+ ctx.restore();
419
+ },
420
+
421
+ /**
422
+ * Draw leek
423
+ */
424
+ drawLeek(ctx, particle) {
425
+ const x = particle.x;
426
+ const y = particle.y;
427
+ const size = particle.size;
428
+
429
+ ctx.save();
430
+ ctx.globalAlpha = particle.opacity;
431
+ ctx.translate(x, y);
432
+ ctx.rotate(particle.rotation);
433
+
434
+ // White bulb (bottom)
435
+ ctx.fillStyle = '#F5F5F5';
436
+ ctx.strokeStyle = '#DCDCDC';
437
+ ctx.lineWidth = size * 0.08;
438
+ ctx.beginPath();
439
+ ctx.ellipse(0, size * 1.2, size * 0.4, size * 0.5, 0, 0, Math.PI * 2);
440
+ ctx.fill();
441
+ ctx.stroke();
442
+
443
+ // Green stalk/leaves
444
+ ctx.fillStyle = '#228B22';
445
+ ctx.strokeStyle = '#006400';
446
+ ctx.lineWidth = size * 0.05;
447
+
448
+ // Main stalk
449
+ ctx.beginPath();
450
+ ctx.moveTo(-size * 0.3, size * 1.2);
451
+ ctx.quadraticCurveTo(-size * 0.25, size * 0.5, -size * 0.15, 0);
452
+ ctx.quadraticCurveTo(0, -size * 0.2, size * 0.15, 0);
453
+ ctx.quadraticCurveTo(size * 0.25, size * 0.5, size * 0.3, size * 1.2);
454
+ ctx.closePath();
455
+ ctx.fill();
456
+ ctx.stroke();
457
+
458
+ // Leaves spreading at top
459
+ ctx.beginPath();
460
+ ctx.moveTo(-size * 0.15, 0);
461
+ ctx.quadraticCurveTo(-size * 0.6, -size * 0.3, -size * 0.8, 0);
462
+ ctx.stroke();
463
+
464
+ ctx.beginPath();
465
+ ctx.moveTo(size * 0.15, 0);
466
+ ctx.quadraticCurveTo(size * 0.6, -size * 0.3, size * 0.8, 0);
467
+ ctx.stroke();
468
+
469
+ // Roots
470
+ ctx.strokeStyle = '#D2B48C';
471
+ ctx.lineWidth = size * 0.05;
472
+ for (let i = 0; i < 5; i++) {
473
+ const rootX = (i - 2) * size * 0.15;
474
+ ctx.beginPath();
475
+ ctx.moveTo(rootX, size * 1.5);
476
+ ctx.lineTo(rootX + Math.sin(i * 1.7) * size * 0.15, size * 1.8);
477
+ ctx.stroke();
478
+ }
479
+
480
+ ctx.restore();
481
+ },
482
+
483
+ /**
484
+ * Draw spring sparkle
485
+ */
486
+ drawSpringSparkle(ctx, particle, time) {
487
+ const x = particle.x;
488
+ const y = particle.y;
489
+ const size = particle.size;
490
+ const twinkle = 0.6 + Math.sin(time * 0.004 + particle.twinklePhase) * 0.4;
491
+
492
+ ctx.save();
493
+ ctx.globalAlpha = particle.opacity * twinkle;
494
+ ctx.translate(x, y);
495
+ ctx.rotate(particle.rotation);
496
+
497
+ // Draw 4-pointed sparkle
498
+ ctx.fillStyle = particle.color;
499
+ ctx.shadowColor = particle.color;
500
+ ctx.shadowBlur = size * 2;
501
+
502
+ ctx.beginPath();
503
+ ctx.moveTo(0, -size);
504
+ ctx.lineTo(size * 0.3, -size * 0.3);
505
+ ctx.lineTo(size, 0);
506
+ ctx.lineTo(size * 0.3, size * 0.3);
507
+ ctx.lineTo(0, size);
508
+ ctx.lineTo(-size * 0.3, size * 0.3);
509
+ ctx.lineTo(-size, 0);
510
+ ctx.lineTo(-size * 0.3, -size * 0.3);
511
+ ctx.closePath();
512
+ ctx.fill();
513
+
514
+ ctx.restore();
515
+ },
516
+
517
+ /**
518
+ * Draw daffodil field
519
+ */
520
+ drawDaffodilField(ctx, particle, time) {
521
+ const x = particle.x;
522
+ const y = particle.y;
523
+
524
+ ctx.save();
525
+ ctx.globalAlpha = particle.opacity;
526
+
527
+ particle.flowers.forEach(flower => {
528
+ const sway = Math.sin(time * 0.001 + flower.swayPhase) * flower.size * 0.2;
529
+
530
+ ctx.save();
531
+ ctx.translate(x + flower.x, y - flower.y);
532
+ ctx.rotate(sway * 0.1);
533
+
534
+ // Stem
535
+ ctx.strokeStyle = '#228B22';
536
+ ctx.lineWidth = flower.size * 0.08;
537
+ ctx.beginPath();
538
+ ctx.moveTo(0, 0);
539
+ ctx.lineTo(sway, -flower.size * 2);
540
+ ctx.stroke();
541
+
542
+ // Flower head (simplified)
543
+ ctx.fillStyle = '#FFDD00';
544
+ ctx.shadowColor = '#FFDD00';
545
+ ctx.shadowBlur = flower.size * 0.5;
546
+ ctx.beginPath();
547
+ ctx.arc(sway, -flower.size * 2, flower.size * 0.5, 0, Math.PI * 2);
548
+ ctx.fill();
549
+
550
+ // Orange center
551
+ ctx.shadowBlur = 0;
552
+ ctx.fillStyle = '#FFA500';
553
+ ctx.beginPath();
554
+ ctx.arc(sway, -flower.size * 2, flower.size * 0.25, 0, Math.PI * 2);
555
+ ctx.fill();
556
+
557
+ ctx.restore();
558
+ });
559
+
560
+ ctx.restore();
561
+ },
562
+
563
+ /**
564
+ * Draw Welsh dragon
565
+ */
566
+ drawWelshDragon(ctx, particle, time) {
567
+ const x = particle.x;
568
+ const y = particle.y;
569
+ const size = particle.size;
570
+ const dir = particle.vx > 0 ? 1 : -1;
571
+
572
+ ctx.save();
573
+ ctx.translate(x, y);
574
+ if (dir === -1) {
575
+ ctx.scale(-1, 1);
576
+ }
577
+
578
+ // Wing flapping
579
+ const wingAngle = Math.sin(time * 0.012 + particle.wingPhase) * (Math.PI / 3);
580
+
581
+ // Wings (behind body)
582
+ ctx.fillStyle = '#DC143C'; // Red
583
+ ctx.strokeStyle = '#8B0000'; // Dark red
584
+ ctx.lineWidth = 2;
585
+
586
+ // Back wing
587
+ ctx.save();
588
+ ctx.translate(-size * 0.3, -size * 0.3);
589
+ ctx.rotate(-wingAngle * 0.9);
590
+ ctx.beginPath();
591
+ ctx.moveTo(0, 0);
592
+ ctx.bezierCurveTo(
593
+ -size * 1.2, -size * 0.8,
594
+ -size * 1.5, -size * 0.3,
595
+ -size * 1.3, size * 0.3
596
+ );
597
+ ctx.bezierCurveTo(
598
+ -size * 0.8, size * 0.2,
599
+ -size * 0.3, 0,
600
+ 0, 0
601
+ );
602
+ ctx.closePath();
603
+ ctx.fill();
604
+ ctx.stroke();
605
+ ctx.restore();
606
+
607
+ // Front wing
608
+ ctx.save();
609
+ ctx.translate(size * 0.3, -size * 0.3);
610
+ ctx.rotate(wingAngle);
611
+ ctx.beginPath();
612
+ ctx.moveTo(0, 0);
613
+ ctx.bezierCurveTo(
614
+ size * 1.2, -size * 0.8,
615
+ size * 1.5, -size * 0.3,
616
+ size * 1.3, size * 0.3
617
+ );
618
+ ctx.bezierCurveTo(
619
+ size * 0.8, size * 0.2,
620
+ size * 0.3, 0,
621
+ 0, 0
622
+ );
623
+ ctx.closePath();
624
+ ctx.fill();
625
+ ctx.stroke();
626
+ ctx.restore();
627
+
628
+ // Body
629
+ ctx.fillStyle = '#DC143C';
630
+ ctx.strokeStyle = '#8B0000';
631
+ ctx.lineWidth = 2;
632
+ ctx.beginPath();
633
+ ctx.ellipse(0, 0, size * 0.7, size * 0.5, 0, 0, Math.PI * 2);
634
+ ctx.fill();
635
+ ctx.stroke();
636
+
637
+ // Tail (spiky)
638
+ ctx.strokeStyle = '#DC143C';
639
+ ctx.lineWidth = size * 0.25;
640
+ ctx.beginPath();
641
+ ctx.moveTo(-size * 0.5, 0);
642
+ ctx.quadraticCurveTo(-size * 1.2, -size * 0.3, -size * 1.5, 0);
643
+ ctx.stroke();
644
+
645
+ // Tail spikes
646
+ ctx.fillStyle = '#FFD700'; // Gold
647
+ for (let i = 0; i < 3; i++) {
648
+ const tailX = -size * (0.8 + i * 0.25);
649
+ ctx.beginPath();
650
+ ctx.moveTo(tailX - size * 0.1, -size * 0.2);
651
+ ctx.lineTo(tailX, -size * 0.5);
652
+ ctx.lineTo(tailX + size * 0.1, -size * 0.2);
653
+ ctx.closePath();
654
+ ctx.fill();
655
+ }
656
+
657
+ // Head
658
+ ctx.fillStyle = '#DC143C';
659
+ ctx.beginPath();
660
+ ctx.ellipse(size * 0.8, -size * 0.2, size * 0.4, size * 0.35, 0, 0, Math.PI * 2);
661
+ ctx.fill();
662
+ ctx.stroke();
663
+
664
+ // Snout
665
+ ctx.fillStyle = '#DC143C';
666
+ ctx.beginPath();
667
+ ctx.moveTo(size * 1.1, -size * 0.2);
668
+ ctx.quadraticCurveTo(size * 1.4, -size * 0.15, size * 1.3, 0);
669
+ ctx.quadraticCurveTo(size * 1.4, 0.1, size * 1.1, -size * 0.2);
670
+ ctx.closePath();
671
+ ctx.fill();
672
+
673
+ // Eye
674
+ ctx.fillStyle = '#FFD700';
675
+ ctx.beginPath();
676
+ ctx.arc(size * 0.9, -size * 0.3, size * 0.12, 0, Math.PI * 2);
677
+ ctx.fill();
678
+ ctx.fillStyle = '#000000';
679
+ ctx.beginPath();
680
+ ctx.arc(size * 0.95, -size * 0.3, size * 0.06, 0, Math.PI * 2);
681
+ ctx.fill();
682
+
683
+ // Horns
684
+ ctx.strokeStyle = '#FFD700';
685
+ ctx.lineWidth = size * 0.1;
686
+ ctx.beginPath();
687
+ ctx.moveTo(size * 0.7, -size * 0.5);
688
+ ctx.lineTo(size * 0.65, -size * 0.8);
689
+ ctx.stroke();
690
+ ctx.beginPath();
691
+ ctx.moveTo(size * 0.9, -size * 0.5);
692
+ ctx.lineTo(size * 0.95, -size * 0.8);
693
+ ctx.stroke();
694
+
695
+ // Fire breath (occasional)
696
+ if (Math.sin(time * 0.005 + particle.breatheFirePhase) > 0.5) {
697
+ const flameGradient = ctx.createLinearGradient(size * 1.3, -size * 0.1, size * 2, -size * 0.1);
698
+ flameGradient.addColorStop(0, '#FF6600');
699
+ flameGradient.addColorStop(0.5, '#FFA500');
700
+ flameGradient.addColorStop(1, 'rgba(255, 255, 0, 0)');
701
+
702
+ ctx.fillStyle = flameGradient;
703
+ ctx.globalAlpha = 0.8;
704
+ ctx.beginPath();
705
+ ctx.moveTo(size * 1.3, -size * 0.1);
706
+ ctx.bezierCurveTo(
707
+ size * 1.6, -size * 0.3,
708
+ size * 1.8, -size * 0.2,
709
+ size * 2, -size * 0.1
710
+ );
711
+ ctx.bezierCurveTo(
712
+ size * 1.8, 0,
713
+ size * 1.6, 0.1,
714
+ size * 1.3, -size * 0.1
715
+ );
716
+ ctx.closePath();
717
+ ctx.fill();
718
+ }
719
+
720
+ ctx.restore();
721
+ },
722
+
723
+ /**
724
+ * Draw Welsh harp (traditional instrument)
725
+ */
726
+ drawHarp(ctx, particle, time) {
727
+ const x = particle.x;
728
+ const y = particle.y;
729
+ const size = particle.size;
730
+
731
+ ctx.save();
732
+ ctx.globalAlpha = particle.opacity;
733
+ ctx.translate(x, y);
734
+
735
+ // Harp frame (curved wooden structure)
736
+ ctx.strokeStyle = '#8b4513';
737
+ ctx.lineWidth = size * 0.08;
738
+ ctx.lineCap = 'round';
739
+
740
+ // Curved pillar (left side)
741
+ ctx.beginPath();
742
+ ctx.moveTo(-size * 0.3, size * 0.5);
743
+ ctx.bezierCurveTo(
744
+ -size * 0.5, size * 0.1,
745
+ -size * 0.4, -size * 0.4,
746
+ -size * 0.2, -size * 0.7
747
+ );
748
+ ctx.stroke();
749
+
750
+ // Straight neck (top)
751
+ ctx.beginPath();
752
+ ctx.moveTo(-size * 0.2, -size * 0.7);
753
+ ctx.lineTo(size * 0.4, -size * 0.5);
754
+ ctx.stroke();
755
+
756
+ // Soundboard (right side)
757
+ ctx.beginPath();
758
+ ctx.moveTo(size * 0.4, -size * 0.5);
759
+ ctx.lineTo(-size * 0.3, size * 0.5);
760
+ ctx.stroke();
761
+
762
+ // Harp strings (golden)
763
+ ctx.strokeStyle = '#FFD700';
764
+ ctx.lineWidth = size * 0.02;
765
+ const stringCount = 12;
766
+ for (let i = 0; i < stringCount; i++) {
767
+ const t = i / (stringCount - 1);
768
+ const startX = -size * 0.3 + t * size * 0.65;
769
+ const startY = size * 0.5 - t * size * 1.0;
770
+ const endX = -size * 0.25 + t * size * 0.55;
771
+ const endY = -size * 0.65 + t * size * 0.15;
772
+
773
+ // Slight wave to strings
774
+ const wave = Math.sin(time * 0.003 + i * 0.3 + particle.glintPhase) * size * 0.02;
775
+
776
+ ctx.beginPath();
777
+ ctx.moveTo(startX + wave, startY);
778
+ ctx.lineTo(endX + wave, endY);
779
+ ctx.stroke();
780
+ }
781
+
782
+ // Decorative carvings on pillar (Celtic patterns)
783
+ ctx.strokeStyle = '#654321';
784
+ ctx.lineWidth = size * 0.03;
785
+ ctx.globalAlpha = 0.6;
786
+ for (let i = 0; i < 4; i++) {
787
+ const yPos = size * 0.3 - i * size * 0.25;
788
+ ctx.beginPath();
789
+ ctx.arc(-size * 0.35, yPos, size * 0.08, 0, Math.PI * 2);
790
+ ctx.stroke();
791
+ }
792
+
793
+ ctx.restore();
794
+ },
795
+
796
+ /**
797
+ * Draw leek bundle (Welsh national vegetable)
798
+ */
799
+ drawLeekBundle(ctx, particle) {
800
+ const x = particle.x;
801
+ const y = particle.y;
802
+ const size = particle.size;
803
+
804
+ ctx.save();
805
+ ctx.globalAlpha = particle.opacity;
806
+ ctx.translate(x, y);
807
+
808
+ // Bundle of 5 leeks
809
+ for (let i = 0; i < 5; i++) {
810
+ ctx.save();
811
+ const offsetX = (i - 2) * size * 0.15;
812
+ const rotation = (i - 2) * 0.1;
813
+ ctx.translate(offsetX, 0);
814
+ ctx.rotate(rotation);
815
+
816
+ // Leek white part (bulb)
817
+ ctx.fillStyle = '#f5f5dc';
818
+ ctx.strokeStyle = '#d3d3c0';
819
+ ctx.lineWidth = 1;
820
+ ctx.fillRect(-size * 0.08, size * 0.2, size * 0.16, size * 0.6);
821
+ ctx.strokeRect(-size * 0.08, size * 0.2, size * 0.16, size * 0.6);
822
+
823
+ // Leek green part (leaves)
824
+ ctx.fillStyle = '#228B22';
825
+ ctx.strokeStyle = '#1a6b1a';
826
+ ctx.beginPath();
827
+ ctx.moveTo(-size * 0.08, size * 0.2);
828
+ ctx.lineTo(-size * 0.12, -size * 0.5);
829
+ ctx.lineTo(0, -size * 0.7);
830
+ ctx.lineTo(size * 0.12, -size * 0.5);
831
+ ctx.lineTo(size * 0.08, size * 0.2);
832
+ ctx.closePath();
833
+ ctx.fill();
834
+ ctx.stroke();
835
+
836
+ // Leaf texture
837
+ ctx.strokeStyle = '#1a5a1a';
838
+ ctx.lineWidth = 0.5;
839
+ ctx.beginPath();
840
+ ctx.moveTo(0, size * 0.2);
841
+ ctx.lineTo(0, -size * 0.65);
842
+ ctx.stroke();
843
+
844
+ ctx.restore();
845
+ }
846
+
847
+ // Ribbon tying the bundle (red Welsh ribbon)
848
+ ctx.fillStyle = '#DC143C';
849
+ ctx.fillRect(-size * 0.5, size * 0.4, size, size * 0.12);
850
+
851
+ // Ribbon bow
852
+ ctx.beginPath();
853
+ ctx.ellipse(-size * 0.6, size * 0.46, size * 0.12, size * 0.08, 0, 0, Math.PI * 2);
854
+ ctx.fill();
855
+ ctx.beginPath();
856
+ ctx.ellipse(size * 0.6, size * 0.46, size * 0.12, size * 0.08, 0, 0, Math.PI * 2);
857
+ ctx.fill();
858
+
859
+ ctx.restore();
860
+ },
861
+
862
+ /**
863
+ * Draw Welsh flag (Y Ddraig Goch - The Red Dragon)
864
+ */
865
+ drawFlag(ctx, particle, time) {
866
+ const x = particle.x;
867
+ const y = particle.y;
868
+ const size = particle.size;
869
+
870
+ ctx.save();
871
+ ctx.globalAlpha = particle.opacity;
872
+ ctx.translate(x, y);
873
+
874
+ // Flagpole
875
+ ctx.strokeStyle = '#654321';
876
+ ctx.lineWidth = size * 0.04;
877
+ ctx.beginPath();
878
+ ctx.moveTo(0, 0);
879
+ ctx.lineTo(0, size * 1.5);
880
+ ctx.stroke();
881
+
882
+ // Flag wave motion
883
+ const waveOffset = Math.sin(time * 0.003 + particle.wavePhase) * size * 0.08;
884
+
885
+ // Flag background (white top, green bottom)
886
+ ctx.fillStyle = '#FFFFFF';
887
+ ctx.beginPath();
888
+ ctx.moveTo(0, 0);
889
+ ctx.bezierCurveTo(
890
+ size * 0.3, 0 + waveOffset * 0.5,
891
+ size * 0.6, 0 - waveOffset * 0.5,
892
+ size * 0.9, 0 + waveOffset
893
+ );
894
+ ctx.lineTo(size * 0.9, size * 0.3 + waveOffset);
895
+ ctx.bezierCurveTo(
896
+ size * 0.6, size * 0.3 - waveOffset * 0.5,
897
+ size * 0.3, size * 0.3 + waveOffset * 0.5,
898
+ 0, size * 0.3
899
+ );
900
+ ctx.closePath();
901
+ ctx.fill();
902
+
903
+ ctx.fillStyle = '#00B140';
904
+ ctx.beginPath();
905
+ ctx.moveTo(0, size * 0.3);
906
+ ctx.bezierCurveTo(
907
+ size * 0.3, size * 0.3 + waveOffset * 0.5,
908
+ size * 0.6, size * 0.3 - waveOffset * 0.5,
909
+ size * 0.9, size * 0.3 + waveOffset
910
+ );
911
+ ctx.lineTo(size * 0.9, size * 0.6 + waveOffset);
912
+ ctx.bezierCurveTo(
913
+ size * 0.6, size * 0.6 - waveOffset * 0.5,
914
+ size * 0.3, size * 0.6 + waveOffset * 0.5,
915
+ 0, size * 0.6
916
+ );
917
+ ctx.closePath();
918
+ ctx.fill();
919
+
920
+ // Y Ddraig Goch (The Red Dragon) - Welsh heraldic dragon
921
+ ctx.fillStyle = '#DC143C';
922
+ ctx.save();
923
+ ctx.translate(size * 0.45, size * 0.3);
924
+ ctx.scale(0.28, 0.28);
925
+
926
+ // ===== DRAGON BODY (muscular, powerful) =====
927
+ ctx.beginPath();
928
+ // Chest (broad)
929
+ ctx.ellipse(size * 0.1, 0, size * 0.45, size * 0.3, -0.2, 0, Math.PI * 2);
930
+ ctx.fill();
931
+
932
+ // Neck (thick, connecting to head)
933
+ ctx.beginPath();
934
+ ctx.ellipse(size * 0.4, -size * 0.25, size * 0.22, size * 0.15, 0.3, 0, Math.PI * 2);
935
+ ctx.fill();
936
+
937
+ // Haunches (rear body)
938
+ ctx.beginPath();
939
+ ctx.ellipse(-size * 0.25, size * 0.1, size * 0.35, size * 0.28, 0.1, 0, Math.PI * 2);
940
+ ctx.fill();
941
+
942
+ // ===== DRAGON HEAD (detailed, fierce) =====
943
+ // Snout/muzzle
944
+ ctx.beginPath();
945
+ ctx.ellipse(size * 0.65, -size * 0.28, size * 0.2, size * 0.12, 0, 0, Math.PI * 2);
946
+ ctx.fill();
947
+
948
+ // Upper jaw (protruding)
949
+ ctx.beginPath();
950
+ ctx.moveTo(size * 0.55, -size * 0.32);
951
+ ctx.bezierCurveTo(
952
+ size * 0.7, -size * 0.38,
953
+ size * 0.85, -size * 0.35,
954
+ size * 0.88, -size * 0.28
955
+ );
956
+ ctx.lineTo(size * 0.82, -size * 0.25);
957
+ ctx.bezierCurveTo(
958
+ size * 0.78, -size * 0.28,
959
+ size * 0.65, -size * 0.3,
960
+ size * 0.55, -size * 0.28
961
+ );
962
+ ctx.closePath();
963
+ ctx.fill();
964
+
965
+ // Lower jaw
966
+ ctx.beginPath();
967
+ ctx.moveTo(size * 0.58, -size * 0.24);
968
+ ctx.bezierCurveTo(
969
+ size * 0.72, -size * 0.22,
970
+ size * 0.84, -size * 0.24,
971
+ size * 0.87, -size * 0.28
972
+ );
973
+ ctx.lineTo(size * 0.82, -size * 0.3);
974
+ ctx.bezierCurveTo(
975
+ size * 0.75, -size * 0.27,
976
+ size * 0.65, -size * 0.26,
977
+ size * 0.58, -size * 0.27
978
+ );
979
+ ctx.closePath();
980
+ ctx.fill();
981
+
982
+ // Eye (fierce)
983
+ ctx.fillStyle = '#FFFF00';
984
+ ctx.beginPath();
985
+ ctx.ellipse(size * 0.58, -size * 0.32, size * 0.04, size * 0.05, 0.2, 0, Math.PI * 2);
986
+ ctx.fill();
987
+ ctx.fillStyle = '#DC143C';
988
+
989
+ // Horns (two curved horns)
990
+ ctx.beginPath();
991
+ // Left horn
992
+ ctx.moveTo(size * 0.5, -size * 0.38);
993
+ ctx.bezierCurveTo(
994
+ size * 0.48, -size * 0.55,
995
+ size * 0.52, -size * 0.65,
996
+ size * 0.58, -size * 0.68
997
+ );
998
+ ctx.lineTo(size * 0.62, -size * 0.64);
999
+ ctx.bezierCurveTo(
1000
+ size * 0.58, -size * 0.62,
1001
+ size * 0.54, -size * 0.52,
1002
+ size * 0.54, -size * 0.38
1003
+ );
1004
+ ctx.closePath();
1005
+ ctx.fill();
1006
+
1007
+ // Right horn
1008
+ ctx.beginPath();
1009
+ ctx.moveTo(size * 0.48, -size * 0.4);
1010
+ ctx.bezierCurveTo(
1011
+ size * 0.42, -size * 0.58,
1012
+ size * 0.38, -size * 0.68,
1013
+ size * 0.35, -size * 0.72
1014
+ );
1015
+ ctx.lineTo(size * 0.38, -size * 0.74);
1016
+ ctx.bezierCurveTo(
1017
+ size * 0.42, -size * 0.7,
1018
+ size * 0.46, -size * 0.58,
1019
+ size * 0.5, -size * 0.42
1020
+ );
1021
+ ctx.closePath();
1022
+ ctx.fill();
1023
+
1024
+ // ===== WINGS (large, bat-like with finger bones) =====
1025
+ // Left wing (rear)
1026
+ ctx.globalAlpha = 0.9;
1027
+ ctx.beginPath();
1028
+ ctx.moveTo(-size * 0.1, -size * 0.15);
1029
+ // Wing membrane with finger bones creating points
1030
+ // Bone 1
1031
+ ctx.bezierCurveTo(
1032
+ -size * 0.3, -size * 0.5,
1033
+ -size * 0.35, -size * 0.75,
1034
+ -size * 0.25, -size * 0.85
1035
+ );
1036
+ ctx.lineTo(-size * 0.22, -size * 0.82);
1037
+ // Bone 2
1038
+ ctx.bezierCurveTo(
1039
+ -size * 0.1, -size * 0.85,
1040
+ 0, -size * 0.9,
1041
+ size * 0.1, -size * 0.88
1042
+ );
1043
+ ctx.lineTo(size * 0.12, -size * 0.84);
1044
+ // Bone 3
1045
+ ctx.bezierCurveTo(
1046
+ size * 0.2, -size * 0.8,
1047
+ size * 0.28, -size * 0.72,
1048
+ size * 0.3, -size * 0.62
1049
+ );
1050
+ ctx.lineTo(size * 0.25, -size * 0.58);
1051
+ // Bone 4
1052
+ ctx.bezierCurveTo(
1053
+ size * 0.22, -size * 0.5,
1054
+ size * 0.18, -size * 0.35,
1055
+ size * 0.1, -size * 0.25
1056
+ );
1057
+ ctx.lineTo(0, -size * 0.2);
1058
+ ctx.closePath();
1059
+ ctx.fill();
1060
+
1061
+ // Right wing (front) - slightly forward
1062
+ ctx.beginPath();
1063
+ ctx.moveTo(size * 0.15, -size * 0.2);
1064
+ ctx.bezierCurveTo(
1065
+ size * 0.3, -size * 0.55,
1066
+ size * 0.45, -size * 0.78,
1067
+ size * 0.6, -size * 0.82
1068
+ );
1069
+ ctx.lineTo(size * 0.62, -size * 0.78);
1070
+ ctx.bezierCurveTo(
1071
+ size * 0.7, -size * 0.75,
1072
+ size * 0.8, -size * 0.68,
1073
+ size * 0.85, -size * 0.58
1074
+ );
1075
+ ctx.lineTo(size * 0.82, -size * 0.54);
1076
+ ctx.bezierCurveTo(
1077
+ size * 0.75, -size * 0.52,
1078
+ size * 0.68, -size * 0.45,
1079
+ size * 0.6, -size * 0.38
1080
+ );
1081
+ ctx.lineTo(size * 0.55, -size * 0.35);
1082
+ ctx.bezierCurveTo(
1083
+ size * 0.45, -size * 0.3,
1084
+ size * 0.32, -size * 0.25,
1085
+ size * 0.2, -size * 0.22
1086
+ );
1087
+ ctx.closePath();
1088
+ ctx.fill();
1089
+
1090
+ ctx.globalAlpha = 1;
1091
+
1092
+ // ===== FRONT LEGS (with claws) =====
1093
+ // Right front leg (foreground)
1094
+ ctx.beginPath();
1095
+ // Upper leg
1096
+ ctx.ellipse(size * 0.25, size * 0.15, size * 0.12, size * 0.25, 0.5, 0, Math.PI * 2);
1097
+ ctx.fill();
1098
+ // Lower leg
1099
+ ctx.beginPath();
1100
+ ctx.moveTo(size * 0.32, size * 0.35);
1101
+ ctx.lineTo(size * 0.38, size * 0.55);
1102
+ ctx.lineTo(size * 0.42, size * 0.57);
1103
+ ctx.lineTo(size * 0.36, size * 0.37);
1104
+ ctx.closePath();
1105
+ ctx.fill();
1106
+ // Claws (three)
1107
+ for (let i = 0; i < 3; i++) {
1108
+ ctx.beginPath();
1109
+ const clawX = size * 0.36 + i * size * 0.04;
1110
+ ctx.moveTo(clawX, size * 0.55);
1111
+ ctx.lineTo(clawX + size * 0.02, size * 0.63);
1112
+ ctx.lineTo(clawX + size * 0.04, size * 0.55);
1113
+ ctx.closePath();
1114
+ ctx.fill();
1115
+ }
1116
+
1117
+ // Left front leg (background)
1118
+ ctx.globalAlpha = 0.85;
1119
+ ctx.beginPath();
1120
+ ctx.ellipse(size * 0.15, size * 0.12, size * 0.1, size * 0.22, 0.3, 0, Math.PI * 2);
1121
+ ctx.fill();
1122
+ ctx.beginPath();
1123
+ ctx.moveTo(size * 0.18, size * 0.3);
1124
+ ctx.lineTo(size * 0.2, size * 0.48);
1125
+ ctx.lineTo(size * 0.24, size * 0.5);
1126
+ ctx.lineTo(size * 0.22, size * 0.32);
1127
+ ctx.closePath();
1128
+ ctx.fill();
1129
+ ctx.globalAlpha = 1;
1130
+
1131
+ // ===== HIND LEGS (with claws) =====
1132
+ // Right hind leg
1133
+ ctx.beginPath();
1134
+ ctx.ellipse(-size * 0.15, size * 0.25, size * 0.15, size * 0.28, 0.8, 0, Math.PI * 2);
1135
+ ctx.fill();
1136
+ ctx.beginPath();
1137
+ ctx.moveTo(-size * 0.08, size * 0.48);
1138
+ ctx.lineTo(-size * 0.02, size * 0.65);
1139
+ ctx.lineTo(size * 0.02, size * 0.67);
1140
+ ctx.lineTo(-size * 0.04, size * 0.5);
1141
+ ctx.closePath();
1142
+ ctx.fill();
1143
+ // Claws
1144
+ for (let i = 0; i < 3; i++) {
1145
+ ctx.beginPath();
1146
+ const clawX = -size * 0.04 + i * size * 0.04;
1147
+ ctx.moveTo(clawX, size * 0.65);
1148
+ ctx.lineTo(clawX + size * 0.02, size * 0.72);
1149
+ ctx.lineTo(clawX + size * 0.04, size * 0.65);
1150
+ ctx.closePath();
1151
+ ctx.fill();
1152
+ }
1153
+
1154
+ // Left hind leg (background)
1155
+ ctx.globalAlpha = 0.85;
1156
+ ctx.beginPath();
1157
+ ctx.ellipse(-size * 0.28, size * 0.22, size * 0.13, size * 0.25, 0.6, 0, Math.PI * 2);
1158
+ ctx.fill();
1159
+ ctx.globalAlpha = 1;
1160
+
1161
+ // ===== TAIL (long, powerful, with spikes) =====
1162
+ ctx.beginPath();
1163
+ ctx.moveTo(-size * 0.5, size * 0.05);
1164
+ ctx.bezierCurveTo(
1165
+ -size * 0.75, size * 0.15,
1166
+ -size * 0.95, 0,
1167
+ -size * 1.05, -size * 0.3
1168
+ );
1169
+ ctx.bezierCurveTo(
1170
+ -size * 1.1, -size * 0.45,
1171
+ -size * 1.05, -size * 0.6,
1172
+ -size * 0.95, -size * 0.68
1173
+ );
1174
+ // Tail tip (pointed)
1175
+ ctx.lineTo(-size * 0.88, -size * 0.72);
1176
+ ctx.lineTo(-size * 0.92, -size * 0.65);
1177
+ ctx.bezierCurveTo(
1178
+ -size * 1.0, -size * 0.56,
1179
+ -size * 1.02, -size * 0.42,
1180
+ -size * 0.98, -size * 0.28
1181
+ );
1182
+ ctx.bezierCurveTo(
1183
+ -size * 0.88, 0,
1184
+ -size * 0.68, size * 0.1,
1185
+ -size * 0.48, size * 0.08
1186
+ );
1187
+ ctx.closePath();
1188
+ ctx.fill();
1189
+
1190
+ // Tail spikes (triangular, along top edge)
1191
+ const spikeCount = 5;
1192
+ for (let i = 0; i < spikeCount; i++) {
1193
+ const t = i / (spikeCount - 1);
1194
+ // Calculate position along tail curve
1195
+ const spikeX = -size * 0.6 - t * size * 0.4;
1196
+ const spikeY = size * 0.1 - t * size * 0.7 + Math.sin(t * Math.PI) * size * 0.15;
1197
+ const spikeSize = size * 0.12 * (1 - t * 0.5);
1198
+
1199
+ ctx.beginPath();
1200
+ ctx.moveTo(spikeX, spikeY);
1201
+ ctx.lineTo(spikeX - spikeSize * 0.3, spikeY - spikeSize);
1202
+ ctx.lineTo(spikeX + spikeSize * 0.3, spikeY - spikeSize * 0.8);
1203
+ ctx.closePath();
1204
+ ctx.fill();
1205
+ }
1206
+
1207
+ ctx.restore(); // closes dragon save (line 856)
1208
+
1209
+ ctx.restore(); // closes flag save (line 804) — THIS WAS MISSING
1210
+ },
1211
+
1212
+ /**
1213
+ * Draw twinkling star
1214
+ */
1215
+ drawTwinklingStar(ctx, particle, time) {
1216
+ const x = particle.x;
1217
+ const y = particle.y;
1218
+ const size = particle.size;
1219
+
1220
+ const twinkleIntensity = 0.5 + Math.sin(time * particle.twinkleSpeed + particle.twinklePhase) * 0.5;
1221
+
1222
+ const starColor = `rgba(255, 240, 200, ${twinkleIntensity})`;
1223
+ const glowColor = `rgba(255, 220, 150, ${twinkleIntensity * 0.4})`;
1224
+
1225
+ ctx.save();
1226
+ ctx.translate(x, y);
1227
+
1228
+ ctx.shadowColor = glowColor;
1229
+ ctx.shadowBlur = size * 3 * twinkleIntensity;
1230
+ ctx.fillStyle = starColor;
1231
+
1232
+ ctx.beginPath();
1233
+ for (let i = 0; i < 4; i++) {
1234
+ const angle = (i * Math.PI) / 2;
1235
+ const outerX = Math.cos(angle) * size;
1236
+ const outerY = Math.sin(angle) * size;
1237
+ const innerAngle = angle + Math.PI / 4;
1238
+ const innerX = Math.cos(innerAngle) * (size * 0.3);
1239
+ const innerY = Math.sin(innerAngle) * (size * 0.3);
1240
+
1241
+ if (i === 0) {
1242
+ ctx.moveTo(outerX, outerY);
1243
+ } else {
1244
+ ctx.lineTo(outerX, outerY);
1245
+ }
1246
+ ctx.lineTo(innerX, innerY);
1247
+ }
1248
+ ctx.closePath();
1249
+ ctx.fill();
1250
+
1251
+ ctx.shadowBlur = size * 2 * twinkleIntensity;
1252
+ ctx.beginPath();
1253
+ ctx.arc(0, 0, size * 0.25, 0, Math.PI * 2);
1254
+ ctx.fill();
1255
+
1256
+ ctx.restore();
1257
+ }
1258
+ };