@fabi-play/avatars 0.0.0-stage → 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 +76 -0
- package/README.md +132 -2
- package/dist/index.d.ts +425 -0
- package/dist/index.js +2205 -0
- package/package.json +56 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2205 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { AGE_RANGES, P as P3 } from "@fabi-play/protocol";
|
|
7
|
+
|
|
8
|
+
// src/defs.ts
|
|
9
|
+
var SOFT = "#4a382c";
|
|
10
|
+
var INK = "#2b2118";
|
|
11
|
+
var COOL = "#1c1f2b";
|
|
12
|
+
var CREAM = "#fbefe3";
|
|
13
|
+
var BLUSH = "#f4897a";
|
|
14
|
+
var NOSE = "#3a2a22";
|
|
15
|
+
var AVATARS = [
|
|
16
|
+
{
|
|
17
|
+
id: "fox",
|
|
18
|
+
name: { en: "Fox", it: "Volpe" },
|
|
19
|
+
band: "6-8",
|
|
20
|
+
palette: { body: "#ee6a43", limbs: "#ee6a43", belly: CREAM, accent: "#ffd9c2", outline: INK, hands: "#5a3426", feet: "#5a3426" },
|
|
21
|
+
head: "wide",
|
|
22
|
+
ears: "pointy",
|
|
23
|
+
face: { eyes: "dot", mouth: "cat", nose: NOSE, muzzle: "cheeks" },
|
|
24
|
+
tail: "fluffy",
|
|
25
|
+
headExtras: [],
|
|
26
|
+
bodyExtras: []
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
// The fox of the app icon (brand/): the same coral fur and dark paws as 'fox', told apart by the icon's face
|
|
30
|
+
// (big glossy eyes, the cream lower face, blush, a big open smile, dark brown inside the ears), a big brush of a
|
|
31
|
+
// tail and a yellow star on the chest.
|
|
32
|
+
id: "mascot",
|
|
33
|
+
name: { en: "Fabi", it: "Fabi" },
|
|
34
|
+
band: "all",
|
|
35
|
+
special: true,
|
|
36
|
+
palette: { body: "#ee6a43", limbs: "#ee6a43", belly: CREAM, accent: "#5a3426", outline: INK, hands: "#5a3426", feet: "#5a3426" },
|
|
37
|
+
head: "wide",
|
|
38
|
+
ears: "pointy",
|
|
39
|
+
face: { eyes: "bead", mouth: "open", nose: NOSE, muzzle: "band", cheeks: BLUSH },
|
|
40
|
+
tail: "brush",
|
|
41
|
+
headExtras: [],
|
|
42
|
+
bodyExtras: ["star"],
|
|
43
|
+
extraColors: ["#ffd84a"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "bunny",
|
|
47
|
+
name: { en: "Bunny", it: "Coniglietto" },
|
|
48
|
+
band: "3-5",
|
|
49
|
+
palette: { body: "#f9d0de", limbs: "#f9d0de", belly: "#fff6f8", accent: "#f4a0b9", outline: SOFT },
|
|
50
|
+
head: "round",
|
|
51
|
+
ears: "long",
|
|
52
|
+
face: { eyes: "dot", mouth: "cat", nose: "#e0607e", muzzle: "none", cheeks: BLUSH },
|
|
53
|
+
tail: "pom",
|
|
54
|
+
headExtras: [],
|
|
55
|
+
bodyExtras: []
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "bear",
|
|
59
|
+
name: { en: "Bear", it: "Orsetto" },
|
|
60
|
+
band: "3-5",
|
|
61
|
+
palette: { body: "#cf9f78", limbs: "#cf9f78", belly: "#f7e0bf", accent: "#f7e0bf", outline: SOFT },
|
|
62
|
+
head: "round",
|
|
63
|
+
ears: "round",
|
|
64
|
+
face: { eyes: "dot", mouth: "smile", nose: NOSE, muzzle: "oval", cheeks: BLUSH },
|
|
65
|
+
tail: "none",
|
|
66
|
+
headExtras: [],
|
|
67
|
+
bodyExtras: []
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "cat",
|
|
71
|
+
name: { en: "Cat", it: "Gattino" },
|
|
72
|
+
band: "3-5",
|
|
73
|
+
palette: { body: "#c8b6ee", limbs: "#c8b6ee", belly: "#f4eeff", accent: "#f7b3c8", outline: SOFT, hands: "#f4eeff", feet: "#f4eeff" },
|
|
74
|
+
head: "wide",
|
|
75
|
+
ears: "pointy",
|
|
76
|
+
face: { eyes: "big", eyeColor: "#3f9a5a", mouth: "cat", nose: "#f07a9a", muzzle: "none", cheeks: BLUSH },
|
|
77
|
+
tail: "long",
|
|
78
|
+
headExtras: ["whiskers"],
|
|
79
|
+
bodyExtras: []
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "panda",
|
|
83
|
+
name: { en: "Panda", it: "Panda" },
|
|
84
|
+
band: "3-5",
|
|
85
|
+
palette: { body: "#ffffff", limbs: "#6e6674", belly: "#ffffff", accent: "#6e6674", outline: SOFT, ears: "#6e6674" },
|
|
86
|
+
head: "wide",
|
|
87
|
+
ears: "round",
|
|
88
|
+
face: { eyes: "mask", mouth: "smile", nose: "#2f2a2c", muzzle: "none", cheeks: "#f6a3a3" },
|
|
89
|
+
tail: "none",
|
|
90
|
+
headExtras: [],
|
|
91
|
+
bodyExtras: []
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "penguin",
|
|
95
|
+
name: { en: "Penguin", it: "Pinguino" },
|
|
96
|
+
band: "3-5",
|
|
97
|
+
palette: { body: "#6c8fca", limbs: "#6c8fca", belly: "#ffffff", accent: "#f9b95c", outline: SOFT, feet: "#f9b95c" },
|
|
98
|
+
head: "tall",
|
|
99
|
+
ears: "none",
|
|
100
|
+
face: { eyes: "dot", mouth: "beak", muzzle: "heart", cheeks: "#f6a3a3" },
|
|
101
|
+
tail: "none",
|
|
102
|
+
headExtras: [],
|
|
103
|
+
bodyExtras: ["flippers", "scarf"],
|
|
104
|
+
extraColors: ["#f6907a"]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "frog",
|
|
108
|
+
name: { en: "Frog", it: "Ranocchio" },
|
|
109
|
+
band: "3-5",
|
|
110
|
+
palette: { body: "#8fd487", limbs: "#8fd487", belly: "#e6f7cc", accent: "#62ad62", outline: SOFT },
|
|
111
|
+
head: "wide",
|
|
112
|
+
ears: "none",
|
|
113
|
+
face: { eyes: "big", eyesOnTop: true, mouth: "open", muzzle: "none", cheeks: "#f59b8f" },
|
|
114
|
+
tail: "none",
|
|
115
|
+
headExtras: [],
|
|
116
|
+
bodyExtras: ["flippers", "spots"],
|
|
117
|
+
extraColors: ["#62ad62", "#b0e6a4"]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "lion",
|
|
121
|
+
name: { en: "Lion", it: "Leoncino" },
|
|
122
|
+
band: "6-8",
|
|
123
|
+
palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
|
|
124
|
+
head: "round",
|
|
125
|
+
ears: "round",
|
|
126
|
+
face: { eyes: "dot", mouth: "cat", nose: "#8a4b2a", muzzle: "oval", cheeks: BLUSH },
|
|
127
|
+
tail: "tuft",
|
|
128
|
+
headExtras: ["mane"],
|
|
129
|
+
bodyExtras: [],
|
|
130
|
+
extraColors: ["#c8692e"]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "unicorn",
|
|
134
|
+
name: { en: "Unicorn", it: "Unicorno" },
|
|
135
|
+
band: "6-8",
|
|
136
|
+
palette: { body: "#fbf5ff", limbs: "#fbf5ff", belly: "#efe0ff", accent: "#fbd46a", outline: INK, hands: "#c9a6f2", feet: "#c9a6f2" },
|
|
137
|
+
head: "tall",
|
|
138
|
+
ears: "unicorn",
|
|
139
|
+
face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f7a6c8" },
|
|
140
|
+
tail: "none",
|
|
141
|
+
headExtras: ["rainbow-mane"],
|
|
142
|
+
bodyExtras: [],
|
|
143
|
+
extraColors: ["#f58cc0", "#8fd3f4", "#a98ff2"]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "dragon",
|
|
147
|
+
name: { en: "Little Dragon", it: "Draghetto" },
|
|
148
|
+
band: "6-8",
|
|
149
|
+
palette: { body: "#8a63d2", limbs: "#8a63d2", belly: "#fbd46a", accent: "#5fd0b5", outline: INK },
|
|
150
|
+
head: "wide",
|
|
151
|
+
ears: "horns",
|
|
152
|
+
face: { eyes: "big", eyeColor: "#3a2a6a", mouth: "teeth", muzzle: "none", cheeks: "#f59bc0" },
|
|
153
|
+
tail: "arrow",
|
|
154
|
+
headExtras: [],
|
|
155
|
+
bodyExtras: ["wings"]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "robot",
|
|
159
|
+
name: { en: "Robot", it: "Robot" },
|
|
160
|
+
band: "9-12",
|
|
161
|
+
palette: { body: "#b9c7d2", limbs: "#8b9aa6", belly: "#b9c7d2", accent: "#ee6a43", outline: COOL, hands: "#ee6a43", feet: "#6b7a86" },
|
|
162
|
+
head: "square",
|
|
163
|
+
ears: "antenna",
|
|
164
|
+
face: { eyes: "screen", eyeColor: "#7ff0ff", mouth: "grill", muzzle: "none" },
|
|
165
|
+
tail: "none",
|
|
166
|
+
headExtras: ["bolts"],
|
|
167
|
+
bodyExtras: ["panel", "joints", "boots"],
|
|
168
|
+
extraColors: ["#ff7a6b", "#fbd46a", "#7ff0ff"]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "astronaut",
|
|
172
|
+
name: { en: "Astronaut", it: "Astronauta" },
|
|
173
|
+
band: "9-12",
|
|
174
|
+
palette: { body: "#f4f1ec", limbs: "#f4f1ec", belly: "#f4f1ec", accent: "#ee6a43", outline: COOL, hands: "#9fb0bd", feet: "#9fb0bd" },
|
|
175
|
+
head: "round",
|
|
176
|
+
ears: "none",
|
|
177
|
+
face: { eyes: "visor", mouth: "smile", muzzle: "none" },
|
|
178
|
+
tail: "none",
|
|
179
|
+
headExtras: ["helmet"],
|
|
180
|
+
bodyExtras: ["backpack", "badge", "boots"],
|
|
181
|
+
extraColors: ["#c9d3db"]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "dinosaur",
|
|
185
|
+
name: { en: "Dinosaur", it: "Dinosauro" },
|
|
186
|
+
band: "6-8",
|
|
187
|
+
palette: { body: "#3fb3a6", limbs: "#3fb3a6", belly: "#c9f0d6", accent: "#f79a3e", outline: INK },
|
|
188
|
+
head: "wide",
|
|
189
|
+
ears: "none",
|
|
190
|
+
face: { eyes: "dot", mouth: "teeth", muzzle: "none", cheeks: "#f59b8f" },
|
|
191
|
+
tail: "spiky",
|
|
192
|
+
headExtras: ["spikes", "spots"],
|
|
193
|
+
bodyExtras: [],
|
|
194
|
+
extraColors: ["#2b8f84", "#7fd3c6"]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "monkey",
|
|
198
|
+
name: { en: "Monkey", it: "Scimmietta" },
|
|
199
|
+
band: "6-8",
|
|
200
|
+
palette: { body: "#7b5234", limbs: "#7b5234", belly: "#f2c9a0", accent: "#f2c9a0", outline: INK, hands: "#f2c9a0", feet: "#f2c9a0" },
|
|
201
|
+
head: "round",
|
|
202
|
+
ears: "side",
|
|
203
|
+
face: { eyes: "dot", mouth: "open", nose: "#5a3a26", muzzle: "monkey", cheeks: BLUSH },
|
|
204
|
+
tail: "curl",
|
|
205
|
+
headExtras: ["tuft"],
|
|
206
|
+
bodyExtras: []
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "koala",
|
|
210
|
+
name: { en: "Koala", it: "Koala" },
|
|
211
|
+
band: "3-5",
|
|
212
|
+
palette: { body: "#b7bcc6", limbs: "#b7bcc6", belly: "#eef0f3", accent: "#f4dbe2", outline: SOFT },
|
|
213
|
+
head: "wide",
|
|
214
|
+
ears: "big-round",
|
|
215
|
+
face: { eyes: "dot", mouth: "smile", nose: "#3b3438", muzzle: "none", cheeks: "#f4a3b0" },
|
|
216
|
+
tail: "none",
|
|
217
|
+
headExtras: [],
|
|
218
|
+
bodyExtras: []
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "tiger",
|
|
222
|
+
name: { en: "Tiger", it: "Tigrotto" },
|
|
223
|
+
band: "6-8",
|
|
224
|
+
palette: { body: "#f9a33a", limbs: "#f9a33a", belly: "#fff1dc", accent: "#fff1dc", outline: INK },
|
|
225
|
+
head: "wide",
|
|
226
|
+
ears: "round",
|
|
227
|
+
face: { eyes: "dot", mouth: "cat", nose: "#e2607a", muzzle: "oval", cheeks: BLUSH },
|
|
228
|
+
tail: "striped",
|
|
229
|
+
headExtras: ["stripes", "whiskers"],
|
|
230
|
+
bodyExtras: [],
|
|
231
|
+
extraColors: ["#3a2a22"]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "owl",
|
|
235
|
+
name: { en: "Owl", it: "Gufetto" },
|
|
236
|
+
band: "3-5",
|
|
237
|
+
palette: { body: "#8ec0e6", limbs: "#8ec0e6", belly: "#e3f2fc", accent: "#639cc9", outline: SOFT, feet: "#f9b95c", hands: "#639cc9" },
|
|
238
|
+
head: "tall",
|
|
239
|
+
ears: "tufts",
|
|
240
|
+
face: { eyes: "big", eyeColor: SOFT, eyeRing: "#fde08a", mouth: "beak", muzzle: "none", cheeks: "#f6a8b4" },
|
|
241
|
+
tail: "none",
|
|
242
|
+
headExtras: [],
|
|
243
|
+
bodyExtras: ["feathers"]
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "alien",
|
|
247
|
+
name: { en: "Alien", it: "Alieno" },
|
|
248
|
+
band: "9-12",
|
|
249
|
+
palette: { body: "#9ee05a", limbs: "#9ee05a", belly: "#d8f5a8", accent: "#b07cf2", outline: COOL },
|
|
250
|
+
head: "bulb",
|
|
251
|
+
ears: "antennae",
|
|
252
|
+
face: { eyes: "almond", eyeColor: "#1e1838", mouth: "smirk", muzzle: "none" },
|
|
253
|
+
tail: "none",
|
|
254
|
+
headExtras: [],
|
|
255
|
+
bodyExtras: ["spots"],
|
|
256
|
+
extraColors: ["#7cc23e", "#7cc23e"]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "ghost",
|
|
260
|
+
name: { en: "Friendly Ghost", it: "Fantasmino" },
|
|
261
|
+
band: "6-8",
|
|
262
|
+
palette: { body: "#e6f4ff", limbs: "#e6f4ff", belly: "#e6f4ff", accent: "#bcd9f2", outline: INK },
|
|
263
|
+
head: "round",
|
|
264
|
+
ears: "none",
|
|
265
|
+
face: { eyes: "dot", mouth: "open", muzzle: "none", cheeks: "#f7a6c8" },
|
|
266
|
+
tail: "none",
|
|
267
|
+
headExtras: ["curl"],
|
|
268
|
+
bodyExtras: ["sheet"]
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "octopus",
|
|
272
|
+
name: { en: "Octopus", it: "Polpetto" },
|
|
273
|
+
band: "6-8",
|
|
274
|
+
palette: { body: "#e0578a", limbs: "#e0578a", belly: "#f7b3cc", accent: "#f7b3cc", outline: INK, hands: "#f7b3cc", feet: "#f7b3cc" },
|
|
275
|
+
head: "tall",
|
|
276
|
+
ears: "none",
|
|
277
|
+
face: { eyes: "big", eyeColor: "#3a2140", mouth: "smile", muzzle: "none", cheeks: "#ffd0a0" },
|
|
278
|
+
tail: "none",
|
|
279
|
+
headExtras: ["spots"],
|
|
280
|
+
bodyExtras: ["tentacles"],
|
|
281
|
+
extraColors: ["#f7b3cc", "#f7b3cc"]
|
|
282
|
+
},
|
|
283
|
+
// ── New in the age bands: 3–5 ──
|
|
284
|
+
{
|
|
285
|
+
id: "chick",
|
|
286
|
+
name: { en: "Chick", it: "Pulcino" },
|
|
287
|
+
band: "3-5",
|
|
288
|
+
palette: { body: "#ffd45e", limbs: "#ffd45e", belly: "#fff2c2", accent: "#ffa04a", outline: SOFT, hands: "#ffc23d", feet: "#ffa04a" },
|
|
289
|
+
head: "round",
|
|
290
|
+
ears: "none",
|
|
291
|
+
face: { eyes: "dot", mouth: "beak", muzzle: "none", cheeks: BLUSH },
|
|
292
|
+
tail: "none",
|
|
293
|
+
headExtras: ["eggshell"],
|
|
294
|
+
bodyExtras: [],
|
|
295
|
+
extraColors: ["#fffaf0"]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "puppy",
|
|
299
|
+
name: { en: "Puppy", it: "Cucciolo" },
|
|
300
|
+
band: "3-5",
|
|
301
|
+
palette: { body: "#f4e4cf", limbs: "#f4e4cf", belly: "#fffaf2", accent: "#f29aa4", outline: SOFT, ears: "#b58262", hands: "#e6caa6", feet: "#e6caa6" },
|
|
302
|
+
head: "wide",
|
|
303
|
+
ears: "floppy",
|
|
304
|
+
face: { eyes: "dot", mouth: "open", nose: NOSE, muzzle: "oval", cheeks: BLUSH },
|
|
305
|
+
tail: "curl",
|
|
306
|
+
headExtras: ["eye-spot"],
|
|
307
|
+
bodyExtras: []
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "lamb",
|
|
311
|
+
name: { en: "Lamb", it: "Agnellino" },
|
|
312
|
+
band: "3-5",
|
|
313
|
+
palette: { body: "#fdf8ee", limbs: "#e3d1c2", belly: "#fffdf8", accent: "#f6bcc2", outline: SOFT, skin: "#f7dccb", hands: "#a58c7e", feet: "#a58c7e" },
|
|
314
|
+
head: "round",
|
|
315
|
+
ears: "droopy",
|
|
316
|
+
face: { eyes: "dot", mouth: "smile", nose: "#e591a0", muzzle: "none", cheeks: BLUSH },
|
|
317
|
+
tail: "pom",
|
|
318
|
+
headExtras: ["wool"],
|
|
319
|
+
bodyExtras: ["wool"]
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: "duckling",
|
|
323
|
+
name: { en: "Duckling", it: "Anatroccolo" },
|
|
324
|
+
band: "3-5",
|
|
325
|
+
palette: { body: "#fff1a6", limbs: "#fff1a6", belly: "#fffbe0", accent: "#ff9f43", outline: SOFT, feet: "#ff9f43" },
|
|
326
|
+
head: "tall",
|
|
327
|
+
ears: "none",
|
|
328
|
+
face: { eyes: "dot", mouth: "bill", muzzle: "none", cheeks: BLUSH },
|
|
329
|
+
tail: "none",
|
|
330
|
+
headExtras: ["curl"],
|
|
331
|
+
bodyExtras: ["flippers", "scarf"],
|
|
332
|
+
extraColors: ["#86cfe0"]
|
|
333
|
+
},
|
|
334
|
+
// ── New in the age bands: 6–8 ──
|
|
335
|
+
{
|
|
336
|
+
id: "shark",
|
|
337
|
+
name: { en: "Shark", it: "Squalo" },
|
|
338
|
+
band: "6-8",
|
|
339
|
+
palette: { body: "#6c95c4", limbs: "#6c95c4", belly: "#eef5fb", accent: "#4c76a8", outline: INK, hands: "#eef5fb", feet: "#4c76a8" },
|
|
340
|
+
head: "wide",
|
|
341
|
+
ears: "fin",
|
|
342
|
+
face: { eyes: "dot", mouth: "jaws", muzzle: "none", cheeks: "#f59bb0" },
|
|
343
|
+
tail: "fish",
|
|
344
|
+
headExtras: ["gills"],
|
|
345
|
+
bodyExtras: []
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: "wolf",
|
|
349
|
+
name: { en: "Wolf", it: "Lupo" },
|
|
350
|
+
band: "6-8",
|
|
351
|
+
palette: { body: "#8f9bb3", limbs: "#8f9bb3", belly: "#eef1f6", accent: "#f3b8c4", outline: INK, hands: "#5a647a", feet: "#5a647a" },
|
|
352
|
+
head: "round",
|
|
353
|
+
ears: "pointy",
|
|
354
|
+
face: { eyes: "dot", mouth: "open", nose: NOSE, muzzle: "oval", cheeks: "#f4a3b0" },
|
|
355
|
+
tail: "fluffy",
|
|
356
|
+
headExtras: ["ruff", "forehead"],
|
|
357
|
+
bodyExtras: [],
|
|
358
|
+
extraColors: ["#636d84"]
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "parrot",
|
|
362
|
+
name: { en: "Parrot", it: "Pappagallo" },
|
|
363
|
+
band: "6-8",
|
|
364
|
+
palette: { body: "#e8503f", limbs: "#e8503f", belly: "#ffd25a", accent: "#f6e6c4", outline: INK, hands: "#3a9bdc", feet: "#8a7a70" },
|
|
365
|
+
head: "round",
|
|
366
|
+
ears: "none",
|
|
367
|
+
face: { eyes: "big", eyeColor: INK, eyeRing: "#ffffff", mouth: "hook", muzzle: "none", cheeks: "#ffb0a0" },
|
|
368
|
+
tail: "plume",
|
|
369
|
+
headExtras: ["crest"],
|
|
370
|
+
bodyExtras: ["feathers"],
|
|
371
|
+
extraColors: ["#ffd25a", "#3fae5a", "#3a9bdc"]
|
|
372
|
+
},
|
|
373
|
+
// ── New in the age bands: 9–12 (people: generic faces, gear that reads at small size) ──
|
|
374
|
+
{
|
|
375
|
+
id: "ninja",
|
|
376
|
+
name: { en: "Ninja", it: "Ninja" },
|
|
377
|
+
band: "9-12",
|
|
378
|
+
palette: { body: "#2f3a56", limbs: "#2f3a56", belly: "#2f3a56", accent: "#36c9bb", outline: COOL, skin: "#b07a52", hands: "#222a40", feet: "#222a40" },
|
|
379
|
+
head: "round",
|
|
380
|
+
ears: "none",
|
|
381
|
+
face: { eyes: "calm", mouth: "none", muzzle: "none", brows: "#2a1d16" },
|
|
382
|
+
tail: "none",
|
|
383
|
+
headExtras: ["ninja-hood"],
|
|
384
|
+
bodyExtras: ["belt"],
|
|
385
|
+
extraColors: ["#36c9bb", "#36c9bb", "#36c9bb"]
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: "skater",
|
|
389
|
+
name: { en: "Skater", it: "Skater" },
|
|
390
|
+
band: "9-12",
|
|
391
|
+
palette: { body: "#3a8fc0", limbs: "#2c3e57", belly: "#2f7aa6", accent: "#ffcf3a", outline: COOL, skin: "#f2c9a5", hair: "#7a4a2a", hands: "#f2c9a5", feet: "#eef1f5" },
|
|
392
|
+
head: "round",
|
|
393
|
+
ears: "human",
|
|
394
|
+
face: { eyes: "calm", mouth: "smirk", muzzle: "none", brows: "#5a3520" },
|
|
395
|
+
tail: "none",
|
|
396
|
+
headExtras: ["hair", "cap"],
|
|
397
|
+
bodyExtras: ["pocket", "joints"],
|
|
398
|
+
extraColors: ["#ff5d73"]
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "explorer",
|
|
402
|
+
name: { en: "Explorer", it: "Giramondo" },
|
|
403
|
+
band: "9-12",
|
|
404
|
+
palette: { body: "#5f8f5b", limbs: "#5f8f5b", belly: "#5f8f5b", accent: "#d9bb7e", outline: COOL, skin: "#7a4a30", hair: "#1d1410", hands: "#7a4a30", feet: "#6b4a2a" },
|
|
405
|
+
head: "round",
|
|
406
|
+
ears: "human",
|
|
407
|
+
face: { eyes: "calm", mouth: "smile", muzzle: "none", brows: "#1d1410" },
|
|
408
|
+
tail: "none",
|
|
409
|
+
headExtras: ["curls", "safari-hat"],
|
|
410
|
+
bodyExtras: ["backpack", "straps", "boots"],
|
|
411
|
+
extraColors: ["#e07b39", "#6b4a2a"]
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "wizard",
|
|
415
|
+
name: { en: "Wizard", it: "Mago" },
|
|
416
|
+
band: "9-12",
|
|
417
|
+
palette: { body: "#5b3fa0", limbs: "#5b3fa0", belly: "#5b3fa0", accent: "#3d5fc8", outline: COOL, skin: "#c98d62", hair: "#a8452a", hands: "#c98d62", feet: "#33255e" },
|
|
418
|
+
head: "round",
|
|
419
|
+
ears: "human",
|
|
420
|
+
face: { eyes: "calm", mouth: "smile", muzzle: "none", brows: "#7a301c" },
|
|
421
|
+
tail: "none",
|
|
422
|
+
headExtras: ["locks", "wizard-hat"],
|
|
423
|
+
bodyExtras: ["robe", "belt"],
|
|
424
|
+
extraColors: ["#ffd84a", "#ffd84a", "#ffd84a"]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "knight",
|
|
428
|
+
name: { en: "Knight", it: "Cavaliere" },
|
|
429
|
+
band: "9-12",
|
|
430
|
+
palette: { body: "#bcc7d3", limbs: "#8e9aa8", belly: "#dce3ea", accent: "#d8443a", outline: COOL, hands: "#6c7786", feet: "#6c7786" },
|
|
431
|
+
head: "bucket",
|
|
432
|
+
ears: "none",
|
|
433
|
+
face: { eyes: "slit", eyeColor: "#bfefff", mouth: "none", muzzle: "none" },
|
|
434
|
+
tail: "none",
|
|
435
|
+
headExtras: ["plume"],
|
|
436
|
+
bodyExtras: ["pauldrons", "emblem", "boots"],
|
|
437
|
+
extraColors: ["#d8443a"]
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "racer",
|
|
441
|
+
name: { en: "Racer", it: "Pilota" },
|
|
442
|
+
band: "9-12",
|
|
443
|
+
palette: { body: "#ff6a2b", limbs: "#ff6a2b", belly: "#ffffff", accent: "#ffffff", outline: COOL, hands: "#232838", feet: "#232838" },
|
|
444
|
+
head: "round",
|
|
445
|
+
ears: "none",
|
|
446
|
+
face: { eyes: "goggle", eyeColor: "#7fd6ff", mouth: "none", muzzle: "none" },
|
|
447
|
+
tail: "none",
|
|
448
|
+
headExtras: ["stripe"],
|
|
449
|
+
bodyExtras: ["racing-stripe", "boots"]
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: "pirate",
|
|
453
|
+
name: { en: "Pirate", it: "Pirata" },
|
|
454
|
+
band: "9-12",
|
|
455
|
+
palette: { body: "#f2efe6", limbs: "#2a3d6e", belly: "#f2efe6", accent: "#d94545", outline: COOL, skin: "#d9a072", hair: "#2b1a12", hands: "#d9a072", feet: "#3a2a20" },
|
|
456
|
+
head: "round",
|
|
457
|
+
ears: "human",
|
|
458
|
+
face: { eyes: "calm", mouth: "smirk", muzzle: "none", brows: "#2b1a12" },
|
|
459
|
+
tail: "none",
|
|
460
|
+
headExtras: ["locks", "bandana", "eye-patch"],
|
|
461
|
+
bodyExtras: ["stripes", "belt", "boots"],
|
|
462
|
+
extraColors: ["#d94545", "#2a3d6e", "#4a3426"]
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: "diver",
|
|
466
|
+
name: { en: "Diver", it: "Sub" },
|
|
467
|
+
band: "9-12",
|
|
468
|
+
palette: { body: "#1f6582", limbs: "#1f6582", belly: "#1f6582", accent: "#ffb020", outline: COOL, skin: "#e8b48a", hair: "#c98f2e", hands: "#1b4f66", feet: "#ffb020" },
|
|
469
|
+
head: "round",
|
|
470
|
+
ears: "human",
|
|
471
|
+
face: { eyes: "calm", mouth: "smile", muzzle: "none", brows: "#8a5a1a" },
|
|
472
|
+
tail: "none",
|
|
473
|
+
headExtras: ["curls", "dive-mask"],
|
|
474
|
+
bodyExtras: ["tank", "fins"],
|
|
475
|
+
extraColors: ["#c9d3db"]
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: "hero",
|
|
479
|
+
name: { en: "Hero", it: "Supereroe" },
|
|
480
|
+
band: "9-12",
|
|
481
|
+
palette: { body: "#3557c8", limbs: "#3557c8", belly: "#3557c8", accent: "#e0403a", outline: COOL, skin: "#5a3825", hair: "#141010", hands: "#e0403a", feet: "#e0403a" },
|
|
482
|
+
head: "round",
|
|
483
|
+
ears: "human",
|
|
484
|
+
face: { eyes: "calm", mouth: "smirk", muzzle: "none", brows: "#141010" },
|
|
485
|
+
tail: "none",
|
|
486
|
+
headExtras: ["hair", "domino"],
|
|
487
|
+
bodyExtras: ["cape", "emblem", "boots"],
|
|
488
|
+
extraColors: ["#ffd84a"]
|
|
489
|
+
}
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
// src/shapes.ts
|
|
493
|
+
var PATH = { M: 0, L: 1, Q: 2, C: 3, Z: 4 };
|
|
494
|
+
var PATH_ARGS = [2, 2, 4, 6, 0];
|
|
495
|
+
var PATH_LETTERS = "MLQCZ";
|
|
496
|
+
function drawShapes(ctx, shapes) {
|
|
497
|
+
for (const s of shapes) drawShape(ctx, s);
|
|
498
|
+
}
|
|
499
|
+
function drawShape(ctx, s) {
|
|
500
|
+
if (s.k === "group") {
|
|
501
|
+
ctx.save();
|
|
502
|
+
ctx.translate(s.x, s.y);
|
|
503
|
+
if (s.rot) ctx.rotate(s.rot);
|
|
504
|
+
if (s.s !== void 0 && s.s !== 1) ctx.scale(s.s, s.s);
|
|
505
|
+
if (s.alpha !== void 0) ctx.globalAlpha *= s.alpha;
|
|
506
|
+
drawShapes(ctx, s.items);
|
|
507
|
+
ctx.restore();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
ctx.save();
|
|
511
|
+
if (s.alpha !== void 0) ctx.globalAlpha *= s.alpha;
|
|
512
|
+
ctx.beginPath();
|
|
513
|
+
switch (s.k) {
|
|
514
|
+
case "circle":
|
|
515
|
+
ctx.arc(s.x, s.y, Math.max(s.r, 0), 0, Math.PI * 2);
|
|
516
|
+
break;
|
|
517
|
+
case "ellipse":
|
|
518
|
+
ctx.ellipse(s.x, s.y, Math.max(s.rx, 0), Math.max(s.ry, 0), s.rot ?? 0, 0, Math.PI * 2);
|
|
519
|
+
break;
|
|
520
|
+
case "rect":
|
|
521
|
+
roundRect(ctx, s);
|
|
522
|
+
break;
|
|
523
|
+
case "path":
|
|
524
|
+
if (typeof s.d === "string") tracePath(ctx, s.d);
|
|
525
|
+
else traceData(ctx, s.d);
|
|
526
|
+
break;
|
|
527
|
+
case "line":
|
|
528
|
+
ctx.moveTo(s.pts[0] ?? 0, s.pts[1] ?? 0);
|
|
529
|
+
for (let i = 2; i + 1 < s.pts.length; i += 2) ctx.lineTo(s.pts[i], s.pts[i + 1]);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
ctx.lineCap = "round";
|
|
533
|
+
ctx.lineJoin = "round";
|
|
534
|
+
if (s.fill && s.k !== "line") {
|
|
535
|
+
ctx.fillStyle = s.fill;
|
|
536
|
+
ctx.fill();
|
|
537
|
+
}
|
|
538
|
+
if (s.stroke && (s.lw ?? 1) > 0) {
|
|
539
|
+
ctx.strokeStyle = s.stroke;
|
|
540
|
+
ctx.lineWidth = s.lw ?? 1;
|
|
541
|
+
ctx.stroke();
|
|
542
|
+
}
|
|
543
|
+
ctx.restore();
|
|
544
|
+
}
|
|
545
|
+
function roundRect(ctx, s) {
|
|
546
|
+
const r = Math.max(0, Math.min(s.r, s.w / 2, s.h / 2));
|
|
547
|
+
ctx.translate(s.x, s.y);
|
|
548
|
+
if (s.rot) ctx.rotate(s.rot);
|
|
549
|
+
const x = -s.w / 2;
|
|
550
|
+
const y = -s.h / 2;
|
|
551
|
+
ctx.moveTo(x + r, y);
|
|
552
|
+
ctx.lineTo(x + s.w - r, y);
|
|
553
|
+
ctx.quadraticCurveTo(x + s.w, y, x + s.w, y + r);
|
|
554
|
+
ctx.lineTo(x + s.w, y + s.h - r);
|
|
555
|
+
ctx.quadraticCurveTo(x + s.w, y + s.h, x + s.w - r, y + s.h);
|
|
556
|
+
ctx.lineTo(x + r, y + s.h);
|
|
557
|
+
ctx.quadraticCurveTo(x, y + s.h, x, y + s.h - r);
|
|
558
|
+
ctx.lineTo(x, y + r);
|
|
559
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
560
|
+
ctx.closePath();
|
|
561
|
+
}
|
|
562
|
+
function traceData(ctx, d) {
|
|
563
|
+
for (let i = 0; i < d.length; ) {
|
|
564
|
+
const c = d[i++];
|
|
565
|
+
if (c === PATH.M) ctx.moveTo(d[i], d[i + 1]);
|
|
566
|
+
else if (c === PATH.L) ctx.lineTo(d[i], d[i + 1]);
|
|
567
|
+
else if (c === PATH.Q) ctx.quadraticCurveTo(d[i], d[i + 1], d[i + 2], d[i + 3]);
|
|
568
|
+
else if (c === PATH.C) ctx.bezierCurveTo(d[i], d[i + 1], d[i + 2], d[i + 3], d[i + 4], d[i + 5]);
|
|
569
|
+
else if (c === PATH.Z) ctx.closePath();
|
|
570
|
+
else return;
|
|
571
|
+
i += PATH_ARGS[c];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function pathToString(d) {
|
|
575
|
+
if (typeof d === "string") return d;
|
|
576
|
+
let out = "";
|
|
577
|
+
for (let i = 0; i < d.length; ) {
|
|
578
|
+
const c = d[i++];
|
|
579
|
+
const n = PATH_ARGS[c];
|
|
580
|
+
if (n === void 0) break;
|
|
581
|
+
out += (out ? " " : "") + PATH_LETTERS[c];
|
|
582
|
+
for (let k = 0; k < n; k++) out += (k ? " " : "") + num(d[i + k]);
|
|
583
|
+
i += n;
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
function pathPoints(d) {
|
|
588
|
+
if (typeof d === "string") return (d.match(/-?\d*\.?\d+(?:e-?\d+)?/gi) ?? []).map(Number);
|
|
589
|
+
const out = [];
|
|
590
|
+
for (let i = 0; i < d.length; ) {
|
|
591
|
+
const n = PATH_ARGS[d[i++]];
|
|
592
|
+
if (n === void 0) break;
|
|
593
|
+
for (let k = 0; k < n; k++) out.push(d[i + k]);
|
|
594
|
+
i += n;
|
|
595
|
+
}
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
function tracePath(ctx, d) {
|
|
599
|
+
const tokens = d.match(/[MLQCZ]|-?\d*\.?\d+(?:e-?\d+)?/gi) ?? [];
|
|
600
|
+
let cmd = "";
|
|
601
|
+
let i = 0;
|
|
602
|
+
const n = () => Number(tokens[i++]);
|
|
603
|
+
while (i < tokens.length) {
|
|
604
|
+
const t = tokens[i];
|
|
605
|
+
if (/[MLQCZ]/i.test(t)) {
|
|
606
|
+
cmd = t.toUpperCase();
|
|
607
|
+
i++;
|
|
608
|
+
if (cmd === "Z") {
|
|
609
|
+
ctx.closePath();
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (cmd === "M") {
|
|
614
|
+
ctx.moveTo(n(), n());
|
|
615
|
+
cmd = "L";
|
|
616
|
+
} else if (cmd === "L") ctx.lineTo(n(), n());
|
|
617
|
+
else if (cmd === "Q") ctx.quadraticCurveTo(n(), n(), n(), n());
|
|
618
|
+
else if (cmd === "C") ctx.bezierCurveTo(n(), n(), n(), n(), n(), n());
|
|
619
|
+
else i++;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
var num = (v) => {
|
|
623
|
+
const r = Math.round(v * 100) / 100;
|
|
624
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
625
|
+
};
|
|
626
|
+
var deg = (rad) => num(rad * 180 / Math.PI);
|
|
627
|
+
var esc = (v) => v.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
628
|
+
function paintAttrs(s, line = false) {
|
|
629
|
+
let a = ` fill="${line || !s.fill ? "none" : esc(s.fill)}"`;
|
|
630
|
+
if (s.stroke && (s.lw ?? 1) > 0) a += ` stroke="${esc(s.stroke)}" stroke-width="${num(s.lw ?? 1)}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
631
|
+
if (s.alpha !== void 0) a += ` opacity="${num(s.alpha)}"`;
|
|
632
|
+
return a;
|
|
633
|
+
}
|
|
634
|
+
function shapesToSvg(shapes) {
|
|
635
|
+
return shapes.map(shapeToSvg).join("");
|
|
636
|
+
}
|
|
637
|
+
function shapeToSvg(s) {
|
|
638
|
+
switch (s.k) {
|
|
639
|
+
case "group": {
|
|
640
|
+
const tf = [`translate(${num(s.x)} ${num(s.y)})`];
|
|
641
|
+
if (s.rot) tf.push(`rotate(${deg(s.rot)})`);
|
|
642
|
+
if (s.s !== void 0 && s.s !== 1) tf.push(`scale(${num(s.s)})`);
|
|
643
|
+
const op = s.alpha !== void 0 ? ` opacity="${num(s.alpha)}"` : "";
|
|
644
|
+
return `<g transform="${tf.join(" ")}"${op}>${shapesToSvg(s.items)}</g>`;
|
|
645
|
+
}
|
|
646
|
+
case "circle":
|
|
647
|
+
return `<circle cx="${num(s.x)}" cy="${num(s.y)}" r="${num(Math.max(s.r, 0))}"${paintAttrs(s)}/>`;
|
|
648
|
+
case "ellipse": {
|
|
649
|
+
const rot = s.rot ? ` transform="rotate(${deg(s.rot)} ${num(s.x)} ${num(s.y)})"` : "";
|
|
650
|
+
return `<ellipse cx="${num(s.x)}" cy="${num(s.y)}" rx="${num(Math.max(s.rx, 0))}" ry="${num(Math.max(s.ry, 0))}"${rot}${paintAttrs(s)}/>`;
|
|
651
|
+
}
|
|
652
|
+
case "rect": {
|
|
653
|
+
const r = Math.max(0, Math.min(s.r, s.w / 2, s.h / 2));
|
|
654
|
+
const rot = s.rot ? ` transform="rotate(${deg(s.rot)} ${num(s.x)} ${num(s.y)})"` : "";
|
|
655
|
+
return `<rect x="${num(s.x - s.w / 2)}" y="${num(s.y - s.h / 2)}" width="${num(s.w)}" height="${num(s.h)}" rx="${num(r)}"${rot}${paintAttrs(s)}/>`;
|
|
656
|
+
}
|
|
657
|
+
case "path":
|
|
658
|
+
return `<path d="${esc(pathToString(s.d))}"${paintAttrs(s)}/>`;
|
|
659
|
+
case "line": {
|
|
660
|
+
const pts2 = [];
|
|
661
|
+
for (let i = 0; i + 1 < s.pts.length; i += 2) pts2.push(`${num(s.pts[i])},${num(s.pts[i + 1])}`);
|
|
662
|
+
return `<polyline points="${pts2.join(" ")}"${paintAttrs(s, true)}/>`;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
var f = (v) => {
|
|
667
|
+
const r = Math.round(v * 100) / 100;
|
|
668
|
+
return r === 0 ? 0 : r;
|
|
669
|
+
};
|
|
670
|
+
function roundedPolygon(pts2, round = 0.5) {
|
|
671
|
+
const n = pts2.length;
|
|
672
|
+
if (n < 3) return [];
|
|
673
|
+
const mid2 = (a, b, t2) => ({ x: a.x + (b.x - a.x) * t2, y: a.y + (b.y - a.y) * t2 });
|
|
674
|
+
const t = Math.min(Math.max(round, 0), 0.5);
|
|
675
|
+
const start = mid2(pts2[0], pts2[1], t);
|
|
676
|
+
const d = [PATH.M, f(start.x), f(start.y)];
|
|
677
|
+
for (let i = 1; i <= n; i++) {
|
|
678
|
+
const prev = pts2[(i - 1) % n];
|
|
679
|
+
const cur = pts2[i % n];
|
|
680
|
+
const next = pts2[(i + 1) % n];
|
|
681
|
+
const a = mid2(prev, cur, 1 - t);
|
|
682
|
+
const b = mid2(cur, next, t);
|
|
683
|
+
d.push(PATH.L, f(a.x), f(a.y), PATH.Q, f(cur.x), f(cur.y), f(b.x), f(b.y));
|
|
684
|
+
}
|
|
685
|
+
d.push(PATH.Z);
|
|
686
|
+
return d;
|
|
687
|
+
}
|
|
688
|
+
function smoothClosed(pts2, tension = 1) {
|
|
689
|
+
const n = pts2.length;
|
|
690
|
+
if (n < 3) return [];
|
|
691
|
+
const d = [PATH.M, f(pts2[0].x), f(pts2[0].y)];
|
|
692
|
+
const k = tension / 6;
|
|
693
|
+
for (let i = 0; i < n; i++) {
|
|
694
|
+
const p0 = pts2[(i - 1 + n) % n];
|
|
695
|
+
const p1 = pts2[i];
|
|
696
|
+
const p2 = pts2[(i + 1) % n];
|
|
697
|
+
const p3 = pts2[(i + 2) % n];
|
|
698
|
+
d.push(PATH.C, f(p1.x + (p2.x - p0.x) * k), f(p1.y + (p2.y - p0.y) * k), f(p2.x - (p3.x - p1.x) * k), f(p2.y - (p3.y - p1.y) * k), f(p2.x), f(p2.y));
|
|
699
|
+
}
|
|
700
|
+
d.push(PATH.Z);
|
|
701
|
+
return d;
|
|
702
|
+
}
|
|
703
|
+
function smoothOpen(pts2) {
|
|
704
|
+
const n = pts2.length;
|
|
705
|
+
if (n < 2) return [];
|
|
706
|
+
const d = [PATH.M, f(pts2[0].x), f(pts2[0].y)];
|
|
707
|
+
for (let i = 0; i < n - 1; i++) {
|
|
708
|
+
const p0 = pts2[Math.max(i - 1, 0)];
|
|
709
|
+
const p1 = pts2[i];
|
|
710
|
+
const p2 = pts2[i + 1];
|
|
711
|
+
const p3 = pts2[Math.min(i + 2, n - 1)];
|
|
712
|
+
d.push(PATH.C, f(p1.x + (p2.x - p0.x) / 6), f(p1.y + (p2.y - p0.y) / 6), f(p2.x - (p3.x - p1.x) / 6), f(p2.y - (p3.y - p1.y) / 6), f(p2.x), f(p2.y));
|
|
713
|
+
}
|
|
714
|
+
return d;
|
|
715
|
+
}
|
|
716
|
+
var STAR = "M0 -1 L0.29 -0.4 L0.95 -0.31 L0.47 0.15 L0.59 0.81 L0 0.5 L-0.59 0.81 L-0.47 0.15 L-0.95 -0.31 L-0.29 -0.4 Z";
|
|
717
|
+
|
|
718
|
+
// src/head.ts
|
|
719
|
+
var WHITE = "#ffffff";
|
|
720
|
+
var SIDES = [-1, 1];
|
|
721
|
+
var MOUTH = "#3a2420";
|
|
722
|
+
var TONGUE = "#f2808f";
|
|
723
|
+
var DARK_GLASS = "#26324a";
|
|
724
|
+
var GOLD = "#ffd84a";
|
|
725
|
+
var pts = (...xy) => {
|
|
726
|
+
const out = [];
|
|
727
|
+
for (let i = 0; i + 1 < xy.length; i += 2) out.push({ x: xy[i], y: xy[i + 1] });
|
|
728
|
+
return out;
|
|
729
|
+
};
|
|
730
|
+
function headShapes(def, ow) {
|
|
731
|
+
const p = def.palette;
|
|
732
|
+
const out = [];
|
|
733
|
+
const ol = { stroke: p.outline, lw: ow };
|
|
734
|
+
const has = (x) => def.headExtras.includes(x);
|
|
735
|
+
const extra = (i, fallback) => def.extraColors?.[i] ?? fallback;
|
|
736
|
+
if (has("mane")) {
|
|
737
|
+
const n = 12;
|
|
738
|
+
for (let i = 0; i < n; i++) {
|
|
739
|
+
const a = i / n * Math.PI * 2;
|
|
740
|
+
out.push({ k: "circle", x: Math.cos(a) * 1.02, y: Math.sin(a) * 1 + 0.04, r: 0.42, fill: extra(0, p.accent), ...ol });
|
|
741
|
+
}
|
|
742
|
+
out.push({ k: "circle", x: 0, y: 0.04, r: 1.08, fill: extra(0, p.accent) });
|
|
743
|
+
}
|
|
744
|
+
if (has("rainbow-mane")) {
|
|
745
|
+
const c = def.extraColors ?? ["#f59ac0", "#fbd46a", "#8fd3f4"];
|
|
746
|
+
const locks = [
|
|
747
|
+
[0.98, 0.05, 0.3, 0.5, -0.25],
|
|
748
|
+
[0.92, 0.58, 0.26, 0.42, -0.1],
|
|
749
|
+
[0.62, 0.92, 0.22, 0.32, 0.4]
|
|
750
|
+
];
|
|
751
|
+
locks.forEach(([x, y, rx, ry, rot], i) => out.push({ k: "ellipse", x, y, rx, ry, rot, fill: c[i % c.length], ...ol }));
|
|
752
|
+
}
|
|
753
|
+
if (has("spikes")) {
|
|
754
|
+
for (const a of [-0.55, 0, 0.55]) {
|
|
755
|
+
const tip = polar(a, 1.48);
|
|
756
|
+
const b1 = polar(a - 0.32, 0.86);
|
|
757
|
+
const b2 = polar(a + 0.32, 0.86);
|
|
758
|
+
out.push({ k: "path", d: roundedPolygon([b1, tip, b2], 0.22), fill: p.accent, ...ol });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (has("curl")) {
|
|
762
|
+
out.push({ k: "path", d: smoothClosed([{ x: -0.2, y: -0.9 }, { x: -0.05, y: -1.3 }, { x: 0.32, y: -1.42 }, { x: 0.16, y: -1.18 }, { x: 0.2, y: -0.9 }]), fill: p.body, ...ol });
|
|
763
|
+
}
|
|
764
|
+
if (has("wool")) {
|
|
765
|
+
for (let i = 0; i <= 8; i++) {
|
|
766
|
+
const a = -1.75 + i / 8 * 3.5;
|
|
767
|
+
out.push({ k: "circle", x: Math.sin(a) * 0.98, y: -Math.cos(a) * 0.92 - 0.04, r: 0.34, fill: p.body, ...ol });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (has("crest")) {
|
|
771
|
+
;
|
|
772
|
+
[-0.38, 0, 0.38].forEach((a, i) => {
|
|
773
|
+
const c = polar(a, 1.12);
|
|
774
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.15, ry: 0.42, rot: a, fill: extra(i, p.accent), ...ol });
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (has("plume")) {
|
|
778
|
+
out.push({ k: "path", d: smoothClosed(pts(-0.12, -0.92, -0.2, -1.42, 0.1, -1.78, 0.62, -1.74, 0.36, -1.46, 0.16, -0.98)), fill: p.accent, ...ol });
|
|
779
|
+
}
|
|
780
|
+
if (has("locks")) {
|
|
781
|
+
out.push({ k: "path", d: smoothClosed(pts(-1.14, 0.58, -1.24, -0.2, -0.86, -0.98, 0, -1.2, 0.86, -0.98, 1.24, -0.2, 1.14, 0.58, 0.8, 0.66, 0, 0.2, -0.8, 0.66)), fill: p.hair ?? p.outline, ...ol });
|
|
782
|
+
}
|
|
783
|
+
if (has("ninja-hood")) {
|
|
784
|
+
for (const [rot, len2] of [
|
|
785
|
+
[0.35, 0.5],
|
|
786
|
+
[0.85, 0.42]
|
|
787
|
+
])
|
|
788
|
+
out.push({ k: "ellipse", x: 1 + Math.cos(rot) * len2 * 0.8, y: -0.5 + Math.sin(rot) * len2 * 0.8, rx: len2, ry: 0.11, rot, fill: p.accent, ...ol });
|
|
789
|
+
}
|
|
790
|
+
if (has("bandana")) {
|
|
791
|
+
for (const [rot, len2] of [
|
|
792
|
+
[Math.PI - 0.5, 0.42],
|
|
793
|
+
[Math.PI - 1, 0.36]
|
|
794
|
+
])
|
|
795
|
+
out.push({ k: "ellipse", x: -0.98 + Math.cos(rot) * len2 * 0.8, y: -0.36 + Math.sin(rot) * len2 * 0.8, rx: len2, ry: 0.12, rot, fill: p.accent, ...ol });
|
|
796
|
+
}
|
|
797
|
+
if (has("ruff")) {
|
|
798
|
+
for (const s of SIDES)
|
|
799
|
+
out.push({ k: "path", d: roundedPolygon(pts(s * 0.7, -0.3, s * 1.32, 0, s * 1.02, 0.18, s * 1.26, 0.46, s * 0.86, 0.56, s * 0.98, 0.84, s * 0.4, 0.7), 0.25), fill: p.body, ...ol });
|
|
800
|
+
}
|
|
801
|
+
if (def.face.eyesOnTop) {
|
|
802
|
+
for (const s of SIDES) out.push({ k: "circle", x: s * 0.46, y: -0.72, r: 0.38, fill: p.body, ...ol });
|
|
803
|
+
}
|
|
804
|
+
out.push(...ears(def, ow));
|
|
805
|
+
out.push(headShape(def, ol));
|
|
806
|
+
const muzzle = def.face.muzzle;
|
|
807
|
+
const mc = p.belly;
|
|
808
|
+
if (muzzle === "oval") out.push({ k: "ellipse", x: 0, y: 0.4, rx: 0.46, ry: 0.32, fill: mc });
|
|
809
|
+
if (muzzle === "monkey") {
|
|
810
|
+
for (const s of SIDES) out.push({ k: "circle", x: s * 0.32, y: -0.04, r: 0.38, fill: mc });
|
|
811
|
+
out.push({ k: "ellipse", x: 0, y: 0.4, rx: 0.56, ry: 0.4, fill: mc });
|
|
812
|
+
}
|
|
813
|
+
if (muzzle === "heart") {
|
|
814
|
+
for (const s of SIDES) out.push({ k: "circle", x: s * 0.36, y: 0.02, r: 0.5, fill: mc });
|
|
815
|
+
out.push({ k: "ellipse", x: 0, y: 0.38, rx: 0.66, ry: 0.48, fill: mc });
|
|
816
|
+
}
|
|
817
|
+
if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
|
|
818
|
+
if (muzzle === "cheeks") for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.4, y: 0.44, rx: 0.44, ry: 0.32, fill: mc });
|
|
819
|
+
if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
|
|
820
|
+
if (has("stripes")) {
|
|
821
|
+
const c = extra(0, p.outline);
|
|
822
|
+
out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
|
|
823
|
+
for (const s of SIDES) {
|
|
824
|
+
out.push({ k: "line", pts: [s * 0.3, -0.93, s * 0.24, -0.7], stroke: c, lw: 0.1 });
|
|
825
|
+
out.push({ k: "line", pts: [s * 0.98, -0.05, s * 0.74, 0], stroke: c, lw: 0.1 });
|
|
826
|
+
out.push({ k: "line", pts: [s * 0.94, 0.22, s * 0.74, 0.22], stroke: c, lw: 0.09 });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (has("spots")) {
|
|
830
|
+
const c = extra(1, p.belly);
|
|
831
|
+
out.push({ k: "circle", x: -0.55, y: -0.5, r: 0.15, fill: c });
|
|
832
|
+
out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
|
|
833
|
+
out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
|
|
834
|
+
}
|
|
835
|
+
if (has("forehead")) out.push({ k: "path", d: smoothClosed(pts(-0.62, -0.66, 0, -0.86, 0.62, -0.66, 0.26, -0.36, 0, -0.1, -0.26, -0.36), 0.9), fill: extra(0, p.accent) });
|
|
836
|
+
if (has("eye-spot")) out.push({ k: "ellipse", x: 0.42, y: -0.06, rx: 0.3, ry: 0.33, rot: 0.3, fill: p.ears ?? p.accent });
|
|
837
|
+
if (has("gills")) {
|
|
838
|
+
for (const s of SIDES) for (const dx of [0, 0.12, 0.24]) out.push({ k: "path", d: `M${s * (0.66 + dx)} 0.06 Q${s * (0.6 + dx)} 0.22 ${s * (0.66 + dx)} 0.38`, stroke: p.accent, lw: ow * 0.9 });
|
|
839
|
+
}
|
|
840
|
+
const hair = p.hair ?? p.outline;
|
|
841
|
+
if (has("hair")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.06, -1.02, -0.58, -0.62, -1.04, 0.08, -1.14, 0.76, -0.92, 1.05, -0.42, 1, -0.06, 0.84, -0.4, 0.42, -0.5, 0.06, -0.4, -0.34, -0.56, -0.74, -0.38)), fill: hair, ...ol });
|
|
842
|
+
if (has("locks")) out.push({ k: "path", d: smoothClosed(pts(-1, -0.08, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1, -0.08, 0.8, -0.38, 0.36, -0.44, 0, -0.4, -0.36, -0.44, -0.8, -0.38)), fill: hair, ...ol });
|
|
843
|
+
if (has("curls")) {
|
|
844
|
+
for (let i = 0; i < 7; i++) {
|
|
845
|
+
const c = polar(-1.3 + i / 6 * 2.6, 0.86);
|
|
846
|
+
out.push({ k: "circle", x: c.x, y: c.y, r: 0.3, fill: hair, ...ol });
|
|
847
|
+
}
|
|
848
|
+
out.push({ k: "ellipse", x: 0, y: -0.64, rx: 0.8, ry: 0.36, fill: hair });
|
|
849
|
+
}
|
|
850
|
+
if (has("ninja-hood")) {
|
|
851
|
+
out.push({ k: "rect", x: 0, y: 0, w: 1.56, h: 0.6, r: 0.28, fill: p.skin ?? WHITE, stroke: p.outline, lw: ow * 0.8 });
|
|
852
|
+
out.push({ k: "path", d: "M-0.98 -0.42 Q0 -0.74 0.98 -0.42 L1.0 -0.62 Q0 -0.94 -1.0 -0.62 Z", fill: p.accent, ...ol });
|
|
853
|
+
}
|
|
854
|
+
if (has("domino")) {
|
|
855
|
+
out.push({ k: "path", d: smoothClosed(pts(-0.86, -0.04, -0.62, -0.3, -0.18, -0.24, 0, -0.16, 0.18, -0.24, 0.62, -0.3, 0.86, -0.04, 0.6, 0.22, 0.16, 0.16, 0, 0.1, -0.16, 0.16, -0.6, 0.22), 0.9), fill: p.accent, ...ol });
|
|
856
|
+
for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.38, y: 0, rx: 0.19, ry: 0.13, fill: WHITE });
|
|
857
|
+
}
|
|
858
|
+
out.push(...eyes(def, ow));
|
|
859
|
+
if (has("brows")) {
|
|
860
|
+
for (const s of SIDES) out.push({ k: "line", pts: [s * 0.72, -0.5, s * 0.14, -0.3], stroke: p.accent, lw: 0.16 });
|
|
861
|
+
}
|
|
862
|
+
if (def.face.brows) for (const s of SIDES) out.push({ k: "line", pts: [s * 0.2, -0.2, s * 0.54, -0.28], stroke: def.face.brows, lw: 0.09 });
|
|
863
|
+
out.push(...mouth(def, ow));
|
|
864
|
+
if (def.face.cheeks) for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.64, y: 0.3, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
|
|
865
|
+
if (has("whiskers")) {
|
|
866
|
+
for (const s of SIDES)
|
|
867
|
+
for (const dy of [-0.1, 0.06]) out.push({ k: "line", pts: [s * 0.62, 0.38 + dy, s * 1.06, 0.3 + dy * 2.2], stroke: p.outline, lw: ow * 0.7, alpha: 0.75 });
|
|
868
|
+
}
|
|
869
|
+
if (has("rainbow-mane")) {
|
|
870
|
+
const c = def.extraColors ?? ["#f59ac0", "#fbd46a", "#8fd3f4"];
|
|
871
|
+
out.push({ k: "ellipse", x: 0.42, y: -0.82, rx: 0.34, ry: 0.22, rot: 0.5, fill: c[0], ...ol });
|
|
872
|
+
out.push({ k: "ellipse", x: 0.1, y: -0.92, rx: 0.3, ry: 0.2, rot: 0.25, fill: c[1 % c.length], ...ol });
|
|
873
|
+
}
|
|
874
|
+
if (def.ears === "unicorn") {
|
|
875
|
+
const horn = [
|
|
876
|
+
{ x: -0.18, y: -0.84 },
|
|
877
|
+
{ x: 0.02, y: -1.72 },
|
|
878
|
+
{ x: 0.18, y: -0.84 }
|
|
879
|
+
];
|
|
880
|
+
out.push({ k: "path", d: roundedPolygon(horn, 0.12), fill: p.accent, ...ol });
|
|
881
|
+
for (const y of [-1, -1.2, -1.4]) {
|
|
882
|
+
const w = 0.16 * ((y + 1.72) / 0.88);
|
|
883
|
+
out.push({ k: "line", pts: [-w, y + 0.04, w, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (has("helmet")) {
|
|
887
|
+
for (const s of SIDES) out.push({ k: "rect", x: s * 1, y: 0.05, w: 0.26, h: 0.56, r: 0.12, fill: p.accent, ...ol });
|
|
888
|
+
out.push({ k: "path", d: "M-0.62 -0.62 Q-0.3 -0.92 0.12 -0.9", stroke: WHITE, lw: 0.1, alpha: 0.9 });
|
|
889
|
+
}
|
|
890
|
+
if (has("bolts")) for (const s of SIDES) out.push({ k: "rect", x: s * 1.08, y: 0.08, w: 0.2, h: 0.44, r: 0.08, fill: p.accent, ...ol });
|
|
891
|
+
if (has("tuft")) {
|
|
892
|
+
out.push({ k: "path", d: smoothClosed([{ x: -0.26, y: -0.95 }, { x: -0.12, y: -1.22 }, { x: 0.02, y: -1.04 }, { x: 0.16, y: -1.28 }, { x: 0.28, y: -0.95 }, { x: 0, y: -0.86 }]), fill: p.body, ...ol });
|
|
893
|
+
}
|
|
894
|
+
if (has("eggshell")) {
|
|
895
|
+
out.push({ k: "path", d: "M-0.88 -0.5 C-0.88 -1.12 -0.46 -1.42 0 -1.42 C0.46 -1.42 0.88 -1.12 0.88 -0.5 L0.66 -0.66 L0.44 -0.48 L0.22 -0.66 L0 -0.48 L-0.22 -0.66 L-0.44 -0.48 L-0.66 -0.66 Z", fill: extra(0, WHITE), ...ol });
|
|
896
|
+
}
|
|
897
|
+
if (has("wool")) for (const [x, y] of [[-0.36, -0.84], [0.02, -0.94], [0.4, -0.82]]) out.push({ k: "circle", x, y, r: 0.27, fill: p.body, ...ol });
|
|
898
|
+
if (has("stripe")) out.push({ k: "rect", x: 0, y: -0.64, w: 0.38, h: 0.62, r: 0.06, fill: p.accent });
|
|
899
|
+
if (has("eye-patch")) {
|
|
900
|
+
out.push({ k: "line", pts: [-0.97, -0.5, 0.97, 0.13], stroke: p.outline, lw: 0.07 });
|
|
901
|
+
out.push({ k: "ellipse", x: 0.38, y: 0.02, rx: 0.21, ry: 0.19, fill: p.outline });
|
|
902
|
+
}
|
|
903
|
+
if (has("bandana")) {
|
|
904
|
+
out.push({ k: "path", d: smoothClosed(pts(-1.05, -0.22, -0.96, -0.72, -0.5, -1.08, 0.1, -1.14, 0.72, -0.96, 1.04, -0.56, 1.05, -0.22, 0.5, -0.46, -0.5, -0.46)), fill: p.accent, ...ol });
|
|
905
|
+
for (const [x, y] of [[-0.5, -0.8], [0.08, -0.92], [0.6, -0.74], [-0.12, -0.62], [0.36, -0.6]]) out.push({ k: "circle", x, y, r: 0.06, fill: WHITE, alpha: 0.85 });
|
|
906
|
+
out.push({ k: "circle", x: -0.98, y: -0.34, r: 0.15, fill: p.accent, ...ol });
|
|
907
|
+
}
|
|
908
|
+
if (has("cap")) {
|
|
909
|
+
for (const s of SIDES) out.push({ k: "line", pts: [s * 0.98, -0.2, s * 0.5, 0.82, -s * 0.05, 0.98], stroke: p.outline, lw: 0.06 });
|
|
910
|
+
out.push({ k: "path", d: smoothClosed(pts(-1.1, -0.16, -1.04, -0.66, -0.6, -1.12, 0, -1.24, 0.6, -1.12, 1.04, -0.66, 1.1, -0.16, 0.5, -0.4, -0.5, -0.4)), fill: p.accent, ...ol });
|
|
911
|
+
out.push({ k: "path", d: "M-0.74 -0.4 Q0 -0.6 0.74 -0.4 Q0.8 -0.24 0.6 -0.22 Q0 -0.36 -0.6 -0.22 Q-0.8 -0.24 -0.74 -0.4 Z", fill: p.accent, ...ol });
|
|
912
|
+
out.push({ k: "circle", x: 0.5, y: -0.82, r: 0.15, fill: extra(0, WHITE), ...ol });
|
|
913
|
+
for (const x of [-0.42, -0.14]) out.push({ k: "line", pts: [x, -1.04, x + 0.02, -0.78], stroke: p.outline, lw: 0.07, alpha: 0.7 });
|
|
914
|
+
}
|
|
915
|
+
if (has("safari-hat")) {
|
|
916
|
+
out.push({ k: "ellipse", x: 0, y: -0.56, rx: 1.48, ry: 0.3, fill: p.accent, ...ol });
|
|
917
|
+
out.push({ k: "path", d: smoothClosed(pts(-0.78, -0.58, -0.7, -1.1, -0.3, -1.38, 0.3, -1.38, 0.7, -1.1, 0.78, -0.58, 0, -0.5), 0.8), fill: p.accent, ...ol });
|
|
918
|
+
out.push({ k: "path", d: "M-0.77 -0.72 Q0 -0.62 0.77 -0.72 L0.74 -0.9 Q0 -0.8 -0.74 -0.9 Z", fill: extra(1, p.outline), stroke: p.outline, lw: ow * 0.8 });
|
|
919
|
+
}
|
|
920
|
+
if (has("wizard-hat")) {
|
|
921
|
+
out.push({ k: "ellipse", x: 0, y: -0.62, rx: 1.32, ry: 0.26, fill: p.accent, ...ol });
|
|
922
|
+
out.push({ k: "path", d: roundedPolygon(pts(-0.8, -0.66, -0.36, -1.5, 0.5, -2.2, 0.3, -1.46, 0.8, -0.66), 0.3), fill: p.accent, ...ol });
|
|
923
|
+
const star = extra(0, GOLD);
|
|
924
|
+
for (const [x, y, k] of [[0.02, -1.08, 0.18], [-0.38, -0.84, 0.1], [0.36, -1.5, 0.11]]) out.push({ k: "group", x, y, s: k, items: [{ k: "path", d: STAR, fill: star }] });
|
|
925
|
+
}
|
|
926
|
+
if (has("dive-mask")) {
|
|
927
|
+
for (const s of SIDES) out.push({ k: "line", pts: [s * 0.7, -0.04, s * 1, -0.12], stroke: p.outline, lw: 0.2 }, { k: "line", pts: [s * 0.7, -0.04, s * 1, -0.12], stroke: p.accent, lw: 0.11 });
|
|
928
|
+
out.push({ k: "rect", x: 0, y: 0, w: 1.42, h: 0.62, r: 0.28, fill: "#bfe9f5", alpha: 0.35 });
|
|
929
|
+
out.push({ k: "rect", x: 0, y: 0, w: 1.42, h: 0.62, r: 0.28, stroke: p.outline, lw: 0.2 });
|
|
930
|
+
out.push({ k: "rect", x: 0, y: 0, w: 1.42, h: 0.62, r: 0.28, stroke: p.accent, lw: 0.11 });
|
|
931
|
+
out.push({ k: "path", d: "M-0.5 -0.16 Q-0.4 -0.24 -0.22 -0.24", stroke: WHITE, lw: 0.06, alpha: 0.9 });
|
|
932
|
+
const tube = "M0.5 0.52 Q0.98 0.56 1.04 0.1 L1.12 -0.92";
|
|
933
|
+
out.push({ k: "path", d: tube, stroke: p.outline, lw: 0.24 }, { k: "path", d: tube, stroke: p.accent, lw: 0.13 });
|
|
934
|
+
}
|
|
935
|
+
return out;
|
|
936
|
+
}
|
|
937
|
+
function headShape(def, ol) {
|
|
938
|
+
const fill = def.headExtras.includes("ninja-hood") ? def.palette.body : def.palette.skin ?? def.palette.body;
|
|
939
|
+
switch (def.head) {
|
|
940
|
+
case "round":
|
|
941
|
+
return { k: "circle", x: 0, y: 0, r: 1, fill, ...ol };
|
|
942
|
+
case "wide":
|
|
943
|
+
return { k: "ellipse", x: 0, y: 0.03, rx: 1.12, ry: 0.94, fill, ...ol };
|
|
944
|
+
case "tall":
|
|
945
|
+
return { k: "ellipse", x: 0, y: -0.02, rx: 0.94, ry: 1.04, fill, ...ol };
|
|
946
|
+
case "square":
|
|
947
|
+
return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
|
|
948
|
+
case "bucket":
|
|
949
|
+
return { k: "path", d: roundedPolygon(pts(-0.94, 0.96, -0.98, -0.36, -0.62, -1.04, 0.62, -1.04, 0.98, -0.36, 0.94, 0.96), 0.42), fill, ...ol };
|
|
950
|
+
case "bulb":
|
|
951
|
+
return {
|
|
952
|
+
k: "path",
|
|
953
|
+
d: smoothClosed([
|
|
954
|
+
{ x: 0, y: -1.02 },
|
|
955
|
+
{ x: 0.86, y: -0.74 },
|
|
956
|
+
{ x: 1.08, y: -0.12 },
|
|
957
|
+
{ x: 0.72, y: 0.62 },
|
|
958
|
+
{ x: 0, y: 0.92 },
|
|
959
|
+
{ x: -0.72, y: 0.62 },
|
|
960
|
+
{ x: -1.08, y: -0.12 },
|
|
961
|
+
{ x: -0.86, y: -0.74 }
|
|
962
|
+
]),
|
|
963
|
+
fill,
|
|
964
|
+
...ol
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function headEllipse(def) {
|
|
969
|
+
if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
|
|
970
|
+
if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
|
|
971
|
+
return { cy: 0.03, rx: 1.12, ry: 0.94 };
|
|
972
|
+
}
|
|
973
|
+
function lowerFace(def, ow) {
|
|
974
|
+
const { cy, rx: hx, ry: hy } = headEllipse(def);
|
|
975
|
+
const rx = hx - ow / 2;
|
|
976
|
+
const ry = hy - ow / 2;
|
|
977
|
+
const top = 0.15;
|
|
978
|
+
const a0 = Math.asin(Math.min(1, (top - cy) / ry));
|
|
979
|
+
const x0 = Math.cos(a0) * rx;
|
|
980
|
+
const r = (v) => Math.round(v * 100) / 100 || 0;
|
|
981
|
+
const d = [PATH.M, r(-x0), top];
|
|
982
|
+
const wave = [
|
|
983
|
+
[-0.84 * x0, 0.02, -0.53 * x0, 0.06, -0.32 * x0, 0.24],
|
|
984
|
+
[-0.17 * x0, 0.11, 0.17 * x0, 0.11, 0.32 * x0, 0.24],
|
|
985
|
+
[0.53 * x0, 0.06, 0.84 * x0, 0.02, x0, top]
|
|
986
|
+
];
|
|
987
|
+
for (const c of wave) d.push(PATH.C, ...c.map(r));
|
|
988
|
+
const n = 24;
|
|
989
|
+
for (let i = 1; i <= n; i++) {
|
|
990
|
+
const a = a0 + (Math.PI - 2 * a0) * i / n;
|
|
991
|
+
d.push(PATH.L, r(Math.cos(a) * rx), r(cy + Math.sin(a) * ry));
|
|
992
|
+
}
|
|
993
|
+
d.push(PATH.Z);
|
|
994
|
+
return d;
|
|
995
|
+
}
|
|
996
|
+
function ears(def, ow) {
|
|
997
|
+
const p = def.palette;
|
|
998
|
+
const ol = { stroke: p.outline, lw: ow };
|
|
999
|
+
const out = [];
|
|
1000
|
+
for (const s of SIDES) {
|
|
1001
|
+
switch (def.ears) {
|
|
1002
|
+
case "pointy":
|
|
1003
|
+
case "unicorn": {
|
|
1004
|
+
const k = def.ears === "unicorn" ? 0.8 : 1;
|
|
1005
|
+
const base1 = { x: s * 0.3, y: -0.82 };
|
|
1006
|
+
const tip = { x: s * 0.86, y: -0.82 - 0.62 * k };
|
|
1007
|
+
const base2 = { x: s * 0.94, y: -0.36 };
|
|
1008
|
+
out.push({ k: "path", d: roundedPolygon([base1, tip, base2], 0.2), fill: p.body, ...ol });
|
|
1009
|
+
const c = { x: (base1.x + tip.x + base2.x) / 3, y: (base1.y + tip.y + base2.y) / 3 };
|
|
1010
|
+
const inner = [base1, tip, base2].map((q) => ({ x: c.x + (q.x - c.x) * 0.55, y: c.y + (q.y - c.y) * 0.55 + 0.04 }));
|
|
1011
|
+
out.push({ k: "path", d: roundedPolygon(inner, 0.25), fill: p.accent });
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
case "round":
|
|
1015
|
+
out.push({ k: "circle", x: s * 0.72, y: -0.72, r: 0.36, fill: p.ears ?? p.body, ...ol });
|
|
1016
|
+
if (!p.ears) out.push({ k: "circle", x: s * 0.74, y: -0.74, r: 0.19, fill: p.accent });
|
|
1017
|
+
break;
|
|
1018
|
+
case "big-round":
|
|
1019
|
+
out.push({ k: "circle", x: s * 0.96, y: -0.46, r: 0.52, fill: p.body, ...ol });
|
|
1020
|
+
out.push({ k: "circle", x: s * 1, y: -0.44, r: 0.32, fill: p.accent });
|
|
1021
|
+
break;
|
|
1022
|
+
case "long":
|
|
1023
|
+
out.push({ k: "ellipse", x: s * 0.38, y: -1.42, rx: 0.25, ry: 0.68, rot: s * 0.14, fill: p.body, ...ol });
|
|
1024
|
+
out.push({ k: "ellipse", x: s * 0.39, y: -1.36, rx: 0.12, ry: 0.48, rot: s * 0.14, fill: p.accent });
|
|
1025
|
+
break;
|
|
1026
|
+
case "side":
|
|
1027
|
+
out.push({ k: "circle", x: s * 1, y: 0.02, r: 0.34, fill: p.body, ...ol });
|
|
1028
|
+
out.push({ k: "circle", x: s * 1.02, y: 0.02, r: 0.2, fill: p.belly });
|
|
1029
|
+
break;
|
|
1030
|
+
case "tufts":
|
|
1031
|
+
out.push({ k: "path", d: roundedPolygon([{ x: s * 0.3, y: -0.84 }, { x: s * 1.22, y: -1.12 }, { x: s * 0.92, y: -0.42 }], 0.16), fill: p.body, ...ol });
|
|
1032
|
+
out.push({ k: "line", pts: [s * 0.62, -0.78, s * 1, -0.98], stroke: p.accent, lw: 0.08 });
|
|
1033
|
+
break;
|
|
1034
|
+
case "antennae": {
|
|
1035
|
+
const tip = { x: s * 0.62, y: -1.42 };
|
|
1036
|
+
out.push({ k: "path", d: `M${s * 0.3} -0.8 Q${s * 0.36} -1.2 ${tip.x} ${tip.y}`, stroke: p.outline, lw: ow * 2.6 });
|
|
1037
|
+
out.push({ k: "path", d: `M${s * 0.3} -0.8 Q${s * 0.36} -1.2 ${tip.x} ${tip.y}`, stroke: p.body, lw: ow * 1.2 });
|
|
1038
|
+
out.push({ k: "circle", x: tip.x, y: tip.y, r: 0.17, fill: p.accent, ...ol });
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
case "floppy":
|
|
1042
|
+
out.push({ k: "ellipse", x: s * 0.96, y: 0.02, rx: 0.3, ry: 0.62, rot: -s * 0.36, fill: p.ears ?? p.body, ...ol });
|
|
1043
|
+
break;
|
|
1044
|
+
case "droopy":
|
|
1045
|
+
out.push({ k: "ellipse", x: s * 1.04, y: -0.22, rx: 0.4, ry: 0.18, rot: s * 0.32, fill: p.skin ?? p.body, ...ol });
|
|
1046
|
+
out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
|
|
1047
|
+
break;
|
|
1048
|
+
case "human":
|
|
1049
|
+
out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
|
|
1050
|
+
break;
|
|
1051
|
+
case "horns": {
|
|
1052
|
+
const pts2 = [
|
|
1053
|
+
{ x: s * 0.3, y: -0.84 },
|
|
1054
|
+
{ x: s * 0.6, y: -1.5 },
|
|
1055
|
+
{ x: s * 0.66, y: -0.68 }
|
|
1056
|
+
];
|
|
1057
|
+
out.push({ k: "path", d: roundedPolygon(pts2, 0.18), fill: p.accent, ...ol });
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (def.ears === "fin") out.push({ k: "path", d: roundedPolygon(pts(-0.4, -0.82, 0.2, -1.62, 0.46, -0.78), 0.2), fill: p.body, ...ol });
|
|
1063
|
+
if (def.ears === "antenna") {
|
|
1064
|
+
out.push({ k: "line", pts: [0, -0.85, 0, -1.42], stroke: p.outline, lw: ow * 2.6 });
|
|
1065
|
+
out.push({ k: "line", pts: [0, -0.85, 0, -1.42], stroke: p.limbs, lw: ow * 1.2 });
|
|
1066
|
+
out.push({ k: "circle", x: 0, y: -1.5, r: 0.18, fill: p.accent, ...ol });
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
}
|
|
1070
|
+
function eyes(def, ow) {
|
|
1071
|
+
const p = def.palette;
|
|
1072
|
+
const f2 = def.face;
|
|
1073
|
+
const out = [];
|
|
1074
|
+
const ink = p.outline;
|
|
1075
|
+
const ex = f2.eyesOnTop ? 0.46 : 0.4;
|
|
1076
|
+
const ey = f2.eyesOnTop ? -0.72 : -0.04;
|
|
1077
|
+
const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
|
|
1078
|
+
switch (f2.eyes) {
|
|
1079
|
+
case "dot":
|
|
1080
|
+
for (const s of SIDES) {
|
|
1081
|
+
out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.12, ry: 0.155, fill: ink });
|
|
1082
|
+
out.push(shine(s * ex + 0.04, ey - 0.06, 0.045));
|
|
1083
|
+
}
|
|
1084
|
+
break;
|
|
1085
|
+
case "bead":
|
|
1086
|
+
for (const s of SIDES) {
|
|
1087
|
+
out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.16, ry: 0.22, fill: ink });
|
|
1088
|
+
out.push(shine(s * ex + 0.05, ey - 0.08, 0.065));
|
|
1089
|
+
}
|
|
1090
|
+
break;
|
|
1091
|
+
case "big":
|
|
1092
|
+
for (const s of SIDES) {
|
|
1093
|
+
if (f2.eyeRing) out.push({ k: "circle", x: s * ex, y: ey, r: 0.36, fill: f2.eyeRing, stroke: ink, lw: ow * 0.8 });
|
|
1094
|
+
out.push({ k: "circle", x: s * ex, y: ey, r: 0.25, fill: WHITE, stroke: ink, lw: ow * 0.8 });
|
|
1095
|
+
out.push({ k: "circle", x: s * ex + s * -0.03, y: ey + 0.03, r: 0.14, fill: f2.eyeColor ?? ink });
|
|
1096
|
+
out.push(shine(s * ex + s * -0.03 + 0.05, ey - 0.03, 0.05));
|
|
1097
|
+
}
|
|
1098
|
+
break;
|
|
1099
|
+
case "mask":
|
|
1100
|
+
for (const s of SIDES) {
|
|
1101
|
+
out.push({ k: "ellipse", x: s * 0.4, y: 0, rx: 0.24, ry: 0.32, rot: s * -0.55, fill: p.limbs });
|
|
1102
|
+
out.push({ k: "circle", x: s * 0.4, y: -0.02, r: 0.11, fill: WHITE });
|
|
1103
|
+
out.push({ k: "circle", x: s * 0.39, y: -0, r: 0.065, fill: ink });
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
case "screen":
|
|
1107
|
+
out.push({ k: "rect", x: 0, y: 0, w: 1.5, h: 0.66, r: 0.26, fill: "#24323d", stroke: ink, lw: ow * 0.8 });
|
|
1108
|
+
for (const s of SIDES) {
|
|
1109
|
+
out.push({ k: "ellipse", x: s * 0.36, y: 0, rx: 0.2, ry: 0.24, fill: f2.eyeColor ?? "#7ff0ff", alpha: 0.3 });
|
|
1110
|
+
out.push({ k: "ellipse", x: s * 0.36, y: 0, rx: 0.12, ry: 0.16, fill: f2.eyeColor ?? "#7ff0ff" });
|
|
1111
|
+
}
|
|
1112
|
+
break;
|
|
1113
|
+
case "almond":
|
|
1114
|
+
for (const s of SIDES) {
|
|
1115
|
+
out.push({ k: "ellipse", x: s * 0.42, y: -0.02, rx: 0.3, ry: 0.2, rot: -s * 0.5, fill: f2.eyeColor ?? ink });
|
|
1116
|
+
out.push(shine(s * 0.42 - s * 0.07, -0.08, 0.07));
|
|
1117
|
+
}
|
|
1118
|
+
break;
|
|
1119
|
+
case "calm":
|
|
1120
|
+
for (const s of SIDES) {
|
|
1121
|
+
out.push({ k: "ellipse", x: s * 0.38, y: 0.02, rx: 0.085, ry: 0.12, fill: ink });
|
|
1122
|
+
out.push(shine(s * 0.38 + 0.03, -0.02, 0.03));
|
|
1123
|
+
}
|
|
1124
|
+
break;
|
|
1125
|
+
case "slit":
|
|
1126
|
+
out.push({ k: "line", pts: [0, -0.98, 0, -0.2], stroke: ink, lw: ow * 0.8, alpha: 0.5 });
|
|
1127
|
+
out.push({ k: "rect", x: 0, y: -0.04, w: 1.56, h: 0.26, r: 0.1, fill: DARK_GLASS, stroke: ink, lw: ow * 0.8 });
|
|
1128
|
+
out.push({ k: "rect", x: 0, y: 0.3, w: 0.22, h: 0.46, r: 0.08, fill: DARK_GLASS, stroke: ink, lw: ow * 0.8 });
|
|
1129
|
+
for (const s of SIDES) {
|
|
1130
|
+
out.push({ k: "ellipse", x: s * 0.4, y: -0.04, rx: 0.12, ry: 0.07, fill: f2.eyeColor ?? WHITE });
|
|
1131
|
+
for (const dy of [0.36, 0.56]) out.push({ k: "circle", x: s * 0.42, y: dy, r: 0.05, fill: ink, alpha: 0.6 });
|
|
1132
|
+
}
|
|
1133
|
+
break;
|
|
1134
|
+
case "goggle":
|
|
1135
|
+
for (const s of SIDES) out.push({ k: "circle", x: s * 0.9, y: 0, r: 0.13, fill: p.accent, stroke: ink, lw: ow * 0.8 });
|
|
1136
|
+
out.push({ k: "rect", x: 0, y: 0, w: 1.66, h: 0.62, r: 0.3, fill: DARK_GLASS, stroke: ink, lw: ow * 0.8 });
|
|
1137
|
+
out.push({ k: "path", d: "M-0.6 -0.16 Q-0.42 -0.24 -0.12 -0.24", stroke: WHITE, lw: 0.07, alpha: 0.8 });
|
|
1138
|
+
for (const s of SIDES) out.push({ k: "path", d: `M${s * 0.5} 0.04 Q${s * 0.34} -0.08 ${s * 0.16} 0.02`, stroke: f2.eyeColor ?? "#7fd6ff", lw: 0.08 });
|
|
1139
|
+
for (const x of [-0.18, 0, 0.18]) out.push({ k: "line", pts: [x, 0.56, x, 0.72], stroke: ink, lw: ow * 0.8, alpha: 0.6 });
|
|
1140
|
+
break;
|
|
1141
|
+
case "visor":
|
|
1142
|
+
out.push({ k: "ellipse", x: 0, y: 0.08, rx: 0.8, ry: 0.64, fill: "#2a3f6b", stroke: ink, lw: ow * 0.8 });
|
|
1143
|
+
out.push({ k: "path", d: "M-0.56 -0.14 Q-0.46 -0.4 -0.16 -0.46", stroke: WHITE, lw: 0.08, alpha: 0.7 });
|
|
1144
|
+
for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.28, y: 0.04, rx: 0.1, ry: 0.14, fill: f2.eyeColor ?? "#cfeeff" });
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
}
|
|
1149
|
+
function mouth(def, ow) {
|
|
1150
|
+
const p = def.palette;
|
|
1151
|
+
const f2 = def.face;
|
|
1152
|
+
const out = [];
|
|
1153
|
+
const dark = f2.eyes === "visor" ? "#cfeeff" : MOUTH;
|
|
1154
|
+
const lw = ow * 1.05;
|
|
1155
|
+
if (f2.nose && f2.mouth !== "beak") {
|
|
1156
|
+
if (f2.mouth === "cat") out.push({ k: "path", d: roundedPolygon([{ x: -0.1, y: 0.22 }, { x: 0.1, y: 0.22 }, { x: 0, y: 0.33 }], 0.3), fill: f2.nose, stroke: f2.nose, lw: ow * 0.6 });
|
|
1157
|
+
else if (def.ears === "big-round") out.push({ k: "ellipse", x: 0, y: 0.22, rx: 0.2, ry: 0.25, fill: f2.nose });
|
|
1158
|
+
else out.push({ k: "ellipse", x: 0, y: 0.25, rx: 0.13, ry: 0.085, fill: f2.nose });
|
|
1159
|
+
}
|
|
1160
|
+
switch (f2.mouth) {
|
|
1161
|
+
case "smile":
|
|
1162
|
+
out.push({ k: "path", d: f2.eyes === "visor" ? "M-0.16 0.32 Q0 0.46 0.16 0.32" : "M-0.17 0.42 Q0 0.58 0.17 0.42", stroke: dark, lw });
|
|
1163
|
+
break;
|
|
1164
|
+
case "open":
|
|
1165
|
+
out.push({ k: "path", d: "M-0.21 0.4 Q0 0.46 0.21 0.4 Q0.19 0.68 0 0.68 Q-0.19 0.68 -0.21 0.4 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
|
|
1166
|
+
out.push({ k: "ellipse", x: 0, y: 0.6, rx: 0.1, ry: 0.06, fill: TONGUE });
|
|
1167
|
+
break;
|
|
1168
|
+
case "cat":
|
|
1169
|
+
out.push({ k: "path", d: "M-0.19 0.4 Q-0.1 0.5 0 0.37 Q0.1 0.5 0.19 0.4", stroke: dark, lw });
|
|
1170
|
+
break;
|
|
1171
|
+
case "beak":
|
|
1172
|
+
out.push({ k: "path", d: roundedPolygon([{ x: -0.17, y: 0.2 }, { x: 0.17, y: 0.2 }, { x: 0, y: 0.48 }], 0.25), fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
|
|
1173
|
+
break;
|
|
1174
|
+
case "teeth":
|
|
1175
|
+
out.push({ k: "path", d: "M-0.36 0.34 Q0 0.44 0.36 0.34 Q0.28 0.66 0 0.66 Q-0.28 0.66 -0.36 0.34 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
|
|
1176
|
+
for (const s of SIDES) out.push({ k: "rect", x: s * 0.12, y: 0.43, w: 0.1, h: 0.1, r: 0.03, fill: WHITE });
|
|
1177
|
+
break;
|
|
1178
|
+
case "grill":
|
|
1179
|
+
out.push({ k: "rect", x: 0, y: 0.56, w: 0.62, h: 0.2, r: 0.08, fill: "#24323d", stroke: p.outline, lw: ow * 0.7 });
|
|
1180
|
+
for (const x of [-0.15, 0, 0.15]) out.push({ k: "line", pts: [x, 0.51, x, 0.61], stroke: "#9fb0bd", lw: ow * 0.6 });
|
|
1181
|
+
break;
|
|
1182
|
+
case "tiny":
|
|
1183
|
+
out.push({ k: "ellipse", x: 0, y: 0.44, rx: 0.08, ry: 0.1, fill: dark });
|
|
1184
|
+
break;
|
|
1185
|
+
case "smirk":
|
|
1186
|
+
out.push({ k: "path", d: "M-0.16 0.46 Q0.06 0.54 0.22 0.38", stroke: dark, lw });
|
|
1187
|
+
break;
|
|
1188
|
+
case "bill":
|
|
1189
|
+
out.push({ k: "ellipse", x: 0, y: 0.36, rx: 0.44, ry: 0.2, fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
|
|
1190
|
+
out.push({ k: "path", d: "M-0.36 0.38 Q0 0.46 0.36 0.38", stroke: p.outline, lw: ow * 0.7 });
|
|
1191
|
+
for (const s of SIDES) out.push({ k: "circle", x: s * 0.1, y: 0.28, r: 0.03, fill: p.outline });
|
|
1192
|
+
break;
|
|
1193
|
+
case "hook":
|
|
1194
|
+
out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
|
|
1195
|
+
out.push({ k: "path", d: "M-0.3 0.12 Q0 -0.02 0.3 0.12 Q0.36 0.52 0.04 0.76 Q0.08 0.52 -0.08 0.46 Q-0.28 0.38 -0.3 0.12 Z", fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
|
|
1196
|
+
break;
|
|
1197
|
+
case "jaws": {
|
|
1198
|
+
out.push({ k: "path", d: "M-0.5 0.3 Q0 0.5 0.5 0.3 Q0.38 0.72 0 0.72 Q-0.38 0.72 -0.5 0.3 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
|
|
1199
|
+
for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
|
|
1200
|
+
const y = 0.3 + 0.4 * (x + 0.5) * (0.5 - x) - 0.01;
|
|
1201
|
+
out.push({ k: "path", d: roundedPolygon(pts(x - 0.075, y, x + 0.075, y, x, y + 0.13), 0.15), fill: WHITE });
|
|
1202
|
+
}
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return out;
|
|
1207
|
+
}
|
|
1208
|
+
var polar = (a, r) => ({ x: Math.sin(a) * r, y: -Math.cos(a) * r });
|
|
1209
|
+
|
|
1210
|
+
// src/poses.ts
|
|
1211
|
+
import { LANDMARKS, P as P2 } from "@fabi-play/protocol";
|
|
1212
|
+
|
|
1213
|
+
// src/rig.ts
|
|
1214
|
+
import { P } from "@fabi-play/protocol";
|
|
1215
|
+
var VISIBLE = 0.5;
|
|
1216
|
+
var FACE = { leftEye: 2, rightEye: 5, leftEar: 7, rightEar: 8 };
|
|
1217
|
+
var BAND_BODY = {
|
|
1218
|
+
"3-5": { head: 0.75, neck: 0.74, arm: 0.3, leg: 0.36, hand: 0.19, foot: 1.2, shoulderMin: 0.4, shoulderPad: 0.07, hipMin: 0.5, hipPad: 0.22, bottom: 0.26, bulge: 1.12, armsOverHead: true },
|
|
1219
|
+
"6-8": { head: 0.6, neck: 0.78, arm: 0.22, leg: 0.28, hand: 0.15, foot: 1, shoulderMin: 0.34, shoulderPad: 0.04, hipMin: 0.4, hipPad: 0.16, bottom: 0.14, bulge: 1.04, armsOverHead: false },
|
|
1220
|
+
"9-12": { head: 0.5, neck: 0.92, arm: 0.17, leg: 0.21, hand: 0.125, foot: 0.9, shoulderMin: 0.36, shoulderPad: 0.07, hipMin: 0.32, hipPad: 0.08, bottom: 0.1, bulge: 1, armsOverHead: false }
|
|
1221
|
+
};
|
|
1222
|
+
var DEFAULT_BAND = "6-8";
|
|
1223
|
+
var bodyBand = (def) => def.band in BAND_BODY ? def.band : DEFAULT_BAND;
|
|
1224
|
+
var HEAD = BAND_BODY[DEFAULT_BAND].head;
|
|
1225
|
+
var MAX_TILT = 0.3;
|
|
1226
|
+
var TILT_DEAD_ZONE = 0.12;
|
|
1227
|
+
var TILT_TRUSTED_TO = 0.9;
|
|
1228
|
+
var TILT_FADE = 0.5;
|
|
1229
|
+
var FACE_VISIBLE = 0.65;
|
|
1230
|
+
var NOSE_FOLLOW = 0.35;
|
|
1231
|
+
var NOSE_REACH = 0.2;
|
|
1232
|
+
var FORESHORTENED = 0.25;
|
|
1233
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
1234
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
1235
|
+
var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
|
|
1236
|
+
var len = (a) => Math.hypot(a.x, a.y);
|
|
1237
|
+
var mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
|
|
1238
|
+
var norm = (a) => {
|
|
1239
|
+
const l = len(a);
|
|
1240
|
+
return l > 1e-9 ? { x: a.x / l, y: a.y / l } : { x: 0, y: 1 };
|
|
1241
|
+
};
|
|
1242
|
+
var finite = (...v) => v.every(Number.isFinite);
|
|
1243
|
+
var wrap = (a) => Math.atan2(Math.sin(a), Math.cos(a));
|
|
1244
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
1245
|
+
function joints(body, box, mirrored = false) {
|
|
1246
|
+
return (i) => {
|
|
1247
|
+
const x = body[i * 3];
|
|
1248
|
+
const y = body[i * 3 + 1];
|
|
1249
|
+
const v = body[i * 3 + 2];
|
|
1250
|
+
if (x === void 0 || y === void 0 || v === void 0 || !finite(x, y, v)) return { x: 0, y: 0, ok: false };
|
|
1251
|
+
return { x: box.x + (mirrored ? 1 - x : x) * box.w, y: box.y + y * box.h, ok: v >= VISIBLE };
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function frameOf(body, box, opts = {}) {
|
|
1255
|
+
const at = joints(body, box, opts.mirrored);
|
|
1256
|
+
const ls = at(P.leftShoulder);
|
|
1257
|
+
const rs = at(P.rightShoulder);
|
|
1258
|
+
if (!ls.ok || !rs.ok) return null;
|
|
1259
|
+
const lh = at(P.leftHip);
|
|
1260
|
+
const rh = at(P.rightHip);
|
|
1261
|
+
const S = mid(ls, rs);
|
|
1262
|
+
const sw = len(sub(rs, ls));
|
|
1263
|
+
const hipsOk = lh.ok && rh.ok;
|
|
1264
|
+
let across = norm(sub(rs, ls));
|
|
1265
|
+
if (sw < 1e-6) across = { x: 1, y: 0 };
|
|
1266
|
+
let d;
|
|
1267
|
+
let L;
|
|
1268
|
+
let U;
|
|
1269
|
+
if (hipsOk) {
|
|
1270
|
+
const H = mid(lh, rh);
|
|
1271
|
+
L = len(sub(H, S));
|
|
1272
|
+
if (L >= sw * FORESHORTENED) d = norm(sub(H, S));
|
|
1273
|
+
else {
|
|
1274
|
+
d = { x: -across.y, y: across.x };
|
|
1275
|
+
if (d.y < 0) d = mul(d, -1);
|
|
1276
|
+
}
|
|
1277
|
+
U = Math.max(L, sw * 1.15);
|
|
1278
|
+
} else {
|
|
1279
|
+
d = { x: -across.y, y: across.x };
|
|
1280
|
+
if (d.y < 0) d = mul(d, -1);
|
|
1281
|
+
U = sw * 1.15;
|
|
1282
|
+
L = U;
|
|
1283
|
+
}
|
|
1284
|
+
U = Math.max(U, 1e-6);
|
|
1285
|
+
const r = { x: d.y, y: -d.x };
|
|
1286
|
+
const angle = Math.atan2(r.y, r.x);
|
|
1287
|
+
const up = mul(d, -1);
|
|
1288
|
+
const bb = BAND_BODY[opts.band ?? DEFAULT_BAND] ?? BAND_BODY[DEFAULT_BAND];
|
|
1289
|
+
const headR = bb.head * U;
|
|
1290
|
+
let head = add(S, mul(up, headR * bb.neck));
|
|
1291
|
+
const nose = at(P.nose);
|
|
1292
|
+
if (nose.ok) {
|
|
1293
|
+
const want = add(nose, mul(up, headR * 0.15));
|
|
1294
|
+
const delta = mul(sub(want, head), NOSE_FOLLOW);
|
|
1295
|
+
const k = Math.min(1, headR * NOSE_REACH / Math.max(len(delta), 1e-6));
|
|
1296
|
+
head = add(head, mul(delta, k));
|
|
1297
|
+
}
|
|
1298
|
+
return { S, r, d, U, L, hipsOk, head, headR, headRot: angle + headTilt(body, at, angle, U), angle };
|
|
1299
|
+
}
|
|
1300
|
+
function headTilt(body, at, angle, U) {
|
|
1301
|
+
const pairs = [
|
|
1302
|
+
[FACE.leftEar, FACE.rightEar],
|
|
1303
|
+
[FACE.leftEye, FACE.rightEye]
|
|
1304
|
+
];
|
|
1305
|
+
for (const [a, b] of pairs) {
|
|
1306
|
+
const p = at(a);
|
|
1307
|
+
const q = at(b);
|
|
1308
|
+
if (!p.ok || !q.ok || !(body[a * 3 + 2] >= FACE_VISIBLE && body[b * 3 + 2] >= FACE_VISIBLE) || len(sub(q, p)) < U * 0.05) continue;
|
|
1309
|
+
const t = wrap(Math.atan2(q.y - p.y, q.x - p.x) - angle);
|
|
1310
|
+
const past = Math.max(Math.abs(t) - TILT_DEAD_ZONE, 0);
|
|
1311
|
+
const trust = clamp((TILT_TRUSTED_TO + TILT_FADE - Math.abs(t)) / TILT_FADE, 0, 1);
|
|
1312
|
+
return Math.sign(t) * Math.min(past, MAX_TILT) * trust;
|
|
1313
|
+
}
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
function avatarShapes(body, def, box, opts = {}) {
|
|
1317
|
+
const band = bodyBand(def);
|
|
1318
|
+
const bb = BAND_BODY[band];
|
|
1319
|
+
const F = frameOf(body, box, { ...opts, band });
|
|
1320
|
+
if (!F) return [];
|
|
1321
|
+
const at = joints(body, box, opts.mirrored);
|
|
1322
|
+
const p = def.palette;
|
|
1323
|
+
const { S, r, d, U, L } = F;
|
|
1324
|
+
const has = (x) => def.bodyExtras.includes(x);
|
|
1325
|
+
const ow = Math.max(1.2, U * 0.045);
|
|
1326
|
+
const ol = { stroke: p.outline, lw: ow };
|
|
1327
|
+
const T = (x, y) => ({ x: S.x + r.x * x + d.x * y, y: S.y + r.y * x + d.y * y });
|
|
1328
|
+
const TP = (pts2) => pts2.map(([x, y]) => T(x, y));
|
|
1329
|
+
const ls = at(P.leftShoulder);
|
|
1330
|
+
const rs = at(P.rightShoulder);
|
|
1331
|
+
const lh = at(P.leftHip);
|
|
1332
|
+
const rh = at(P.rightHip);
|
|
1333
|
+
const sw = len(sub(rs, ls));
|
|
1334
|
+
const hw = F.hipsOk ? len(sub(rh, lh)) : sw * 0.7;
|
|
1335
|
+
const ht = Math.max(sw / 2 + bb.shoulderPad * U, bb.shoulderMin * U);
|
|
1336
|
+
const hb = Math.max(hw / 2 + bb.hipPad * U, bb.hipMin * U);
|
|
1337
|
+
const y0 = -0.1 * U;
|
|
1338
|
+
const y1 = L + bb.bottom * U;
|
|
1339
|
+
const back = [];
|
|
1340
|
+
const legs = [];
|
|
1341
|
+
const torso = [];
|
|
1342
|
+
const arms = [];
|
|
1343
|
+
const front = [];
|
|
1344
|
+
const limb = (out, pts2, w, color) => {
|
|
1345
|
+
const flat = pts2.flatMap((q) => [q.x, q.y]);
|
|
1346
|
+
out.push({ k: "line", pts: flat, stroke: p.outline, lw: w + ow * 2 });
|
|
1347
|
+
out.push({ k: "line", pts: flat, stroke: color, lw: w });
|
|
1348
|
+
};
|
|
1349
|
+
const legW = bb.leg * U;
|
|
1350
|
+
const armW = bb.arm * U;
|
|
1351
|
+
const pad = Math.max(bb.arm / 0.22, 0.9);
|
|
1352
|
+
if (F.hipsOk) {
|
|
1353
|
+
for (const [hip, kneeI, ankleI, side] of [
|
|
1354
|
+
[lh, P.leftKnee, P.leftAnkle, -1],
|
|
1355
|
+
[rh, P.rightKnee, P.rightAnkle, 1]
|
|
1356
|
+
]) {
|
|
1357
|
+
const knee = at(kneeI);
|
|
1358
|
+
const ankle = at(ankleI);
|
|
1359
|
+
if (!knee.ok) continue;
|
|
1360
|
+
const top = add(hip, mul(d, -0.12 * U));
|
|
1361
|
+
const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
|
|
1362
|
+
limb(legs, pts2, legW, p.limbs);
|
|
1363
|
+
if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
|
|
1364
|
+
if (ankle.ok) legs.push(foot(def, ankle, F, side, ow, bb.foot));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
for (const [sh, elbowI, wristI, side] of [
|
|
1368
|
+
[ls, P.leftElbow, P.leftWrist, -1],
|
|
1369
|
+
[rs, P.rightElbow, P.rightWrist, 1]
|
|
1370
|
+
]) {
|
|
1371
|
+
const elbow = at(elbowI);
|
|
1372
|
+
const wrist = at(wristI);
|
|
1373
|
+
if (has("pauldrons")) {
|
|
1374
|
+
const c = add(sh, mul(d, 0.02 * U));
|
|
1375
|
+
arms.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.25 * U, ry: 0.17 * U, rot: F.angle + side * 0.25, fill: p.body, ...ol });
|
|
1376
|
+
const q = add(c, mul(d, 0.05 * U));
|
|
1377
|
+
arms.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.17 * U, ry: 0.07 * U, rot: F.angle + side * 0.25, fill: p.belly });
|
|
1378
|
+
}
|
|
1379
|
+
if (!elbow.ok) continue;
|
|
1380
|
+
const raised = bb.armsOverHead && (elbow.x - S.x) * d.x + (elbow.y - S.y) * d.y < -0.25 * U;
|
|
1381
|
+
const out = raised ? front : arms;
|
|
1382
|
+
const top = add(sh, mul(d, 0.04 * U));
|
|
1383
|
+
limb(out, wrist.ok ? [top, elbow, wrist] : [top, elbow], armW, p.limbs);
|
|
1384
|
+
if (has("joints")) out.push({ k: "circle", x: elbow.x, y: elbow.y, r: 0.13 * U * pad, fill: p.accent, ...ol });
|
|
1385
|
+
if (!wrist.ok) continue;
|
|
1386
|
+
const dir = norm(sub(wrist, elbow));
|
|
1387
|
+
const hand = add(wrist, mul(dir, 0.05 * U));
|
|
1388
|
+
if (has("feathers")) {
|
|
1389
|
+
for (const a of [-0.55, 0, 0.55]) {
|
|
1390
|
+
const c = Math.cos(a);
|
|
1391
|
+
const s = Math.sin(a) * side;
|
|
1392
|
+
const fd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
|
|
1393
|
+
const q = add(hand, mul(fd, 0.16 * U));
|
|
1394
|
+
out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.2 * U, ry: 0.09 * U, rot: Math.atan2(fd.y, fd.x), fill: p.hands ?? p.accent, ...ol });
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
out.push({ k: "circle", x: hand.x, y: hand.y, r: bb.hand * U, fill: p.hands ?? p.limbs, ...ol });
|
|
1398
|
+
}
|
|
1399
|
+
if (has("wings")) {
|
|
1400
|
+
for (const s of [-1, 1]) {
|
|
1401
|
+
const w = TP([
|
|
1402
|
+
[s * ht * 0.5, 0.05 * U],
|
|
1403
|
+
[s * (ht + 0.85 * U), -0.6 * U],
|
|
1404
|
+
[s * (ht + 0.72 * U), 0.05 * U],
|
|
1405
|
+
[s * (ht + 0.45 * U), -0.02 * U],
|
|
1406
|
+
[s * (ht + 0.3 * U), 0.32 * U],
|
|
1407
|
+
[s * ht * 0.6, 0.55 * U]
|
|
1408
|
+
]);
|
|
1409
|
+
back.push({ k: "path", d: roundedPolygon(w, 0.18), fill: p.accent, ...ol });
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (has("cape")) {
|
|
1413
|
+
const yb = L + 0.8 * U;
|
|
1414
|
+
const wb = hb + 0.36 * U;
|
|
1415
|
+
const cape = TP([
|
|
1416
|
+
[-ht * 0.92, y0 + 0.04 * U],
|
|
1417
|
+
[ht * 0.92, y0 + 0.04 * U],
|
|
1418
|
+
[ht + 0.16 * U, L * 0.5],
|
|
1419
|
+
[wb, yb],
|
|
1420
|
+
[wb * 0.5, yb - 0.1 * U],
|
|
1421
|
+
[0, yb],
|
|
1422
|
+
[-wb * 0.5, yb - 0.1 * U],
|
|
1423
|
+
[-wb, yb],
|
|
1424
|
+
[-ht - 0.16 * U, L * 0.5]
|
|
1425
|
+
]);
|
|
1426
|
+
back.push({ k: "path", d: smoothClosed(cape, 0.6), fill: p.accent, ...ol });
|
|
1427
|
+
}
|
|
1428
|
+
if (has("backpack")) {
|
|
1429
|
+
const c = T(0, 0.35 * U);
|
|
1430
|
+
back.push({ k: "rect", x: c.x, y: c.y, w: ht * 2 + 0.34 * U, h: 0.8 * U, r: 0.16 * U, rot: F.angle, fill: def.extraColors?.[0] ?? p.limbs, ...ol });
|
|
1431
|
+
}
|
|
1432
|
+
if (has("tank")) {
|
|
1433
|
+
const c = T(ht * 0.95, 0.24 * U);
|
|
1434
|
+
const col = def.extraColors?.[0] ?? p.accent;
|
|
1435
|
+
const valve = T(ht * 0.95, -0.3 * U);
|
|
1436
|
+
back.push({ k: "rect", x: valve.x, y: valve.y, w: 0.14 * U, h: 0.16 * U, r: 0.04 * U, rot: F.angle, fill: p.outline });
|
|
1437
|
+
back.push({ k: "rect", x: c.x, y: c.y, w: 0.34 * U, h: 1 * U, r: 0.17 * U, rot: F.angle, fill: col, ...ol });
|
|
1438
|
+
}
|
|
1439
|
+
if (F.hipsOk && !opts.portrait) back.push(...tail(def, F, hb, ow));
|
|
1440
|
+
if (has("tentacles") && F.hipsOk) {
|
|
1441
|
+
for (const s of [-1, 1]) {
|
|
1442
|
+
const pts2 = TP([
|
|
1443
|
+
[s * hb * 0.7, L * 0.9],
|
|
1444
|
+
[s * (hb + 0.22 * U), L + 0.3 * U],
|
|
1445
|
+
[s * (hb + 0.48 * U), L + 0.22 * U],
|
|
1446
|
+
[s * (hb + 0.5 * U), L - 0.02 * U],
|
|
1447
|
+
[s * (hb + 0.34 * U), L - 0.06 * U]
|
|
1448
|
+
]);
|
|
1449
|
+
back.push({ k: "path", d: smoothOpen(pts2), stroke: p.outline, lw: 0.2 * U + ow * 2 });
|
|
1450
|
+
back.push({ k: "path", d: smoothOpen(pts2), stroke: p.limbs, lw: 0.2 * U });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
const bulge = Math.max(ht, hb) * bb.bulge;
|
|
1454
|
+
if (has("wool")) {
|
|
1455
|
+
for (const [x, y] of [
|
|
1456
|
+
[ht * 0.7, y0 + 0.02 * U],
|
|
1457
|
+
[bulge, L * 0.3],
|
|
1458
|
+
[bulge * 1.02, L * 0.62],
|
|
1459
|
+
[(bulge + hb) / 2, L * 0.92],
|
|
1460
|
+
[hb * 0.55, y1]
|
|
1461
|
+
])
|
|
1462
|
+
for (const s of [-1, 1]) {
|
|
1463
|
+
const q2 = T(s * x, y);
|
|
1464
|
+
torso.push({ k: "circle", x: q2.x, y: q2.y, r: 0.2 * U, fill: p.body, ...ol });
|
|
1465
|
+
}
|
|
1466
|
+
const q = T(0, y1 + 0.02 * U);
|
|
1467
|
+
torso.push({ k: "circle", x: q.x, y: q.y, r: 0.2 * U, fill: p.body, ...ol });
|
|
1468
|
+
}
|
|
1469
|
+
if (has("robe")) {
|
|
1470
|
+
const yb = L + 0.58 * U;
|
|
1471
|
+
const wb = hb * 1.4;
|
|
1472
|
+
torso.push({
|
|
1473
|
+
k: "path",
|
|
1474
|
+
d: smoothClosed(
|
|
1475
|
+
TP([
|
|
1476
|
+
[-ht, y0],
|
|
1477
|
+
[0, y0 - 0.04 * U],
|
|
1478
|
+
[ht, y0],
|
|
1479
|
+
[bulge, L * 0.55],
|
|
1480
|
+
[wb, yb],
|
|
1481
|
+
[0, yb + 0.06 * U],
|
|
1482
|
+
[-wb, yb],
|
|
1483
|
+
[-bulge, L * 0.55]
|
|
1484
|
+
]),
|
|
1485
|
+
0.7
|
|
1486
|
+
),
|
|
1487
|
+
fill: p.body,
|
|
1488
|
+
...ol
|
|
1489
|
+
});
|
|
1490
|
+
const hem = TP([
|
|
1491
|
+
[-wb * 0.94, yb - 0.1 * U],
|
|
1492
|
+
[0, yb - 0.04 * U],
|
|
1493
|
+
[wb * 0.94, yb - 0.1 * U]
|
|
1494
|
+
]);
|
|
1495
|
+
torso.push({ k: "path", d: smoothOpen(hem), stroke: def.extraColors?.[0] ?? p.accent, lw: 0.07 * U });
|
|
1496
|
+
} else if (has("sheet")) {
|
|
1497
|
+
const yb = L + 0.78 * U;
|
|
1498
|
+
const wb = hb * 1.3;
|
|
1499
|
+
torso.push({
|
|
1500
|
+
k: "path",
|
|
1501
|
+
d: smoothClosed(
|
|
1502
|
+
TP([
|
|
1503
|
+
[-ht, y0],
|
|
1504
|
+
[ht, y0],
|
|
1505
|
+
[bulge, L * 0.55],
|
|
1506
|
+
[wb, yb],
|
|
1507
|
+
[wb * 0.5, yb - 0.2 * U],
|
|
1508
|
+
[0, yb],
|
|
1509
|
+
[-wb * 0.5, yb - 0.2 * U],
|
|
1510
|
+
[-wb, yb],
|
|
1511
|
+
[-bulge, L * 0.55]
|
|
1512
|
+
]),
|
|
1513
|
+
0.9
|
|
1514
|
+
),
|
|
1515
|
+
fill: p.body,
|
|
1516
|
+
...ol
|
|
1517
|
+
});
|
|
1518
|
+
} else {
|
|
1519
|
+
torso.push({
|
|
1520
|
+
k: "path",
|
|
1521
|
+
d: smoothClosed(
|
|
1522
|
+
TP([
|
|
1523
|
+
[-ht, y0],
|
|
1524
|
+
[0, y0 - 0.04 * U],
|
|
1525
|
+
[ht, y0],
|
|
1526
|
+
[bulge, L * 0.55],
|
|
1527
|
+
[hb, y1],
|
|
1528
|
+
[0, y1 + 0.03 * U],
|
|
1529
|
+
[-hb, y1],
|
|
1530
|
+
[-bulge, L * 0.55]
|
|
1531
|
+
]),
|
|
1532
|
+
0.85
|
|
1533
|
+
),
|
|
1534
|
+
fill: p.body,
|
|
1535
|
+
...ol
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
const minW = Math.min(ht, hb);
|
|
1539
|
+
if (has("pocket")) {
|
|
1540
|
+
const c = T(0, L * 0.74);
|
|
1541
|
+
torso.push({ k: "rect", x: c.x, y: c.y, w: minW * 1.3, h: 0.3 * U, r: 0.1 * U, rot: F.angle, fill: p.belly, stroke: p.outline, lw: ow * 0.8 });
|
|
1542
|
+
} else if (has("racing-stripe")) {
|
|
1543
|
+
for (const x of [-0.1, 0.1]) {
|
|
1544
|
+
const a = T(x * U, y0 + 0.02 * U);
|
|
1545
|
+
const b = T(x * U, y1 - 0.04 * U);
|
|
1546
|
+
torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: p.belly, lw: 0.1 * U });
|
|
1547
|
+
}
|
|
1548
|
+
} else if (p.belly && p.belly !== p.body && !has("panel") && !has("sheet") && !has("stripes")) {
|
|
1549
|
+
const c = T(0, L * 0.58);
|
|
1550
|
+
torso.push({ k: "ellipse", x: c.x, y: c.y, rx: minW * 0.64, ry: Math.max(L * 0.36, 0.2 * U), rot: F.angle, fill: p.belly });
|
|
1551
|
+
}
|
|
1552
|
+
if (has("spots")) {
|
|
1553
|
+
const c = def.extraColors?.[1] ?? p.belly;
|
|
1554
|
+
for (const [x, y, rr] of [
|
|
1555
|
+
[-0.55, 0.35, 0.09],
|
|
1556
|
+
[0.5, 0.62, 0.11],
|
|
1557
|
+
[-0.35, 0.86, 0.07]
|
|
1558
|
+
]) {
|
|
1559
|
+
const q = T(x * minW, y * L);
|
|
1560
|
+
torso.push({ k: "circle", x: q.x, y: q.y, r: rr * U, fill: c });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (has("stripes")) {
|
|
1564
|
+
const c = def.extraColors?.[1] ?? p.accent;
|
|
1565
|
+
for (const y of [0.22, 0.44, 0.66]) {
|
|
1566
|
+
const a = T(-minW * 0.98, L * y);
|
|
1567
|
+
const b = T(minW * 0.98, L * y);
|
|
1568
|
+
torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.1 * U });
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (has("straps")) {
|
|
1572
|
+
const c = def.extraColors?.[1] ?? p.outline;
|
|
1573
|
+
for (const s of [-1, 1]) {
|
|
1574
|
+
const a = T(s * ht * 0.62, y0 + 0.02 * U);
|
|
1575
|
+
const b = T(s * minW * 0.62, L * 0.72);
|
|
1576
|
+
torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: p.outline, lw: 0.13 * U + ow * 2 });
|
|
1577
|
+
torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.13 * U });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (has("belt")) {
|
|
1581
|
+
const c = T(0, L * 0.9);
|
|
1582
|
+
torso.push({ k: "rect", x: c.x, y: c.y, w: hb * 1.92, h: 0.17 * U, r: 0.06 * U, rot: F.angle, fill: def.extraColors?.[2] ?? "#4a3426", ...ol });
|
|
1583
|
+
torso.push({ k: "rect", x: c.x, y: c.y, w: 0.2 * U, h: 0.19 * U, r: 0.04 * U, rot: F.angle, fill: "#ffd84a", ...ol });
|
|
1584
|
+
}
|
|
1585
|
+
if (has("emblem")) {
|
|
1586
|
+
const c = T(0, L * 0.4);
|
|
1587
|
+
const fill = def.extraColors?.[0] ?? p.accent;
|
|
1588
|
+
const shield = roundedPolygon(
|
|
1589
|
+
[
|
|
1590
|
+
{ x: -0.22, y: -0.2 },
|
|
1591
|
+
{ x: 0.22, y: -0.2 },
|
|
1592
|
+
{ x: 0.22, y: 0.04 },
|
|
1593
|
+
{ x: 0, y: 0.26 },
|
|
1594
|
+
{ x: -0.22, y: 0.04 }
|
|
1595
|
+
],
|
|
1596
|
+
0.25
|
|
1597
|
+
);
|
|
1598
|
+
torso.push({
|
|
1599
|
+
k: "group",
|
|
1600
|
+
x: c.x,
|
|
1601
|
+
y: c.y,
|
|
1602
|
+
rot: F.angle,
|
|
1603
|
+
s: U,
|
|
1604
|
+
items: [
|
|
1605
|
+
{ k: "path", d: shield, fill, stroke: p.outline, lw: ow / U },
|
|
1606
|
+
{ k: "group", x: 0, y: 0.01, s: 0.12, items: [{ k: "path", d: STAR, fill: fill === p.accent ? "#ffffff" : p.accent }] }
|
|
1607
|
+
]
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
if (has("panel")) {
|
|
1611
|
+
const c = T(0, L * 0.5);
|
|
1612
|
+
torso.push({ k: "rect", x: c.x, y: c.y, w: minW * 1.2, h: 0.38 * U, r: 0.1 * U, rot: F.angle, fill: "#24323d", ...ol });
|
|
1613
|
+
const lights = def.extraColors ?? ["#ff7a6b", "#fbd46a", "#7ff0ff"];
|
|
1614
|
+
lights.slice(0, 3).forEach((col, i) => {
|
|
1615
|
+
const q = T((i - 1) * minW * 0.34, L * 0.5);
|
|
1616
|
+
torso.push({ k: "circle", x: q.x, y: q.y, r: 0.075 * U, fill: col });
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
if (has("badge")) {
|
|
1620
|
+
const c = T(-minW * 0.42, L * 0.36);
|
|
1621
|
+
torso.push({ k: "circle", x: c.x, y: c.y, r: 0.15 * U, fill: p.accent, ...ol });
|
|
1622
|
+
torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle, s: 0.09 * U, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
|
|
1623
|
+
}
|
|
1624
|
+
if (has("star")) {
|
|
1625
|
+
const c = T(-minW * 0.5, L * 0.3);
|
|
1626
|
+
const k = 0.15 * U;
|
|
1627
|
+
torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle - 0.2, s: k, items: [{ k: "path", d: STAR, fill: def.extraColors?.[0] ?? "#ffd84a", stroke: p.outline, lw: ow * 0.8 / k }] });
|
|
1628
|
+
}
|
|
1629
|
+
if (has("scarf")) {
|
|
1630
|
+
const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
|
|
1631
|
+
const c = T(0, y);
|
|
1632
|
+
const col = def.extraColors?.[0] ?? p.accent;
|
|
1633
|
+
const tailEnd = T(ht * 0.45, y + 0.28 * U);
|
|
1634
|
+
torso.push({ k: "rect", x: tailEnd.x, y: tailEnd.y, w: 0.16 * U, h: 0.42 * U, r: 0.06 * U, rot: F.angle + 0.15, fill: col, ...ol });
|
|
1635
|
+
torso.push({ k: "rect", x: c.x, y: c.y, w: ht * 2.05, h: 0.2 * U, r: 0.1 * U, rot: F.angle, fill: col, ...ol });
|
|
1636
|
+
}
|
|
1637
|
+
const head = { k: "group", x: F.head.x, y: F.head.y, rot: F.headRot, s: F.headR, items: cachedHead(def, ow / F.headR) };
|
|
1638
|
+
return [...back, ...legs, ...torso, ...arms, head, ...front];
|
|
1639
|
+
}
|
|
1640
|
+
var headCache = /* @__PURE__ */ new WeakMap();
|
|
1641
|
+
var HEAD_CACHE_SIZE = 64;
|
|
1642
|
+
function cachedHead(def, ow) {
|
|
1643
|
+
const key = Number(ow.toPrecision(4));
|
|
1644
|
+
let byOutline = headCache.get(def);
|
|
1645
|
+
if (!byOutline) headCache.set(def, byOutline = /* @__PURE__ */ new Map());
|
|
1646
|
+
let shapes = byOutline.get(key);
|
|
1647
|
+
if (!shapes) {
|
|
1648
|
+
if (byOutline.size >= HEAD_CACHE_SIZE) byOutline.clear();
|
|
1649
|
+
byOutline.set(key, shapes = headShapes(def, key));
|
|
1650
|
+
}
|
|
1651
|
+
return shapes;
|
|
1652
|
+
}
|
|
1653
|
+
function foot(def, ankle, F, side, ow, k) {
|
|
1654
|
+
const p = def.palette;
|
|
1655
|
+
const { d, r } = F;
|
|
1656
|
+
const U = F.U * k;
|
|
1657
|
+
const c = add(add(ankle, mul(d, 0.07 * U)), mul(r, side * 0.06 * U));
|
|
1658
|
+
const ol = { stroke: p.outline, lw: ow };
|
|
1659
|
+
const fill = p.feet ?? p.limbs;
|
|
1660
|
+
if (def.bodyExtras.includes("fins")) {
|
|
1661
|
+
const fin = roundedPolygon(
|
|
1662
|
+
[
|
|
1663
|
+
{ x: -0.13, y: -0.06 },
|
|
1664
|
+
{ x: 0.13, y: -0.06 },
|
|
1665
|
+
{ x: 0.24, y: 0.46 },
|
|
1666
|
+
{ x: -0.24, y: 0.46 }
|
|
1667
|
+
],
|
|
1668
|
+
0.22
|
|
1669
|
+
);
|
|
1670
|
+
return {
|
|
1671
|
+
k: "group",
|
|
1672
|
+
x: c.x,
|
|
1673
|
+
y: c.y,
|
|
1674
|
+
rot: F.angle,
|
|
1675
|
+
s: U,
|
|
1676
|
+
items: [
|
|
1677
|
+
{ k: "path", d: fin, fill, stroke: p.outline, lw: ow / U },
|
|
1678
|
+
{ k: "line", pts: [0, 0.06, 0, 0.38], stroke: p.outline, lw: ow / U, alpha: 0.5 }
|
|
1679
|
+
]
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
if (def.bodyExtras.includes("flippers")) return { k: "ellipse", x: c.x, y: c.y, rx: 0.27 * U, ry: 0.11 * U, rot: F.angle, fill, ...ol };
|
|
1683
|
+
if (def.bodyExtras.includes("boots")) return { k: "rect", x: c.x, y: c.y, w: 0.4 * U, h: 0.24 * U, r: 0.09 * U, rot: F.angle, fill, ...ol };
|
|
1684
|
+
return { k: "ellipse", x: c.x, y: c.y, rx: 0.2 * U, ry: 0.13 * U, rot: F.angle, fill, ...ol };
|
|
1685
|
+
}
|
|
1686
|
+
function tail(def, F, hb, ow) {
|
|
1687
|
+
const p = def.palette;
|
|
1688
|
+
const { U, L, S, r, d } = F;
|
|
1689
|
+
const T = (x, y) => ({ x: S.x + r.x * x + d.x * y, y: S.y + r.y * x + d.y * y });
|
|
1690
|
+
const TP = (pts2) => pts2.map(([x, y]) => T(x, y));
|
|
1691
|
+
const ol = { stroke: p.outline, lw: ow };
|
|
1692
|
+
const stroke = (pts2, w, color) => [
|
|
1693
|
+
{ k: "path", d: smoothOpen(pts2), stroke: p.outline, lw: w + ow * 2 },
|
|
1694
|
+
{ k: "path", d: smoothOpen(pts2), stroke: color, lw: w }
|
|
1695
|
+
];
|
|
1696
|
+
const out = [];
|
|
1697
|
+
switch (def.tail) {
|
|
1698
|
+
case "none":
|
|
1699
|
+
break;
|
|
1700
|
+
case "fluffy": {
|
|
1701
|
+
const pts2 = TP([
|
|
1702
|
+
[hb * 0.5, L * 0.95],
|
|
1703
|
+
[hb + 0.28 * U, L * 1],
|
|
1704
|
+
[hb + 0.72 * U, L * 0.62],
|
|
1705
|
+
[hb + 0.78 * U, L * 0.12],
|
|
1706
|
+
[hb + 0.52 * U, L * 0.32],
|
|
1707
|
+
[hb + 0.3 * U, L * 0.6],
|
|
1708
|
+
[hb * 0.6, L * 0.7]
|
|
1709
|
+
]);
|
|
1710
|
+
out.push({ k: "path", d: smoothClosed(pts2), fill: p.body, ...ol });
|
|
1711
|
+
const tip = T(hb + 0.72 * U, L * 0.2);
|
|
1712
|
+
out.push({ k: "ellipse", x: tip.x, y: tip.y, rx: 0.13 * U, ry: 0.2 * U, rot: F.angle + 0.25, fill: p.belly, ...ol });
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
case "brush": {
|
|
1716
|
+
const pts2 = TP([
|
|
1717
|
+
[hb * 0.4, L * 0.98],
|
|
1718
|
+
[hb + 0.42 * U, L * 1.06],
|
|
1719
|
+
[hb + 0.98 * U, L * 0.66],
|
|
1720
|
+
[hb + 1.06 * U, L * 0.12],
|
|
1721
|
+
[hb + 0.86 * U, -0.04 * U],
|
|
1722
|
+
[hb + 0.66 * U, L * 0.36],
|
|
1723
|
+
[hb + 0.3 * U, L * 0.66],
|
|
1724
|
+
[hb * 0.5, L * 0.68]
|
|
1725
|
+
]);
|
|
1726
|
+
out.push({ k: "path", d: smoothClosed(pts2), fill: p.body, ...ol });
|
|
1727
|
+
const tip = TP([
|
|
1728
|
+
[hb + 1.03 * U, L * 0.36],
|
|
1729
|
+
[hb + 1.06 * U, L * 0.12],
|
|
1730
|
+
[hb + 0.86 * U, -0.04 * U],
|
|
1731
|
+
[hb + 0.72 * U, L * 0.24],
|
|
1732
|
+
[hb + 0.86 * U, L * 0.34]
|
|
1733
|
+
]);
|
|
1734
|
+
out.push({ k: "path", d: smoothClosed(tip), fill: p.belly, ...ol });
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
case "pom": {
|
|
1738
|
+
const c = T(hb + 0.06 * U, L * 0.88);
|
|
1739
|
+
out.push({ k: "circle", x: c.x, y: c.y, r: 0.2 * U, fill: p.belly, ...ol });
|
|
1740
|
+
break;
|
|
1741
|
+
}
|
|
1742
|
+
case "long":
|
|
1743
|
+
case "striped":
|
|
1744
|
+
case "tuft": {
|
|
1745
|
+
const pts2 = TP([
|
|
1746
|
+
[hb * 0.6, L * 0.9],
|
|
1747
|
+
[hb + 0.35 * U, L * 0.85],
|
|
1748
|
+
[hb + 0.58 * U, L * 0.4],
|
|
1749
|
+
[hb + 0.5 * U, -0.02 * U]
|
|
1750
|
+
]);
|
|
1751
|
+
out.push(...stroke(pts2, 0.14 * U, p.body));
|
|
1752
|
+
if (def.tail === "tuft") {
|
|
1753
|
+
const end = pts2[pts2.length - 1];
|
|
1754
|
+
out.push({ k: "circle", x: end.x, y: end.y, r: 0.16 * U, fill: def.extraColors?.[0] ?? p.accent, ...ol });
|
|
1755
|
+
}
|
|
1756
|
+
if (def.tail === "striped") {
|
|
1757
|
+
for (const t of [0.35, 0.6, 0.85]) {
|
|
1758
|
+
const q = T(hb + 0.35 * U + (0.58 - 0.35) * U * (t * 1.4 > 1 ? 1 : t * 1.4), L * (0.85 - (0.85 - 0) * t));
|
|
1759
|
+
out.push({ k: "line", pts: [q.x - r.x * 0.08 * U, q.y - r.y * 0.08 * U, q.x + r.x * 0.08 * U, q.y + r.y * 0.08 * U], stroke: def.extraColors?.[0] ?? p.outline, lw: 0.06 * U });
|
|
1760
|
+
}
|
|
1761
|
+
const end = pts2[pts2.length - 1];
|
|
1762
|
+
out.push({ k: "circle", x: end.x, y: end.y, r: 0.07 * U, fill: def.extraColors?.[0] ?? p.outline });
|
|
1763
|
+
}
|
|
1764
|
+
break;
|
|
1765
|
+
}
|
|
1766
|
+
case "curl": {
|
|
1767
|
+
const pts2 = TP([
|
|
1768
|
+
[hb * 0.6, L * 0.9],
|
|
1769
|
+
[hb + 0.4 * U, L * 0.88],
|
|
1770
|
+
[hb + 0.62 * U, L * 0.45],
|
|
1771
|
+
[hb + 0.5 * U, L * 0.12],
|
|
1772
|
+
[hb + 0.3 * U, L * 0.24],
|
|
1773
|
+
[hb + 0.36 * U, L * 0.42]
|
|
1774
|
+
]);
|
|
1775
|
+
out.push(...stroke(pts2, 0.12 * U, p.limbs));
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
case "fish": {
|
|
1779
|
+
const line = TP([
|
|
1780
|
+
[hb * 0.6, L * 0.9],
|
|
1781
|
+
[hb + 0.36 * U, L * 0.86],
|
|
1782
|
+
[hb + 0.6 * U, L * 0.5]
|
|
1783
|
+
]);
|
|
1784
|
+
out.push(...stroke(line, 0.17 * U, p.body));
|
|
1785
|
+
const tip = line[line.length - 1];
|
|
1786
|
+
const fin = roundedPolygon(
|
|
1787
|
+
[
|
|
1788
|
+
{ x: 0, y: 0.06 },
|
|
1789
|
+
{ x: -0.26, y: -0.34 },
|
|
1790
|
+
{ x: 0.02, y: -0.12 },
|
|
1791
|
+
{ x: 0.32, y: -0.3 }
|
|
1792
|
+
],
|
|
1793
|
+
0.2
|
|
1794
|
+
);
|
|
1795
|
+
out.push({ k: "group", x: tip.x, y: tip.y, rot: F.angle + 0.35, s: U, items: [{ k: "path", d: fin, fill: p.body, stroke: p.outline, lw: ow / U }] });
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
case "plume": {
|
|
1799
|
+
const cols = def.extraColors ?? [p.accent];
|
|
1800
|
+
[0, 1, 2].forEach((i) => {
|
|
1801
|
+
const c = T(hb + (0.12 + i * 0.13) * U, L + (0.32 - i * 0.06) * U);
|
|
1802
|
+
out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.11 * U, ry: 0.44 * U, rot: F.angle - 0.55 + i * 0.28, fill: cols[(i + 1) % cols.length], ...ol });
|
|
1803
|
+
});
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
case "spiky":
|
|
1807
|
+
case "arrow": {
|
|
1808
|
+
const pts2 = TP([
|
|
1809
|
+
[hb * 0.3, L * 0.55],
|
|
1810
|
+
[hb + 0.35 * U, L * 0.68],
|
|
1811
|
+
[hb + 0.95 * U, L * 0.86],
|
|
1812
|
+
[hb + 0.35 * U, L * 1.08],
|
|
1813
|
+
[hb * 0.3, L * 1.08]
|
|
1814
|
+
]);
|
|
1815
|
+
if (def.tail === "spiky") {
|
|
1816
|
+
for (const t of [0.15, 0.45, 0.72]) {
|
|
1817
|
+
const x = hb + (0.2 + t * 0.7) * U;
|
|
1818
|
+
const y = L * (0.66 + t * 0.19);
|
|
1819
|
+
out.push({ k: "path", d: roundedPolygon(TP([[x - 0.12 * U, y + 0.05 * U], [x, y - 0.2 * U], [x + 0.12 * U, y + 0.07 * U]]), 0.2), fill: p.accent, ...ol });
|
|
1820
|
+
}
|
|
1821
|
+
out.push({ k: "path", d: smoothClosed(pts2, 0.7), fill: p.body, ...ol });
|
|
1822
|
+
} else {
|
|
1823
|
+
const line = TP([
|
|
1824
|
+
[hb * 0.6, L * 0.9],
|
|
1825
|
+
[hb + 0.4 * U, L * 0.92],
|
|
1826
|
+
[hb + 0.62 * U, L * 0.5],
|
|
1827
|
+
[hb + 0.62 * U, L * 0.18]
|
|
1828
|
+
]);
|
|
1829
|
+
out.push(...stroke(line, 0.13 * U, p.body));
|
|
1830
|
+
const tip = line[line.length - 1];
|
|
1831
|
+
out.push({ k: "group", x: tip.x, y: tip.y, rot: F.angle, s: U, items: [{ k: "path", d: roundedPolygon([{ x: -0.15, y: 0.04 }, { x: 0, y: -0.22 }, { x: 0.15, y: 0.04 }], 0.2), fill: p.accent, stroke: p.outline, lw: ow / U }] });
|
|
1832
|
+
}
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return out;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/poses.ts
|
|
1840
|
+
var STAND = {
|
|
1841
|
+
nose: [0, -0.62],
|
|
1842
|
+
leftShoulder: [-0.42, 0],
|
|
1843
|
+
rightShoulder: [0.42, 0],
|
|
1844
|
+
leftElbow: [-0.58, 0.5],
|
|
1845
|
+
rightElbow: [0.58, 0.5],
|
|
1846
|
+
leftWrist: [-0.66, 0.98],
|
|
1847
|
+
rightWrist: [0.66, 0.98],
|
|
1848
|
+
leftHip: [-0.27, 1],
|
|
1849
|
+
rightHip: [0.27, 1],
|
|
1850
|
+
leftKnee: [-0.31, 1.6],
|
|
1851
|
+
rightKnee: [0.31, 1.6],
|
|
1852
|
+
leftAnkle: [-0.33, 2.18],
|
|
1853
|
+
rightAnkle: [0.33, 2.18]
|
|
1854
|
+
};
|
|
1855
|
+
function poseBody(spec, { cx = 0.5, cy = 0.36, T = 0.18 } = {}) {
|
|
1856
|
+
const s = { ...STAND, ...spec };
|
|
1857
|
+
const b = new Array(LANDMARKS * 3).fill(0);
|
|
1858
|
+
const set = (i, [x, y], v = 0.99) => {
|
|
1859
|
+
b[i * 3] = cx + x * T;
|
|
1860
|
+
b[i * 3 + 1] = cy + y * T;
|
|
1861
|
+
b[i * 3 + 2] = v;
|
|
1862
|
+
};
|
|
1863
|
+
for (const key of Object.keys(P2)) set(P2[key], s[key]);
|
|
1864
|
+
const tilt = s.tilt ?? 0;
|
|
1865
|
+
const around = (dx, dy) => [s.nose[0] + dx * Math.cos(tilt) - dy * Math.sin(tilt), s.nose[1] + dx * Math.sin(tilt) + dy * Math.cos(tilt)];
|
|
1866
|
+
set(FACE.leftEye, around(-0.1, -0.08));
|
|
1867
|
+
set(FACE.rightEye, around(0.1, -0.08));
|
|
1868
|
+
set(FACE.leftEar, around(-0.22, -0.02));
|
|
1869
|
+
set(FACE.rightEar, around(0.22, -0.02));
|
|
1870
|
+
return b;
|
|
1871
|
+
}
|
|
1872
|
+
var neutralPose = () => poseBody({});
|
|
1873
|
+
var armsUpPose = () => poseBody({
|
|
1874
|
+
leftElbow: [-0.62, -0.48],
|
|
1875
|
+
rightElbow: [0.62, -0.48],
|
|
1876
|
+
leftWrist: [-0.72, -1],
|
|
1877
|
+
rightWrist: [0.72, -1]
|
|
1878
|
+
});
|
|
1879
|
+
var starJumpPose = () => poseBody(
|
|
1880
|
+
{
|
|
1881
|
+
leftElbow: [-0.92, -0.32],
|
|
1882
|
+
rightElbow: [0.92, -0.32],
|
|
1883
|
+
leftWrist: [-1.32, -0.7],
|
|
1884
|
+
rightWrist: [1.32, -0.7],
|
|
1885
|
+
leftKnee: [-0.6, 1.56],
|
|
1886
|
+
rightKnee: [0.6, 1.56],
|
|
1887
|
+
leftAnkle: [-0.92, 2.08],
|
|
1888
|
+
rightAnkle: [0.92, 2.08],
|
|
1889
|
+
tilt: 0.12
|
|
1890
|
+
},
|
|
1891
|
+
{ cy: 0.3 }
|
|
1892
|
+
);
|
|
1893
|
+
var squatPose = () => poseBody(
|
|
1894
|
+
{
|
|
1895
|
+
nose: [0, -0.6],
|
|
1896
|
+
leftElbow: [-0.62, 0.42],
|
|
1897
|
+
rightElbow: [0.62, 0.42],
|
|
1898
|
+
leftWrist: [-0.4, 0.62],
|
|
1899
|
+
rightWrist: [0.4, 0.62],
|
|
1900
|
+
leftHip: [-0.3, 0.86],
|
|
1901
|
+
rightHip: [0.3, 0.86],
|
|
1902
|
+
leftKnee: [-0.72, 1.08],
|
|
1903
|
+
rightKnee: [0.72, 1.08],
|
|
1904
|
+
leftAnkle: [-0.5, 1.6],
|
|
1905
|
+
rightAnkle: [0.5, 1.6]
|
|
1906
|
+
},
|
|
1907
|
+
{ cy: 0.48 }
|
|
1908
|
+
);
|
|
1909
|
+
var wavePose = () => poseBody({
|
|
1910
|
+
rightElbow: [0.86, -0.12],
|
|
1911
|
+
rightWrist: [0.98, -0.66],
|
|
1912
|
+
tilt: -0.2
|
|
1913
|
+
});
|
|
1914
|
+
var TEST_POSES = { neutral: neutralPose, armsUp: armsUpPose, starJump: starJumpPose, squat: squatPose, wave: wavePose };
|
|
1915
|
+
|
|
1916
|
+
// src/interpolate.ts
|
|
1917
|
+
var SEEN = 0.5;
|
|
1918
|
+
var JUMP = 0.15;
|
|
1919
|
+
var BodyInterpolator = class {
|
|
1920
|
+
constructor(o = {}) {
|
|
1921
|
+
__publicField(this, "tracks", /* @__PURE__ */ new Map());
|
|
1922
|
+
/** Page clock minus sender clock, at its smallest (the fastest delivery): NaN before the first body. */
|
|
1923
|
+
__publicField(this, "offset", NaN);
|
|
1924
|
+
__publicField(this, "lastArrival", NaN);
|
|
1925
|
+
/** The time between bodies (sender's clock), smoothed. */
|
|
1926
|
+
__publicField(this, "interval", 33);
|
|
1927
|
+
__publicField(this, "lastT", NaN);
|
|
1928
|
+
__publicField(this, "lostMs");
|
|
1929
|
+
__publicField(this, "maxExtrapolateMs");
|
|
1930
|
+
this.lostMs = o.lostMs ?? 250;
|
|
1931
|
+
this.maxExtrapolateMs = o.maxExtrapolateMs ?? 25;
|
|
1932
|
+
}
|
|
1933
|
+
/** The time between bodies the drawing is behind by, in ms. */
|
|
1934
|
+
get delayMs() {
|
|
1935
|
+
return this.interval;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* A new body for this player, taken at t (the sender's clock), arriving now (this page's clock, by default
|
|
1939
|
+
* performance.now()). Bodies for several players of one camera frame share t.
|
|
1940
|
+
*/
|
|
1941
|
+
push(player, t, body, arrival = now()) {
|
|
1942
|
+
if (!Number.isFinite(t)) t = arrival;
|
|
1943
|
+
this.clock(t, arrival);
|
|
1944
|
+
const tr = this.tracks.get(player);
|
|
1945
|
+
if (!tr || t - tr.tb > this.lostMs || t < tr.tb - this.lostMs || body.length !== tr.b.length) {
|
|
1946
|
+
this.tracks.set(player, { a: null, ta: t, b: body, tb: t, out: null, outAt: NaN });
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (body === tr.b || t <= tr.tb) {
|
|
1950
|
+
if (t === tr.tb && body !== tr.b) tr.b = body, tr.out = null;
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
tr.a = tr.b;
|
|
1954
|
+
tr.ta = tr.tb;
|
|
1955
|
+
tr.b = body;
|
|
1956
|
+
tr.tb = t;
|
|
1957
|
+
tr.out = null;
|
|
1958
|
+
}
|
|
1959
|
+
/** The player's body to draw at time now (this page's clock); null for a player never pushed or forgotten. */
|
|
1960
|
+
at(player, at = now()) {
|
|
1961
|
+
const tr = this.tracks.get(player);
|
|
1962
|
+
if (!tr) return null;
|
|
1963
|
+
if (!tr.a) return tr.b;
|
|
1964
|
+
if (tr.out && tr.outAt === at) return tr.out;
|
|
1965
|
+
const rt = at - this.offset - this.interval;
|
|
1966
|
+
const span = tr.tb - tr.ta;
|
|
1967
|
+
const ahead = Math.min(span * 0.5, this.maxExtrapolateMs);
|
|
1968
|
+
const u = Math.min(Math.max((rt - tr.ta) / span, 0), 1 + ahead / span);
|
|
1969
|
+
tr.out = blend(tr.a, tr.b, u);
|
|
1970
|
+
tr.outAt = at;
|
|
1971
|
+
return tr.out;
|
|
1972
|
+
}
|
|
1973
|
+
/** A player gone: their next body starts afresh. */
|
|
1974
|
+
forget(player) {
|
|
1975
|
+
this.tracks.delete(player);
|
|
1976
|
+
}
|
|
1977
|
+
/** Every player gone (a new scene). */
|
|
1978
|
+
clear() {
|
|
1979
|
+
this.tracks.clear();
|
|
1980
|
+
}
|
|
1981
|
+
clock(t, arrival) {
|
|
1982
|
+
const o = arrival - t;
|
|
1983
|
+
if (!Number.isFinite(this.offset) || o < this.offset || o > this.offset + 1e3) this.offset = o;
|
|
1984
|
+
else if (Number.isFinite(this.lastArrival)) this.offset += Math.min(o - this.offset, (arrival - this.lastArrival) / 1e3);
|
|
1985
|
+
this.lastArrival = arrival;
|
|
1986
|
+
if (Number.isFinite(this.lastT) && t > this.lastT) {
|
|
1987
|
+
const dt = t - this.lastT;
|
|
1988
|
+
if (dt < this.interval * 3 + 20) this.interval += (Math.min(Math.max(dt, 8), 100) - this.interval) * 0.1;
|
|
1989
|
+
}
|
|
1990
|
+
if (!(t <= this.lastT)) this.lastT = t;
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
var now = () => globalThis.performance?.now() ?? Date.now();
|
|
1994
|
+
function blend(a, b, u) {
|
|
1995
|
+
const out = b.slice();
|
|
1996
|
+
const near = u < 0.5 ? a : b;
|
|
1997
|
+
for (let i = 0; i + 2 < b.length; i += 3) {
|
|
1998
|
+
const ax = a[i];
|
|
1999
|
+
const ay = a[i + 1];
|
|
2000
|
+
const bx = b[i];
|
|
2001
|
+
const by = b[i + 1];
|
|
2002
|
+
if (a[i + 2] >= SEEN && b[i + 2] >= SEEN && Math.abs(bx - ax) + Math.abs(by - ay) < JUMP) {
|
|
2003
|
+
out[i] = ax + (bx - ax) * u;
|
|
2004
|
+
out[i + 1] = ay + (by - ay) * u;
|
|
2005
|
+
out[i + 2] = a[i + 2] + (b[i + 2] - a[i + 2]) * Math.min(u, 1);
|
|
2006
|
+
} else {
|
|
2007
|
+
out[i] = near[i];
|
|
2008
|
+
out[i + 1] = near[i + 1];
|
|
2009
|
+
out[i + 2] = near[i + 2];
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return out;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// src/index.ts
|
|
2016
|
+
var AVATAR_IDS = AVATARS.map((a) => a.id);
|
|
2017
|
+
var MASCOT_ID = "mascot";
|
|
2018
|
+
var AVATAR_BANDS = {
|
|
2019
|
+
"3-5": [MASCOT_ID, "bunny", "bear", "panda", "penguin", "koala", "frog", "cat", "owl", "chick", "puppy", "lamb", "duckling"],
|
|
2020
|
+
"6-8": [MASCOT_ID, "fox", "lion", "dragon", "unicorn", "tiger", "monkey", "dinosaur", "octopus", "ghost", "shark", "wolf", "parrot"],
|
|
2021
|
+
"9-12": [MASCOT_ID, "robot", "astronaut", "ninja", "skater", "alien", "explorer", "wizard", "knight", "racer", "pirate", "diver", "hero"]
|
|
2022
|
+
};
|
|
2023
|
+
var BY_ID = new Map(AVATARS.map((a) => [a.id, a]));
|
|
2024
|
+
var DEFAULT_AVATAR = BY_ID.get("fox");
|
|
2025
|
+
var isAvatarId = (v) => typeof v === "string" && BY_ID.has(v);
|
|
2026
|
+
var avatarById = (id) => BY_ID.get(id);
|
|
2027
|
+
function avatarsFor(ageRange) {
|
|
2028
|
+
const ids = AVATAR_BANDS[AGE_RANGES.includes(ageRange) ? ageRange : "6-8"];
|
|
2029
|
+
return ids.map((id) => BY_ID.get(id));
|
|
2030
|
+
}
|
|
2031
|
+
var bandOf = (id) => BY_ID.get(id)?.band ?? DEFAULT_AVATAR.band;
|
|
2032
|
+
var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
|
|
2033
|
+
function drawAvatar(ctx, body, def, box, opts = {}) {
|
|
2034
|
+
const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored });
|
|
2035
|
+
if (!shapes.length) return false;
|
|
2036
|
+
ctx.save();
|
|
2037
|
+
if (opts.alpha !== void 0) ctx.globalAlpha *= Math.min(Math.max(opts.alpha, 0), 1);
|
|
2038
|
+
drawShapes(ctx, shapes);
|
|
2039
|
+
ctx.restore();
|
|
2040
|
+
return true;
|
|
2041
|
+
}
|
|
2042
|
+
function iconShapes(def, size) {
|
|
2043
|
+
const d = resolve(def);
|
|
2044
|
+
const body = neutralPose();
|
|
2045
|
+
const band = bodyBand(d);
|
|
2046
|
+
const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
|
|
2047
|
+
const top = -bounds(headShapes(d, 0.05)).minY;
|
|
2048
|
+
const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
|
|
2049
|
+
const k = headR / BAND_BODY[band].head / unit.U;
|
|
2050
|
+
const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
|
|
2051
|
+
const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
|
|
2052
|
+
return avatarShapes(body, d, box, { portrait: true });
|
|
2053
|
+
}
|
|
2054
|
+
function drawAvatarIcon(ctx, def, x, y, size) {
|
|
2055
|
+
ctx.save();
|
|
2056
|
+
ctx.beginPath();
|
|
2057
|
+
ctx.rect(x, y, size, size);
|
|
2058
|
+
ctx.clip();
|
|
2059
|
+
ctx.translate(x, y);
|
|
2060
|
+
drawShapes(ctx, iconShapes(def, size));
|
|
2061
|
+
ctx.restore();
|
|
2062
|
+
}
|
|
2063
|
+
var clipCount = 0;
|
|
2064
|
+
function avatarIconSvg(def, size, opts = {}) {
|
|
2065
|
+
const d = resolve(def);
|
|
2066
|
+
const s = Math.round(size * 100) / 100;
|
|
2067
|
+
const bg = opts.background ? `<rect width="${s}" height="${s}" rx="${opts.radius ?? 0}" fill="${opts.background}"/>` : "";
|
|
2068
|
+
const clip = `av-${d.id}-${String(s).replace(".", "_")}-${++clipCount}`;
|
|
2069
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}"><title>${d.name.en}</title><defs><clipPath id="${clip}"><rect width="${s}" height="${s}" rx="${opts.radius ?? 0}"/></clipPath></defs>` + bg + `<g clip-path="url(#${clip})">${shapesToSvg(iconShapes(d, size))}</g></svg>`;
|
|
2070
|
+
}
|
|
2071
|
+
var avatarIconDataUri = (def, size, opts) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(avatarIconSvg(def, size, opts))}`;
|
|
2072
|
+
function smoothBody(prev, next, amount = 0.5) {
|
|
2073
|
+
if (!prev || prev.length !== next.length) return next.slice();
|
|
2074
|
+
const out = next.slice();
|
|
2075
|
+
for (let i = 0; i + 2 < next.length; i += 3) {
|
|
2076
|
+
const pv = prev[i + 2];
|
|
2077
|
+
const nv = next[i + 2];
|
|
2078
|
+
const wasSeen = pv >= 0.5;
|
|
2079
|
+
const isSeen = nv >= 0.5;
|
|
2080
|
+
if (wasSeen && nv >= 0.3) out[i + 2] = Math.max(nv, 0.5);
|
|
2081
|
+
if (wasSeen && isSeen && Math.hypot(next[i] - prev[i], next[i + 1] - prev[i + 1]) < 0.15) {
|
|
2082
|
+
out[i] = prev[i] + (next[i] - prev[i]) * amount;
|
|
2083
|
+
out[i + 1] = prev[i + 1] + (next[i + 1] - prev[i + 1]) * amount;
|
|
2084
|
+
} else if (wasSeen && !isSeen && out[i + 2] >= 0.5) {
|
|
2085
|
+
out[i] = prev[i];
|
|
2086
|
+
out[i + 1] = prev[i + 1];
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
smoothFace(prev, next, out, amount * FACE_SMOOTHING);
|
|
2090
|
+
return out;
|
|
2091
|
+
}
|
|
2092
|
+
var FACE_SMOOTHING = 0.35;
|
|
2093
|
+
var FACE_POINTS = 11;
|
|
2094
|
+
function smoothFace(prev, next, out, amount) {
|
|
2095
|
+
const ls = P3.leftShoulder * 3;
|
|
2096
|
+
const rs = P3.rightShoulder * 3;
|
|
2097
|
+
const seen = (b, i) => b[i + 2] >= 0.5;
|
|
2098
|
+
if (!seen(prev, ls) || !seen(prev, rs) || !seen(next, ls) || !seen(next, rs)) return;
|
|
2099
|
+
const mid2 = (b, k) => (b[ls + k] + b[rs + k]) / 2;
|
|
2100
|
+
const prevS = [mid2(prev, 0), mid2(prev, 1)];
|
|
2101
|
+
const nextS = [mid2(next, 0), mid2(next, 1)];
|
|
2102
|
+
const outS = [mid2(out, 0), mid2(out, 1)];
|
|
2103
|
+
for (let p = 0; p < FACE_POINTS && p * 3 + 2 < next.length; p++) {
|
|
2104
|
+
const i = p * 3;
|
|
2105
|
+
if (!seen(prev, i) || !seen(next, i)) continue;
|
|
2106
|
+
const po = [prev[i] - prevS[0], prev[i + 1] - prevS[1]];
|
|
2107
|
+
const no = [next[i] - nextS[0], next[i + 1] - nextS[1]];
|
|
2108
|
+
if (Math.hypot(no[0] - po[0], no[1] - po[1]) >= 0.15) continue;
|
|
2109
|
+
out[i] = outS[0] + po[0] + (no[0] - po[0]) * amount;
|
|
2110
|
+
out[i + 1] = outS[1] + po[1] + (no[1] - po[1]) * amount;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function bounds(shapes) {
|
|
2114
|
+
const b = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
|
2115
|
+
const addPt = (x, y) => {
|
|
2116
|
+
b.minX = Math.min(b.minX, x);
|
|
2117
|
+
b.minY = Math.min(b.minY, y);
|
|
2118
|
+
b.maxX = Math.max(b.maxX, x);
|
|
2119
|
+
b.maxY = Math.max(b.maxY, y);
|
|
2120
|
+
};
|
|
2121
|
+
for (const s of shapes) {
|
|
2122
|
+
const pad = "lw" in s && s.stroke ? (s.lw ?? 1) / 2 : 0;
|
|
2123
|
+
switch (s.k) {
|
|
2124
|
+
case "circle":
|
|
2125
|
+
addPt(s.x - s.r - pad, s.y - s.r - pad);
|
|
2126
|
+
addPt(s.x + s.r + pad, s.y + s.r + pad);
|
|
2127
|
+
break;
|
|
2128
|
+
case "ellipse":
|
|
2129
|
+
case "rect": {
|
|
2130
|
+
const rx = s.k === "ellipse" ? s.rx : s.w / 2;
|
|
2131
|
+
const ry = s.k === "ellipse" ? s.ry : s.h / 2;
|
|
2132
|
+
const r = Math.max(rx, ry) + pad;
|
|
2133
|
+
addPt(s.x - r, s.y - r);
|
|
2134
|
+
addPt(s.x + r, s.y + r);
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
case "path": {
|
|
2138
|
+
const n = pathPoints(s.d);
|
|
2139
|
+
for (let i = 0; i + 1 < n.length; i += 2) addPt(n[i] - pad, n[i + 1] - pad), addPt(n[i] + pad, n[i + 1] + pad);
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
case "line":
|
|
2143
|
+
for (let i = 0; i + 1 < s.pts.length; i += 2) addPt(s.pts[i] - pad, s.pts[i + 1] - pad), addPt(s.pts[i] + pad, s.pts[i + 1] + pad);
|
|
2144
|
+
break;
|
|
2145
|
+
case "group": {
|
|
2146
|
+
const inner = bounds(s.items);
|
|
2147
|
+
if (!Number.isFinite(inner.minX)) break;
|
|
2148
|
+
const k = s.s ?? 1;
|
|
2149
|
+
const c = Math.cos(s.rot ?? 0);
|
|
2150
|
+
const sn = Math.sin(s.rot ?? 0);
|
|
2151
|
+
for (const [x, y] of [
|
|
2152
|
+
[inner.minX, inner.minY],
|
|
2153
|
+
[inner.maxX, inner.minY],
|
|
2154
|
+
[inner.minX, inner.maxY],
|
|
2155
|
+
[inner.maxX, inner.maxY]
|
|
2156
|
+
])
|
|
2157
|
+
addPt(s.x + (x * c - y * sn) * k, s.y + (x * sn + y * c) * k);
|
|
2158
|
+
break;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return b;
|
|
2163
|
+
}
|
|
2164
|
+
export {
|
|
2165
|
+
AVATARS,
|
|
2166
|
+
AVATAR_BANDS,
|
|
2167
|
+
AVATAR_IDS,
|
|
2168
|
+
BAND_BODY,
|
|
2169
|
+
BodyInterpolator,
|
|
2170
|
+
DEFAULT_AVATAR,
|
|
2171
|
+
FACE,
|
|
2172
|
+
HEAD,
|
|
2173
|
+
MASCOT_ID,
|
|
2174
|
+
MAX_TILT,
|
|
2175
|
+
PATH,
|
|
2176
|
+
STAR,
|
|
2177
|
+
TEST_POSES,
|
|
2178
|
+
TILT_DEAD_ZONE,
|
|
2179
|
+
VISIBLE,
|
|
2180
|
+
armsUpPose,
|
|
2181
|
+
avatarById,
|
|
2182
|
+
avatarIconDataUri,
|
|
2183
|
+
avatarIconSvg,
|
|
2184
|
+
avatarShapes,
|
|
2185
|
+
avatarsFor,
|
|
2186
|
+
bandOf,
|
|
2187
|
+
blend,
|
|
2188
|
+
bodyBand,
|
|
2189
|
+
bounds,
|
|
2190
|
+
drawAvatar,
|
|
2191
|
+
drawAvatarIcon,
|
|
2192
|
+
drawShapes,
|
|
2193
|
+
frameOf,
|
|
2194
|
+
iconShapes,
|
|
2195
|
+
isAvatarId,
|
|
2196
|
+
neutralPose,
|
|
2197
|
+
pathPoints,
|
|
2198
|
+
pathToString,
|
|
2199
|
+
poseBody,
|
|
2200
|
+
shapesToSvg,
|
|
2201
|
+
smoothBody,
|
|
2202
|
+
squatPose,
|
|
2203
|
+
starJumpPose,
|
|
2204
|
+
wavePose
|
|
2205
|
+
};
|