domma-cms 0.2.0 → 0.3.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 (72) hide show
  1. package/README.md +2 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/js/api.js +1 -242
  4. package/admin/js/app.js +5 -279
  5. package/admin/js/config/sidebar-config.js +1 -115
  6. package/admin/js/lib/card.js +1 -63
  7. package/admin/js/lib/image-editor.js +1 -869
  8. package/admin/js/lib/markdown-toolbar.js +46 -421
  9. package/admin/js/templates/layouts.html +44 -7
  10. package/admin/js/templates/page-editor.html +9 -0
  11. package/admin/js/templates/settings.html +18 -1
  12. package/admin/js/templates/users.html +29 -4
  13. package/admin/js/views/collection-editor.js +3 -487
  14. package/admin/js/views/collection-entries.js +1 -484
  15. package/admin/js/views/collections.js +1 -153
  16. package/admin/js/views/dashboard.js +1 -56
  17. package/admin/js/views/documentation.js +1 -12
  18. package/admin/js/views/index.js +1 -39
  19. package/admin/js/views/layouts.js +9 -42
  20. package/admin/js/views/login.js +7 -251
  21. package/admin/js/views/media.js +1 -240
  22. package/admin/js/views/navigation.js +14 -212
  23. package/admin/js/views/page-editor.js +53 -661
  24. package/admin/js/views/pages.js +5 -72
  25. package/admin/js/views/plugins.js +13 -90
  26. package/admin/js/views/settings.js +1 -199
  27. package/admin/js/views/tutorials.js +1 -12
  28. package/admin/js/views/user-editor.js +1 -88
  29. package/admin/js/views/users.js +7 -76
  30. package/bin/cli.js +18 -9
  31. package/config/auth.json +1 -17
  32. package/config/navigation.json +15 -0
  33. package/config/site.json +5 -4
  34. package/package.json +1 -1
  35. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  36. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  37. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  38. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  39. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  40. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  41. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  42. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  43. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  44. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  45. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  46. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  47. package/plugins/example-analytics/stats.json +16 -12
  48. package/plugins/form-builder/admin/templates/form-editor.html +158 -130
  49. package/plugins/form-builder/admin/views/form-editor.js +3 -1
  50. package/plugins/form-builder/data/forms/contact-details.json +71 -35
  51. package/plugins/form-builder/data/forms/feedback.json +130 -0
  52. package/plugins/form-builder/data/submissions/feedback.json +1 -0
  53. package/plugins/form-builder/public/form-logic-engine.js +1 -568
  54. package/public/css/site.css +1 -302
  55. package/public/js/btt.js +1 -90
  56. package/public/js/cookie-consent.js +1 -61
  57. package/public/js/site.js +1 -204
  58. package/scripts/setup.js +12 -9
  59. package/server/middleware/auth.js +44 -21
  60. package/server/routes/api/auth.js +38 -8
  61. package/server/routes/api/collections.js +18 -5
  62. package/server/routes/api/layouts.js +18 -4
  63. package/server/routes/api/media.js +2 -3
  64. package/server/routes/api/navigation.js +2 -3
  65. package/server/routes/api/pages.js +3 -3
  66. package/server/routes/api/settings.js +2 -3
  67. package/server/routes/api/users.js +4 -6
  68. package/server/routes/public.js +3 -3
  69. package/server/server.js +8 -0
  70. package/server/services/markdown.js +102 -3
  71. package/server/services/userTypes.js +167 -0
  72. package/plugins/form-builder/email.js +0 -103
@@ -1,1258 +1 @@
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
- };
1
+ export default{name:"st-davids",displayName:"St David's Day",emoji:"\u{1F33B}",intensityConfig:{light:{count:50,speedRange:[.4,1.2],sizeRange:[2,4],daffodilFields:2,dragonChance:2e-4,twinklingStars:10},medium:{count:100,speedRange:[.5,1.5],sizeRange:[2,5],daffodilFields:3,dragonChance:4e-4,twinklingStars:18},heavy:{count:150,speedRange:[.6,1.8],sizeRange:[3,6],daffodilFields:4,dragonChance:6e-4,twinklingStars:25}},particles:["daffodil-petal","daffodil","spring-sparkle"],decorations:["daffodil-field","welsh-dragon","flag","harp","leek-bundle","twinkling-star"],colors:{primary:"#FFDD00",secondary:"#228B22",accent:"#DC143C",white:"#FFFFFF"},createDaffodilPetal(e,l,i){const r=["#FFDD00","#FFE135","#FFED4E","#FFF68F"];return{type:"daffodil-petal",x:-30,y:Math.random()*l,vx:i.speedRange[0]+Math.random()*(i.speedRange[1]-i.speedRange[0]),size:(i.sizeRange[0]+Math.random()*(i.sizeRange[1]-i.sizeRange[0]))*1.5,speed:(Math.random()-.5)*.2,opacity:.75+Math.random()*.25,windOffset:Math.random()*Math.PI*2,windSpeed:.015+Math.random()*.02,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.03,color:r[Math.floor(Math.random()*r.length)],flutter:Math.random()*Math.PI*2,flutterSpeed:.02+Math.random()*.02,active:!0}},createDaffodil(e,l,i){return{type:"daffodil",x:-30,y:Math.random()*l,vx:i.speedRange[0]+Math.random()*(i.speedRange[1]-i.speedRange[0]),size:i.sizeRange[0]+Math.random()*(i.sizeRange[1]-i.sizeRange[0]),speed:.1,opacity:.8+Math.random()*.2,windOffset:Math.random()*Math.PI*2,windSpeed:.015+Math.random()*.02,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.02,active:!0}},createLeek(e,l,i){return{type:"leek",x:-30,y:Math.random()*l,vx:(i.speedRange[0]+Math.random()*(i.speedRange[1]-i.speedRange[0]))*.8,size:i.sizeRange[0]+Math.random()*(i.sizeRange[1]-i.sizeRange[0])*1.2,speed:.15,opacity:.7+Math.random()*.3,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.03,windOffset:Math.random()*Math.PI*2,windSpeed:.02+Math.random()*.03,active:!0}},createSpringSparkle(e,l,i){const r=["#FFD700","#FFFFFF","#90EE90","#98FB98"];return{type:"spring-sparkle",x:-20,y:Math.random()*l,vx:(i.speedRange[0]+Math.random()*(i.speedRange[1]-i.speedRange[0]))*.8,size:i.sizeRange[0]+Math.random()*(i.sizeRange[1]-i.sizeRange[0])*.6,vy:(Math.random()-.5)*.2,opacity:.6+Math.random()*.4,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.04,color:r[Math.floor(Math.random()*r.length)],twinklePhase:Math.random()*Math.PI*2,windOffset:Math.random()*Math.PI*2,windSpeed:.015+Math.random()*.02,active:!0,static:!1}},createFallingParticle(e,l,i){const r=Math.random();return r<.6?this.createDaffodilPetal(e,l,i):r<.8?this.createDaffodil(e,l,i):r<.95?this.createSpringSparkle(e,l,i):this.createLeek(e,l,i)},createDaffodilField(e,l,i={}){const r=[],o=15+Math.floor(Math.random()*10);for(let a=0;a<o;a++)r.push({x:Math.random()*e*.3,y:Math.random()*40,size:5+Math.random()*8,swayPhase:Math.random()*Math.PI*2});return{type:"daffodil-field",x:i.x!==void 0?i.x:Math.random()*e*.7,y:i.y!==void 0?i.y:l-50,flowers:r,opacity:.9,time:0,active:!0,static:!0}},createInitialDecorations(e,l,i){const r=[],o=i.daffodilFields||3;for(let n=0;n<o;n++)r.push(this.createDaffodilField(e,l,{x:e/(o+1)*(n+1),y:l-50}));r.push({type:"welsh-dragon",x:150,y:l-80,baseY:l-80,vx:0,size:35+Math.random()*10,opacity:1,time:0,wingPhase:Math.random()*Math.PI*2,breatheFirePhase:Math.random()*Math.PI*2,active:!0,static:!0}),r.push({type:"harp",x:e-200,y:l-90,size:40+Math.random()*10,opacity:1,glintPhase:Math.random()*Math.PI*2,active:!0,static:!0}),r.push({type:"leek-bundle",x:e*.3,y:l-40,size:25+Math.random()*5,opacity:1,active:!0,static:!0}),r.push({type:"flag",x:e-100,y:80,size:50,opacity:1,wavePhase:Math.random()*Math.PI*2,active:!0,static:!0});const a=i.twinklingStars||18;for(let n=0;n<a;n++)r.push(this.createTwinklingStar(e,l));return r},createTwinklingStar(e,l){return{type:"twinkling-star",x:Math.random()*e,y:Math.random()*(l*.5),size:1+Math.random()*2,opacity:.6+Math.random()*.3,twinklePhase:Math.random()*Math.PI*2,twinkleSpeed:.003+Math.random()*.003,active:!0,static:!0}},spawnSpecialParticle(e,l,i,r){const o=Math.random();if(o<r.dragonChance){if(e.some(n=>n.type==="welsh-dragon"))return null;const a=Math.random()<.5;return{type:"welsh-dragon",x:a?-100:l+100,y:Math.random()*i*.4+50,baseY:Math.random()*i*.4+50,vx:a?2+Math.random()*1:-(2+Math.random()*1),size:25+Math.random()*15,opacity:1,waveAmplitude:25+Math.random()*20,waveFrequency:.002+Math.random()*.002,waveOffset:Math.random()*Math.PI*2,time:0,wingPhase:Math.random()*Math.PI*2,breatheFirePhase:Math.random()*Math.PI*2,fireParticles:[],active:!0,static:!1}}return o<4e-4&&e.filter(n=>n.type==="daffodil-field").length<r.daffodilFields?this.createDaffodilField(l,i):null},drawDaffodilPetal(e,l,i){const r=l.x,o=l.y,a=l.size,n=Math.sin(i*l.flutterSpeed+l.flutter)*.3;e.save(),e.globalAlpha=l.opacity,e.translate(r,o),e.rotate(l.rotation+n),e.fillStyle=l.color,e.strokeStyle="#FFB700",e.lineWidth=a*.08,e.beginPath(),e.ellipse(0,0,a*.6,a*1.2,0,0,Math.PI*2),e.fill(),e.stroke(),e.strokeStyle="rgba(255, 200, 0, 0.4)",e.lineWidth=a*.06,e.beginPath(),e.moveTo(0,-a*1.1),e.lineTo(0,a*1.1),e.stroke(),e.restore()},drawDaffodil(e,l){const i=l.x,r=l.y,o=l.size;e.save(),e.globalAlpha=l.opacity,e.translate(i,r),e.rotate(l.rotation),e.fillStyle="#FFA500",e.strokeStyle="#FF8C00",e.lineWidth=o*.08,e.beginPath(),e.moveTo(-o*.25,0),e.lineTo(-o*.3,o*.3),e.lineTo(o*.3,o*.3),e.lineTo(o*.25,0),e.closePath(),e.fill(),e.stroke(),e.fillStyle="#FFDD00",e.strokeStyle="#FFC700",e.lineWidth=o*.06;for(let a=0;a<6;a++){const n=a/6*Math.PI*2-Math.PI/2;e.save(),e.rotate(n),e.translate(0,-o*.7),e.beginPath(),e.ellipse(0,0,o*.4,o*.6,0,0,Math.PI*2),e.fill(),e.stroke(),e.restore()}e.strokeStyle="#228B22",e.lineWidth=o*.12,e.beginPath(),e.moveTo(0,o*.3),e.lineTo(0,o*1.5),e.stroke(),e.restore()},drawLeek(e,l){const i=l.x,r=l.y,o=l.size;e.save(),e.globalAlpha=l.opacity,e.translate(i,r),e.rotate(l.rotation),e.fillStyle="#F5F5F5",e.strokeStyle="#DCDCDC",e.lineWidth=o*.08,e.beginPath(),e.ellipse(0,o*1.2,o*.4,o*.5,0,0,Math.PI*2),e.fill(),e.stroke(),e.fillStyle="#228B22",e.strokeStyle="#006400",e.lineWidth=o*.05,e.beginPath(),e.moveTo(-o*.3,o*1.2),e.quadraticCurveTo(-o*.25,o*.5,-o*.15,0),e.quadraticCurveTo(0,-o*.2,o*.15,0),e.quadraticCurveTo(o*.25,o*.5,o*.3,o*1.2),e.closePath(),e.fill(),e.stroke(),e.beginPath(),e.moveTo(-o*.15,0),e.quadraticCurveTo(-o*.6,-o*.3,-o*.8,0),e.stroke(),e.beginPath(),e.moveTo(o*.15,0),e.quadraticCurveTo(o*.6,-o*.3,o*.8,0),e.stroke(),e.strokeStyle="#D2B48C",e.lineWidth=o*.05;for(let a=0;a<5;a++){const n=(a-2)*o*.15;e.beginPath(),e.moveTo(n,o*1.5),e.lineTo(n+Math.sin(a*1.7)*o*.15,o*1.8),e.stroke()}e.restore()},drawSpringSparkle(e,l,i){const r=l.x,o=l.y,a=l.size,n=.6+Math.sin(i*.004+l.twinklePhase)*.4;e.save(),e.globalAlpha=l.opacity*n,e.translate(r,o),e.rotate(l.rotation),e.fillStyle=l.color,e.shadowColor=l.color,e.shadowBlur=a*2,e.beginPath(),e.moveTo(0,-a),e.lineTo(a*.3,-a*.3),e.lineTo(a,0),e.lineTo(a*.3,a*.3),e.lineTo(0,a),e.lineTo(-a*.3,a*.3),e.lineTo(-a,0),e.lineTo(-a*.3,-a*.3),e.closePath(),e.fill(),e.restore()},drawDaffodilField(e,l,i){const r=l.x,o=l.y;e.save(),e.globalAlpha=l.opacity,l.flowers.forEach(a=>{const n=Math.sin(i*.001+a.swayPhase)*a.size*.2;e.save(),e.translate(r+a.x,o-a.y),e.rotate(n*.1),e.strokeStyle="#228B22",e.lineWidth=a.size*.08,e.beginPath(),e.moveTo(0,0),e.lineTo(n,-a.size*2),e.stroke(),e.fillStyle="#FFDD00",e.shadowColor="#FFDD00",e.shadowBlur=a.size*.5,e.beginPath(),e.arc(n,-a.size*2,a.size*.5,0,Math.PI*2),e.fill(),e.shadowBlur=0,e.fillStyle="#FFA500",e.beginPath(),e.arc(n,-a.size*2,a.size*.25,0,Math.PI*2),e.fill(),e.restore()}),e.restore()},drawWelshDragon(e,l,i){const r=l.x,o=l.y,a=l.size,n=l.vx>0?1:-1;e.save(),e.translate(r,o),n===-1&&e.scale(-1,1);const d=Math.sin(i*.012+l.wingPhase)*(Math.PI/3);e.fillStyle="#DC143C",e.strokeStyle="#8B0000",e.lineWidth=2,e.save(),e.translate(-a*.3,-a*.3),e.rotate(-d*.9),e.beginPath(),e.moveTo(0,0),e.bezierCurveTo(-a*1.2,-a*.8,-a*1.5,-a*.3,-a*1.3,a*.3),e.bezierCurveTo(-a*.8,a*.2,-a*.3,0,0,0),e.closePath(),e.fill(),e.stroke(),e.restore(),e.save(),e.translate(a*.3,-a*.3),e.rotate(d),e.beginPath(),e.moveTo(0,0),e.bezierCurveTo(a*1.2,-a*.8,a*1.5,-a*.3,a*1.3,a*.3),e.bezierCurveTo(a*.8,a*.2,a*.3,0,0,0),e.closePath(),e.fill(),e.stroke(),e.restore(),e.fillStyle="#DC143C",e.strokeStyle="#8B0000",e.lineWidth=2,e.beginPath(),e.ellipse(0,0,a*.7,a*.5,0,0,Math.PI*2),e.fill(),e.stroke(),e.strokeStyle="#DC143C",e.lineWidth=a*.25,e.beginPath(),e.moveTo(-a*.5,0),e.quadraticCurveTo(-a*1.2,-a*.3,-a*1.5,0),e.stroke(),e.fillStyle="#FFD700";for(let s=0;s<3;s++){const h=-a*(.8+s*.25);e.beginPath(),e.moveTo(h-a*.1,-a*.2),e.lineTo(h,-a*.5),e.lineTo(h+a*.1,-a*.2),e.closePath(),e.fill()}if(e.fillStyle="#DC143C",e.beginPath(),e.ellipse(a*.8,-a*.2,a*.4,a*.35,0,0,Math.PI*2),e.fill(),e.stroke(),e.fillStyle="#DC143C",e.beginPath(),e.moveTo(a*1.1,-a*.2),e.quadraticCurveTo(a*1.4,-a*.15,a*1.3,0),e.quadraticCurveTo(a*1.4,.1,a*1.1,-a*.2),e.closePath(),e.fill(),e.fillStyle="#FFD700",e.beginPath(),e.arc(a*.9,-a*.3,a*.12,0,Math.PI*2),e.fill(),e.fillStyle="#000000",e.beginPath(),e.arc(a*.95,-a*.3,a*.06,0,Math.PI*2),e.fill(),e.strokeStyle="#FFD700",e.lineWidth=a*.1,e.beginPath(),e.moveTo(a*.7,-a*.5),e.lineTo(a*.65,-a*.8),e.stroke(),e.beginPath(),e.moveTo(a*.9,-a*.5),e.lineTo(a*.95,-a*.8),e.stroke(),Math.sin(i*.005+l.breatheFirePhase)>.5){const s=e.createLinearGradient(a*1.3,-a*.1,a*2,-a*.1);s.addColorStop(0,"#FF6600"),s.addColorStop(.5,"#FFA500"),s.addColorStop(1,"rgba(255, 255, 0, 0)"),e.fillStyle=s,e.globalAlpha=.8,e.beginPath(),e.moveTo(a*1.3,-a*.1),e.bezierCurveTo(a*1.6,-a*.3,a*1.8,-a*.2,a*2,-a*.1),e.bezierCurveTo(a*1.8,0,a*1.6,.1,a*1.3,-a*.1),e.closePath(),e.fill()}e.restore()},drawHarp(e,l,i){const r=l.x,o=l.y,a=l.size;e.save(),e.globalAlpha=l.opacity,e.translate(r,o),e.strokeStyle="#8b4513",e.lineWidth=a*.08,e.lineCap="round",e.beginPath(),e.moveTo(-a*.3,a*.5),e.bezierCurveTo(-a*.5,a*.1,-a*.4,-a*.4,-a*.2,-a*.7),e.stroke(),e.beginPath(),e.moveTo(-a*.2,-a*.7),e.lineTo(a*.4,-a*.5),e.stroke(),e.beginPath(),e.moveTo(a*.4,-a*.5),e.lineTo(-a*.3,a*.5),e.stroke(),e.strokeStyle="#FFD700",e.lineWidth=a*.02;const n=12;for(let d=0;d<n;d++){const s=d/(n-1),h=-a*.3+s*a*.65,t=a*.5-s*a*1,f=-a*.25+s*a*.55,P=-a*.65+s*a*.15,T=Math.sin(i*.003+d*.3+l.glintPhase)*a*.02;e.beginPath(),e.moveTo(h+T,t),e.lineTo(f+T,P),e.stroke()}e.strokeStyle="#654321",e.lineWidth=a*.03,e.globalAlpha=.6;for(let d=0;d<4;d++){const s=a*.3-d*a*.25;e.beginPath(),e.arc(-a*.35,s,a*.08,0,Math.PI*2),e.stroke()}e.restore()},drawLeekBundle(e,l){const i=l.x,r=l.y,o=l.size;e.save(),e.globalAlpha=l.opacity,e.translate(i,r);for(let a=0;a<5;a++){e.save();const n=(a-2)*o*.15,d=(a-2)*.1;e.translate(n,0),e.rotate(d),e.fillStyle="#f5f5dc",e.strokeStyle="#d3d3c0",e.lineWidth=1,e.fillRect(-o*.08,o*.2,o*.16,o*.6),e.strokeRect(-o*.08,o*.2,o*.16,o*.6),e.fillStyle="#228B22",e.strokeStyle="#1a6b1a",e.beginPath(),e.moveTo(-o*.08,o*.2),e.lineTo(-o*.12,-o*.5),e.lineTo(0,-o*.7),e.lineTo(o*.12,-o*.5),e.lineTo(o*.08,o*.2),e.closePath(),e.fill(),e.stroke(),e.strokeStyle="#1a5a1a",e.lineWidth=.5,e.beginPath(),e.moveTo(0,o*.2),e.lineTo(0,-o*.65),e.stroke(),e.restore()}e.fillStyle="#DC143C",e.fillRect(-o*.5,o*.4,o,o*.12),e.beginPath(),e.ellipse(-o*.6,o*.46,o*.12,o*.08,0,0,Math.PI*2),e.fill(),e.beginPath(),e.ellipse(o*.6,o*.46,o*.12,o*.08,0,0,Math.PI*2),e.fill(),e.restore()},drawFlag(e,l,i){const r=l.x,o=l.y,a=l.size;e.save(),e.globalAlpha=l.opacity,e.translate(r,o),e.strokeStyle="#654321",e.lineWidth=a*.04,e.beginPath(),e.moveTo(0,0),e.lineTo(0,a*1.5),e.stroke();const n=Math.sin(i*.003+l.wavePhase)*a*.08;e.fillStyle="#FFFFFF",e.beginPath(),e.moveTo(0,0),e.bezierCurveTo(a*.3,0+n*.5,a*.6,0-n*.5,a*.9,0+n),e.lineTo(a*.9,a*.3+n),e.bezierCurveTo(a*.6,a*.3-n*.5,a*.3,a*.3+n*.5,0,a*.3),e.closePath(),e.fill(),e.fillStyle="#00B140",e.beginPath(),e.moveTo(0,a*.3),e.bezierCurveTo(a*.3,a*.3+n*.5,a*.6,a*.3-n*.5,a*.9,a*.3+n),e.lineTo(a*.9,a*.6+n),e.bezierCurveTo(a*.6,a*.6-n*.5,a*.3,a*.6+n*.5,0,a*.6),e.closePath(),e.fill(),e.fillStyle="#DC143C",e.save(),e.translate(a*.45,a*.3),e.scale(.28,.28),e.beginPath(),e.ellipse(a*.1,0,a*.45,a*.3,-.2,0,Math.PI*2),e.fill(),e.beginPath(),e.ellipse(a*.4,-a*.25,a*.22,a*.15,.3,0,Math.PI*2),e.fill(),e.beginPath(),e.ellipse(-a*.25,a*.1,a*.35,a*.28,.1,0,Math.PI*2),e.fill(),e.beginPath(),e.ellipse(a*.65,-a*.28,a*.2,a*.12,0,0,Math.PI*2),e.fill(),e.beginPath(),e.moveTo(a*.55,-a*.32),e.bezierCurveTo(a*.7,-a*.38,a*.85,-a*.35,a*.88,-a*.28),e.lineTo(a*.82,-a*.25),e.bezierCurveTo(a*.78,-a*.28,a*.65,-a*.3,a*.55,-a*.28),e.closePath(),e.fill(),e.beginPath(),e.moveTo(a*.58,-a*.24),e.bezierCurveTo(a*.72,-a*.22,a*.84,-a*.24,a*.87,-a*.28),e.lineTo(a*.82,-a*.3),e.bezierCurveTo(a*.75,-a*.27,a*.65,-a*.26,a*.58,-a*.27),e.closePath(),e.fill(),e.fillStyle="#FFFF00",e.beginPath(),e.ellipse(a*.58,-a*.32,a*.04,a*.05,.2,0,Math.PI*2),e.fill(),e.fillStyle="#DC143C",e.beginPath(),e.moveTo(a*.5,-a*.38),e.bezierCurveTo(a*.48,-a*.55,a*.52,-a*.65,a*.58,-a*.68),e.lineTo(a*.62,-a*.64),e.bezierCurveTo(a*.58,-a*.62,a*.54,-a*.52,a*.54,-a*.38),e.closePath(),e.fill(),e.beginPath(),e.moveTo(a*.48,-a*.4),e.bezierCurveTo(a*.42,-a*.58,a*.38,-a*.68,a*.35,-a*.72),e.lineTo(a*.38,-a*.74),e.bezierCurveTo(a*.42,-a*.7,a*.46,-a*.58,a*.5,-a*.42),e.closePath(),e.fill(),e.globalAlpha=.9,e.beginPath(),e.moveTo(-a*.1,-a*.15),e.bezierCurveTo(-a*.3,-a*.5,-a*.35,-a*.75,-a*.25,-a*.85),e.lineTo(-a*.22,-a*.82),e.bezierCurveTo(-a*.1,-a*.85,0,-a*.9,a*.1,-a*.88),e.lineTo(a*.12,-a*.84),e.bezierCurveTo(a*.2,-a*.8,a*.28,-a*.72,a*.3,-a*.62),e.lineTo(a*.25,-a*.58),e.bezierCurveTo(a*.22,-a*.5,a*.18,-a*.35,a*.1,-a*.25),e.lineTo(0,-a*.2),e.closePath(),e.fill(),e.beginPath(),e.moveTo(a*.15,-a*.2),e.bezierCurveTo(a*.3,-a*.55,a*.45,-a*.78,a*.6,-a*.82),e.lineTo(a*.62,-a*.78),e.bezierCurveTo(a*.7,-a*.75,a*.8,-a*.68,a*.85,-a*.58),e.lineTo(a*.82,-a*.54),e.bezierCurveTo(a*.75,-a*.52,a*.68,-a*.45,a*.6,-a*.38),e.lineTo(a*.55,-a*.35),e.bezierCurveTo(a*.45,-a*.3,a*.32,-a*.25,a*.2,-a*.22),e.closePath(),e.fill(),e.globalAlpha=1,e.beginPath(),e.ellipse(a*.25,a*.15,a*.12,a*.25,.5,0,Math.PI*2),e.fill(),e.beginPath(),e.moveTo(a*.32,a*.35),e.lineTo(a*.38,a*.55),e.lineTo(a*.42,a*.57),e.lineTo(a*.36,a*.37),e.closePath(),e.fill();for(let s=0;s<3;s++){e.beginPath();const h=a*.36+s*a*.04;e.moveTo(h,a*.55),e.lineTo(h+a*.02,a*.63),e.lineTo(h+a*.04,a*.55),e.closePath(),e.fill()}e.globalAlpha=.85,e.beginPath(),e.ellipse(a*.15,a*.12,a*.1,a*.22,.3,0,Math.PI*2),e.fill(),e.beginPath(),e.moveTo(a*.18,a*.3),e.lineTo(a*.2,a*.48),e.lineTo(a*.24,a*.5),e.lineTo(a*.22,a*.32),e.closePath(),e.fill(),e.globalAlpha=1,e.beginPath(),e.ellipse(-a*.15,a*.25,a*.15,a*.28,.8,0,Math.PI*2),e.fill(),e.beginPath(),e.moveTo(-a*.08,a*.48),e.lineTo(-a*.02,a*.65),e.lineTo(a*.02,a*.67),e.lineTo(-a*.04,a*.5),e.closePath(),e.fill();for(let s=0;s<3;s++){e.beginPath();const h=-a*.04+s*a*.04;e.moveTo(h,a*.65),e.lineTo(h+a*.02,a*.72),e.lineTo(h+a*.04,a*.65),e.closePath(),e.fill()}e.globalAlpha=.85,e.beginPath(),e.ellipse(-a*.28,a*.22,a*.13,a*.25,.6,0,Math.PI*2),e.fill(),e.globalAlpha=1,e.beginPath(),e.moveTo(-a*.5,a*.05),e.bezierCurveTo(-a*.75,a*.15,-a*.95,0,-a*1.05,-a*.3),e.bezierCurveTo(-a*1.1,-a*.45,-a*1.05,-a*.6,-a*.95,-a*.68),e.lineTo(-a*.88,-a*.72),e.lineTo(-a*.92,-a*.65),e.bezierCurveTo(-a*1,-a*.56,-a*1.02,-a*.42,-a*.98,-a*.28),e.bezierCurveTo(-a*.88,0,-a*.68,a*.1,-a*.48,a*.08),e.closePath(),e.fill();const d=5;for(let s=0;s<d;s++){const h=s/(d-1),t=-a*.6-h*a*.4,f=a*.1-h*a*.7+Math.sin(h*Math.PI)*a*.15,P=a*.12*(1-h*.5);e.beginPath(),e.moveTo(t,f),e.lineTo(t-P*.3,f-P),e.lineTo(t+P*.3,f-P*.8),e.closePath(),e.fill()}e.restore(),e.restore()},drawTwinklingStar(e,l,i){const r=l.x,o=l.y,a=l.size,n=.5+Math.sin(i*l.twinkleSpeed+l.twinklePhase)*.5,d=`rgba(255, 240, 200, ${n})`,s=`rgba(255, 220, 150, ${n*.4})`;e.save(),e.translate(r,o),e.shadowColor=s,e.shadowBlur=a*3*n,e.fillStyle=d,e.beginPath();for(let h=0;h<4;h++){const t=h*Math.PI/2,f=Math.cos(t)*a,P=Math.sin(t)*a,T=t+Math.PI/4,M=Math.cos(T)*(a*.3),y=Math.sin(T)*(a*.3);h===0?e.moveTo(f,P):e.lineTo(f,P),e.lineTo(M,y)}e.closePath(),e.fill(),e.shadowBlur=a*2*n,e.beginPath(),e.arc(0,0,a*.25,0,Math.PI*2),e.fill(),e.restore()}};