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.
- package/LICENSE +21 -0
- package/README.md +469 -0
- package/admin/css/admin.css +1123 -0
- package/admin/index.html +72 -0
- package/admin/js/api.js +210 -0
- package/admin/js/app.js +270 -0
- package/admin/js/config/sidebar-config.js +107 -0
- package/admin/js/lib/card.js +63 -0
- package/admin/js/lib/image-editor.js +869 -0
- package/admin/js/lib/markdown-toolbar.js +421 -0
- package/admin/js/templates/dashboard.html +50 -0
- package/admin/js/templates/documentation.html +237 -0
- package/admin/js/templates/layouts.html +11 -0
- package/admin/js/templates/login.html +58 -0
- package/admin/js/templates/media.html +16 -0
- package/admin/js/templates/navigation.html +50 -0
- package/admin/js/templates/page-editor.html +126 -0
- package/admin/js/templates/pages.html +18 -0
- package/admin/js/templates/plugins.html +12 -0
- package/admin/js/templates/settings.html +190 -0
- package/admin/js/templates/tutorials.html +233 -0
- package/admin/js/templates/user-editor.html +12 -0
- package/admin/js/templates/users.html +10 -0
- package/admin/js/views/dashboard.js +48 -0
- package/admin/js/views/documentation.js +12 -0
- package/admin/js/views/index.js +33 -0
- package/admin/js/views/layouts.js +49 -0
- package/admin/js/views/login.js +254 -0
- package/admin/js/views/media.js +240 -0
- package/admin/js/views/navigation.js +152 -0
- package/admin/js/views/page-editor.js +479 -0
- package/admin/js/views/pages.js +64 -0
- package/admin/js/views/plugins.js +100 -0
- package/admin/js/views/settings.js +64 -0
- package/admin/js/views/tutorials.js +12 -0
- package/admin/js/views/user-editor.js +88 -0
- package/admin/js/views/users.js +73 -0
- package/bin/cli.js +334 -0
- package/config/auth.json +20 -0
- package/config/content.json +10 -0
- package/config/navigation.json +63 -0
- package/config/plugins.json +47 -0
- package/config/presets.json +34 -0
- package/config/server.json +6 -0
- package/config/site.json +33 -0
- package/package.json +67 -0
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
- package/plugins/back-to-top/config.js +10 -0
- package/plugins/back-to-top/plugin.js +24 -0
- package/plugins/back-to-top/plugin.json +36 -0
- package/plugins/back-to-top/public/inject-body.html +105 -0
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
- package/plugins/cookie-consent/config.js +30 -0
- package/plugins/cookie-consent/plugin.js +24 -0
- package/plugins/cookie-consent/plugin.json +36 -0
- package/plugins/cookie-consent/public/inject-body.html +69 -0
- package/plugins/custom-css/admin/templates/custom-css.html +17 -0
- package/plugins/custom-css/admin/views/custom-css.js +35 -0
- package/plugins/custom-css/config.js +1 -0
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +63 -0
- package/plugins/custom-css/plugin.json +32 -0
- package/plugins/custom-css/public/inject-head.html +1 -0
- package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
- package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
- package/plugins/domma-effects/config.js +9 -0
- package/plugins/domma-effects/plugin.js +22 -0
- package/plugins/domma-effects/plugin.json +36 -0
- package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
- package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
- package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
- package/plugins/domma-effects/public/celebrations/index.js +535 -0
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
- package/plugins/domma-effects/public/inject-body.html +268 -0
- package/plugins/example-analytics/admin/templates/analytics.html +10 -0
- package/plugins/example-analytics/admin/views/analytics.js +51 -0
- package/plugins/example-analytics/config.js +6 -0
- package/plugins/example-analytics/plugin.js +58 -0
- package/plugins/example-analytics/plugin.json +27 -0
- package/plugins/example-analytics/public/inject-body.html +13 -0
- package/plugins/example-analytics/public/inject-head.html +1 -0
- package/plugins/example-analytics/stats.json +1 -0
- package/plugins/form-builder/admin/templates/form-editor.html +158 -0
- package/plugins/form-builder/admin/templates/form-settings.html +29 -0
- package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
- package/plugins/form-builder/admin/templates/forms-list.html +17 -0
- package/plugins/form-builder/admin/views/form-editor.js +817 -0
- package/plugins/form-builder/admin/views/form-settings.js +38 -0
- package/plugins/form-builder/admin/views/form-submissions.js +295 -0
- package/plugins/form-builder/admin/views/forms-list.js +164 -0
- package/plugins/form-builder/config.js +9 -0
- package/plugins/form-builder/data/forms/contact-details.json +63 -0
- package/plugins/form-builder/data/forms/contact.json +52 -0
- package/plugins/form-builder/data/submissions/contact-details.json +1 -0
- package/plugins/form-builder/data/submissions/contact.json +14 -0
- package/plugins/form-builder/email.js +103 -0
- package/plugins/form-builder/plugin.js +454 -0
- package/plugins/form-builder/plugin.json +56 -0
- package/plugins/form-builder/public/inject-body.html +270 -0
- package/plugins/form-builder/public/inject-head.html +42 -0
- package/public/css/site.css +189 -0
- package/public/js/site.js +109 -0
- package/scripts/copy-domma.js +48 -0
- package/scripts/fresh.js +41 -0
- package/scripts/reset.js +124 -0
- package/scripts/seed.js +666 -0
- package/scripts/setup.js +263 -0
- package/server/config.js +56 -0
- package/server/middleware/auth.js +97 -0
- package/server/routes/api/auth.js +116 -0
- package/server/routes/api/layouts.js +25 -0
- package/server/routes/api/media.js +93 -0
- package/server/routes/api/navigation.js +37 -0
- package/server/routes/api/pages.js +118 -0
- package/server/routes/api/plugins.js +46 -0
- package/server/routes/api/settings.js +25 -0
- package/server/routes/api/users.js +110 -0
- package/server/routes/public.js +108 -0
- package/server/server.js +169 -0
- package/server/services/content.js +298 -0
- package/server/services/images.js +334 -0
- package/server/services/markdown.js +297 -0
- package/server/services/plugins.js +246 -0
- package/server/services/renderer.js +80 -0
- package/server/services/users.js +212 -0
- package/server/templates/page.html +78 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* St Andrew's Day Theme for Domma Celebrations
|
|
3
|
+
* (November 30th, Scottish Celebration)
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Falling thistles (Scottish national flower)
|
|
7
|
+
* - Snowflakes (Scottish winter)
|
|
8
|
+
* - Saltire flag patterns (white X on blue)
|
|
9
|
+
* - Bagpiper silhouette
|
|
10
|
+
* - Tartan patterns
|
|
11
|
+
* - Blue and white color scheme with purple thistles
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
name: 'st-andrews',
|
|
16
|
+
displayName: 'St Andrew\'s Day',
|
|
17
|
+
emoji: '🏴',
|
|
18
|
+
|
|
19
|
+
// Intensity configurations
|
|
20
|
+
intensityConfig: {
|
|
21
|
+
light: {
|
|
22
|
+
count: 50,
|
|
23
|
+
speedRange: [0.5, 1.5],
|
|
24
|
+
sizeRange: [2, 4],
|
|
25
|
+
thistles: 4,
|
|
26
|
+
bagpiperChance: 0.0002,
|
|
27
|
+
twinklingStars: 12
|
|
28
|
+
},
|
|
29
|
+
medium: {
|
|
30
|
+
count: 100,
|
|
31
|
+
speedRange: [0.6, 2.0],
|
|
32
|
+
sizeRange: [2, 5],
|
|
33
|
+
thistles: 6,
|
|
34
|
+
bagpiperChance: 0.0004,
|
|
35
|
+
twinklingStars: 20
|
|
36
|
+
},
|
|
37
|
+
heavy: {
|
|
38
|
+
count: 200,
|
|
39
|
+
speedRange: [0.8, 2.5],
|
|
40
|
+
sizeRange: [3, 6],
|
|
41
|
+
thistles: 8,
|
|
42
|
+
bagpiperChance: 0.0006,
|
|
43
|
+
twinklingStars: 30
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
particles: ['heather-petal', 'heather', 'saltire-sparkle'],
|
|
48
|
+
decorations: ['thistle-plant', 'bagpiper', 'saltire-flag', 'tartan-pattern', 'highland-scene', 'twinkling-star'],
|
|
49
|
+
colors: {
|
|
50
|
+
primary: '#0065BD', // Scottish blue (Saltire)
|
|
51
|
+
secondary: '#FFFFFF', // White (Saltire)
|
|
52
|
+
accent: '#8B008B', // Thistle purple (decorations only)
|
|
53
|
+
tartan: ['#0065BD', '#006400', '#8B0000', '#FFDD00']
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create heather petal particle (simple purple petal)
|
|
58
|
+
*/
|
|
59
|
+
createHeatherPetal(canvasWidth, canvasHeight, config) {
|
|
60
|
+
// Purple/pink heather shades
|
|
61
|
+
const purpleShades = ['#9370DB', '#BA55D3', '#DA70D6', '#DDA0DD', '#8B008B'];
|
|
62
|
+
return {
|
|
63
|
+
type: 'heather-petal',
|
|
64
|
+
x: -30, // Start from left edge
|
|
65
|
+
y: Math.random() * canvasHeight, // Random height
|
|
66
|
+
vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]), // Horizontal drift
|
|
67
|
+
size: (config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0])) * 1.5, // Larger petals
|
|
68
|
+
speed: (Math.random() - 0.5) * 0.2, // Gentle vertical bobbing
|
|
69
|
+
opacity: 0.75 + Math.random() * 0.25,
|
|
70
|
+
windOffset: Math.random() * Math.PI * 2,
|
|
71
|
+
windSpeed: 0.015 + Math.random() * 0.02,
|
|
72
|
+
rotation: Math.random() * Math.PI * 2,
|
|
73
|
+
rotationSpeed: (Math.random() - 0.5) * 0.03,
|
|
74
|
+
color: purpleShades[Math.floor(Math.random() * purpleShades.length)],
|
|
75
|
+
flutter: Math.random() * Math.PI * 2,
|
|
76
|
+
flutterSpeed: 0.02 + Math.random() * 0.02,
|
|
77
|
+
active: true
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create thistle particle (white and blue - Scottish Saltire colors - full flower)
|
|
83
|
+
*/
|
|
84
|
+
createThistle(canvasWidth, canvasHeight, config) {
|
|
85
|
+
const isWhite = Math.random() < 0.5; // 50% white, 50% blue
|
|
86
|
+
const flowerColor = isWhite ? '#FFFFFF' : '#0065BD';
|
|
87
|
+
const petalColors = isWhite
|
|
88
|
+
? ['#FFFFFF', '#f0f0f0', '#e0e0e0'] // White shades
|
|
89
|
+
: ['#0065BD', '#1a7dd4', '#3399ee']; // Blue shades
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
type: 'thistle',
|
|
93
|
+
x: -30, // Start from left edge
|
|
94
|
+
y: Math.random() * canvasHeight, // Random height
|
|
95
|
+
vx: config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]) * 0.7, // Horizontal drift
|
|
96
|
+
size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
|
|
97
|
+
speed: (Math.random() - 0.5) * 0.15, // Gentle vertical bobbing
|
|
98
|
+
opacity: 0.7 + Math.random() * 0.3,
|
|
99
|
+
windOffset: Math.random() * Math.PI * 2,
|
|
100
|
+
windSpeed: 0.015 + Math.random() * 0.02,
|
|
101
|
+
rotation: Math.random() * Math.PI * 2,
|
|
102
|
+
rotationSpeed: (Math.random() - 0.5) * 0.02,
|
|
103
|
+
flowerColor: flowerColor,
|
|
104
|
+
petalColors: petalColors,
|
|
105
|
+
active: true
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create heather particle (Scottish Highland heather - purple flowers - full sprig)
|
|
111
|
+
*/
|
|
112
|
+
createHeather(canvasWidth, canvasHeight, config) {
|
|
113
|
+
// Heather color variations (purple/pink shades)
|
|
114
|
+
const colorChoice = Math.random();
|
|
115
|
+
let color;
|
|
116
|
+
if (colorChoice < 0.5) {
|
|
117
|
+
color = '#9370DB'; // 50% medium purple
|
|
118
|
+
} else if (colorChoice < 0.8) {
|
|
119
|
+
color = '#BA55D3'; // 30% medium orchid
|
|
120
|
+
} else {
|
|
121
|
+
color = '#DA70D6'; // 20% orchid pink
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type: 'heather',
|
|
126
|
+
x: -30, // Start from left edge
|
|
127
|
+
y: Math.random() * canvasHeight, // Random height
|
|
128
|
+
vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.6, // Horizontal drift
|
|
129
|
+
size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]),
|
|
130
|
+
speed: (Math.random() - 0.5) * 0.15, // Gentle vertical bobbing
|
|
131
|
+
opacity: 0.75 + Math.random() * 0.25,
|
|
132
|
+
windOffset: Math.random() * Math.PI * 2,
|
|
133
|
+
windSpeed: 0.02 + Math.random() * 0.02,
|
|
134
|
+
rotation: Math.random() * Math.PI * 2,
|
|
135
|
+
rotationSpeed: (Math.random() - 0.5) * 0.03,
|
|
136
|
+
color: color,
|
|
137
|
+
sway: Math.random() * Math.PI * 2,
|
|
138
|
+
swaySpeed: 0.02 + Math.random() * 0.02,
|
|
139
|
+
bellCount: 3 + Math.floor(Math.random() * 3), // 3-5 bells per sprig
|
|
140
|
+
active: true
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create saltire sparkle particle
|
|
146
|
+
*/
|
|
147
|
+
createSaltireSparkle(canvasWidth, canvasHeight, config) {
|
|
148
|
+
const colors = ['#0065BD', '#FFFFFF']; // Scottish blue and white
|
|
149
|
+
return {
|
|
150
|
+
type: 'saltire-sparkle',
|
|
151
|
+
x: -20, // Start from left edge
|
|
152
|
+
y: Math.random() * canvasHeight, // Random height
|
|
153
|
+
vx: (config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0])) * 0.8, // Horizontal drift
|
|
154
|
+
size: config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.6,
|
|
155
|
+
vy: (Math.random() - 0.5) * 0.2, // Minimal random vertical movement
|
|
156
|
+
opacity: 0.6 + Math.random() * 0.4,
|
|
157
|
+
rotation: Math.random() * Math.PI * 2,
|
|
158
|
+
rotationSpeed: (Math.random() - 0.5) * 0.04,
|
|
159
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
160
|
+
twinklePhase: Math.random() * Math.PI * 2,
|
|
161
|
+
windOffset: Math.random() * Math.PI * 2,
|
|
162
|
+
windSpeed: 0.015 + Math.random() * 0.02,
|
|
163
|
+
active: true,
|
|
164
|
+
static: false
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create drifting particle (randomly picks type)
|
|
170
|
+
* Note: St. Andrew's Day particles drift horizontally (left-to-right), not vertically
|
|
171
|
+
*/
|
|
172
|
+
createFallingParticle(canvasWidth, canvasHeight, config) {
|
|
173
|
+
const choice = Math.random();
|
|
174
|
+
|
|
175
|
+
// 60% heather petals, 20% full heather, 15% sparkles, 5% thistles
|
|
176
|
+
if (choice < 0.6) {
|
|
177
|
+
return this.createHeatherPetal(canvasWidth, canvasHeight, config);
|
|
178
|
+
} else if (choice < 0.8) {
|
|
179
|
+
return this.createHeather(canvasWidth, canvasHeight, config);
|
|
180
|
+
} else if (choice < 0.95) {
|
|
181
|
+
return this.createSaltireSparkle(canvasWidth, canvasHeight, config);
|
|
182
|
+
} else {
|
|
183
|
+
return this.createThistle(canvasWidth, canvasHeight, config);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create static thistle plant decoration
|
|
189
|
+
*/
|
|
190
|
+
createThistlePlant(canvasWidth, canvasHeight, options = {}) {
|
|
191
|
+
return {
|
|
192
|
+
type: 'thistle-plant',
|
|
193
|
+
x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
|
|
194
|
+
y: options.y !== undefined ? options.y : canvasHeight - 40,
|
|
195
|
+
size: 15 + Math.random() * 10,
|
|
196
|
+
opacity: 0.8 + Math.random() * 0.2,
|
|
197
|
+
swayPhase: Math.random() * Math.PI * 2,
|
|
198
|
+
active: true,
|
|
199
|
+
static: true
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create initial static decorations (Scottish-themed)
|
|
205
|
+
*/
|
|
206
|
+
createInitialDecorations(canvasWidth, canvasHeight, config) {
|
|
207
|
+
const decorations = [];
|
|
208
|
+
|
|
209
|
+
// Thistle plants (Scottish national flower, multiple positions)
|
|
210
|
+
const thistleCount = config.thistles || 5;
|
|
211
|
+
for (let i = 0; i < thistleCount; i++) {
|
|
212
|
+
decorations.push(this.createThistlePlant(canvasWidth, canvasHeight, {
|
|
213
|
+
x: 80 + (i / (thistleCount - 1)) * (canvasWidth - 160),
|
|
214
|
+
y: canvasHeight - 60 - Math.random() * 30
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Bagpiper (Highland piper in traditional dress, left side)
|
|
219
|
+
decorations.push({
|
|
220
|
+
type: 'bagpiper',
|
|
221
|
+
x: 150,
|
|
222
|
+
y: canvasHeight - 60,
|
|
223
|
+
baseY: canvasHeight - 60,
|
|
224
|
+
vx: 0, // Static display
|
|
225
|
+
size: 30 + Math.random() * 8,
|
|
226
|
+
opacity: 1,
|
|
227
|
+
time: 0,
|
|
228
|
+
legPhase: 0,
|
|
229
|
+
bagInflate: 0.5,
|
|
230
|
+
active: true,
|
|
231
|
+
static: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Saltire flag (St Andrew's Cross, top right)
|
|
235
|
+
decorations.push({
|
|
236
|
+
type: 'saltire-flag',
|
|
237
|
+
x: canvasWidth - 100,
|
|
238
|
+
y: 80,
|
|
239
|
+
size: 60,
|
|
240
|
+
opacity: 1,
|
|
241
|
+
wavePhase: Math.random() * Math.PI * 2,
|
|
242
|
+
active: true,
|
|
243
|
+
static: true
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Tartan pattern display (decorative Scottish pattern, bottom center)
|
|
247
|
+
decorations.push({
|
|
248
|
+
type: 'tartan-pattern',
|
|
249
|
+
x: canvasWidth * 0.5,
|
|
250
|
+
y: canvasHeight - 80,
|
|
251
|
+
size: 80 + Math.random() * 20,
|
|
252
|
+
opacity: 0.9,
|
|
253
|
+
active: true,
|
|
254
|
+
static: true
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Highland scene silhouette (mountains, castle, loch - right side)
|
|
258
|
+
decorations.push({
|
|
259
|
+
type: 'highland-scene',
|
|
260
|
+
x: canvasWidth - 200,
|
|
261
|
+
y: canvasHeight - 150,
|
|
262
|
+
size: 100,
|
|
263
|
+
opacity: 0.7,
|
|
264
|
+
active: true,
|
|
265
|
+
static: true
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Create twinkling stars (Scottish winter night sky)
|
|
269
|
+
const starCount = config.twinklingStars || 20;
|
|
270
|
+
for (let i = 0; i < starCount; i++) {
|
|
271
|
+
decorations.push(this.createTwinklingStar(canvasWidth, canvasHeight));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return decorations;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create twinkling star particle
|
|
279
|
+
*/
|
|
280
|
+
createTwinklingStar(canvasWidth, canvasHeight) {
|
|
281
|
+
return {
|
|
282
|
+
type: 'twinkling-star',
|
|
283
|
+
x: Math.random() * canvasWidth,
|
|
284
|
+
y: Math.random() * (canvasHeight * 0.6), // Upper portion of sky
|
|
285
|
+
size: 1 + Math.random() * 2,
|
|
286
|
+
opacity: 0.6 + Math.random() * 0.3,
|
|
287
|
+
twinklePhase: Math.random() * Math.PI * 2,
|
|
288
|
+
twinkleSpeed: 0.003 + Math.random() * 0.003,
|
|
289
|
+
active: true,
|
|
290
|
+
static: true
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Spawn special St Andrew's particles
|
|
296
|
+
*/
|
|
297
|
+
spawnSpecialParticle(specialParticles, canvasWidth, canvasHeight, config) {
|
|
298
|
+
const choice = Math.random();
|
|
299
|
+
|
|
300
|
+
// Bagpiper (very rare, max 1)
|
|
301
|
+
if (choice < config.bagpiperChance) {
|
|
302
|
+
if (specialParticles.some(p => p.type === 'bagpiper')) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const fromLeft = Math.random() < 0.5;
|
|
306
|
+
return {
|
|
307
|
+
type: 'bagpiper',
|
|
308
|
+
x: fromLeft ? -60 : canvasWidth + 60,
|
|
309
|
+
y: canvasHeight - 50,
|
|
310
|
+
vx: fromLeft ? 0.8 + Math.random() * 0.5 : -(0.8 + Math.random() * 0.5),
|
|
311
|
+
size: 18 + Math.random() * 8,
|
|
312
|
+
opacity: 0.9,
|
|
313
|
+
time: 0,
|
|
314
|
+
marchPhase: Math.random() * Math.PI * 2,
|
|
315
|
+
active: true,
|
|
316
|
+
static: false
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Saltire flag (rare, max 2)
|
|
321
|
+
if (choice < 0.0004) {
|
|
322
|
+
const flagCount = specialParticles.filter(p => p.type === 'saltire-flag').length;
|
|
323
|
+
if (flagCount < 2) {
|
|
324
|
+
return {
|
|
325
|
+
type: 'saltire-flag',
|
|
326
|
+
x: Math.random() * canvasWidth * 0.6 + canvasWidth * 0.2,
|
|
327
|
+
y: 50 + Math.random() * 100,
|
|
328
|
+
size: 40 + Math.random() * 20,
|
|
329
|
+
opacity: 0.8,
|
|
330
|
+
waveOffset: Math.random() * Math.PI * 2,
|
|
331
|
+
time: 0,
|
|
332
|
+
active: true,
|
|
333
|
+
static: false
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Sparkles
|
|
339
|
+
if (choice < 0.01) {
|
|
340
|
+
return this.createSaltireSparkle(canvasWidth, canvasHeight, config);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return null;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Draw heather petal (simple purple bell-shaped petal)
|
|
348
|
+
*/
|
|
349
|
+
drawHeatherPetal(ctx, particle, time) {
|
|
350
|
+
const x = particle.x;
|
|
351
|
+
const y = particle.y;
|
|
352
|
+
const size = particle.size;
|
|
353
|
+
|
|
354
|
+
// Flutter effect (petal curling as it drifts)
|
|
355
|
+
const flutter = Math.sin(time * particle.flutterSpeed + particle.flutter) * 0.3;
|
|
356
|
+
|
|
357
|
+
ctx.save();
|
|
358
|
+
ctx.globalAlpha = particle.opacity;
|
|
359
|
+
ctx.translate(x, y);
|
|
360
|
+
ctx.rotate(particle.rotation + flutter);
|
|
361
|
+
|
|
362
|
+
// Heather bell petal shape (small bell)
|
|
363
|
+
ctx.fillStyle = particle.color;
|
|
364
|
+
ctx.strokeStyle = '#6A0DAD'; // Purple edge
|
|
365
|
+
ctx.lineWidth = size * 0.08;
|
|
366
|
+
|
|
367
|
+
ctx.beginPath();
|
|
368
|
+
// Bell shape with slight flare at bottom
|
|
369
|
+
ctx.moveTo(0, -size * 0.6);
|
|
370
|
+
ctx.bezierCurveTo(
|
|
371
|
+
size * 0.4, -size * 0.5,
|
|
372
|
+
size * 0.5, size * 0.2,
|
|
373
|
+
size * 0.3, size * 0.8
|
|
374
|
+
);
|
|
375
|
+
ctx.lineTo(-size * 0.3, size * 0.8);
|
|
376
|
+
ctx.bezierCurveTo(
|
|
377
|
+
-size * 0.5, size * 0.2,
|
|
378
|
+
-size * 0.4, -size * 0.5,
|
|
379
|
+
0, -size * 0.6
|
|
380
|
+
);
|
|
381
|
+
ctx.closePath();
|
|
382
|
+
ctx.fill();
|
|
383
|
+
ctx.stroke();
|
|
384
|
+
|
|
385
|
+
// Add bell texture lines
|
|
386
|
+
ctx.strokeStyle = 'rgba(106, 13, 173, 0.3)';
|
|
387
|
+
ctx.lineWidth = size * 0.04;
|
|
388
|
+
for (let i = 0; i < 4; i++) {
|
|
389
|
+
const yPos = -size * 0.5 + i * size * 0.35;
|
|
390
|
+
ctx.beginPath();
|
|
391
|
+
ctx.moveTo(-size * 0.25, yPos);
|
|
392
|
+
ctx.lineTo(size * 0.25, yPos);
|
|
393
|
+
ctx.stroke();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
ctx.restore();
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Draw heather (Scottish Highland heather sprig with bell-shaped flowers)
|
|
401
|
+
*/
|
|
402
|
+
drawHeather(ctx, particle) {
|
|
403
|
+
const x = particle.x;
|
|
404
|
+
const y = particle.y;
|
|
405
|
+
const size = particle.size;
|
|
406
|
+
const sway = Math.sin(particle.sway) * size * 0.4;
|
|
407
|
+
|
|
408
|
+
ctx.save();
|
|
409
|
+
ctx.globalAlpha = particle.opacity;
|
|
410
|
+
ctx.translate(x + sway, y);
|
|
411
|
+
ctx.rotate(particle.rotation);
|
|
412
|
+
|
|
413
|
+
// Stem (thin green-brown)
|
|
414
|
+
ctx.strokeStyle = '#6b5c4d';
|
|
415
|
+
ctx.lineWidth = size * 0.12;
|
|
416
|
+
ctx.lineCap = 'round';
|
|
417
|
+
ctx.beginPath();
|
|
418
|
+
ctx.moveTo(0, size * 0.6);
|
|
419
|
+
ctx.lineTo(0, -size * 0.8);
|
|
420
|
+
ctx.stroke();
|
|
421
|
+
|
|
422
|
+
// Bell-shaped flowers along the stem (small purple bells)
|
|
423
|
+
const bellCount = particle.bellCount || 4;
|
|
424
|
+
for (let i = 0; i < bellCount; i++) {
|
|
425
|
+
const yPos = -size * 0.7 + (i / bellCount) * size * 1.2;
|
|
426
|
+
const bellSize = size * (0.18 + Math.random() * 0.08);
|
|
427
|
+
const side = i % 2 === 0 ? 1 : -1; // Alternate sides
|
|
428
|
+
|
|
429
|
+
ctx.save();
|
|
430
|
+
ctx.translate(side * size * 0.15, yPos);
|
|
431
|
+
ctx.rotate(side * 0.3);
|
|
432
|
+
|
|
433
|
+
// Bell flower (small oval with darker top)
|
|
434
|
+
const bellGradient = ctx.createLinearGradient(0, -bellSize, 0, bellSize * 0.3);
|
|
435
|
+
bellGradient.addColorStop(0, particle.color);
|
|
436
|
+
bellGradient.addColorStop(0.6, particle.color);
|
|
437
|
+
bellGradient.addColorStop(1, `${particle.color}CC`); // Slightly darker at bottom
|
|
438
|
+
ctx.fillStyle = bellGradient;
|
|
439
|
+
|
|
440
|
+
// Bell shape (elongated oval, wider at bottom)
|
|
441
|
+
ctx.beginPath();
|
|
442
|
+
ctx.ellipse(0, 0, bellSize * 0.4, bellSize, 0, 0, Math.PI * 2);
|
|
443
|
+
ctx.fill();
|
|
444
|
+
|
|
445
|
+
// Bell opening (darker rim)
|
|
446
|
+
ctx.strokeStyle = `${particle.color}AA`;
|
|
447
|
+
ctx.lineWidth = bellSize * 0.1;
|
|
448
|
+
ctx.beginPath();
|
|
449
|
+
ctx.ellipse(0, bellSize * 0.7, bellSize * 0.35, bellSize * 0.15, 0, 0, Math.PI * 2);
|
|
450
|
+
ctx.stroke();
|
|
451
|
+
|
|
452
|
+
// Small stamen (thin line protruding from bell)
|
|
453
|
+
ctx.strokeStyle = '#8B7355';
|
|
454
|
+
ctx.lineWidth = bellSize * 0.08;
|
|
455
|
+
ctx.beginPath();
|
|
456
|
+
ctx.moveTo(0, bellSize * 0.8);
|
|
457
|
+
ctx.lineTo(0, bellSize * 1.1);
|
|
458
|
+
ctx.stroke();
|
|
459
|
+
|
|
460
|
+
ctx.restore();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Small leaves at base (green-grey)
|
|
464
|
+
ctx.fillStyle = '#6b8b6b';
|
|
465
|
+
for (let i = 0; i < 3; i++) {
|
|
466
|
+
const angle = (i / 3) * Math.PI * 2 - Math.PI / 2;
|
|
467
|
+
ctx.save();
|
|
468
|
+
ctx.rotate(angle);
|
|
469
|
+
ctx.beginPath();
|
|
470
|
+
ctx.ellipse(0, size * 0.5, size * 0.1, size * 0.25, 0, 0, Math.PI * 2);
|
|
471
|
+
ctx.fill();
|
|
472
|
+
ctx.restore();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
ctx.restore();
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Draw thistle (Scottish thistle with realistic spiky bracts)
|
|
480
|
+
*/
|
|
481
|
+
drawThistle(ctx, particle) {
|
|
482
|
+
const x = particle.x;
|
|
483
|
+
const y = particle.y;
|
|
484
|
+
const size = particle.size;
|
|
485
|
+
|
|
486
|
+
ctx.save();
|
|
487
|
+
ctx.globalAlpha = particle.opacity;
|
|
488
|
+
ctx.translate(x, y);
|
|
489
|
+
ctx.rotate(particle.rotation);
|
|
490
|
+
|
|
491
|
+
// Stem (woody, slightly curved)
|
|
492
|
+
ctx.strokeStyle = '#5a704a';
|
|
493
|
+
ctx.lineWidth = size * 0.1;
|
|
494
|
+
ctx.lineCap = 'round';
|
|
495
|
+
ctx.beginPath();
|
|
496
|
+
ctx.moveTo(0, size * 2);
|
|
497
|
+
ctx.quadraticCurveTo(size * 0.1, size * 1.2, 0, size * 0.5);
|
|
498
|
+
ctx.stroke();
|
|
499
|
+
|
|
500
|
+
// Spiky bracts (green spiny base around flower head)
|
|
501
|
+
const bractCount = 16;
|
|
502
|
+
ctx.strokeStyle = '#4a7a3a';
|
|
503
|
+
ctx.fillStyle = '#5a8a4a';
|
|
504
|
+
ctx.lineWidth = size * 0.06;
|
|
505
|
+
ctx.lineCap = 'round';
|
|
506
|
+
|
|
507
|
+
for (let i = 0; i < bractCount; i++) {
|
|
508
|
+
const angle = (i / bractCount) * Math.PI * 2;
|
|
509
|
+
const bractLength = size * (0.35 + Math.random() * 0.15);
|
|
510
|
+
const bendAngle = angle + (Math.random() - 0.5) * 0.3;
|
|
511
|
+
|
|
512
|
+
// Bract spine (sharp and pointed)
|
|
513
|
+
ctx.save();
|
|
514
|
+
ctx.rotate(angle);
|
|
515
|
+
ctx.beginPath();
|
|
516
|
+
ctx.moveTo(0, -size * 0.2);
|
|
517
|
+
|
|
518
|
+
// Curved spiky bract
|
|
519
|
+
const controlX = size * 0.15;
|
|
520
|
+
const controlY = -size * 0.3;
|
|
521
|
+
const endX = Math.sin(0.4) * bractLength;
|
|
522
|
+
const endY = -Math.cos(0.4) * bractLength - size * 0.2;
|
|
523
|
+
|
|
524
|
+
ctx.quadraticCurveTo(controlX, controlY, endX, endY);
|
|
525
|
+
ctx.stroke();
|
|
526
|
+
|
|
527
|
+
// Tiny spines along bract edge
|
|
528
|
+
for (let j = 0; j < 3; j++) {
|
|
529
|
+
const t = (j + 1) / 4;
|
|
530
|
+
const spineX = t * endX;
|
|
531
|
+
const spineY = -size * 0.2 + t * (endY + size * 0.2);
|
|
532
|
+
ctx.beginPath();
|
|
533
|
+
ctx.moveTo(spineX, spineY);
|
|
534
|
+
ctx.lineTo(spineX + size * 0.08, spineY - size * 0.06);
|
|
535
|
+
ctx.stroke();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
ctx.restore();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Flower head base (bulbous receptacle)
|
|
542
|
+
ctx.fillStyle = '#6a8a5a';
|
|
543
|
+
ctx.beginPath();
|
|
544
|
+
ctx.ellipse(0, -size * 0.15, size * 0.28, size * 0.35, 0, 0, Math.PI * 2);
|
|
545
|
+
ctx.fill();
|
|
546
|
+
|
|
547
|
+
// Flower petals (white or blue - Scottish Saltire colors)
|
|
548
|
+
const petalLayers = 3;
|
|
549
|
+
const petalColors = particle.petalColors || ['#9b59b6', '#b980d1', '#d4a5e3']; // Fallback to purple
|
|
550
|
+
|
|
551
|
+
for (let layer = 0; layer < petalLayers; layer++) {
|
|
552
|
+
const layerRadius = size * (0.25 + layer * 0.08);
|
|
553
|
+
const petalCount = 8 + layer * 4;
|
|
554
|
+
const layerColor = petalColors[layer];
|
|
555
|
+
|
|
556
|
+
ctx.fillStyle = layerColor;
|
|
557
|
+
|
|
558
|
+
for (let i = 0; i < petalCount; i++) {
|
|
559
|
+
const angle = (i / petalCount) * Math.PI * 2 + layer * 0.15;
|
|
560
|
+
const petalX = Math.cos(angle) * layerRadius;
|
|
561
|
+
const petalY = Math.sin(angle) * layerRadius - size * 0.15;
|
|
562
|
+
|
|
563
|
+
// Thin wispy petal
|
|
564
|
+
ctx.save();
|
|
565
|
+
ctx.translate(petalX, petalY);
|
|
566
|
+
ctx.rotate(angle);
|
|
567
|
+
ctx.beginPath();
|
|
568
|
+
ctx.moveTo(0, 0);
|
|
569
|
+
ctx.lineTo(0, -size * (0.2 + layer * 0.05));
|
|
570
|
+
ctx.lineWidth = size * 0.02;
|
|
571
|
+
ctx.strokeStyle = layerColor;
|
|
572
|
+
ctx.stroke();
|
|
573
|
+
ctx.restore();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Serrated leaves (2-3 along stem with spiny edges)
|
|
578
|
+
ctx.fillStyle = '#4a6a3a';
|
|
579
|
+
ctx.strokeStyle = '#3a5a2a';
|
|
580
|
+
ctx.lineWidth = 1;
|
|
581
|
+
|
|
582
|
+
for (let i = 0; i < 2; i++) {
|
|
583
|
+
const leafY = size * (0.8 + i * 0.6);
|
|
584
|
+
const leafSide = i % 2 === 0 ? 1 : -1;
|
|
585
|
+
|
|
586
|
+
ctx.save();
|
|
587
|
+
ctx.translate(0, leafY);
|
|
588
|
+
|
|
589
|
+
// Leaf base shape with deep serrations
|
|
590
|
+
ctx.beginPath();
|
|
591
|
+
ctx.moveTo(0, 0);
|
|
592
|
+
|
|
593
|
+
// Create deeply lobed, spiny leaf edge
|
|
594
|
+
const lobes = 5;
|
|
595
|
+
const depth = size * 0.15; // Moved outside loop so it's accessible below
|
|
596
|
+
|
|
597
|
+
for (let j = 0; j <= lobes; j++) {
|
|
598
|
+
const t = j / lobes;
|
|
599
|
+
const leafWidth = size * 0.25 * Math.sin(t * Math.PI);
|
|
600
|
+
|
|
601
|
+
if (j < lobes) {
|
|
602
|
+
// Deep cut between lobes
|
|
603
|
+
ctx.lineTo(leafSide * leafWidth * 0.3, -depth * t);
|
|
604
|
+
// Sharp spine point
|
|
605
|
+
ctx.lineTo(leafSide * (leafWidth + size * 0.08), -depth * (t + 0.05));
|
|
606
|
+
ctx.lineTo(leafSide * leafWidth * 0.7, -depth * (t + 0.1));
|
|
607
|
+
} else {
|
|
608
|
+
ctx.lineTo(0, -depth);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Return path (smooth inner edge)
|
|
613
|
+
ctx.quadraticCurveTo(leafSide * size * 0.05, -depth * 0.5, 0, 0);
|
|
614
|
+
ctx.closePath();
|
|
615
|
+
ctx.fill();
|
|
616
|
+
ctx.stroke();
|
|
617
|
+
|
|
618
|
+
// Leaf vein
|
|
619
|
+
ctx.strokeStyle = '#2a4a1a';
|
|
620
|
+
ctx.lineWidth = 0.5;
|
|
621
|
+
ctx.beginPath();
|
|
622
|
+
ctx.moveTo(0, 0);
|
|
623
|
+
ctx.lineTo(0, -depth);
|
|
624
|
+
ctx.stroke();
|
|
625
|
+
|
|
626
|
+
ctx.restore();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
ctx.restore();
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Draw thistle plant (ground decoration)
|
|
634
|
+
*/
|
|
635
|
+
drawThistlePlant(ctx, particle, time) {
|
|
636
|
+
const x = particle.x;
|
|
637
|
+
const y = particle.y;
|
|
638
|
+
const size = particle.size;
|
|
639
|
+
|
|
640
|
+
// Gentle sway
|
|
641
|
+
const sway = Math.sin(time * 0.001 + particle.swayPhase) * 0.1;
|
|
642
|
+
|
|
643
|
+
ctx.save();
|
|
644
|
+
ctx.globalAlpha = particle.opacity;
|
|
645
|
+
ctx.translate(x, y);
|
|
646
|
+
ctx.rotate(sway);
|
|
647
|
+
|
|
648
|
+
// Multiple stems
|
|
649
|
+
for (let stem = 0; stem < 3; stem++) {
|
|
650
|
+
const stemX = (stem - 1) * size * 0.4;
|
|
651
|
+
const stemHeight = size * (1.5 + stem * 0.3);
|
|
652
|
+
|
|
653
|
+
// Stem
|
|
654
|
+
ctx.strokeStyle = '#8B4513';
|
|
655
|
+
ctx.lineWidth = size * 0.1;
|
|
656
|
+
ctx.beginPath();
|
|
657
|
+
ctx.moveTo(stemX, 0);
|
|
658
|
+
ctx.quadraticCurveTo(stemX + sway * size, -stemHeight * 0.5, stemX, -stemHeight);
|
|
659
|
+
ctx.stroke();
|
|
660
|
+
|
|
661
|
+
// Flower head
|
|
662
|
+
ctx.fillStyle = '#8B008B';
|
|
663
|
+
ctx.beginPath();
|
|
664
|
+
ctx.arc(stemX, -stemHeight, size * 0.4, 0, Math.PI * 2);
|
|
665
|
+
ctx.fill();
|
|
666
|
+
|
|
667
|
+
// Spikes
|
|
668
|
+
const spikeCount = 8;
|
|
669
|
+
ctx.strokeStyle = '#8B008B';
|
|
670
|
+
ctx.lineWidth = size * 0.05;
|
|
671
|
+
for (let i = 0; i < spikeCount; i++) {
|
|
672
|
+
const angle = (i / spikeCount) * Math.PI * 2;
|
|
673
|
+
ctx.beginPath();
|
|
674
|
+
ctx.moveTo(stemX, -stemHeight);
|
|
675
|
+
ctx.lineTo(
|
|
676
|
+
stemX + Math.cos(angle) * size * 0.6,
|
|
677
|
+
-stemHeight + Math.sin(angle) * size * 0.6
|
|
678
|
+
);
|
|
679
|
+
ctx.stroke();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
ctx.restore();
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Draw bagpiper
|
|
688
|
+
*/
|
|
689
|
+
drawBagpiper(ctx, particle, time) {
|
|
690
|
+
const x = particle.x;
|
|
691
|
+
const y = particle.y;
|
|
692
|
+
const size = particle.size;
|
|
693
|
+
const dir = particle.vx > 0 ? 1 : -1;
|
|
694
|
+
|
|
695
|
+
ctx.save();
|
|
696
|
+
ctx.translate(x, y);
|
|
697
|
+
if (dir === -1) {
|
|
698
|
+
ctx.scale(-1, 1);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const marchPhase = Math.sin(time * 0.015 + particle.marchPhase) * (Math.PI / 6);
|
|
702
|
+
|
|
703
|
+
// Legs (marching)
|
|
704
|
+
ctx.strokeStyle = '#000080'; // Navy blue kilt
|
|
705
|
+
ctx.lineWidth = size * 0.15;
|
|
706
|
+
ctx.beginPath();
|
|
707
|
+
ctx.moveTo(-size * 0.2, size * 0.6);
|
|
708
|
+
ctx.lineTo(-size * 0.3, size * 1.4 + Math.sin(marchPhase) * size * 0.15);
|
|
709
|
+
ctx.stroke();
|
|
710
|
+
ctx.beginPath();
|
|
711
|
+
ctx.moveTo(size * 0.2, size * 0.6);
|
|
712
|
+
ctx.lineTo(size * 0.3, size * 1.4 + Math.sin(marchPhase + Math.PI) * size * 0.15);
|
|
713
|
+
ctx.stroke();
|
|
714
|
+
|
|
715
|
+
// Kilt (tartan pattern suggested)
|
|
716
|
+
ctx.fillStyle = '#000080';
|
|
717
|
+
ctx.fillRect(-size * 0.45, size * 0.2, size * 0.9, size * 0.5);
|
|
718
|
+
|
|
719
|
+
// Sporran (decorative pouch)
|
|
720
|
+
ctx.fillStyle = '#8B4513';
|
|
721
|
+
ctx.fillRect(-size * 0.25, size * 0.5, size * 0.5, size * 0.25);
|
|
722
|
+
|
|
723
|
+
// Body (red tunic)
|
|
724
|
+
ctx.fillStyle = '#8B0000';
|
|
725
|
+
ctx.fillRect(-size * 0.4, -size * 0.2, size * 0.8, size * 0.5);
|
|
726
|
+
|
|
727
|
+
// Arms (holding bagpipes)
|
|
728
|
+
ctx.strokeStyle = '#FFD7BA';
|
|
729
|
+
ctx.lineWidth = size * 0.12;
|
|
730
|
+
// Left arm
|
|
731
|
+
ctx.beginPath();
|
|
732
|
+
ctx.moveTo(-size * 0.3, 0);
|
|
733
|
+
ctx.lineTo(-size * 0.6, size * 0.3);
|
|
734
|
+
ctx.stroke();
|
|
735
|
+
// Right arm
|
|
736
|
+
ctx.beginPath();
|
|
737
|
+
ctx.moveTo(size * 0.3, 0);
|
|
738
|
+
ctx.lineTo(size * 0.7, size * 0.2);
|
|
739
|
+
ctx.stroke();
|
|
740
|
+
|
|
741
|
+
// Bagpipes
|
|
742
|
+
ctx.fillStyle = '#654321';
|
|
743
|
+
// Bag
|
|
744
|
+
ctx.beginPath();
|
|
745
|
+
ctx.ellipse(size * 0.1, size * 0.25, size * 0.35, size * 0.25, 0, 0, Math.PI * 2);
|
|
746
|
+
ctx.fill();
|
|
747
|
+
// Pipes (drones)
|
|
748
|
+
ctx.strokeStyle = '#654321';
|
|
749
|
+
ctx.lineWidth = size * 0.08;
|
|
750
|
+
ctx.beginPath();
|
|
751
|
+
ctx.moveTo(size * 0.15, size * 0.05);
|
|
752
|
+
ctx.lineTo(size * 0.15, -size * 0.7);
|
|
753
|
+
ctx.stroke();
|
|
754
|
+
ctx.beginPath();
|
|
755
|
+
ctx.moveTo(size * 0.3, size * 0.1);
|
|
756
|
+
ctx.lineTo(size * 0.3, -size * 0.6);
|
|
757
|
+
ctx.stroke();
|
|
758
|
+
// Chanter (melody pipe)
|
|
759
|
+
ctx.beginPath();
|
|
760
|
+
ctx.moveTo(size * 0.55, size * 0.2);
|
|
761
|
+
ctx.lineTo(size * 0.65, size * 0.7);
|
|
762
|
+
ctx.stroke();
|
|
763
|
+
|
|
764
|
+
// Head
|
|
765
|
+
ctx.fillStyle = '#FFD7BA';
|
|
766
|
+
ctx.beginPath();
|
|
767
|
+
ctx.arc(0, -size * 0.45, size * 0.35, 0, Math.PI * 2);
|
|
768
|
+
ctx.fill();
|
|
769
|
+
|
|
770
|
+
// Glengarry (Scottish cap)
|
|
771
|
+
ctx.fillStyle = '#000080';
|
|
772
|
+
ctx.fillRect(-size * 0.4, -size * 0.8, size * 0.8, size * 0.15);
|
|
773
|
+
ctx.beginPath();
|
|
774
|
+
ctx.moveTo(-size * 0.35, -size * 0.8);
|
|
775
|
+
ctx.lineTo(0, -size * 1.1);
|
|
776
|
+
ctx.lineTo(size * 0.35, -size * 0.8);
|
|
777
|
+
ctx.closePath();
|
|
778
|
+
ctx.fill();
|
|
779
|
+
|
|
780
|
+
// Feather plume
|
|
781
|
+
ctx.strokeStyle = '#ff0000';
|
|
782
|
+
ctx.lineWidth = 2;
|
|
783
|
+
ctx.beginPath();
|
|
784
|
+
ctx.moveTo(0, -size * 1.1);
|
|
785
|
+
ctx.quadraticCurveTo(size * 0.2, -size * 1.3, size * 0.3, -size * 1.4);
|
|
786
|
+
ctx.stroke();
|
|
787
|
+
|
|
788
|
+
ctx.restore();
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Draw Saltire flag (white X on blue)
|
|
793
|
+
*/
|
|
794
|
+
drawSaltireFlag(ctx, particle, time) {
|
|
795
|
+
const x = particle.x;
|
|
796
|
+
const y = particle.y;
|
|
797
|
+
const size = particle.size;
|
|
798
|
+
|
|
799
|
+
// Wave effect
|
|
800
|
+
const wavePhase = time * 0.002 + particle.waveOffset;
|
|
801
|
+
|
|
802
|
+
ctx.save();
|
|
803
|
+
ctx.globalAlpha = particle.opacity;
|
|
804
|
+
ctx.translate(x, y);
|
|
805
|
+
|
|
806
|
+
// Flagpole
|
|
807
|
+
ctx.fillStyle = '#654321';
|
|
808
|
+
ctx.fillRect(-size * 0.05, 0, size * 0.1, size * 1.5);
|
|
809
|
+
|
|
810
|
+
// Flag (waving)
|
|
811
|
+
const segments = 10;
|
|
812
|
+
const flagWidth = size * 1.2;
|
|
813
|
+
const flagHeight = size * 0.8;
|
|
814
|
+
|
|
815
|
+
ctx.fillStyle = '#0065BD'; // Scottish blue
|
|
816
|
+
|
|
817
|
+
for (let i = 0; i < segments; i++) {
|
|
818
|
+
const segmentWidth = flagWidth / segments;
|
|
819
|
+
const x1 = i * segmentWidth;
|
|
820
|
+
const x2 = (i + 1) * segmentWidth;
|
|
821
|
+
const wave1 = Math.sin(wavePhase + i * 0.3) * size * 0.1;
|
|
822
|
+
const wave2 = Math.sin(wavePhase + (i + 1) * 0.3) * size * 0.1;
|
|
823
|
+
|
|
824
|
+
ctx.beginPath();
|
|
825
|
+
ctx.moveTo(x1, wave1);
|
|
826
|
+
ctx.lineTo(x2, wave2);
|
|
827
|
+
ctx.lineTo(x2, flagHeight + wave2);
|
|
828
|
+
ctx.lineTo(x1, flagHeight + wave1);
|
|
829
|
+
ctx.closePath();
|
|
830
|
+
ctx.fill();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// White X (Saltire)
|
|
834
|
+
ctx.strokeStyle = '#FFFFFF';
|
|
835
|
+
ctx.lineWidth = size * 0.12;
|
|
836
|
+
ctx.shadowColor = 'rgba(255, 255, 255, 0.5)';
|
|
837
|
+
ctx.shadowBlur = 5;
|
|
838
|
+
|
|
839
|
+
// Draw X with wave
|
|
840
|
+
for (let i = 0; i < segments; i++) {
|
|
841
|
+
const segmentWidth = flagWidth / segments;
|
|
842
|
+
const x1 = i * segmentWidth;
|
|
843
|
+
const x2 = (i + 1) * segmentWidth;
|
|
844
|
+
const wave1 = Math.sin(wavePhase + i * 0.3) * size * 0.1;
|
|
845
|
+
const wave2 = Math.sin(wavePhase + (i + 1) * 0.3) * size * 0.1;
|
|
846
|
+
|
|
847
|
+
// Top-left to bottom-right
|
|
848
|
+
ctx.beginPath();
|
|
849
|
+
ctx.moveTo(x1, wave1);
|
|
850
|
+
ctx.lineTo(x2, flagHeight + wave2);
|
|
851
|
+
ctx.stroke();
|
|
852
|
+
|
|
853
|
+
// Bottom-left to top-right
|
|
854
|
+
ctx.beginPath();
|
|
855
|
+
ctx.moveTo(x1, flagHeight + wave1);
|
|
856
|
+
ctx.lineTo(x2, wave2);
|
|
857
|
+
ctx.stroke();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
ctx.shadowBlur = 0;
|
|
861
|
+
ctx.restore();
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Draw saltire sparkle
|
|
866
|
+
*/
|
|
867
|
+
drawSaltireSparkle(ctx, particle, time) {
|
|
868
|
+
const x = particle.x;
|
|
869
|
+
const y = particle.y;
|
|
870
|
+
const size = particle.size;
|
|
871
|
+
const twinkle = 0.6 + Math.sin(time * 0.004 + particle.twinklePhase) * 0.4;
|
|
872
|
+
|
|
873
|
+
ctx.save();
|
|
874
|
+
ctx.globalAlpha = particle.opacity * twinkle;
|
|
875
|
+
ctx.translate(x, y);
|
|
876
|
+
ctx.rotate(particle.rotation);
|
|
877
|
+
|
|
878
|
+
// Draw 4-pointed sparkle (Scottish blue or white)
|
|
879
|
+
ctx.fillStyle = particle.color;
|
|
880
|
+
ctx.shadowColor = particle.color;
|
|
881
|
+
ctx.shadowBlur = size * 2;
|
|
882
|
+
|
|
883
|
+
ctx.beginPath();
|
|
884
|
+
ctx.moveTo(0, -size);
|
|
885
|
+
ctx.lineTo(size * 0.3, -size * 0.3);
|
|
886
|
+
ctx.lineTo(size, 0);
|
|
887
|
+
ctx.lineTo(size * 0.3, size * 0.3);
|
|
888
|
+
ctx.lineTo(0, size);
|
|
889
|
+
ctx.lineTo(-size * 0.3, size * 0.3);
|
|
890
|
+
ctx.lineTo(-size, 0);
|
|
891
|
+
ctx.lineTo(-size * 0.3, -size * 0.3);
|
|
892
|
+
ctx.closePath();
|
|
893
|
+
ctx.fill();
|
|
894
|
+
|
|
895
|
+
ctx.restore();
|
|
896
|
+
},
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Draw authentic Scottish tartan pattern (Stewart/Royal Stewart style)
|
|
900
|
+
*/
|
|
901
|
+
drawTartanPattern(ctx, particle) {
|
|
902
|
+
const x = particle.x;
|
|
903
|
+
const y = particle.y;
|
|
904
|
+
const size = particle.size;
|
|
905
|
+
|
|
906
|
+
ctx.save();
|
|
907
|
+
ctx.globalAlpha = particle.opacity;
|
|
908
|
+
ctx.translate(x, y);
|
|
909
|
+
|
|
910
|
+
// Create authentic tartan weave pattern
|
|
911
|
+
const patternSize = 120; // Smaller repeating unit
|
|
912
|
+
const patternCanvas = document.createElement('canvas');
|
|
913
|
+
patternCanvas.width = patternSize;
|
|
914
|
+
patternCanvas.height = patternSize;
|
|
915
|
+
const pCtx = patternCanvas.getContext('2d');
|
|
916
|
+
|
|
917
|
+
// Royal Stewart tartan colors
|
|
918
|
+
const NAVY = '#1a237e';
|
|
919
|
+
const RED = '#c62828';
|
|
920
|
+
const FOREST = '#1b5e20';
|
|
921
|
+
const YELLOW = '#f9a825';
|
|
922
|
+
const WHITE = '#ffffff';
|
|
923
|
+
const BLACK = '#000000';
|
|
924
|
+
|
|
925
|
+
// Tartan stripe sequence (width, color) - authentic Royal Stewart pattern
|
|
926
|
+
const stripeSequence = [
|
|
927
|
+
[4, RED], [2, BLACK], [2, RED], [2, BLACK], [4, RED],
|
|
928
|
+
[8, NAVY], [2, BLACK], [2, NAVY], [2, BLACK], [8, NAVY],
|
|
929
|
+
[2, FOREST], [2, YELLOW], [2, FOREST],
|
|
930
|
+
[8, NAVY], [2, BLACK], [2, NAVY], [2, BLACK], [8, NAVY],
|
|
931
|
+
[4, RED], [2, BLACK], [2, RED], [2, BLACK], [4, RED]
|
|
932
|
+
];
|
|
933
|
+
|
|
934
|
+
// Draw horizontal stripes
|
|
935
|
+
let ypos = 0;
|
|
936
|
+
for (let i = 0; i < 2; i++) { // Repeat pattern twice
|
|
937
|
+
for (const [width, color] of stripeSequence) {
|
|
938
|
+
pCtx.fillStyle = color;
|
|
939
|
+
pCtx.fillRect(0, ypos, patternSize, width);
|
|
940
|
+
ypos += width;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Draw vertical stripes with blending (creates the weave effect)
|
|
945
|
+
pCtx.globalCompositeOperation = 'multiply';
|
|
946
|
+
pCtx.globalAlpha = 0.8;
|
|
947
|
+
|
|
948
|
+
let xpos = 0;
|
|
949
|
+
for (let i = 0; i < 2; i++) { // Repeat pattern twice
|
|
950
|
+
for (const [width, color] of stripeSequence) {
|
|
951
|
+
pCtx.fillStyle = color;
|
|
952
|
+
pCtx.fillRect(xpos, 0, width, patternSize);
|
|
953
|
+
xpos += width;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Add white highlights (creates tartan shine)
|
|
958
|
+
pCtx.globalCompositeOperation = 'lighten';
|
|
959
|
+
pCtx.globalAlpha = 0.15;
|
|
960
|
+
xpos = 0;
|
|
961
|
+
for (let i = 0; i < 2; i++) {
|
|
962
|
+
for (const [width, color] of stripeSequence) {
|
|
963
|
+
if (color === YELLOW || color === WHITE) {
|
|
964
|
+
pCtx.fillStyle = WHITE;
|
|
965
|
+
pCtx.fillRect(xpos, 0, width, patternSize);
|
|
966
|
+
}
|
|
967
|
+
xpos += width;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Reset composite operation
|
|
972
|
+
pCtx.globalCompositeOperation = 'source-over';
|
|
973
|
+
pCtx.globalAlpha = 1;
|
|
974
|
+
|
|
975
|
+
// Draw the tartan pattern (tiled)
|
|
976
|
+
const pattern = ctx.createPattern(patternCanvas, 'repeat');
|
|
977
|
+
ctx.fillStyle = pattern;
|
|
978
|
+
ctx.fillRect(-size / 2, -size / 2, size, size);
|
|
979
|
+
|
|
980
|
+
// Fabric edge (darker border to show it's fabric)
|
|
981
|
+
ctx.strokeStyle = '#2a2a2a';
|
|
982
|
+
ctx.lineWidth = size * 0.03;
|
|
983
|
+
ctx.strokeRect(-size / 2, -size / 2, size, size);
|
|
984
|
+
|
|
985
|
+
// Inner decorative border (silver/white tartan trim)
|
|
986
|
+
ctx.strokeStyle = '#e0e0e0';
|
|
987
|
+
ctx.lineWidth = size * 0.015;
|
|
988
|
+
ctx.strokeRect(-size / 2 + size * 0.04, -size / 2 + size * 0.04,
|
|
989
|
+
size - size * 0.08, size - size * 0.08);
|
|
990
|
+
|
|
991
|
+
// Fabric texture overlay (subtle diagonal lines)
|
|
992
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
|
993
|
+
ctx.lineWidth = 0.5;
|
|
994
|
+
for (let i = -size; i < size; i += 4) {
|
|
995
|
+
ctx.beginPath();
|
|
996
|
+
ctx.moveTo(-size / 2 + i, -size / 2);
|
|
997
|
+
ctx.lineTo(-size / 2 + i + size, size / 2);
|
|
998
|
+
ctx.stroke();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Silver thistle brooch in corner (holds the tartan)
|
|
1002
|
+
ctx.save();
|
|
1003
|
+
ctx.translate(-size * 0.35, -size * 0.35);
|
|
1004
|
+
|
|
1005
|
+
// Brooch circle (silver)
|
|
1006
|
+
ctx.fillStyle = '#c0c0c0';
|
|
1007
|
+
ctx.strokeStyle = '#808080';
|
|
1008
|
+
ctx.lineWidth = 2;
|
|
1009
|
+
ctx.beginPath();
|
|
1010
|
+
ctx.arc(0, 0, size * 0.08, 0, Math.PI * 2);
|
|
1011
|
+
ctx.fill();
|
|
1012
|
+
ctx.stroke();
|
|
1013
|
+
|
|
1014
|
+
// Thistle emblem on brooch
|
|
1015
|
+
ctx.scale(0.06, 0.06);
|
|
1016
|
+
ctx.fillStyle = '#8B008B';
|
|
1017
|
+
ctx.strokeStyle = '#6A006A';
|
|
1018
|
+
ctx.lineWidth = 2;
|
|
1019
|
+
|
|
1020
|
+
// Stylized thistle flower
|
|
1021
|
+
for (let i = 0; i < 6; i++) {
|
|
1022
|
+
const angle = (i / 6) * Math.PI * 2;
|
|
1023
|
+
ctx.beginPath();
|
|
1024
|
+
ctx.moveTo(0, 0);
|
|
1025
|
+
ctx.lineTo(Math.cos(angle) * size * 0.4, Math.sin(angle) * size * 0.4);
|
|
1026
|
+
ctx.stroke();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Center dot
|
|
1030
|
+
ctx.beginPath();
|
|
1031
|
+
ctx.arc(0, 0, size * 0.15, 0, Math.PI * 2);
|
|
1032
|
+
ctx.fill();
|
|
1033
|
+
|
|
1034
|
+
ctx.restore();
|
|
1035
|
+
|
|
1036
|
+
ctx.restore();
|
|
1037
|
+
},
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Draw Highland scene silhouette (mountains, castle, loch)
|
|
1041
|
+
*/
|
|
1042
|
+
drawHighlandScene(ctx, particle) {
|
|
1043
|
+
const x = particle.x;
|
|
1044
|
+
const y = particle.y;
|
|
1045
|
+
const size = particle.size;
|
|
1046
|
+
|
|
1047
|
+
ctx.save();
|
|
1048
|
+
ctx.globalAlpha = particle.opacity;
|
|
1049
|
+
ctx.translate(x, y);
|
|
1050
|
+
|
|
1051
|
+
// Sky gradient (misty blue)
|
|
1052
|
+
const skyGradient = ctx.createLinearGradient(0, -size, 0, size * 0.5);
|
|
1053
|
+
skyGradient.addColorStop(0, '#4a7ba7');
|
|
1054
|
+
skyGradient.addColorStop(1, '#b0c4de');
|
|
1055
|
+
ctx.fillStyle = skyGradient;
|
|
1056
|
+
ctx.fillRect(-size, -size, size * 2, size * 1.5);
|
|
1057
|
+
|
|
1058
|
+
// Distant mountains (layered)
|
|
1059
|
+
ctx.fillStyle = 'rgba(80, 100, 130, 0.6)';
|
|
1060
|
+
ctx.beginPath();
|
|
1061
|
+
ctx.moveTo(-size, size * 0.3);
|
|
1062
|
+
ctx.lineTo(-size * 0.6, -size * 0.3);
|
|
1063
|
+
ctx.lineTo(-size * 0.2, size * 0.1);
|
|
1064
|
+
ctx.lineTo(size * 0.2, -size * 0.5);
|
|
1065
|
+
ctx.lineTo(size * 0.6, size * 0.2);
|
|
1066
|
+
ctx.lineTo(size, size * 0.3);
|
|
1067
|
+
ctx.closePath();
|
|
1068
|
+
ctx.fill();
|
|
1069
|
+
|
|
1070
|
+
// Closer mountains (darker)
|
|
1071
|
+
ctx.fillStyle = 'rgba(60, 80, 100, 0.8)';
|
|
1072
|
+
ctx.beginPath();
|
|
1073
|
+
ctx.moveTo(-size, size * 0.4);
|
|
1074
|
+
ctx.lineTo(-size * 0.7, size * 0.1);
|
|
1075
|
+
ctx.lineTo(-size * 0.3, -size * 0.1);
|
|
1076
|
+
ctx.lineTo(size * 0.1, -size * 0.2);
|
|
1077
|
+
ctx.lineTo(size * 0.5, size * 0.15);
|
|
1078
|
+
ctx.lineTo(size, size * 0.4);
|
|
1079
|
+
ctx.closePath();
|
|
1080
|
+
ctx.fill();
|
|
1081
|
+
|
|
1082
|
+
// Highland castle silhouette (on hill)
|
|
1083
|
+
ctx.fillStyle = '#2a3a4a';
|
|
1084
|
+
|
|
1085
|
+
// Castle base
|
|
1086
|
+
ctx.fillRect(-size * 0.15, size * 0.25, size * 0.3, size * 0.2);
|
|
1087
|
+
|
|
1088
|
+
// Castle tower (left)
|
|
1089
|
+
ctx.fillRect(-size * 0.2, size * 0.15, size * 0.1, size * 0.15);
|
|
1090
|
+
|
|
1091
|
+
// Castle tower (right)
|
|
1092
|
+
ctx.fillRect(size * 0.1, size * 0.15, size * 0.1, size * 0.15);
|
|
1093
|
+
|
|
1094
|
+
// Central keep (tall)
|
|
1095
|
+
ctx.fillRect(-size * 0.06, size * 0.05, size * 0.12, size * 0.25);
|
|
1096
|
+
|
|
1097
|
+
// Battlements
|
|
1098
|
+
for (let i = 0; i < 5; i++) {
|
|
1099
|
+
if (i % 2 === 0) {
|
|
1100
|
+
ctx.fillRect(-size * 0.06 + i * size * 0.06, size * 0.02, size * 0.03, size * 0.03);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Loch (water) at bottom
|
|
1105
|
+
const lochGradient = ctx.createLinearGradient(0, size * 0.45, 0, size * 0.6);
|
|
1106
|
+
lochGradient.addColorStop(0, '#1e3a5f');
|
|
1107
|
+
lochGradient.addColorStop(1, '#0a1f3d');
|
|
1108
|
+
ctx.fillStyle = lochGradient;
|
|
1109
|
+
ctx.fillRect(-size, size * 0.45, size * 2, size * 0.15);
|
|
1110
|
+
|
|
1111
|
+
// Reflection in loch (simplified)
|
|
1112
|
+
ctx.globalAlpha = particle.opacity * 0.3;
|
|
1113
|
+
ctx.fillStyle = '#4a6a8a';
|
|
1114
|
+
ctx.fillRect(-size * 0.15, size * 0.48, size * 0.3, size * 0.08);
|
|
1115
|
+
|
|
1116
|
+
// Mist over loch
|
|
1117
|
+
ctx.globalAlpha = particle.opacity * 0.2;
|
|
1118
|
+
ctx.fillStyle = '#d0e0f0';
|
|
1119
|
+
ctx.fillRect(-size, size * 0.5, size * 2, size * 0.05);
|
|
1120
|
+
|
|
1121
|
+
ctx.restore();
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Draw twinkling star
|
|
1126
|
+
*/
|
|
1127
|
+
drawTwinklingStar(ctx, particle, time) {
|
|
1128
|
+
const x = particle.x;
|
|
1129
|
+
const y = particle.y;
|
|
1130
|
+
const size = particle.size;
|
|
1131
|
+
|
|
1132
|
+
// Calculate twinkle intensity
|
|
1133
|
+
const twinkleIntensity = 0.5 + Math.sin(time * particle.twinkleSpeed + particle.twinklePhase) * 0.5;
|
|
1134
|
+
|
|
1135
|
+
// Scottish blue and white color scheme
|
|
1136
|
+
const starColor = `rgba(200, 220, 255, ${twinkleIntensity})`;
|
|
1137
|
+
const glowColor = `rgba(150, 180, 255, ${twinkleIntensity * 0.4})`;
|
|
1138
|
+
|
|
1139
|
+
ctx.save();
|
|
1140
|
+
ctx.translate(x, y);
|
|
1141
|
+
|
|
1142
|
+
// Outer glow
|
|
1143
|
+
ctx.shadowColor = glowColor;
|
|
1144
|
+
ctx.shadowBlur = size * 3 * twinkleIntensity;
|
|
1145
|
+
ctx.fillStyle = starColor;
|
|
1146
|
+
|
|
1147
|
+
// Draw 4-pointed star
|
|
1148
|
+
ctx.beginPath();
|
|
1149
|
+
for (let i = 0; i < 4; i++) {
|
|
1150
|
+
const angle = (i * Math.PI) / 2;
|
|
1151
|
+
const outerX = Math.cos(angle) * size;
|
|
1152
|
+
const outerY = Math.sin(angle) * size;
|
|
1153
|
+
const innerAngle = angle + Math.PI / 4;
|
|
1154
|
+
const innerX = Math.cos(innerAngle) * (size * 0.3);
|
|
1155
|
+
const innerY = Math.sin(innerAngle) * (size * 0.3);
|
|
1156
|
+
|
|
1157
|
+
if (i === 0) {
|
|
1158
|
+
ctx.moveTo(outerX, outerY);
|
|
1159
|
+
} else {
|
|
1160
|
+
ctx.lineTo(outerX, outerY);
|
|
1161
|
+
}
|
|
1162
|
+
ctx.lineTo(innerX, innerY);
|
|
1163
|
+
}
|
|
1164
|
+
ctx.closePath();
|
|
1165
|
+
ctx.fill();
|
|
1166
|
+
|
|
1167
|
+
// Bright center
|
|
1168
|
+
ctx.shadowBlur = size * 2 * twinkleIntensity;
|
|
1169
|
+
ctx.beginPath();
|
|
1170
|
+
ctx.arc(0, 0, size * 0.25, 0, Math.PI * 2);
|
|
1171
|
+
ctx.fill();
|
|
1172
|
+
|
|
1173
|
+
ctx.restore();
|
|
1174
|
+
}
|
|
1175
|
+
};
|