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,1361 @@
1
+ /**
2
+ * Valentine's Day Theme for Domma Celebrations
3
+ *
4
+ * Features:
5
+ * - Floating hearts in various sizes and shades of pink/red
6
+ * - Rose petals gently falling with realistic physics
7
+ * - Cupid flying across the screen with bow and arrow
8
+ * - Love letters floating gracefully
9
+ * - Heart garlands strung across the viewport
10
+ * - Romantic sparkles and glitter effects
11
+ * - Soft pink/red color palette with gold accents
12
+ */
13
+
14
+ export default {
15
+ name: 'valentines',
16
+ displayName: 'Valentine\'s Day',
17
+ emoji: '💕',
18
+
19
+ // Intensity configurations
20
+ intensityConfig: {
21
+ light: {
22
+ count: 40, // Fewer hearts for subtlety
23
+ speedRange: [0.3, 0.8],
24
+ sizeRange: [2, 4],
25
+ garlands: 1,
26
+ cupidChance: 0.0003
27
+ },
28
+ medium: {
29
+ count: 80,
30
+ speedRange: [0.4, 1.2],
31
+ sizeRange: [2, 5],
32
+ garlands: 2,
33
+ cupidChance: 0.0005
34
+ },
35
+ heavy: {
36
+ count: 150,
37
+ speedRange: [0.5, 1.5],
38
+ sizeRange: [3, 6],
39
+ garlands: 3,
40
+ cupidChance: 0.0008
41
+ }
42
+ },
43
+
44
+ particles: ['heart', 'rose-petal', 'sparkle', 'lips'],
45
+ decorations: ['cupid', 'love-letter', 'heart-garland', 'envelope', 'butterfly', 'heart-moon', 'neon-sign'],
46
+ colors: {
47
+ primary: '#ff1493', // Deep pink
48
+ secondary: '#ff69b4', // Hot pink
49
+ accent: '#dc143c', // Crimson red
50
+ light: '#ffb6c1', // Light pink
51
+ gold: '#ffd700' // Gold accents
52
+ },
53
+
54
+ /**
55
+ * Create floating heart particle
56
+ */
57
+ createHeart(canvasWidth, canvasHeight, config) {
58
+ const depth = Math.random();
59
+ let size, speed, opacity;
60
+ const heartColors = ['#ff1493', '#ff69b4', '#dc143c', '#ffb6c1', '#ff0080'];
61
+
62
+ if (depth < 0.33) {
63
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 1.5;
64
+ speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]);
65
+ opacity = 0.8 + Math.random() * 0.2;
66
+ } else if (depth < 0.66) {
67
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]);
68
+ speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]);
69
+ opacity = 0.6 + Math.random() * 0.2;
70
+ } else {
71
+ size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.8;
72
+ speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]) * 0.8;
73
+ opacity = 0.4 + Math.random() * 0.2;
74
+ }
75
+
76
+ return {
77
+ type: 'heart',
78
+ x: Math.random() * canvasWidth,
79
+ y: Math.random() * canvasHeight,
80
+ size: size,
81
+ vx: (Math.random() - 0.5) * 0.3, // Gentle horizontal drift
82
+ vy: (Math.random() - 0.7) * 0.2, // Slight upward bias (floating)
83
+ speed: speed * 0.3, // Much slower overall movement
84
+ opacity: opacity,
85
+ color: heartColors[Math.floor(Math.random() * heartColors.length)],
86
+ windOffset: Math.random() * Math.PI * 2,
87
+ windSpeed: 0.005 + Math.random() * 0.01, // Slower wind
88
+ rotation: Math.random() * Math.PI * 2,
89
+ rotationSpeed: (Math.random() - 0.5) * 0.01, // Slower rotation
90
+ pulsePhase: Math.random() * Math.PI * 2,
91
+ pulseSpeed: 0.002 + Math.random() * 0.003,
92
+ floatPhase: Math.random() * Math.PI * 2, // For bobbing motion
93
+ floatSpeed: 0.001 + Math.random() * 0.002,
94
+ active: true,
95
+ depth: depth
96
+ };
97
+ },
98
+
99
+ /**
100
+ * Create rose petal particle
101
+ */
102
+ createRosePetal(canvasWidth, canvasHeight, config) {
103
+ const petalColors = ['#ff1493', '#ff69b4', '#dc143c', '#ff758f', '#ffb6c1'];
104
+
105
+ return {
106
+ type: 'rose-petal',
107
+ x: Math.random() * canvasWidth,
108
+ y: -20,
109
+ size: 1.5 + Math.random() * 2,
110
+ speed: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]) * 0.6,
111
+ opacity: 0.7 + Math.random() * 0.3,
112
+ color: petalColors[Math.floor(Math.random() * petalColors.length)],
113
+ windOffset: Math.random() * Math.PI * 2,
114
+ windSpeed: 0.02 + Math.random() * 0.03,
115
+ rotation: Math.random() * Math.PI * 2,
116
+ rotationSpeed: (Math.random() - 0.5) * 0.05,
117
+ flutterPhase: Math.random() * Math.PI * 2,
118
+ active: true
119
+ };
120
+ },
121
+
122
+ /**
123
+ * Create sparkle particle
124
+ */
125
+ createSparkle(canvasWidth, canvasHeight, config) {
126
+ return {
127
+ type: 'sparkle',
128
+ x: Math.random() * canvasWidth,
129
+ y: Math.random() * canvasHeight,
130
+ size: 0.5 + Math.random() * 1.5,
131
+ opacity: 1,
132
+ life: 20 + Math.random() * 30,
133
+ maxLife: 20 + Math.random() * 30,
134
+ color: Math.random() < 0.5 ? '#ffd700' : '#ffffff',
135
+ twinklePhase: Math.random() * Math.PI * 2,
136
+ active: true,
137
+ static: true
138
+ };
139
+ },
140
+
141
+ /**
142
+ * Create floating kissy lips particle
143
+ */
144
+ createLips(canvasWidth, canvasHeight, config) {
145
+ const lipColors = ['#dc143c', '#ff1493', '#c71585', '#ff0066'];
146
+
147
+ return {
148
+ type: 'lips',
149
+ x: Math.random() * canvasWidth,
150
+ y: Math.random() * canvasHeight,
151
+ size: 3 + Math.random() * 3,
152
+ vx: (Math.random() - 0.5) * 0.4,
153
+ vy: (Math.random() - 0.7) * 0.2, // Slight upward bias (floating)
154
+ speed: 0.3 + Math.random() * 0.2,
155
+ opacity: 0.8 + Math.random() * 0.2,
156
+ color: lipColors[Math.floor(Math.random() * lipColors.length)],
157
+ rotation: Math.random() * Math.PI * 2,
158
+ rotationSpeed: (Math.random() - 0.5) * 0.01,
159
+ pulsePhase: Math.random() * Math.PI * 2,
160
+ pulseSpeed: 0.003 + Math.random() * 0.002,
161
+ floatPhase: Math.random() * Math.PI * 2,
162
+ floatSpeed: 0.001 + Math.random() * 0.002,
163
+ kissPhase: Math.random() * Math.PI * 2,
164
+ active: true
165
+ };
166
+ },
167
+
168
+ /**
169
+ * Create heart garland decoration
170
+ */
171
+ createHeartGarland(canvasWidth, canvasHeight, options = {}) {
172
+ const heartCount = 8 + Math.floor(Math.random() * 5);
173
+ const hearts = [];
174
+
175
+ for (let i = 0; i < heartCount; i++) {
176
+ hearts.push({
177
+ offset: i / heartCount,
178
+ size: 8 + Math.random() * 4,
179
+ color: Math.random() < 0.5 ? '#ff1493' : '#ff69b4',
180
+ pulsePhase: Math.random() * Math.PI * 2
181
+ });
182
+ }
183
+
184
+ return {
185
+ type: 'heart-garland',
186
+ x: 0,
187
+ y: options.y !== undefined ? options.y : 50 + Math.random() * 100,
188
+ width: canvasWidth,
189
+ hearts: hearts,
190
+ sag: 40 + Math.random() * 30,
191
+ swayPhase: Math.random() * Math.PI * 2,
192
+ swayAmplitude: 10 + Math.random() * 10,
193
+ opacity: 0.8 + Math.random() * 0.2,
194
+ active: true,
195
+ static: true
196
+ };
197
+ },
198
+
199
+ /**
200
+ * Create love letter decoration
201
+ */
202
+ createLoveLetter(canvasWidth, canvasHeight, options = {}) {
203
+ const fromLeft = Math.random() < 0.5;
204
+
205
+ return {
206
+ type: 'love-letter',
207
+ x: fromLeft ? -50 : canvasWidth + 50,
208
+ y: Math.random() * canvasHeight * 0.6,
209
+ baseY: Math.random() * canvasHeight * 0.6,
210
+ vx: fromLeft ? 1 + Math.random() * 1 : -(1 + Math.random() * 1),
211
+ size: 12 + Math.random() * 6,
212
+ opacity: 0.9 + Math.random() * 0.1,
213
+ rotation: (Math.random() - 0.5) * 0.3,
214
+ rotationSpeed: (Math.random() - 0.5) * 0.005,
215
+ waveAmplitude: 15 + Math.random() * 15,
216
+ waveFrequency: 0.002 + Math.random() * 0.002,
217
+ waveOffset: Math.random() * Math.PI * 2,
218
+ time: 0,
219
+ active: true,
220
+ static: false
221
+ };
222
+ },
223
+
224
+ /**
225
+ * Create falling particle (hearts, rose petals, lips)
226
+ */
227
+ createFallingParticle(canvasWidth, canvasHeight, config) {
228
+ // Mix of hearts (60%), rose petals (25%), and lips (15%)
229
+ const choice = Math.random();
230
+
231
+ if (choice < 0.6) {
232
+ return this.createHeart(canvasWidth, canvasHeight, config);
233
+ } else if (choice < 0.85) {
234
+ return this.createRosePetal(canvasWidth, canvasHeight, config);
235
+ } else {
236
+ return this.createLips(canvasWidth, canvasHeight, config);
237
+ }
238
+ },
239
+
240
+ /**
241
+ * Create initial static decorations (heart garlands)
242
+ */
243
+ createInitialDecorations(canvasWidth, canvasHeight, config) {
244
+ const decorations = [];
245
+
246
+ // Create heart garlands
247
+ const garlandCount = config.garlands || 2;
248
+ for (let i = 0; i < garlandCount; i++) {
249
+ decorations.push(this.createHeartGarland(canvasWidth, canvasHeight, {
250
+ y: 50 + i * (canvasHeight / (garlandCount + 1))
251
+ }));
252
+ }
253
+
254
+ // Love letters/envelopes at bottom
255
+ const envelopeCount = 4;
256
+ for (let i = 0; i < envelopeCount; i++) {
257
+ decorations.push({
258
+ type: 'envelope',
259
+ x: (canvasWidth / (envelopeCount + 1)) * (i + 1) + (Math.random() - 0.5) * 30,
260
+ y: canvasHeight - 25 - Math.random() * 10,
261
+ size: 15 + Math.random() * 5,
262
+ opacity: 0.85,
263
+ rotation: (Math.random() - 0.5) * 0.3,
264
+ active: true,
265
+ static: true
266
+ });
267
+ }
268
+
269
+ // Butterflies (not static - they fly around)
270
+ const butterflyCount = config.butterflies || 3;
271
+ for (let i = 0; i < butterflyCount; i++) {
272
+ decorations.push({
273
+ type: 'butterfly',
274
+ x: Math.random() * canvasWidth,
275
+ y: Math.random() * canvasHeight * 0.6 + 50,
276
+ baseY: Math.random() * canvasHeight * 0.6 + 50,
277
+ size: 12 + Math.random() * 6,
278
+ vx: (Math.random() - 0.5) * 2,
279
+ vy: 0,
280
+ waveOffset: Math.random() * Math.PI * 2,
281
+ waveFrequency: 0.003 + Math.random() * 0.002,
282
+ waveAmplitude: 20 + Math.random() * 15,
283
+ wingPhase: Math.random() * Math.PI * 2,
284
+ opacity: 0.9,
285
+ color: Math.random() < 0.5 ? 'pink' : 'purple',
286
+ active: true,
287
+ static: false
288
+ });
289
+ }
290
+
291
+ // Pink heart-shaped moon (romantic atmospheric element)
292
+ decorations.push({
293
+ type: 'heart-moon',
294
+ x: canvasWidth * 0.85,
295
+ y: canvasHeight * 0.15,
296
+ size: 60 + Math.random() * 20,
297
+ opacity: 0.9,
298
+ glowPhase: Math.random() * Math.PI * 2,
299
+ active: true,
300
+ static: true
301
+ });
302
+
303
+ return decorations;
304
+ },
305
+
306
+ /**
307
+ * Spawn special Valentine's particles
308
+ */
309
+ spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight, config) {
310
+ const choice = Math.random();
311
+
312
+ // Cupid (very rare)
313
+ if (choice < config.cupidChance) {
314
+ if (specialParticles.some(p => p.type === 'cupid')) {
315
+ return null;
316
+ }
317
+ const fromLeft = Math.random() < 0.5;
318
+ return {
319
+ type: 'cupid',
320
+ x: fromLeft ? -100 : canvasWidth + 100,
321
+ y: Math.random() * canvasHeight * 0.4 + 50,
322
+ baseY: Math.random() * canvasHeight * 0.4 + 50,
323
+ vx: fromLeft ? 2 + Math.random() * 1.5 : -(2 + Math.random() * 1.5),
324
+ size: 15 + Math.random() * 8,
325
+ opacity: 1,
326
+ waveAmplitude: 20 + Math.random() * 20,
327
+ waveFrequency: 0.002 + Math.random() * 0.002,
328
+ waveOffset: Math.random() * Math.PI * 2,
329
+ time: 0,
330
+ wingPhase: Math.random() * Math.PI * 2,
331
+ active: true,
332
+ static: false
333
+ };
334
+ }
335
+
336
+ // Love letter (rare)
337
+ if (choice < 0.001) {
338
+ return this.createLoveLetter(canvasWidth, canvasHeight);
339
+ }
340
+
341
+ // Neon sign (very rare, only if none active)
342
+ if (choice < 0.0015) {
343
+ if (specialParticles.some(p => p.type === 'neon-sign' && p.active)) {
344
+ return null;
345
+ }
346
+ return {
347
+ type: 'neon-sign',
348
+ x: canvasWidth / 2,
349
+ y: canvasHeight * 0.3,
350
+ size: 40 + Math.random() * 20,
351
+ opacity: 0,
352
+ targetOpacity: 1,
353
+ phase: 'fade-in', // fade-in → stable → fade-out
354
+ phaseTime: 0,
355
+ fadeInDuration: 1500, // 1.5 seconds to fade in with crackle
356
+ stableDuration: 5000, // 5 seconds stable
357
+ fadeOutDuration: 1000, // 1 second to fade out
358
+ crackleIntensity: 1, // Starts high, decreases
359
+ flickerPhase: Math.random() * Math.PI * 2,
360
+ glowPhase: Math.random() * Math.PI * 2,
361
+ tubeFlickers: [], // Individual tube flicker states
362
+ active: true,
363
+ static: false
364
+ };
365
+ }
366
+
367
+ // Sparkles (common)
368
+ if (choice < 0.02) {
369
+ return this.createSparkle(canvasWidth, canvasHeight, config);
370
+ }
371
+
372
+ return null;
373
+ },
374
+
375
+ /**
376
+ * Draw heart shape
377
+ */
378
+ drawHeart(ctx, particle, time) {
379
+ const x = particle.x;
380
+ const y = particle.y;
381
+ const size = particle.size;
382
+
383
+ // Pulsing effect
384
+ const pulse = 1 + Math.sin(time * particle.pulseSpeed + particle.pulsePhase) * 0.15;
385
+
386
+ ctx.save();
387
+ ctx.globalAlpha = particle.opacity;
388
+ ctx.translate(x, y);
389
+ ctx.rotate(particle.rotation);
390
+ ctx.scale(pulse, pulse);
391
+
392
+ // Heart shape
393
+ ctx.fillStyle = particle.color;
394
+ ctx.shadowColor = particle.color;
395
+ ctx.shadowBlur = size * 0.8;
396
+
397
+ ctx.beginPath();
398
+ ctx.moveTo(0, size * 0.3);
399
+
400
+ // Left curve
401
+ ctx.bezierCurveTo(
402
+ -size, -size * 0.3,
403
+ -size * 0.6, -size,
404
+ 0, -size * 0.4
405
+ );
406
+
407
+ // Right curve
408
+ ctx.bezierCurveTo(
409
+ size * 0.6, -size,
410
+ size, -size * 0.3,
411
+ 0, size * 0.3
412
+ );
413
+
414
+ ctx.closePath();
415
+ ctx.fill();
416
+
417
+ // Highlight
418
+ ctx.shadowBlur = 0;
419
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
420
+ ctx.beginPath();
421
+ ctx.arc(-size * 0.25, -size * 0.5, size * 0.15, 0, Math.PI * 2);
422
+ ctx.fill();
423
+
424
+ ctx.restore();
425
+ },
426
+
427
+ /**
428
+ * Draw rose petal
429
+ */
430
+ drawRosePetal(ctx, particle, time) {
431
+ const x = particle.x;
432
+ const y = particle.y;
433
+ const size = particle.size;
434
+
435
+ // Flutter effect
436
+ const flutter = Math.sin(time * 0.01 + particle.flutterPhase) * 0.3;
437
+
438
+ ctx.save();
439
+ ctx.globalAlpha = particle.opacity;
440
+ ctx.translate(x, y);
441
+ ctx.rotate(particle.rotation + flutter);
442
+
443
+ // Petal shape
444
+ ctx.fillStyle = particle.color;
445
+ ctx.strokeStyle = 'rgba(139, 0, 0, 0.3)'; // Dark red vein
446
+ ctx.lineWidth = 0.5;
447
+
448
+ ctx.beginPath();
449
+ ctx.moveTo(0, 0);
450
+ ctx.bezierCurveTo(
451
+ size * 2, -size,
452
+ size * 2, size,
453
+ 0, 0
454
+ );
455
+ ctx.closePath();
456
+ ctx.fill();
457
+
458
+ // Vein
459
+ ctx.beginPath();
460
+ ctx.moveTo(0, 0);
461
+ ctx.quadraticCurveTo(size, 0, size * 1.5, 0);
462
+ ctx.stroke();
463
+
464
+ ctx.restore();
465
+ },
466
+
467
+ /**
468
+ * Draw sparkle
469
+ */
470
+ drawSparkle(ctx, particle, time) {
471
+ const x = particle.x;
472
+ const y = particle.y;
473
+ const size = particle.size;
474
+ const lifeRatio = particle.life / particle.maxLife;
475
+ const twinkle = (Math.sin(time * 0.01 + particle.twinklePhase) + 1) * 0.5;
476
+
477
+ ctx.save();
478
+ ctx.globalAlpha = particle.opacity * lifeRatio * twinkle;
479
+ ctx.translate(x, y);
480
+
481
+ // Four-pointed star
482
+ ctx.fillStyle = particle.color;
483
+ ctx.shadowColor = particle.color;
484
+ ctx.shadowBlur = size * 2;
485
+
486
+ ctx.beginPath();
487
+ for (let i = 0; i < 4; i++) {
488
+ const angle = (i / 4) * Math.PI * 2 - Math.PI / 2;
489
+ const tipX = Math.cos(angle) * size * 3;
490
+ const tipY = Math.sin(angle) * size * 3;
491
+ const midAngle = angle + Math.PI / 4;
492
+ const midX = Math.cos(midAngle) * size * 0.5;
493
+ const midY = Math.sin(midAngle) * size * 0.5;
494
+
495
+ if (i === 0) ctx.moveTo(tipX, tipY);
496
+ else {
497
+ ctx.lineTo(midX, midY);
498
+ ctx.lineTo(tipX, tipY);
499
+ }
500
+ }
501
+ ctx.closePath();
502
+ ctx.fill();
503
+
504
+ ctx.restore();
505
+ },
506
+
507
+ /**
508
+ * Draw kissy lips with puckered effect
509
+ */
510
+ drawLips(ctx, particle, time) {
511
+ const x = particle.x;
512
+ const y = particle.y;
513
+ const size = particle.size;
514
+
515
+ // Pulsing/kissing effect
516
+ const pulse = 1 + Math.sin(time * particle.pulseSpeed + particle.pulsePhase) * 0.1;
517
+ const kissAnimation = Math.sin(time * 0.008 + particle.kissPhase);
518
+
519
+ ctx.save();
520
+ ctx.globalAlpha = particle.opacity;
521
+ ctx.translate(x, y);
522
+ ctx.rotate(particle.rotation);
523
+ ctx.scale(pulse, pulse);
524
+
525
+ // Lip color with gradient (darker at edges)
526
+ const lipGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size * 2);
527
+ lipGradient.addColorStop(0, particle.color);
528
+ lipGradient.addColorStop(0.6, particle.color);
529
+ lipGradient.addColorStop(1, '#8b0000');
530
+
531
+ ctx.fillStyle = lipGradient;
532
+ ctx.strokeStyle = '#8b0000';
533
+ ctx.lineWidth = size * 0.1;
534
+
535
+ // Upper lip (two curves meeting in middle)
536
+ ctx.beginPath();
537
+ ctx.moveTo(-size, 0);
538
+ // Left side of cupid's bow
539
+ ctx.bezierCurveTo(
540
+ -size * 0.8, -size * 0.8,
541
+ -size * 0.3, -size * 1.0,
542
+ 0, -size * 0.6
543
+ );
544
+ // Right side of cupid's bow
545
+ ctx.bezierCurveTo(
546
+ size * 0.3, -size * 1.0,
547
+ size * 0.8, -size * 0.8,
548
+ size, 0
549
+ );
550
+ ctx.fill();
551
+ ctx.stroke();
552
+
553
+ // Lower lip (fuller, puckered)
554
+ const puckerAmount = kissAnimation * size * 0.15;
555
+ ctx.beginPath();
556
+ ctx.moveTo(-size, 0);
557
+ ctx.bezierCurveTo(
558
+ -size * 0.7, size * (0.7 + puckerAmount),
559
+ -size * 0.3, size * (1.0 + puckerAmount),
560
+ 0, size * (0.8 + puckerAmount)
561
+ );
562
+ ctx.bezierCurveTo(
563
+ size * 0.3, size * (1.0 + puckerAmount),
564
+ size * 0.7, size * (0.7 + puckerAmount),
565
+ size, 0
566
+ );
567
+ ctx.fill();
568
+ ctx.stroke();
569
+
570
+ // Glossy highlight on upper lip
571
+ ctx.globalAlpha = particle.opacity * 0.5;
572
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
573
+ ctx.beginPath();
574
+ ctx.ellipse(-size * 0.4, -size * 0.4, size * 0.2, size * 0.15, -0.3, 0, Math.PI * 2);
575
+ ctx.fill();
576
+ ctx.beginPath();
577
+ ctx.ellipse(size * 0.4, -size * 0.4, size * 0.2, size * 0.15, 0.3, 0, Math.PI * 2);
578
+ ctx.fill();
579
+
580
+ // Glossy highlight on lower lip (center)
581
+ ctx.beginPath();
582
+ ctx.ellipse(0, size * 0.5, size * 0.3, size * 0.2, 0, 0, Math.PI * 2);
583
+ ctx.fill();
584
+
585
+ // Optional: add tiny sparkle for "mwah" kiss effect
586
+ if (kissAnimation > 0.8) {
587
+ const sparkleDistance = size * 1.5;
588
+ const sparkleX = Math.sin(particle.kissPhase) * sparkleDistance;
589
+ const sparkleY = -Math.abs(Math.cos(particle.kissPhase)) * sparkleDistance;
590
+
591
+ ctx.globalAlpha = particle.opacity * (kissAnimation - 0.8) * 5;
592
+ ctx.fillStyle = '#ffd700';
593
+ ctx.shadowColor = '#ffd700';
594
+ ctx.shadowBlur = size * 0.5;
595
+ ctx.beginPath();
596
+ ctx.arc(sparkleX, sparkleY, size * 0.15, 0, Math.PI * 2);
597
+ ctx.fill();
598
+
599
+ // Star sparkle
600
+ for (let i = 0; i < 4; i++) {
601
+ const angle = (i / 4) * Math.PI * 2 + time * 0.01;
602
+ const tipX = sparkleX + Math.cos(angle) * size * 0.4;
603
+ const tipY = sparkleY + Math.sin(angle) * size * 0.4;
604
+ ctx.beginPath();
605
+ ctx.moveTo(sparkleX, sparkleY);
606
+ ctx.lineTo(tipX, tipY);
607
+ ctx.lineWidth = size * 0.05;
608
+ ctx.strokeStyle = '#ffffff';
609
+ ctx.stroke();
610
+ }
611
+ }
612
+
613
+ ctx.restore();
614
+ },
615
+
616
+ /**
617
+ * Draw cupid (side profile)
618
+ */
619
+ drawCupid(ctx, particle, time) {
620
+ const x = particle.x;
621
+ const y = particle.y;
622
+ const size = particle.size;
623
+ const dir = particle.vx > 0 ? 1 : -1;
624
+
625
+ ctx.save();
626
+ ctx.globalAlpha = particle.opacity;
627
+ ctx.translate(x, y);
628
+ if (dir === -1) {
629
+ ctx.scale(-1, 1);
630
+ }
631
+
632
+ // Wing flapping
633
+ const wingAngle = Math.sin(time * 0.015 + particle.wingPhase) * (Math.PI / 6);
634
+
635
+ // Back wing (behind body)
636
+ ctx.fillStyle = '#ffffff';
637
+ ctx.strokeStyle = '#ffb6c1';
638
+ ctx.lineWidth = 1.5;
639
+ ctx.save();
640
+ ctx.translate(-size * 0.2, -size * 0.3);
641
+ ctx.rotate(-wingAngle);
642
+ ctx.beginPath();
643
+ ctx.moveTo(0, 0);
644
+ ctx.bezierCurveTo(
645
+ -size * 1.2, -size * 0.8,
646
+ -size * 1.5, -size * 0.3,
647
+ -size * 1.2, size * 0.3
648
+ );
649
+ ctx.bezierCurveTo(
650
+ -size * 0.8, size * 0.2,
651
+ -size * 0.3, 0,
652
+ 0, 0
653
+ );
654
+ ctx.closePath();
655
+ ctx.fill();
656
+ ctx.stroke();
657
+ ctx.restore();
658
+
659
+ // Body (side profile - oval)
660
+ ctx.fillStyle = '#ffdbac'; // Peachy skin tone
661
+ ctx.beginPath();
662
+ ctx.ellipse(0, 0, size * 0.4, size * 0.5, 0, 0, Math.PI * 2);
663
+ ctx.fill();
664
+
665
+ // Head (side profile)
666
+ ctx.beginPath();
667
+ ctx.ellipse(size * 0.3, -size * 0.6, size * 0.35, size * 0.4, 0, 0, Math.PI * 2);
668
+ ctx.fill();
669
+
670
+ // Hair (curly golden locks)
671
+ ctx.fillStyle = '#ffd700';
672
+ ctx.beginPath();
673
+ ctx.arc(size * 0.1, -size * 0.9, size * 0.22, 0, Math.PI * 2);
674
+ ctx.fill();
675
+ ctx.beginPath();
676
+ ctx.arc(size * 0.4, -size * 0.95, size * 0.18, 0, Math.PI * 2);
677
+ ctx.fill();
678
+ ctx.beginPath();
679
+ ctx.arc(size * 0.15, -size * 0.75, size * 0.15, 0, Math.PI * 2);
680
+ ctx.fill();
681
+
682
+ // Eye (single eye, profile view)
683
+ ctx.fillStyle = '#000000';
684
+ ctx.beginPath();
685
+ ctx.arc(size * 0.42, -size * 0.62, size * 0.05, 0, Math.PI * 2);
686
+ ctx.fill();
687
+
688
+ // Rosy cheek
689
+ ctx.fillStyle = 'rgba(255, 105, 180, 0.3)';
690
+ ctx.beginPath();
691
+ ctx.arc(size * 0.25, -size * 0.5, size * 0.12, 0, Math.PI * 2);
692
+ ctx.fill();
693
+
694
+ // Legs (bent in flying position)
695
+ ctx.strokeStyle = '#ffdbac';
696
+ ctx.lineWidth = size * 0.15;
697
+ ctx.lineCap = 'round';
698
+ ctx.beginPath();
699
+ ctx.moveTo(-size * 0.15, size * 0.4);
700
+ ctx.lineTo(-size * 0.3, size * 0.8);
701
+ ctx.stroke();
702
+ ctx.beginPath();
703
+ ctx.moveTo(-size * 0.05, size * 0.4);
704
+ ctx.lineTo(-size * 0.15, size * 0.75);
705
+ ctx.stroke();
706
+
707
+ // Arms holding bow
708
+ ctx.strokeStyle = '#ffdbac';
709
+ ctx.lineWidth = size * 0.12;
710
+ ctx.beginPath();
711
+ ctx.moveTo(size * 0.1, -size * 0.2);
712
+ ctx.lineTo(size * 0.6, -size * 0.3);
713
+ ctx.stroke();
714
+ ctx.beginPath();
715
+ ctx.moveTo(size * 0.05, 0);
716
+ ctx.lineTo(size * 0.9, -size * 0.35);
717
+ ctx.stroke();
718
+
719
+ // Bow (curved wooden bow)
720
+ ctx.strokeStyle = '#8B4513';
721
+ ctx.lineWidth = 2.5;
722
+ ctx.beginPath();
723
+ ctx.moveTo(size * 0.9, -size * 0.6);
724
+ ctx.quadraticCurveTo(size * 1.1, -size * 0.35, size * 0.9, -size * 0.1);
725
+ ctx.stroke();
726
+
727
+ // Bow string (drawn back)
728
+ ctx.strokeStyle = '#daa520';
729
+ ctx.lineWidth = 1.5;
730
+ ctx.beginPath();
731
+ ctx.moveTo(size * 0.9, -size * 0.6);
732
+ ctx.lineTo(size * 0.6, -size * 0.35);
733
+ ctx.lineTo(size * 0.9, -size * 0.1);
734
+ ctx.stroke();
735
+
736
+ // Arrow (nocked and ready)
737
+ ctx.strokeStyle = '#8B4513';
738
+ ctx.lineWidth = 2;
739
+ ctx.beginPath();
740
+ ctx.moveTo(size * 0.6, -size * 0.35);
741
+ ctx.lineTo(size * 1.8, -size * 0.35);
742
+ ctx.stroke();
743
+
744
+ // Arrowhead (heart-shaped)
745
+ ctx.fillStyle = '#dc143c';
746
+ ctx.beginPath();
747
+ ctx.moveTo(size * 1.8, -size * 0.35);
748
+ ctx.lineTo(size * 2.0, -size * 0.35);
749
+ ctx.lineTo(size * 1.9, -size * 0.25);
750
+ ctx.closePath();
751
+ ctx.fill();
752
+ ctx.beginPath();
753
+ ctx.moveTo(size * 1.8, -size * 0.35);
754
+ ctx.lineTo(size * 2.0, -size * 0.35);
755
+ ctx.lineTo(size * 1.9, -size * 0.45);
756
+ ctx.closePath();
757
+ ctx.fill();
758
+
759
+ // Arrow fletching
760
+ ctx.fillStyle = '#ff69b4';
761
+ ctx.beginPath();
762
+ ctx.moveTo(size * 0.6, -size * 0.35);
763
+ ctx.lineTo(size * 0.5, -size * 0.25);
764
+ ctx.lineTo(size * 0.5, -size * 0.45);
765
+ ctx.closePath();
766
+ ctx.fill();
767
+
768
+ // Front wing (over body)
769
+ ctx.fillStyle = '#ffffff';
770
+ ctx.strokeStyle = '#ffb6c1';
771
+ ctx.lineWidth = 1.5;
772
+ ctx.save();
773
+ ctx.translate(size * 0.1, -size * 0.3);
774
+ ctx.rotate(wingAngle);
775
+ ctx.beginPath();
776
+ ctx.moveTo(0, 0);
777
+ ctx.bezierCurveTo(
778
+ -size * 1.0, -size * 0.6,
779
+ -size * 1.3, -size * 0.2,
780
+ -size * 1.0, size * 0.2
781
+ );
782
+ ctx.bezierCurveTo(
783
+ -size * 0.6, size * 0.15,
784
+ -size * 0.2, 0,
785
+ 0, 0
786
+ );
787
+ ctx.closePath();
788
+ ctx.fill();
789
+ ctx.stroke();
790
+ ctx.restore();
791
+
792
+ ctx.restore();
793
+ },
794
+
795
+ /**
796
+ * Draw love letter
797
+ */
798
+ drawLoveLetter(ctx, particle, time) {
799
+ const x = particle.x;
800
+ const y = particle.y;
801
+ const size = particle.size;
802
+
803
+ ctx.save();
804
+ ctx.globalAlpha = particle.opacity;
805
+ ctx.translate(x, y);
806
+ ctx.rotate(particle.rotation);
807
+
808
+ // Envelope
809
+ ctx.fillStyle = '#fff5e6';
810
+ ctx.strokeStyle = '#daa520';
811
+ ctx.lineWidth = 1;
812
+ ctx.fillRect(-size, -size * 0.7, size * 2, size * 1.4);
813
+ ctx.strokeRect(-size, -size * 0.7, size * 2, size * 1.4);
814
+
815
+ // Envelope flap (top triangle)
816
+ ctx.fillStyle = '#ffe4b3';
817
+ ctx.beginPath();
818
+ ctx.moveTo(-size, -size * 0.7);
819
+ ctx.lineTo(0, 0);
820
+ ctx.lineTo(size, -size * 0.7);
821
+ ctx.closePath();
822
+ ctx.fill();
823
+ ctx.stroke();
824
+
825
+ // Heart seal
826
+ ctx.fillStyle = '#dc143c';
827
+ ctx.shadowColor = '#dc143c';
828
+ ctx.shadowBlur = 5;
829
+ ctx.beginPath();
830
+ ctx.moveTo(0, size * 0.1);
831
+ ctx.bezierCurveTo(-size * 0.3, -size * 0.1, -size * 0.2, -size * 0.3, 0, -size * 0.15);
832
+ ctx.bezierCurveTo(size * 0.2, -size * 0.3, size * 0.3, -size * 0.1, 0, size * 0.1);
833
+ ctx.closePath();
834
+ ctx.fill();
835
+ ctx.shadowBlur = 0;
836
+
837
+ ctx.restore();
838
+ },
839
+
840
+ /**
841
+ * Draw heart garland
842
+ */
843
+ drawHeartGarland(ctx, particle, time) {
844
+ const y = particle.y;
845
+ const width = particle.width;
846
+ const sag = particle.sag;
847
+ const hearts = particle.hearts;
848
+
849
+ ctx.save();
850
+ ctx.globalAlpha = particle.opacity;
851
+
852
+ // Sway effect
853
+ const swayOffset = Math.sin(time * 0.001 + particle.swayPhase) * particle.swayAmplitude;
854
+
855
+ // String
856
+ ctx.strokeStyle = '#daa520';
857
+ ctx.lineWidth = 2;
858
+ ctx.beginPath();
859
+ ctx.moveTo(0, y);
860
+ ctx.quadraticCurveTo(width / 2, y + sag + swayOffset, width, y);
861
+ ctx.stroke();
862
+
863
+ // Hearts along the string
864
+ hearts.forEach(heart => {
865
+ const heartX = heart.offset * width;
866
+ const heartY = y + Math.sin(Math.PI * heart.offset) * (sag + swayOffset);
867
+
868
+ // Pulsing
869
+ const pulse = 1 + Math.sin(time * 0.003 + heart.pulsePhase) * 0.1;
870
+
871
+ ctx.save();
872
+ ctx.translate(heartX, heartY);
873
+ ctx.scale(pulse, pulse);
874
+
875
+ ctx.fillStyle = heart.color;
876
+ ctx.shadowColor = heart.color;
877
+ ctx.shadowBlur = heart.size * 0.5;
878
+
879
+ // Heart shape
880
+ ctx.beginPath();
881
+ ctx.moveTo(0, heart.size * 0.3);
882
+ ctx.bezierCurveTo(
883
+ -heart.size, -heart.size * 0.3,
884
+ -heart.size * 0.6, -heart.size,
885
+ 0, -heart.size * 0.4
886
+ );
887
+ ctx.bezierCurveTo(
888
+ heart.size * 0.6, -heart.size,
889
+ heart.size, -heart.size * 0.3,
890
+ 0, heart.size * 0.3
891
+ );
892
+ ctx.closePath();
893
+ ctx.fill();
894
+
895
+ ctx.shadowBlur = 0;
896
+ ctx.restore();
897
+ });
898
+
899
+ ctx.restore();
900
+ },
901
+
902
+ /**
903
+ * Draw love letter/envelope
904
+ */
905
+ drawEnvelope(ctx, particle, time) {
906
+ const x = particle.x;
907
+ const y = particle.y;
908
+ const size = particle.size;
909
+
910
+ ctx.save();
911
+ ctx.globalAlpha = particle.opacity;
912
+ ctx.translate(x, y);
913
+ ctx.rotate(particle.rotation);
914
+
915
+ // Envelope body
916
+ ctx.fillStyle = '#fff5f5';
917
+ ctx.strokeStyle = '#ff69b4';
918
+ ctx.lineWidth = size * 0.05;
919
+ ctx.fillRect(-size * 0.7, -size * 0.5, size * 1.4, size);
920
+ ctx.strokeRect(-size * 0.7, -size * 0.5, size * 1.4, size);
921
+
922
+ // Envelope flap (closed)
923
+ ctx.fillStyle = '#ffe4e1';
924
+ ctx.beginPath();
925
+ ctx.moveTo(-size * 0.7, -size * 0.5);
926
+ ctx.lineTo(0, size * 0.1);
927
+ ctx.lineTo(size * 0.7, -size * 0.5);
928
+ ctx.closePath();
929
+ ctx.fill();
930
+ ctx.stroke();
931
+
932
+ // Heart seal
933
+ ctx.fillStyle = '#ff1493';
934
+ ctx.shadowColor = '#ff1493';
935
+ ctx.shadowBlur = size * 0.3;
936
+ ctx.beginPath();
937
+ ctx.moveTo(0, size * 0.25);
938
+ ctx.bezierCurveTo(
939
+ -size * 0.3, -size * 0.1,
940
+ -size * 0.2, -size * 0.3,
941
+ 0, -size * 0.15
942
+ );
943
+ ctx.bezierCurveTo(
944
+ size * 0.2, -size * 0.3,
945
+ size * 0.3, -size * 0.1,
946
+ 0, size * 0.25
947
+ );
948
+ ctx.closePath();
949
+ ctx.fill();
950
+
951
+ ctx.shadowBlur = 0;
952
+ ctx.restore();
953
+ },
954
+
955
+ /**
956
+ * Draw butterfly
957
+ */
958
+ drawButterfly(ctx, particle, time) {
959
+ const x = particle.x + Math.sin(time * particle.waveFrequency + particle.waveOffset) * particle.waveAmplitude;
960
+ const y = particle.baseY + Math.cos(time * particle.waveFrequency * 0.7 + particle.waveOffset) * (particle.waveAmplitude * 0.6);
961
+ const size = particle.size;
962
+ const dir = particle.vx > 0 ? 1 : -1;
963
+
964
+ ctx.save();
965
+ ctx.globalAlpha = particle.opacity;
966
+ ctx.translate(x, y);
967
+ if (dir === -1) {
968
+ ctx.scale(-1, 1);
969
+ }
970
+
971
+ // Wing flap animation
972
+ const wingAngle = Math.sin(time * 0.015 + particle.wingPhase) * (Math.PI / 6);
973
+
974
+ // Color palette
975
+ const colors = {
976
+ pink: { main: '#ffb3d9', accent: '#ff69b4', outline: '#ff1493' },
977
+ purple: { main: '#dda0dd', accent: '#ba55d3', outline: '#9932cc' }
978
+ };
979
+ const col = colors[particle.color];
980
+
981
+ // Left wings (back)
982
+ ctx.save();
983
+ ctx.translate(-size * 0.15, 0);
984
+ ctx.rotate(-wingAngle);
985
+
986
+ // Upper left wing
987
+ ctx.fillStyle = col.main;
988
+ ctx.strokeStyle = col.outline;
989
+ ctx.lineWidth = size * 0.03;
990
+ ctx.beginPath();
991
+ ctx.moveTo(0, 0);
992
+ ctx.bezierCurveTo(
993
+ -size * 0.8, -size * 0.6,
994
+ -size * 0.5, -size * 1.2,
995
+ 0, -size * 0.5
996
+ );
997
+ ctx.closePath();
998
+ ctx.fill();
999
+ ctx.stroke();
1000
+
1001
+ // Lower left wing
1002
+ ctx.beginPath();
1003
+ ctx.moveTo(0, 0);
1004
+ ctx.bezierCurveTo(
1005
+ -size * 0.7, size * 0.4,
1006
+ -size * 0.4, size * 0.9,
1007
+ 0, size * 0.4
1008
+ );
1009
+ ctx.closePath();
1010
+ ctx.fill();
1011
+ ctx.stroke();
1012
+
1013
+ // Wing patterns (spots)
1014
+ ctx.fillStyle = col.accent;
1015
+ ctx.beginPath();
1016
+ ctx.arc(-size * 0.4, -size * 0.5, size * 0.12, 0, Math.PI * 2);
1017
+ ctx.fill();
1018
+ ctx.beginPath();
1019
+ ctx.arc(-size * 0.35, size * 0.35, size * 0.1, 0, Math.PI * 2);
1020
+ ctx.fill();
1021
+
1022
+ ctx.restore();
1023
+
1024
+ // Right wings (front)
1025
+ ctx.save();
1026
+ ctx.translate(size * 0.15, 0);
1027
+ ctx.rotate(wingAngle);
1028
+
1029
+ // Upper right wing
1030
+ ctx.fillStyle = col.main;
1031
+ ctx.strokeStyle = col.outline;
1032
+ ctx.lineWidth = size * 0.03;
1033
+ ctx.beginPath();
1034
+ ctx.moveTo(0, 0);
1035
+ ctx.bezierCurveTo(
1036
+ size * 0.8, -size * 0.6,
1037
+ size * 0.5, -size * 1.2,
1038
+ 0, -size * 0.5
1039
+ );
1040
+ ctx.closePath();
1041
+ ctx.fill();
1042
+ ctx.stroke();
1043
+
1044
+ // Lower right wing
1045
+ ctx.beginPath();
1046
+ ctx.moveTo(0, 0);
1047
+ ctx.bezierCurveTo(
1048
+ size * 0.7, size * 0.4,
1049
+ size * 0.4, size * 0.9,
1050
+ 0, size * 0.4
1051
+ );
1052
+ ctx.closePath();
1053
+ ctx.fill();
1054
+ ctx.stroke();
1055
+
1056
+ // Wing patterns
1057
+ ctx.fillStyle = col.accent;
1058
+ ctx.beginPath();
1059
+ ctx.arc(size * 0.4, -size * 0.5, size * 0.12, 0, Math.PI * 2);
1060
+ ctx.fill();
1061
+ ctx.beginPath();
1062
+ ctx.arc(size * 0.35, size * 0.35, size * 0.1, 0, Math.PI * 2);
1063
+ ctx.fill();
1064
+
1065
+ ctx.restore();
1066
+
1067
+ // Body
1068
+ ctx.fillStyle = '#4a4a4a';
1069
+ ctx.fillRect(-size * 0.05, -size * 0.5, size * 0.1, size * 0.9);
1070
+
1071
+ // Head
1072
+ ctx.fillStyle = '#2a2a2a';
1073
+ ctx.beginPath();
1074
+ ctx.arc(0, -size * 0.55, size * 0.12, 0, Math.PI * 2);
1075
+ ctx.fill();
1076
+
1077
+ // Antennae
1078
+ ctx.strokeStyle = '#2a2a2a';
1079
+ ctx.lineWidth = size * 0.02;
1080
+ ctx.beginPath();
1081
+ ctx.moveTo(0, -size * 0.6);
1082
+ ctx.quadraticCurveTo(-size * 0.15, -size * 0.8, -size * 0.2, -size * 0.75);
1083
+ ctx.stroke();
1084
+ ctx.beginPath();
1085
+ ctx.moveTo(0, -size * 0.6);
1086
+ ctx.quadraticCurveTo(size * 0.15, -size * 0.8, size * 0.2, -size * 0.75);
1087
+ ctx.stroke();
1088
+
1089
+ // Antennae tips
1090
+ ctx.fillStyle = col.outline;
1091
+ ctx.beginPath();
1092
+ ctx.arc(-size * 0.2, -size * 0.75, size * 0.04, 0, Math.PI * 2);
1093
+ ctx.fill();
1094
+ ctx.beginPath();
1095
+ ctx.arc(size * 0.2, -size * 0.75, size * 0.04, 0, Math.PI * 2);
1096
+ ctx.fill();
1097
+
1098
+ ctx.restore();
1099
+ },
1100
+
1101
+ /**
1102
+ * Draw pink heart-shaped moon (romantic atmospheric element)
1103
+ */
1104
+ drawHeartMoon(ctx, particle, time) {
1105
+ const x = particle.x;
1106
+ const y = particle.y;
1107
+ const size = particle.size;
1108
+ const glowIntensity = 0.3 + Math.sin(time * 0.002 + particle.glowPhase) * 0.2;
1109
+
1110
+ ctx.save();
1111
+ ctx.globalAlpha = particle.opacity;
1112
+ ctx.translate(x, y);
1113
+
1114
+ // Romantic pink glow (pulsing)
1115
+ const glowGradient = ctx.createRadialGradient(0, 0, size * 0.5, 0, 0, size * 2.5);
1116
+ glowGradient.addColorStop(0, `rgba(255, 182, 193, ${glowIntensity * 0.8})`); // Light pink
1117
+ glowGradient.addColorStop(0.4, `rgba(255, 105, 180, ${glowIntensity * 0.5})`); // Hot pink
1118
+ glowGradient.addColorStop(0.7, `rgba(255, 20, 147, ${glowIntensity * 0.3})`); // Deep pink
1119
+ glowGradient.addColorStop(1, 'rgba(255, 20, 147, 0)');
1120
+
1121
+ ctx.fillStyle = glowGradient;
1122
+ ctx.beginPath();
1123
+ ctx.arc(0, 0, size * 2.5, 0, Math.PI * 2);
1124
+ ctx.fill();
1125
+
1126
+ // Heart-shaped moon body
1127
+ const moonGradient = ctx.createRadialGradient(-size * 0.3, -size * 0.3, 0, 0, 0, size);
1128
+ moonGradient.addColorStop(0, '#ffcce0'); // Pale pink center (highlight)
1129
+ moonGradient.addColorStop(0.5, '#ffb3d9'); // Soft pink
1130
+ moonGradient.addColorStop(1, '#ff69b4'); // Hot pink edge
1131
+
1132
+ ctx.fillStyle = moonGradient;
1133
+
1134
+ // Draw heart shape
1135
+ ctx.beginPath();
1136
+ ctx.moveTo(0, size * 0.3); // Bottom point
1137
+
1138
+ // Left side of heart
1139
+ ctx.bezierCurveTo(
1140
+ -size * 0.5, size * 0.5, // Control point 1
1141
+ -size, size * 0.2, // Control point 2
1142
+ -size, -size * 0.2 // End of left lobe top
1143
+ );
1144
+ // Left lobe curve (top left)
1145
+ ctx.bezierCurveTo(
1146
+ -size, -size * 0.6, // Control (top of curve)
1147
+ -size * 0.5, -size * 0.7, // Control
1148
+ 0, -size * 0.3 // Center dip between lobes
1149
+ );
1150
+
1151
+ // Right lobe curve (top right)
1152
+ ctx.bezierCurveTo(
1153
+ size * 0.5, -size * 0.7, // Control
1154
+ size, -size * 0.6, // Control (top of curve)
1155
+ size, -size * 0.2 // End of right lobe top
1156
+ );
1157
+ // Right side of heart
1158
+ ctx.bezierCurveTo(
1159
+ size, size * 0.2, // Control point 1
1160
+ size * 0.5, size * 0.5, // Control point 2
1161
+ 0, size * 0.3 // Bottom point
1162
+ );
1163
+
1164
+ ctx.closePath();
1165
+ ctx.fill();
1166
+
1167
+ // Heart outline (subtle)
1168
+ ctx.strokeStyle = '#ff1493'; // Deep pink
1169
+ ctx.lineWidth = size * 0.02;
1170
+ ctx.stroke();
1171
+
1172
+ // Crater details (lighter pink spots for texture)
1173
+ ctx.fillStyle = 'rgba(255, 240, 245, 0.4)';
1174
+ ctx.beginPath();
1175
+ ctx.arc(-size * 0.4, -size * 0.1, size * 0.15, 0, Math.PI * 2);
1176
+ ctx.fill();
1177
+
1178
+ ctx.beginPath();
1179
+ ctx.arc(size * 0.3, 0, size * 0.12, 0, Math.PI * 2);
1180
+ ctx.fill();
1181
+
1182
+ ctx.beginPath();
1183
+ ctx.arc(-size * 0.2, size * 0.15, size * 0.1, 0, Math.PI * 2);
1184
+ ctx.fill();
1185
+
1186
+ // Sparkles on the moon surface
1187
+ ctx.fillStyle = `rgba(255, 255, 255, ${glowIntensity})`;
1188
+ const sparklePositions = [
1189
+ { x: -size * 0.3, y: -size * 0.4 },
1190
+ { x: size * 0.2, y: -size * 0.3 },
1191
+ { x: 0, y: 0 },
1192
+ { x: -size * 0.5, y: size * 0.1 },
1193
+ { x: size * 0.4, y: size * 0.15 }
1194
+ ];
1195
+
1196
+ for (const pos of sparklePositions) {
1197
+ ctx.save();
1198
+ ctx.translate(pos.x, pos.y);
1199
+
1200
+ // Four-pointed star sparkle
1201
+ ctx.beginPath();
1202
+ const sparkleSize = size * 0.08;
1203
+ ctx.moveTo(0, -sparkleSize);
1204
+ ctx.lineTo(sparkleSize * 0.2, -sparkleSize * 0.2);
1205
+ ctx.lineTo(sparkleSize, 0);
1206
+ ctx.lineTo(sparkleSize * 0.2, sparkleSize * 0.2);
1207
+ ctx.lineTo(0, sparkleSize);
1208
+ ctx.lineTo(-sparkleSize * 0.2, sparkleSize * 0.2);
1209
+ ctx.lineTo(-sparkleSize, 0);
1210
+ ctx.lineTo(-sparkleSize * 0.2, -sparkleSize * 0.2);
1211
+ ctx.closePath();
1212
+ ctx.fill();
1213
+
1214
+ ctx.restore();
1215
+ }
1216
+
1217
+ ctx.restore();
1218
+ },
1219
+
1220
+ /**
1221
+ * Draw "Happy Valentine's Day" neon sign (crackles in, stays 5s, fades out)
1222
+ */
1223
+ drawNeonSign(ctx, particle, time) {
1224
+ const x = particle.x;
1225
+ const y = particle.y;
1226
+ const size = particle.size;
1227
+
1228
+ // Update phase timing
1229
+ particle.phaseTime += 16; // Assuming ~60fps
1230
+
1231
+ // Phase transitions
1232
+ if (particle.phase === 'fade-in' && particle.phaseTime >= particle.fadeInDuration) {
1233
+ particle.phase = 'stable';
1234
+ particle.phaseTime = 0;
1235
+ particle.opacity = particle.targetOpacity;
1236
+ particle.crackleIntensity = 0; // Stop crackling
1237
+ } else if (particle.phase === 'stable' && particle.phaseTime >= particle.stableDuration) {
1238
+ particle.phase = 'fade-out';
1239
+ particle.phaseTime = 0;
1240
+ } else if (particle.phase === 'fade-out' && particle.phaseTime >= particle.fadeOutDuration) {
1241
+ particle.active = false;
1242
+ return;
1243
+ }
1244
+
1245
+ // Calculate opacity based on phase
1246
+ if (particle.phase === 'fade-in') {
1247
+ particle.opacity = (particle.phaseTime / particle.fadeInDuration) * particle.targetOpacity;
1248
+ particle.crackleIntensity = 1 - (particle.phaseTime / particle.fadeInDuration); // Decreases over time
1249
+ } else if (particle.phase === 'fade-out') {
1250
+ particle.opacity = particle.targetOpacity * (1 - particle.phaseTime / particle.fadeOutDuration);
1251
+ }
1252
+
1253
+ ctx.save();
1254
+ ctx.translate(x, y);
1255
+
1256
+ // Neon colors (hot pink with slight variations)
1257
+ const neonPink = '#ff1493';
1258
+ const neonRed = '#ff0066';
1259
+ const neonWhite = '#ffffff';
1260
+
1261
+ // Text to display
1262
+ const text = 'Happy Valentine\'s Day';
1263
+
1264
+ // Font settings
1265
+ const fontSize = size;
1266
+ ctx.font = `bold ${fontSize}px 'Arial Black', sans-serif`;
1267
+ ctx.textAlign = 'center';
1268
+ ctx.textBaseline = 'middle';
1269
+
1270
+ // Measure text for effects
1271
+ const textWidth = ctx.measureText(text).width;
1272
+
1273
+ // Crackling effect during fade-in
1274
+ let crackleOffset = 0;
1275
+ if (particle.crackleIntensity > 0) {
1276
+ // Random flicker/crackle displacement
1277
+ crackleOffset = (Math.random() - 0.5) * particle.crackleIntensity * 5;
1278
+ }
1279
+
1280
+ // Individual letter flicker during crackle
1281
+ const letters = text.split('');
1282
+ let currentX = -textWidth / 2;
1283
+
1284
+ for (let i = 0; i < letters.length; i++) {
1285
+ const letter = letters[i];
1286
+ const letterWidth = ctx.measureText(letter).width;
1287
+
1288
+ // Random flicker for this letter during crackle
1289
+ const letterFlicker = particle.crackleIntensity > 0
1290
+ ? Math.random() < particle.crackleIntensity * 0.3
1291
+ : false;
1292
+
1293
+ const letterOpacity = letterFlicker
1294
+ ? particle.opacity * (0.3 + Math.random() * 0.7)
1295
+ : particle.opacity;
1296
+
1297
+ ctx.save();
1298
+ ctx.translate(currentX + letterWidth / 2, crackleOffset);
1299
+ ctx.globalAlpha = letterOpacity;
1300
+
1301
+ // Neon glow effect (multiple layers)
1302
+ const glowIntensity = 0.8 + Math.sin(time * 0.002 + particle.glowPhase + i * 0.5) * 0.2;
1303
+
1304
+ // Outer glow (largest)
1305
+ ctx.shadowColor = neonPink;
1306
+ ctx.shadowBlur = size * 0.8 * glowIntensity;
1307
+ ctx.fillStyle = neonPink;
1308
+ ctx.fillText(letter, 0, 0);
1309
+
1310
+ // Middle glow
1311
+ ctx.shadowBlur = size * 0.5 * glowIntensity;
1312
+ ctx.fillStyle = neonRed;
1313
+ ctx.fillText(letter, 0, 0);
1314
+
1315
+ // Inner glow (bright core)
1316
+ ctx.shadowBlur = size * 0.3 * glowIntensity;
1317
+ ctx.fillStyle = neonWhite;
1318
+ ctx.fillText(letter, 0, 0);
1319
+
1320
+ // Core text (very bright)
1321
+ ctx.shadowBlur = 0;
1322
+ ctx.fillStyle = neonWhite;
1323
+ ctx.globalAlpha = letterOpacity * 0.9;
1324
+ ctx.fillText(letter, 0, 0);
1325
+
1326
+ ctx.restore();
1327
+
1328
+ currentX += letterWidth;
1329
+ }
1330
+
1331
+ // Add spark/crackle particles during fade-in
1332
+ if (particle.crackleIntensity > 0.3) {
1333
+ for (let i = 0; i < 3; i++) {
1334
+ if (Math.random() < particle.crackleIntensity * 0.5) {
1335
+ const sparkX = (Math.random() - 0.5) * textWidth;
1336
+ const sparkY = (Math.random() - 0.5) * size * 0.3;
1337
+ const sparkSize = 2 + Math.random() * 3;
1338
+
1339
+ ctx.save();
1340
+ ctx.globalAlpha = particle.opacity * Math.random();
1341
+ ctx.fillStyle = Math.random() < 0.5 ? neonPink : neonWhite;
1342
+ ctx.shadowColor = neonPink;
1343
+ ctx.shadowBlur = sparkSize * 4;
1344
+ ctx.beginPath();
1345
+ ctx.arc(sparkX, sparkY, sparkSize, 0, Math.PI * 2);
1346
+ ctx.fill();
1347
+ ctx.restore();
1348
+ }
1349
+ }
1350
+ }
1351
+
1352
+ // Background glow plate (like neon backing)
1353
+ if (particle.phase === 'stable' || particle.phase === 'fade-out') {
1354
+ ctx.globalAlpha = particle.opacity * 0.1;
1355
+ ctx.fillStyle = '#ff1493';
1356
+ ctx.fillRect(-textWidth * 0.55, -size * 0.6, textWidth * 1.1, size * 1.2);
1357
+ }
1358
+
1359
+ ctx.restore();
1360
+ }
1361
+ };