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