@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,2612 @@
1
+ /*! @fabi-play/sdk 0.1.0 | Fabi Play Kit License | https://docs.fabiplay.app */
2
+ (function (root, factory) {
3
+ var script = typeof document !== 'undefined' ? document.currentScript : null;
4
+ if (typeof define === 'function' && define.amd) define([], function () { return factory(script); });
5
+ else if (typeof module === 'object' && module && module.exports) module.exports = factory(script);
6
+ else root.FabiPlay = factory(script);
7
+ })(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : this, function (script) {
8
+ 'use strict';
9
+ var module = { exports: {} };
10
+ var exports = module.exports;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
15
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") {
22
+ for (let key of __getOwnPropNames(from))
23
+ if (!__hasOwnProp.call(to, key) && key !== except)
24
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
25
+ }
26
+ return to;
27
+ };
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
30
+
31
+ // src/umd.ts
32
+ var umd_exports = {};
33
+ __export(umd_exports, {
34
+ install: () => install
35
+ });
36
+ module.exports = __toCommonJS(umd_exports);
37
+
38
+ // src/adapters.ts
39
+ function muteDuringBreaks(play, win = window) {
40
+ const contexts = /* @__PURE__ */ new Set();
41
+ const Original = win.AudioContext;
42
+ if (Original) {
43
+ win.AudioContext = class extends Original {
44
+ constructor(...args) {
45
+ super(...args);
46
+ contexts.add(this);
47
+ }
48
+ };
49
+ }
50
+ let paused = [];
51
+ let suspended = [];
52
+ void Promise.resolve(play).then((p) => {
53
+ p.on("break-start", () => {
54
+ paused = [...win.document.querySelectorAll("audio, video")].filter((m) => !m.paused);
55
+ paused.forEach((m) => m.pause());
56
+ suspended = [...contexts].filter((c) => c.state === "running");
57
+ suspended.forEach((c) => void c.suspend());
58
+ });
59
+ p.on("break-done", () => {
60
+ paused.forEach((m) => void m.play().catch(() => void 0));
61
+ suspended.forEach((c) => void c.resume());
62
+ paused = [];
63
+ suspended = [];
64
+ });
65
+ });
66
+ }
67
+ var DEFAULT_KEYS = { jump: "Space", squat: "ArrowDown", left: "ArrowLeft", right: "ArrowRight", clap: "Enter" };
68
+ function parseKeys(value) {
69
+ if (value.trim() === "default") return void 0;
70
+ const map = {};
71
+ for (const pair of value.split(",")) {
72
+ const [action, code] = pair.split(":").map((s) => s.trim());
73
+ if (action && code) map[action] = code;
74
+ }
75
+ return map;
76
+ }
77
+ var NAMED = {
78
+ Space: [" ", 32],
79
+ Enter: ["Enter", 13],
80
+ Escape: ["Escape", 27],
81
+ ShiftLeft: ["Shift", 16],
82
+ ArrowLeft: ["ArrowLeft", 37],
83
+ ArrowUp: ["ArrowUp", 38],
84
+ ArrowRight: ["ArrowRight", 39],
85
+ ArrowDown: ["ArrowDown", 40]
86
+ };
87
+ function keyInfo(code) {
88
+ const named = NAMED[code];
89
+ if (named) return { key: named[0], code, keyCode: named[1] };
90
+ const letter = /^Key([A-Z])$/.exec(code);
91
+ if (letter) return { key: letter[1].toLowerCase(), code, keyCode: letter[1].charCodeAt(0) };
92
+ const digit = /^Digit([0-9])$/.exec(code);
93
+ if (digit) return { key: digit[1], code, keyCode: 48 + Number(digit[1]) };
94
+ throw new Error(`Fabi Play: unknown key "${code}" (use Space, Enter, Arrow\u2026, KeyA\u2026, Digit0\u2026)`);
95
+ }
96
+ var domKeys = (doc = document) => ({
97
+ dispatch(type, k) {
98
+ const e = new KeyboardEvent(type, { key: k.key, code: k.code, bubbles: true, cancelable: true });
99
+ for (const name of ["keyCode", "which"]) Object.defineProperty(e, name, { get: () => k.keyCode });
100
+ (doc.activeElement ?? doc.body ?? doc).dispatchEvent(e);
101
+ }
102
+ });
103
+ var TAP_MS = 80;
104
+ var MAX_HOLD_MS = 800;
105
+ function keyboard(play, map = DEFAULT_KEYS, options = {}) {
106
+ const player = options.player ?? 1;
107
+ const target = options.target ?? domKeys();
108
+ const later = options.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
109
+ const held = /* @__PURE__ */ new Set();
110
+ let lane = 0;
111
+ const down = (a) => {
112
+ const code = map[a];
113
+ if (!code || held.has(a)) return;
114
+ held.add(a);
115
+ target.dispatch("keydown", keyInfo(code));
116
+ };
117
+ const up = (a) => {
118
+ const code = map[a];
119
+ if (!code || !held.has(a)) return;
120
+ held.delete(a);
121
+ target.dispatch("keyup", keyInfo(code));
122
+ };
123
+ const tap = (a) => {
124
+ down(a);
125
+ later(() => up(a), TAP_MS);
126
+ };
127
+ const onGesture = (g) => {
128
+ if (g.player !== player) return;
129
+ const lanes = { "lane-left": -1, "lane-center": 0, "lane-right": 1 };
130
+ switch (g.gesture) {
131
+ case "jump":
132
+ down("jump");
133
+ later(() => up("jump"), MAX_HOLD_MS);
134
+ break;
135
+ case "land":
136
+ up("jump");
137
+ break;
138
+ case "squat":
139
+ down("squat");
140
+ break;
141
+ case "stand":
142
+ up("squat");
143
+ break;
144
+ case "clap":
145
+ tap("clap");
146
+ break;
147
+ default: {
148
+ const next = lanes[g.gesture];
149
+ if (next === void 0) break;
150
+ const dir = next > lane ? "right" : "left";
151
+ for (let i = 0; i < Math.abs(next - lane); i++) later(() => tap(dir), i * (TAP_MS * 2));
152
+ lane = next;
153
+ }
154
+ }
155
+ };
156
+ const onFrame = (f2) => {
157
+ const p = f2.players.find((x) => x.id === player);
158
+ if (!p) return;
159
+ if (p.shape === "arms-up") down("arms-up");
160
+ else up("arms-up");
161
+ };
162
+ const offs = [play.on("gesture", onGesture), play.on("frame", onFrame), play.on("break-start", () => [...held].forEach(up))];
163
+ return () => {
164
+ offs.forEach((off) => off());
165
+ [...held].forEach(up);
166
+ };
167
+ }
168
+ function domPointer(doc = document) {
169
+ const ring = doc.createElement("div");
170
+ ring.setAttribute("aria-hidden", "true");
171
+ ring.style.cssText = "position:fixed;left:0;top:0;width:56px;height:56px;margin:-28px 0 0 -28px;border-radius:50%;border:6px solid #FBE7B8;background:rgba(251,231,184,.25);pointer-events:none;z-index:2147483647;box-sizing:border-box;transition:transform 60ms linear";
172
+ doc.body.appendChild(ring);
173
+ const fire = (x, y, ...types) => {
174
+ const el = doc.elementFromPoint(x, y) ?? doc.body;
175
+ for (const type of types) {
176
+ const init = { bubbles: true, cancelable: true, clientX: x, clientY: y, pointerId: 1, pointerType: "touch", isPrimary: true };
177
+ el.dispatchEvent(type.startsWith("pointer") ? new PointerEvent(type, init) : new MouseEvent(type, init));
178
+ }
179
+ };
180
+ return {
181
+ size: () => ({ width: doc.documentElement.clientWidth, height: doc.documentElement.clientHeight }),
182
+ move: (x, y) => fire(x, y, "pointermove", "mousemove"),
183
+ click: (x, y) => fire(x, y, "pointerdown", "mousedown", "pointerup", "mouseup", "click"),
184
+ cursor: (x, y, progress) => {
185
+ ring.style.transform = `translate(${x}px, ${y}px) scale(${1 - progress * 0.35})`;
186
+ ring.style.borderColor = progress > 0 ? "#EE6A43" : "#FBE7B8";
187
+ }
188
+ };
189
+ }
190
+ function pointer(play, options = {}) {
191
+ const player = options.player ?? 1;
192
+ const hand = options.hand ?? "any";
193
+ const dwell = options.dwellMs ?? 800;
194
+ const radius = options.radius ?? 0.04;
195
+ const target = options.target ?? domPointer();
196
+ let anchor = null;
197
+ let clicked = false;
198
+ return play.on("frame", (f2) => {
199
+ const p = f2.players.find((x2) => x2.id === player);
200
+ if (!p) return;
201
+ const pick = (a, b) => a && b ? a.y < b.y ? a : b : a ?? b;
202
+ const h = hand === "left" ? p.leftHand : hand === "right" ? p.rightHand : pick(p.leftHand, p.rightHand);
203
+ if (!h) return;
204
+ const { width, height } = target.size();
205
+ const x = h.x * width;
206
+ const y = h.y * height;
207
+ target.move(x, y);
208
+ const far = anchor ? Math.hypot(x - anchor.x, y - anchor.y) > radius * Math.hypot(width, height) : true;
209
+ if (far) {
210
+ anchor = { x, y, t: f2.t };
211
+ clicked = false;
212
+ }
213
+ const progress = clicked ? 0 : Math.min((f2.t - anchor.t) / dwell, 1);
214
+ if (progress >= 1) {
215
+ clicked = true;
216
+ target.click(anchor.x, anchor.y);
217
+ }
218
+ target.cursor(x, y, clicked ? 0 : progress);
219
+ });
220
+ }
221
+
222
+ // ../protocol/src/index.ts
223
+ var PROTOCOL_VERSION = 1;
224
+ var LANDMARKS = 33;
225
+ var P = {
226
+ nose: 0,
227
+ leftShoulder: 11,
228
+ rightShoulder: 12,
229
+ leftElbow: 13,
230
+ rightElbow: 14,
231
+ leftWrist: 15,
232
+ rightWrist: 16,
233
+ leftHip: 23,
234
+ rightHip: 24,
235
+ leftKnee: 25,
236
+ rightKnee: 26,
237
+ leftAnkle: 27,
238
+ rightAnkle: 28
239
+ };
240
+ var AGE_RANGES = ["3-5", "6-8", "9-12"];
241
+ var isAgeRange = (v) => AGE_RANGES.includes(v);
242
+ var MAX_PLAYERS = 4;
243
+ var DEFAULT_AVATARS_BY_AGE = {
244
+ "3-5": ["bunny", "bear", "panda", "penguin"],
245
+ "6-8": ["fox", "lion", "dragon", "unicorn"],
246
+ "9-12": ["robot", "astronaut", "ninja", "skater"]
247
+ };
248
+ var DEFAULT_AVATARS = DEFAULT_AVATARS_BY_AGE["6-8"];
249
+ var defaultAvatarFor = (slot, ageRange) => {
250
+ const list = isAgeRange(ageRange) ? DEFAULT_AVATARS_BY_AGE[ageRange] : DEFAULT_AVATARS;
251
+ return list[Number.isInteger(slot) && slot >= 0 ? slot % list.length : 0];
252
+ };
253
+ var NS = "fabi-play";
254
+ var wire = (m) => ({ ns: NS, ...m });
255
+ function parseView(v) {
256
+ if (!isObject(v)) return void 0;
257
+ const { x, y, w, h } = v;
258
+ return isNumber(x) && isNumber(y) && isNumber(w) && isNumber(h) && w > 0 && h > 0 ? { x, y, w, h } : void 0;
259
+ }
260
+ 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 };
261
+ var DEVICE_TIERS = ["high", "medium", "low"];
262
+ function parseDeviceHint(v) {
263
+ if (!isObject(v)) return void 0;
264
+ const out = {};
265
+ if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
266
+ if (isNumber(v.hz) && v.hz >= 20 && v.hz <= 240) out.hz = Math.round(v.hz);
267
+ return out.tier || out.hz ? out : void 0;
268
+ }
269
+ function parseGameStats(v) {
270
+ const out = {};
271
+ if (!isObject(v)) return out;
272
+ if (isNumber(v.fps) && v.fps >= 0 && v.fps < 1e3) out.fps = v.fps;
273
+ if (typeof v.renderer === "string" && /^[a-z0-9-]{1,16}$/.test(v.renderer)) out.renderer = v.renderer;
274
+ if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
275
+ return out;
276
+ }
277
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
278
+ var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
279
+ var POSE_HEADER = 12;
280
+ var POSE_BODY_BYTES = LANDMARKS * 4;
281
+ var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
282
+ var POSE_MAX_BYTES = poseBytes(MAX_PLAYERS);
283
+
284
+ // ../avatars/src/defs.ts
285
+ var SOFT = "#4a382c";
286
+ var INK = "#2b2118";
287
+ var COOL = "#1c1f2b";
288
+ var CREAM = "#fbefe3";
289
+ var BLUSH = "#f4897a";
290
+ var NOSE = "#3a2a22";
291
+ var AVATARS = [
292
+ {
293
+ id: "fox",
294
+ name: { en: "Fox", it: "Volpe" },
295
+ band: "6-8",
296
+ palette: { body: "#ee6a43", limbs: "#ee6a43", belly: CREAM, accent: "#ffd9c2", outline: INK, hands: "#5a3426", feet: "#5a3426" },
297
+ head: "wide",
298
+ ears: "pointy",
299
+ face: { eyes: "dot", mouth: "cat", nose: NOSE, muzzle: "cheeks" },
300
+ tail: "fluffy",
301
+ headExtras: [],
302
+ bodyExtras: []
303
+ },
304
+ {
305
+ // The fox of the app icon (brand/): the same coral fur and dark paws as 'fox', told apart by the icon's face
306
+ // (big glossy eyes, the cream lower face, blush, a big open smile, dark brown inside the ears), a big brush of a
307
+ // tail and a yellow star on the chest.
308
+ id: "mascot",
309
+ name: { en: "Fabi", it: "Fabi" },
310
+ band: "all",
311
+ special: true,
312
+ palette: { body: "#ee6a43", limbs: "#ee6a43", belly: CREAM, accent: "#5a3426", outline: INK, hands: "#5a3426", feet: "#5a3426" },
313
+ head: "wide",
314
+ ears: "pointy",
315
+ face: { eyes: "bead", mouth: "open", nose: NOSE, muzzle: "band", cheeks: BLUSH },
316
+ tail: "brush",
317
+ headExtras: [],
318
+ bodyExtras: ["star"],
319
+ extraColors: ["#ffd84a"]
320
+ },
321
+ {
322
+ id: "bunny",
323
+ name: { en: "Bunny", it: "Coniglietto" },
324
+ band: "3-5",
325
+ palette: { body: "#f9d0de", limbs: "#f9d0de", belly: "#fff6f8", accent: "#f4a0b9", outline: SOFT },
326
+ head: "round",
327
+ ears: "long",
328
+ face: { eyes: "dot", mouth: "cat", nose: "#e0607e", muzzle: "none", cheeks: BLUSH },
329
+ tail: "pom",
330
+ headExtras: [],
331
+ bodyExtras: []
332
+ },
333
+ {
334
+ id: "bear",
335
+ name: { en: "Bear", it: "Orsetto" },
336
+ band: "3-5",
337
+ palette: { body: "#cf9f78", limbs: "#cf9f78", belly: "#f7e0bf", accent: "#f7e0bf", outline: SOFT },
338
+ head: "round",
339
+ ears: "round",
340
+ face: { eyes: "dot", mouth: "smile", nose: NOSE, muzzle: "oval", cheeks: BLUSH },
341
+ tail: "none",
342
+ headExtras: [],
343
+ bodyExtras: []
344
+ },
345
+ {
346
+ id: "cat",
347
+ name: { en: "Cat", it: "Gattino" },
348
+ band: "3-5",
349
+ palette: { body: "#c8b6ee", limbs: "#c8b6ee", belly: "#f4eeff", accent: "#f7b3c8", outline: SOFT, hands: "#f4eeff", feet: "#f4eeff" },
350
+ head: "wide",
351
+ ears: "pointy",
352
+ face: { eyes: "big", eyeColor: "#3f9a5a", mouth: "cat", nose: "#f07a9a", muzzle: "none", cheeks: BLUSH },
353
+ tail: "long",
354
+ headExtras: ["whiskers"],
355
+ bodyExtras: []
356
+ },
357
+ {
358
+ id: "panda",
359
+ name: { en: "Panda", it: "Panda" },
360
+ band: "3-5",
361
+ palette: { body: "#ffffff", limbs: "#6e6674", belly: "#ffffff", accent: "#6e6674", outline: SOFT, ears: "#6e6674" },
362
+ head: "wide",
363
+ ears: "round",
364
+ face: { eyes: "mask", mouth: "smile", nose: "#2f2a2c", muzzle: "none", cheeks: "#f6a3a3" },
365
+ tail: "none",
366
+ headExtras: [],
367
+ bodyExtras: []
368
+ },
369
+ {
370
+ id: "penguin",
371
+ name: { en: "Penguin", it: "Pinguino" },
372
+ band: "3-5",
373
+ palette: { body: "#6c8fca", limbs: "#6c8fca", belly: "#ffffff", accent: "#f9b95c", outline: SOFT, feet: "#f9b95c" },
374
+ head: "tall",
375
+ ears: "none",
376
+ face: { eyes: "dot", mouth: "beak", muzzle: "heart", cheeks: "#f6a3a3" },
377
+ tail: "none",
378
+ headExtras: [],
379
+ bodyExtras: ["flippers", "scarf"],
380
+ extraColors: ["#f6907a"]
381
+ },
382
+ {
383
+ id: "frog",
384
+ name: { en: "Frog", it: "Ranocchio" },
385
+ band: "3-5",
386
+ palette: { body: "#8fd487", limbs: "#8fd487", belly: "#e6f7cc", accent: "#62ad62", outline: SOFT },
387
+ head: "wide",
388
+ ears: "none",
389
+ face: { eyes: "big", eyesOnTop: true, mouth: "open", muzzle: "none", cheeks: "#f59b8f" },
390
+ tail: "none",
391
+ headExtras: [],
392
+ bodyExtras: ["flippers", "spots"],
393
+ extraColors: ["#62ad62", "#b0e6a4"]
394
+ },
395
+ {
396
+ id: "lion",
397
+ name: { en: "Lion", it: "Leoncino" },
398
+ band: "6-8",
399
+ palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
400
+ head: "round",
401
+ ears: "round",
402
+ face: { eyes: "dot", mouth: "cat", nose: "#8a4b2a", muzzle: "oval", cheeks: BLUSH },
403
+ tail: "tuft",
404
+ headExtras: ["mane"],
405
+ bodyExtras: [],
406
+ extraColors: ["#c8692e"]
407
+ },
408
+ {
409
+ id: "unicorn",
410
+ name: { en: "Unicorn", it: "Unicorno" },
411
+ band: "6-8",
412
+ palette: { body: "#fbf5ff", limbs: "#fbf5ff", belly: "#efe0ff", accent: "#fbd46a", outline: INK, hands: "#c9a6f2", feet: "#c9a6f2" },
413
+ head: "tall",
414
+ ears: "unicorn",
415
+ face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f7a6c8" },
416
+ tail: "none",
417
+ headExtras: ["rainbow-mane"],
418
+ bodyExtras: [],
419
+ extraColors: ["#f58cc0", "#8fd3f4", "#a98ff2"]
420
+ },
421
+ {
422
+ id: "dragon",
423
+ name: { en: "Little Dragon", it: "Draghetto" },
424
+ band: "6-8",
425
+ palette: { body: "#8a63d2", limbs: "#8a63d2", belly: "#fbd46a", accent: "#5fd0b5", outline: INK },
426
+ head: "wide",
427
+ ears: "horns",
428
+ face: { eyes: "big", eyeColor: "#3a2a6a", mouth: "teeth", muzzle: "none", cheeks: "#f59bc0" },
429
+ tail: "arrow",
430
+ headExtras: [],
431
+ bodyExtras: ["wings"]
432
+ },
433
+ {
434
+ id: "robot",
435
+ name: { en: "Robot", it: "Robot" },
436
+ band: "9-12",
437
+ palette: { body: "#b9c7d2", limbs: "#8b9aa6", belly: "#b9c7d2", accent: "#ee6a43", outline: COOL, hands: "#ee6a43", feet: "#6b7a86" },
438
+ head: "square",
439
+ ears: "antenna",
440
+ face: { eyes: "screen", eyeColor: "#7ff0ff", mouth: "grill", muzzle: "none" },
441
+ tail: "none",
442
+ headExtras: ["bolts"],
443
+ bodyExtras: ["panel", "joints", "boots"],
444
+ extraColors: ["#ff7a6b", "#fbd46a", "#7ff0ff"]
445
+ },
446
+ {
447
+ id: "astronaut",
448
+ name: { en: "Astronaut", it: "Astronauta" },
449
+ band: "9-12",
450
+ palette: { body: "#f4f1ec", limbs: "#f4f1ec", belly: "#f4f1ec", accent: "#ee6a43", outline: COOL, hands: "#9fb0bd", feet: "#9fb0bd" },
451
+ head: "round",
452
+ ears: "none",
453
+ face: { eyes: "visor", mouth: "smile", muzzle: "none" },
454
+ tail: "none",
455
+ headExtras: ["helmet"],
456
+ bodyExtras: ["backpack", "badge", "boots"],
457
+ extraColors: ["#c9d3db"]
458
+ },
459
+ {
460
+ id: "dinosaur",
461
+ name: { en: "Dinosaur", it: "Dinosauro" },
462
+ band: "6-8",
463
+ palette: { body: "#3fb3a6", limbs: "#3fb3a6", belly: "#c9f0d6", accent: "#f79a3e", outline: INK },
464
+ head: "wide",
465
+ ears: "none",
466
+ face: { eyes: "dot", mouth: "teeth", muzzle: "none", cheeks: "#f59b8f" },
467
+ tail: "spiky",
468
+ headExtras: ["spikes", "spots"],
469
+ bodyExtras: [],
470
+ extraColors: ["#2b8f84", "#7fd3c6"]
471
+ },
472
+ {
473
+ id: "monkey",
474
+ name: { en: "Monkey", it: "Scimmietta" },
475
+ band: "6-8",
476
+ palette: { body: "#7b5234", limbs: "#7b5234", belly: "#f2c9a0", accent: "#f2c9a0", outline: INK, hands: "#f2c9a0", feet: "#f2c9a0" },
477
+ head: "round",
478
+ ears: "side",
479
+ face: { eyes: "dot", mouth: "open", nose: "#5a3a26", muzzle: "monkey", cheeks: BLUSH },
480
+ tail: "curl",
481
+ headExtras: ["tuft"],
482
+ bodyExtras: []
483
+ },
484
+ {
485
+ id: "koala",
486
+ name: { en: "Koala", it: "Koala" },
487
+ band: "3-5",
488
+ palette: { body: "#b7bcc6", limbs: "#b7bcc6", belly: "#eef0f3", accent: "#f4dbe2", outline: SOFT },
489
+ head: "wide",
490
+ ears: "big-round",
491
+ face: { eyes: "dot", mouth: "smile", nose: "#3b3438", muzzle: "none", cheeks: "#f4a3b0" },
492
+ tail: "none",
493
+ headExtras: [],
494
+ bodyExtras: []
495
+ },
496
+ {
497
+ id: "tiger",
498
+ name: { en: "Tiger", it: "Tigrotto" },
499
+ band: "6-8",
500
+ palette: { body: "#f9a33a", limbs: "#f9a33a", belly: "#fff1dc", accent: "#fff1dc", outline: INK },
501
+ head: "wide",
502
+ ears: "round",
503
+ face: { eyes: "dot", mouth: "cat", nose: "#e2607a", muzzle: "oval", cheeks: BLUSH },
504
+ tail: "striped",
505
+ headExtras: ["stripes", "whiskers"],
506
+ bodyExtras: [],
507
+ extraColors: ["#3a2a22"]
508
+ },
509
+ {
510
+ id: "owl",
511
+ name: { en: "Owl", it: "Gufetto" },
512
+ band: "3-5",
513
+ palette: { body: "#8ec0e6", limbs: "#8ec0e6", belly: "#e3f2fc", accent: "#639cc9", outline: SOFT, feet: "#f9b95c", hands: "#639cc9" },
514
+ head: "tall",
515
+ ears: "tufts",
516
+ face: { eyes: "big", eyeColor: SOFT, eyeRing: "#fde08a", mouth: "beak", muzzle: "none", cheeks: "#f6a8b4" },
517
+ tail: "none",
518
+ headExtras: [],
519
+ bodyExtras: ["feathers"]
520
+ },
521
+ {
522
+ id: "alien",
523
+ name: { en: "Alien", it: "Alieno" },
524
+ band: "9-12",
525
+ palette: { body: "#9ee05a", limbs: "#9ee05a", belly: "#d8f5a8", accent: "#b07cf2", outline: COOL },
526
+ head: "bulb",
527
+ ears: "antennae",
528
+ face: { eyes: "almond", eyeColor: "#1e1838", mouth: "smirk", muzzle: "none" },
529
+ tail: "none",
530
+ headExtras: [],
531
+ bodyExtras: ["spots"],
532
+ extraColors: ["#7cc23e", "#7cc23e"]
533
+ },
534
+ {
535
+ id: "ghost",
536
+ name: { en: "Friendly Ghost", it: "Fantasmino" },
537
+ band: "6-8",
538
+ palette: { body: "#e6f4ff", limbs: "#e6f4ff", belly: "#e6f4ff", accent: "#bcd9f2", outline: INK },
539
+ head: "round",
540
+ ears: "none",
541
+ face: { eyes: "dot", mouth: "open", muzzle: "none", cheeks: "#f7a6c8" },
542
+ tail: "none",
543
+ headExtras: ["curl"],
544
+ bodyExtras: ["sheet"]
545
+ },
546
+ {
547
+ id: "octopus",
548
+ name: { en: "Octopus", it: "Polpetto" },
549
+ band: "6-8",
550
+ palette: { body: "#e0578a", limbs: "#e0578a", belly: "#f7b3cc", accent: "#f7b3cc", outline: INK, hands: "#f7b3cc", feet: "#f7b3cc" },
551
+ head: "tall",
552
+ ears: "none",
553
+ face: { eyes: "big", eyeColor: "#3a2140", mouth: "smile", muzzle: "none", cheeks: "#ffd0a0" },
554
+ tail: "none",
555
+ headExtras: ["spots"],
556
+ bodyExtras: ["tentacles"],
557
+ extraColors: ["#f7b3cc", "#f7b3cc"]
558
+ },
559
+ // ── New in the age bands: 3–5 ──
560
+ {
561
+ id: "chick",
562
+ name: { en: "Chick", it: "Pulcino" },
563
+ band: "3-5",
564
+ palette: { body: "#ffd45e", limbs: "#ffd45e", belly: "#fff2c2", accent: "#ffa04a", outline: SOFT, hands: "#ffc23d", feet: "#ffa04a" },
565
+ head: "round",
566
+ ears: "none",
567
+ face: { eyes: "dot", mouth: "beak", muzzle: "none", cheeks: BLUSH },
568
+ tail: "none",
569
+ headExtras: ["eggshell"],
570
+ bodyExtras: [],
571
+ extraColors: ["#fffaf0"]
572
+ },
573
+ {
574
+ id: "puppy",
575
+ name: { en: "Puppy", it: "Cucciolo" },
576
+ band: "3-5",
577
+ palette: { body: "#f4e4cf", limbs: "#f4e4cf", belly: "#fffaf2", accent: "#f29aa4", outline: SOFT, ears: "#b58262", hands: "#e6caa6", feet: "#e6caa6" },
578
+ head: "wide",
579
+ ears: "floppy",
580
+ face: { eyes: "dot", mouth: "open", nose: NOSE, muzzle: "oval", cheeks: BLUSH },
581
+ tail: "curl",
582
+ headExtras: ["eye-spot"],
583
+ bodyExtras: []
584
+ },
585
+ {
586
+ id: "lamb",
587
+ name: { en: "Lamb", it: "Agnellino" },
588
+ band: "3-5",
589
+ palette: { body: "#fdf8ee", limbs: "#e3d1c2", belly: "#fffdf8", accent: "#f6bcc2", outline: SOFT, skin: "#f7dccb", hands: "#a58c7e", feet: "#a58c7e" },
590
+ head: "round",
591
+ ears: "droopy",
592
+ face: { eyes: "dot", mouth: "smile", nose: "#e591a0", muzzle: "none", cheeks: BLUSH },
593
+ tail: "pom",
594
+ headExtras: ["wool"],
595
+ bodyExtras: ["wool"]
596
+ },
597
+ {
598
+ id: "duckling",
599
+ name: { en: "Duckling", it: "Anatroccolo" },
600
+ band: "3-5",
601
+ palette: { body: "#fff1a6", limbs: "#fff1a6", belly: "#fffbe0", accent: "#ff9f43", outline: SOFT, feet: "#ff9f43" },
602
+ head: "tall",
603
+ ears: "none",
604
+ face: { eyes: "dot", mouth: "bill", muzzle: "none", cheeks: BLUSH },
605
+ tail: "none",
606
+ headExtras: ["curl"],
607
+ bodyExtras: ["flippers", "scarf"],
608
+ extraColors: ["#86cfe0"]
609
+ },
610
+ // ── New in the age bands: 6–8 ──
611
+ {
612
+ id: "shark",
613
+ name: { en: "Shark", it: "Squalo" },
614
+ band: "6-8",
615
+ palette: { body: "#6c95c4", limbs: "#6c95c4", belly: "#eef5fb", accent: "#4c76a8", outline: INK, hands: "#eef5fb", feet: "#4c76a8" },
616
+ head: "wide",
617
+ ears: "fin",
618
+ face: { eyes: "dot", mouth: "jaws", muzzle: "none", cheeks: "#f59bb0" },
619
+ tail: "fish",
620
+ headExtras: ["gills"],
621
+ bodyExtras: []
622
+ },
623
+ {
624
+ id: "wolf",
625
+ name: { en: "Wolf", it: "Lupo" },
626
+ band: "6-8",
627
+ palette: { body: "#8f9bb3", limbs: "#8f9bb3", belly: "#eef1f6", accent: "#f3b8c4", outline: INK, hands: "#5a647a", feet: "#5a647a" },
628
+ head: "round",
629
+ ears: "pointy",
630
+ face: { eyes: "dot", mouth: "open", nose: NOSE, muzzle: "oval", cheeks: "#f4a3b0" },
631
+ tail: "fluffy",
632
+ headExtras: ["ruff", "forehead"],
633
+ bodyExtras: [],
634
+ extraColors: ["#636d84"]
635
+ },
636
+ {
637
+ id: "parrot",
638
+ name: { en: "Parrot", it: "Pappagallo" },
639
+ band: "6-8",
640
+ palette: { body: "#e8503f", limbs: "#e8503f", belly: "#ffd25a", accent: "#f6e6c4", outline: INK, hands: "#3a9bdc", feet: "#8a7a70" },
641
+ head: "round",
642
+ ears: "none",
643
+ face: { eyes: "big", eyeColor: INK, eyeRing: "#ffffff", mouth: "hook", muzzle: "none", cheeks: "#ffb0a0" },
644
+ tail: "plume",
645
+ headExtras: ["crest"],
646
+ bodyExtras: ["feathers"],
647
+ extraColors: ["#ffd25a", "#3fae5a", "#3a9bdc"]
648
+ },
649
+ // ── New in the age bands: 9–12 (people: generic faces, gear that reads at small size) ──
650
+ {
651
+ id: "ninja",
652
+ name: { en: "Ninja", it: "Ninja" },
653
+ band: "9-12",
654
+ palette: { body: "#2f3a56", limbs: "#2f3a56", belly: "#2f3a56", accent: "#36c9bb", outline: COOL, skin: "#b07a52", hands: "#222a40", feet: "#222a40" },
655
+ head: "round",
656
+ ears: "none",
657
+ face: { eyes: "calm", mouth: "none", muzzle: "none", brows: "#2a1d16" },
658
+ tail: "none",
659
+ headExtras: ["ninja-hood"],
660
+ bodyExtras: ["belt"],
661
+ extraColors: ["#36c9bb", "#36c9bb", "#36c9bb"]
662
+ },
663
+ {
664
+ id: "skater",
665
+ name: { en: "Skater", it: "Skater" },
666
+ band: "9-12",
667
+ palette: { body: "#3a8fc0", limbs: "#2c3e57", belly: "#2f7aa6", accent: "#ffcf3a", outline: COOL, skin: "#f2c9a5", hair: "#7a4a2a", hands: "#f2c9a5", feet: "#eef1f5" },
668
+ head: "round",
669
+ ears: "human",
670
+ face: { eyes: "calm", mouth: "smirk", muzzle: "none", brows: "#5a3520" },
671
+ tail: "none",
672
+ headExtras: ["hair", "cap"],
673
+ bodyExtras: ["pocket", "joints"],
674
+ extraColors: ["#ff5d73"]
675
+ },
676
+ {
677
+ id: "explorer",
678
+ name: { en: "Explorer", it: "Giramondo" },
679
+ band: "9-12",
680
+ palette: { body: "#5f8f5b", limbs: "#5f8f5b", belly: "#5f8f5b", accent: "#d9bb7e", outline: COOL, skin: "#7a4a30", hair: "#1d1410", hands: "#7a4a30", feet: "#6b4a2a" },
681
+ head: "round",
682
+ ears: "human",
683
+ face: { eyes: "calm", mouth: "smile", muzzle: "none", brows: "#1d1410" },
684
+ tail: "none",
685
+ headExtras: ["curls", "safari-hat"],
686
+ bodyExtras: ["backpack", "straps", "boots"],
687
+ extraColors: ["#e07b39", "#6b4a2a"]
688
+ },
689
+ {
690
+ id: "wizard",
691
+ name: { en: "Wizard", it: "Mago" },
692
+ band: "9-12",
693
+ palette: { body: "#5b3fa0", limbs: "#5b3fa0", belly: "#5b3fa0", accent: "#3d5fc8", outline: COOL, skin: "#c98d62", hair: "#a8452a", hands: "#c98d62", feet: "#33255e" },
694
+ head: "round",
695
+ ears: "human",
696
+ face: { eyes: "calm", mouth: "smile", muzzle: "none", brows: "#7a301c" },
697
+ tail: "none",
698
+ headExtras: ["locks", "wizard-hat"],
699
+ bodyExtras: ["robe", "belt"],
700
+ extraColors: ["#ffd84a", "#ffd84a", "#ffd84a"]
701
+ },
702
+ {
703
+ id: "knight",
704
+ name: { en: "Knight", it: "Cavaliere" },
705
+ band: "9-12",
706
+ palette: { body: "#bcc7d3", limbs: "#8e9aa8", belly: "#dce3ea", accent: "#d8443a", outline: COOL, hands: "#6c7786", feet: "#6c7786" },
707
+ head: "bucket",
708
+ ears: "none",
709
+ face: { eyes: "slit", eyeColor: "#bfefff", mouth: "none", muzzle: "none" },
710
+ tail: "none",
711
+ headExtras: ["plume"],
712
+ bodyExtras: ["pauldrons", "emblem", "boots"],
713
+ extraColors: ["#d8443a"]
714
+ },
715
+ {
716
+ id: "racer",
717
+ name: { en: "Racer", it: "Pilota" },
718
+ band: "9-12",
719
+ palette: { body: "#ff6a2b", limbs: "#ff6a2b", belly: "#ffffff", accent: "#ffffff", outline: COOL, hands: "#232838", feet: "#232838" },
720
+ head: "round",
721
+ ears: "none",
722
+ face: { eyes: "goggle", eyeColor: "#7fd6ff", mouth: "none", muzzle: "none" },
723
+ tail: "none",
724
+ headExtras: ["stripe"],
725
+ bodyExtras: ["racing-stripe", "boots"]
726
+ },
727
+ {
728
+ id: "pirate",
729
+ name: { en: "Pirate", it: "Pirata" },
730
+ band: "9-12",
731
+ palette: { body: "#f2efe6", limbs: "#2a3d6e", belly: "#f2efe6", accent: "#d94545", outline: COOL, skin: "#d9a072", hair: "#2b1a12", hands: "#d9a072", feet: "#3a2a20" },
732
+ head: "round",
733
+ ears: "human",
734
+ face: { eyes: "calm", mouth: "smirk", muzzle: "none", brows: "#2b1a12" },
735
+ tail: "none",
736
+ headExtras: ["locks", "bandana", "eye-patch"],
737
+ bodyExtras: ["stripes", "belt", "boots"],
738
+ extraColors: ["#d94545", "#2a3d6e", "#4a3426"]
739
+ },
740
+ {
741
+ id: "diver",
742
+ name: { en: "Diver", it: "Sub" },
743
+ band: "9-12",
744
+ palette: { body: "#1f6582", limbs: "#1f6582", belly: "#1f6582", accent: "#ffb020", outline: COOL, skin: "#e8b48a", hair: "#c98f2e", hands: "#1b4f66", feet: "#ffb020" },
745
+ head: "round",
746
+ ears: "human",
747
+ face: { eyes: "calm", mouth: "smile", muzzle: "none", brows: "#8a5a1a" },
748
+ tail: "none",
749
+ headExtras: ["curls", "dive-mask"],
750
+ bodyExtras: ["tank", "fins"],
751
+ extraColors: ["#c9d3db"]
752
+ },
753
+ {
754
+ id: "hero",
755
+ name: { en: "Hero", it: "Supereroe" },
756
+ band: "9-12",
757
+ palette: { body: "#3557c8", limbs: "#3557c8", belly: "#3557c8", accent: "#e0403a", outline: COOL, skin: "#5a3825", hair: "#141010", hands: "#e0403a", feet: "#e0403a" },
758
+ head: "round",
759
+ ears: "human",
760
+ face: { eyes: "calm", mouth: "smirk", muzzle: "none", brows: "#141010" },
761
+ tail: "none",
762
+ headExtras: ["hair", "domino"],
763
+ bodyExtras: ["cape", "emblem", "boots"],
764
+ extraColors: ["#ffd84a"]
765
+ }
766
+ ];
767
+
768
+ // ../avatars/src/shapes.ts
769
+ var PATH = { M: 0, L: 1, Q: 2, C: 3, Z: 4 };
770
+ var PATH_ARGS = [2, 2, 4, 6, 0];
771
+ function drawShapes(ctx, shapes) {
772
+ for (const s of shapes) drawShape(ctx, s);
773
+ }
774
+ function drawShape(ctx, s) {
775
+ if (s.k === "group") {
776
+ ctx.save();
777
+ ctx.translate(s.x, s.y);
778
+ if (s.rot) ctx.rotate(s.rot);
779
+ if (s.s !== void 0 && s.s !== 1) ctx.scale(s.s, s.s);
780
+ if (s.alpha !== void 0) ctx.globalAlpha *= s.alpha;
781
+ drawShapes(ctx, s.items);
782
+ ctx.restore();
783
+ return;
784
+ }
785
+ ctx.save();
786
+ if (s.alpha !== void 0) ctx.globalAlpha *= s.alpha;
787
+ ctx.beginPath();
788
+ switch (s.k) {
789
+ case "circle":
790
+ ctx.arc(s.x, s.y, Math.max(s.r, 0), 0, Math.PI * 2);
791
+ break;
792
+ case "ellipse":
793
+ ctx.ellipse(s.x, s.y, Math.max(s.rx, 0), Math.max(s.ry, 0), s.rot ?? 0, 0, Math.PI * 2);
794
+ break;
795
+ case "rect":
796
+ roundRect(ctx, s);
797
+ break;
798
+ case "path":
799
+ if (typeof s.d === "string") tracePath(ctx, s.d);
800
+ else traceData(ctx, s.d);
801
+ break;
802
+ case "line":
803
+ ctx.moveTo(s.pts[0] ?? 0, s.pts[1] ?? 0);
804
+ for (let i = 2; i + 1 < s.pts.length; i += 2) ctx.lineTo(s.pts[i], s.pts[i + 1]);
805
+ break;
806
+ }
807
+ ctx.lineCap = "round";
808
+ ctx.lineJoin = "round";
809
+ if (s.fill && s.k !== "line") {
810
+ ctx.fillStyle = s.fill;
811
+ ctx.fill();
812
+ }
813
+ if (s.stroke && (s.lw ?? 1) > 0) {
814
+ ctx.strokeStyle = s.stroke;
815
+ ctx.lineWidth = s.lw ?? 1;
816
+ ctx.stroke();
817
+ }
818
+ ctx.restore();
819
+ }
820
+ function roundRect(ctx, s) {
821
+ const r = Math.max(0, Math.min(s.r, s.w / 2, s.h / 2));
822
+ ctx.translate(s.x, s.y);
823
+ if (s.rot) ctx.rotate(s.rot);
824
+ const x = -s.w / 2;
825
+ const y = -s.h / 2;
826
+ ctx.moveTo(x + r, y);
827
+ ctx.lineTo(x + s.w - r, y);
828
+ ctx.quadraticCurveTo(x + s.w, y, x + s.w, y + r);
829
+ ctx.lineTo(x + s.w, y + s.h - r);
830
+ ctx.quadraticCurveTo(x + s.w, y + s.h, x + s.w - r, y + s.h);
831
+ ctx.lineTo(x + r, y + s.h);
832
+ ctx.quadraticCurveTo(x, y + s.h, x, y + s.h - r);
833
+ ctx.lineTo(x, y + r);
834
+ ctx.quadraticCurveTo(x, y, x + r, y);
835
+ ctx.closePath();
836
+ }
837
+ function traceData(ctx, d) {
838
+ for (let i = 0; i < d.length; ) {
839
+ const c = d[i++];
840
+ if (c === PATH.M) ctx.moveTo(d[i], d[i + 1]);
841
+ else if (c === PATH.L) ctx.lineTo(d[i], d[i + 1]);
842
+ else if (c === PATH.Q) ctx.quadraticCurveTo(d[i], d[i + 1], d[i + 2], d[i + 3]);
843
+ else if (c === PATH.C) ctx.bezierCurveTo(d[i], d[i + 1], d[i + 2], d[i + 3], d[i + 4], d[i + 5]);
844
+ else if (c === PATH.Z) ctx.closePath();
845
+ else return;
846
+ i += PATH_ARGS[c];
847
+ }
848
+ }
849
+ function pathPoints(d) {
850
+ if (typeof d === "string") return (d.match(/-?\d*\.?\d+(?:e-?\d+)?/gi) ?? []).map(Number);
851
+ const out = [];
852
+ for (let i = 0; i < d.length; ) {
853
+ const n = PATH_ARGS[d[i++]];
854
+ if (n === void 0) break;
855
+ for (let k = 0; k < n; k++) out.push(d[i + k]);
856
+ i += n;
857
+ }
858
+ return out;
859
+ }
860
+ function tracePath(ctx, d) {
861
+ const tokens = d.match(/[MLQCZ]|-?\d*\.?\d+(?:e-?\d+)?/gi) ?? [];
862
+ let cmd = "";
863
+ let i = 0;
864
+ const n = () => Number(tokens[i++]);
865
+ while (i < tokens.length) {
866
+ const t = tokens[i];
867
+ if (/[MLQCZ]/i.test(t)) {
868
+ cmd = t.toUpperCase();
869
+ i++;
870
+ if (cmd === "Z") {
871
+ ctx.closePath();
872
+ continue;
873
+ }
874
+ }
875
+ if (cmd === "M") {
876
+ ctx.moveTo(n(), n());
877
+ cmd = "L";
878
+ } else if (cmd === "L") ctx.lineTo(n(), n());
879
+ else if (cmd === "Q") ctx.quadraticCurveTo(n(), n(), n(), n());
880
+ else if (cmd === "C") ctx.bezierCurveTo(n(), n(), n(), n(), n(), n());
881
+ else i++;
882
+ }
883
+ }
884
+ var f = (v) => {
885
+ const r = Math.round(v * 100) / 100;
886
+ return r === 0 ? 0 : r;
887
+ };
888
+ function roundedPolygon(pts2, round = 0.5) {
889
+ const n = pts2.length;
890
+ if (n < 3) return [];
891
+ const mid2 = (a, b, t2) => ({ x: a.x + (b.x - a.x) * t2, y: a.y + (b.y - a.y) * t2 });
892
+ const t = Math.min(Math.max(round, 0), 0.5);
893
+ const start = mid2(pts2[0], pts2[1], t);
894
+ const d = [PATH.M, f(start.x), f(start.y)];
895
+ for (let i = 1; i <= n; i++) {
896
+ const prev = pts2[(i - 1) % n];
897
+ const cur = pts2[i % n];
898
+ const next = pts2[(i + 1) % n];
899
+ const a = mid2(prev, cur, 1 - t);
900
+ const b = mid2(cur, next, t);
901
+ d.push(PATH.L, f(a.x), f(a.y), PATH.Q, f(cur.x), f(cur.y), f(b.x), f(b.y));
902
+ }
903
+ d.push(PATH.Z);
904
+ return d;
905
+ }
906
+ function smoothClosed(pts2, tension = 1) {
907
+ const n = pts2.length;
908
+ if (n < 3) return [];
909
+ const d = [PATH.M, f(pts2[0].x), f(pts2[0].y)];
910
+ const k = tension / 6;
911
+ for (let i = 0; i < n; i++) {
912
+ const p0 = pts2[(i - 1 + n) % n];
913
+ const p1 = pts2[i];
914
+ const p2 = pts2[(i + 1) % n];
915
+ const p3 = pts2[(i + 2) % n];
916
+ 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));
917
+ }
918
+ d.push(PATH.Z);
919
+ return d;
920
+ }
921
+ function smoothOpen(pts2) {
922
+ const n = pts2.length;
923
+ if (n < 2) return [];
924
+ const d = [PATH.M, f(pts2[0].x), f(pts2[0].y)];
925
+ for (let i = 0; i < n - 1; i++) {
926
+ const p0 = pts2[Math.max(i - 1, 0)];
927
+ const p1 = pts2[i];
928
+ const p2 = pts2[i + 1];
929
+ const p3 = pts2[Math.min(i + 2, n - 1)];
930
+ 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));
931
+ }
932
+ return d;
933
+ }
934
+ 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";
935
+
936
+ // ../avatars/src/head.ts
937
+ var WHITE = "#ffffff";
938
+ var SIDES = [-1, 1];
939
+ var MOUTH = "#3a2420";
940
+ var TONGUE = "#f2808f";
941
+ var DARK_GLASS = "#26324a";
942
+ var GOLD = "#ffd84a";
943
+ var pts = (...xy) => {
944
+ const out = [];
945
+ for (let i = 0; i + 1 < xy.length; i += 2) out.push({ x: xy[i], y: xy[i + 1] });
946
+ return out;
947
+ };
948
+ function headShapes(def, ow) {
949
+ const p = def.palette;
950
+ const out = [];
951
+ const ol = { stroke: p.outline, lw: ow };
952
+ const has = (x) => def.headExtras.includes(x);
953
+ const extra = (i, fallback) => def.extraColors?.[i] ?? fallback;
954
+ if (has("mane")) {
955
+ const n = 12;
956
+ for (let i = 0; i < n; i++) {
957
+ const a = i / n * Math.PI * 2;
958
+ 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 });
959
+ }
960
+ out.push({ k: "circle", x: 0, y: 0.04, r: 1.08, fill: extra(0, p.accent) });
961
+ }
962
+ if (has("rainbow-mane")) {
963
+ const c = def.extraColors ?? ["#f59ac0", "#fbd46a", "#8fd3f4"];
964
+ const locks = [
965
+ [0.98, 0.05, 0.3, 0.5, -0.25],
966
+ [0.92, 0.58, 0.26, 0.42, -0.1],
967
+ [0.62, 0.92, 0.22, 0.32, 0.4]
968
+ ];
969
+ locks.forEach(([x, y, rx, ry, rot], i) => out.push({ k: "ellipse", x, y, rx, ry, rot, fill: c[i % c.length], ...ol }));
970
+ }
971
+ if (has("spikes")) {
972
+ for (const a of [-0.55, 0, 0.55]) {
973
+ const tip = polar(a, 1.48);
974
+ const b1 = polar(a - 0.32, 0.86);
975
+ const b2 = polar(a + 0.32, 0.86);
976
+ out.push({ k: "path", d: roundedPolygon([b1, tip, b2], 0.22), fill: p.accent, ...ol });
977
+ }
978
+ }
979
+ if (has("curl")) {
980
+ 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 });
981
+ }
982
+ if (has("wool")) {
983
+ for (let i = 0; i <= 8; i++) {
984
+ const a = -1.75 + i / 8 * 3.5;
985
+ 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 });
986
+ }
987
+ }
988
+ if (has("crest")) {
989
+ ;
990
+ [-0.38, 0, 0.38].forEach((a, i) => {
991
+ const c = polar(a, 1.12);
992
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.15, ry: 0.42, rot: a, fill: extra(i, p.accent), ...ol });
993
+ });
994
+ }
995
+ if (has("plume")) {
996
+ 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 });
997
+ }
998
+ if (has("locks")) {
999
+ 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 });
1000
+ }
1001
+ if (has("ninja-hood")) {
1002
+ for (const [rot, len2] of [
1003
+ [0.35, 0.5],
1004
+ [0.85, 0.42]
1005
+ ])
1006
+ 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 });
1007
+ }
1008
+ if (has("bandana")) {
1009
+ for (const [rot, len2] of [
1010
+ [Math.PI - 0.5, 0.42],
1011
+ [Math.PI - 1, 0.36]
1012
+ ])
1013
+ 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 });
1014
+ }
1015
+ if (has("ruff")) {
1016
+ for (const s of SIDES)
1017
+ 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 });
1018
+ }
1019
+ if (def.face.eyesOnTop) {
1020
+ for (const s of SIDES) out.push({ k: "circle", x: s * 0.46, y: -0.72, r: 0.38, fill: p.body, ...ol });
1021
+ }
1022
+ out.push(...ears(def, ow));
1023
+ out.push(headShape(def, ol));
1024
+ const muzzle = def.face.muzzle;
1025
+ const mc = p.belly;
1026
+ if (muzzle === "oval") out.push({ k: "ellipse", x: 0, y: 0.4, rx: 0.46, ry: 0.32, fill: mc });
1027
+ if (muzzle === "monkey") {
1028
+ for (const s of SIDES) out.push({ k: "circle", x: s * 0.32, y: -0.04, r: 0.38, fill: mc });
1029
+ out.push({ k: "ellipse", x: 0, y: 0.4, rx: 0.56, ry: 0.4, fill: mc });
1030
+ }
1031
+ if (muzzle === "heart") {
1032
+ for (const s of SIDES) out.push({ k: "circle", x: s * 0.36, y: 0.02, r: 0.5, fill: mc });
1033
+ out.push({ k: "ellipse", x: 0, y: 0.38, rx: 0.66, ry: 0.48, fill: mc });
1034
+ }
1035
+ if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
1036
+ 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 });
1037
+ if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
1038
+ if (has("stripes")) {
1039
+ const c = extra(0, p.outline);
1040
+ out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
1041
+ for (const s of SIDES) {
1042
+ out.push({ k: "line", pts: [s * 0.3, -0.93, s * 0.24, -0.7], stroke: c, lw: 0.1 });
1043
+ out.push({ k: "line", pts: [s * 0.98, -0.05, s * 0.74, 0], stroke: c, lw: 0.1 });
1044
+ out.push({ k: "line", pts: [s * 0.94, 0.22, s * 0.74, 0.22], stroke: c, lw: 0.09 });
1045
+ }
1046
+ }
1047
+ if (has("spots")) {
1048
+ const c = extra(1, p.belly);
1049
+ out.push({ k: "circle", x: -0.55, y: -0.5, r: 0.15, fill: c });
1050
+ out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
1051
+ out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
1052
+ }
1053
+ 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) });
1054
+ 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 });
1055
+ if (has("gills")) {
1056
+ 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 });
1057
+ }
1058
+ const hair = p.hair ?? p.outline;
1059
+ 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 });
1060
+ 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 });
1061
+ if (has("curls")) {
1062
+ for (let i = 0; i < 7; i++) {
1063
+ const c = polar(-1.3 + i / 6 * 2.6, 0.86);
1064
+ out.push({ k: "circle", x: c.x, y: c.y, r: 0.3, fill: hair, ...ol });
1065
+ }
1066
+ out.push({ k: "ellipse", x: 0, y: -0.64, rx: 0.8, ry: 0.36, fill: hair });
1067
+ }
1068
+ if (has("ninja-hood")) {
1069
+ 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 });
1070
+ 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 });
1071
+ }
1072
+ if (has("domino")) {
1073
+ 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 });
1074
+ for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.38, y: 0, rx: 0.19, ry: 0.13, fill: WHITE });
1075
+ }
1076
+ out.push(...eyes(def, ow));
1077
+ if (has("brows")) {
1078
+ 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 });
1079
+ }
1080
+ 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 });
1081
+ out.push(...mouth(def, ow));
1082
+ 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 });
1083
+ if (has("whiskers")) {
1084
+ for (const s of SIDES)
1085
+ 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 });
1086
+ }
1087
+ if (has("rainbow-mane")) {
1088
+ const c = def.extraColors ?? ["#f59ac0", "#fbd46a", "#8fd3f4"];
1089
+ out.push({ k: "ellipse", x: 0.42, y: -0.82, rx: 0.34, ry: 0.22, rot: 0.5, fill: c[0], ...ol });
1090
+ 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 });
1091
+ }
1092
+ if (def.ears === "unicorn") {
1093
+ const horn = [
1094
+ { x: -0.18, y: -0.84 },
1095
+ { x: 0.02, y: -1.72 },
1096
+ { x: 0.18, y: -0.84 }
1097
+ ];
1098
+ out.push({ k: "path", d: roundedPolygon(horn, 0.12), fill: p.accent, ...ol });
1099
+ for (const y of [-1, -1.2, -1.4]) {
1100
+ const w = 0.16 * ((y + 1.72) / 0.88);
1101
+ out.push({ k: "line", pts: [-w, y + 0.04, w, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
1102
+ }
1103
+ }
1104
+ if (has("helmet")) {
1105
+ 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 });
1106
+ 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 });
1107
+ }
1108
+ 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 });
1109
+ if (has("tuft")) {
1110
+ 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 });
1111
+ }
1112
+ if (has("eggshell")) {
1113
+ 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 });
1114
+ }
1115
+ 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 });
1116
+ if (has("stripe")) out.push({ k: "rect", x: 0, y: -0.64, w: 0.38, h: 0.62, r: 0.06, fill: p.accent });
1117
+ if (has("eye-patch")) {
1118
+ out.push({ k: "line", pts: [-0.97, -0.5, 0.97, 0.13], stroke: p.outline, lw: 0.07 });
1119
+ out.push({ k: "ellipse", x: 0.38, y: 0.02, rx: 0.21, ry: 0.19, fill: p.outline });
1120
+ }
1121
+ if (has("bandana")) {
1122
+ 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 });
1123
+ 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 });
1124
+ out.push({ k: "circle", x: -0.98, y: -0.34, r: 0.15, fill: p.accent, ...ol });
1125
+ }
1126
+ if (has("cap")) {
1127
+ 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 });
1128
+ 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 });
1129
+ 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 });
1130
+ out.push({ k: "circle", x: 0.5, y: -0.82, r: 0.15, fill: extra(0, WHITE), ...ol });
1131
+ 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 });
1132
+ }
1133
+ if (has("safari-hat")) {
1134
+ out.push({ k: "ellipse", x: 0, y: -0.56, rx: 1.48, ry: 0.3, fill: p.accent, ...ol });
1135
+ 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 });
1136
+ 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 });
1137
+ }
1138
+ if (has("wizard-hat")) {
1139
+ out.push({ k: "ellipse", x: 0, y: -0.62, rx: 1.32, ry: 0.26, fill: p.accent, ...ol });
1140
+ 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 });
1141
+ const star = extra(0, GOLD);
1142
+ 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 }] });
1143
+ }
1144
+ if (has("dive-mask")) {
1145
+ 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 });
1146
+ out.push({ k: "rect", x: 0, y: 0, w: 1.42, h: 0.62, r: 0.28, fill: "#bfe9f5", alpha: 0.35 });
1147
+ out.push({ k: "rect", x: 0, y: 0, w: 1.42, h: 0.62, r: 0.28, stroke: p.outline, lw: 0.2 });
1148
+ out.push({ k: "rect", x: 0, y: 0, w: 1.42, h: 0.62, r: 0.28, stroke: p.accent, lw: 0.11 });
1149
+ 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 });
1150
+ const tube = "M0.5 0.52 Q0.98 0.56 1.04 0.1 L1.12 -0.92";
1151
+ out.push({ k: "path", d: tube, stroke: p.outline, lw: 0.24 }, { k: "path", d: tube, stroke: p.accent, lw: 0.13 });
1152
+ }
1153
+ return out;
1154
+ }
1155
+ function headShape(def, ol) {
1156
+ const fill = def.headExtras.includes("ninja-hood") ? def.palette.body : def.palette.skin ?? def.palette.body;
1157
+ switch (def.head) {
1158
+ case "round":
1159
+ return { k: "circle", x: 0, y: 0, r: 1, fill, ...ol };
1160
+ case "wide":
1161
+ return { k: "ellipse", x: 0, y: 0.03, rx: 1.12, ry: 0.94, fill, ...ol };
1162
+ case "tall":
1163
+ return { k: "ellipse", x: 0, y: -0.02, rx: 0.94, ry: 1.04, fill, ...ol };
1164
+ case "square":
1165
+ return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
1166
+ case "bucket":
1167
+ 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 };
1168
+ case "bulb":
1169
+ return {
1170
+ k: "path",
1171
+ d: smoothClosed([
1172
+ { x: 0, y: -1.02 },
1173
+ { x: 0.86, y: -0.74 },
1174
+ { x: 1.08, y: -0.12 },
1175
+ { x: 0.72, y: 0.62 },
1176
+ { x: 0, y: 0.92 },
1177
+ { x: -0.72, y: 0.62 },
1178
+ { x: -1.08, y: -0.12 },
1179
+ { x: -0.86, y: -0.74 }
1180
+ ]),
1181
+ fill,
1182
+ ...ol
1183
+ };
1184
+ }
1185
+ }
1186
+ function headEllipse(def) {
1187
+ if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
1188
+ if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
1189
+ return { cy: 0.03, rx: 1.12, ry: 0.94 };
1190
+ }
1191
+ function lowerFace(def, ow) {
1192
+ const { cy, rx: hx, ry: hy } = headEllipse(def);
1193
+ const rx = hx - ow / 2;
1194
+ const ry = hy - ow / 2;
1195
+ const top = 0.15;
1196
+ const a0 = Math.asin(Math.min(1, (top - cy) / ry));
1197
+ const x0 = Math.cos(a0) * rx;
1198
+ const r = (v) => Math.round(v * 100) / 100 || 0;
1199
+ const d = [PATH.M, r(-x0), top];
1200
+ const wave = [
1201
+ [-0.84 * x0, 0.02, -0.53 * x0, 0.06, -0.32 * x0, 0.24],
1202
+ [-0.17 * x0, 0.11, 0.17 * x0, 0.11, 0.32 * x0, 0.24],
1203
+ [0.53 * x0, 0.06, 0.84 * x0, 0.02, x0, top]
1204
+ ];
1205
+ for (const c of wave) d.push(PATH.C, ...c.map(r));
1206
+ const n = 24;
1207
+ for (let i = 1; i <= n; i++) {
1208
+ const a = a0 + (Math.PI - 2 * a0) * i / n;
1209
+ d.push(PATH.L, r(Math.cos(a) * rx), r(cy + Math.sin(a) * ry));
1210
+ }
1211
+ d.push(PATH.Z);
1212
+ return d;
1213
+ }
1214
+ function ears(def, ow) {
1215
+ const p = def.palette;
1216
+ const ol = { stroke: p.outline, lw: ow };
1217
+ const out = [];
1218
+ for (const s of SIDES) {
1219
+ switch (def.ears) {
1220
+ case "pointy":
1221
+ case "unicorn": {
1222
+ const k = def.ears === "unicorn" ? 0.8 : 1;
1223
+ const base1 = { x: s * 0.3, y: -0.82 };
1224
+ const tip = { x: s * 0.86, y: -0.82 - 0.62 * k };
1225
+ const base2 = { x: s * 0.94, y: -0.36 };
1226
+ out.push({ k: "path", d: roundedPolygon([base1, tip, base2], 0.2), fill: p.body, ...ol });
1227
+ const c = { x: (base1.x + tip.x + base2.x) / 3, y: (base1.y + tip.y + base2.y) / 3 };
1228
+ 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 }));
1229
+ out.push({ k: "path", d: roundedPolygon(inner, 0.25), fill: p.accent });
1230
+ break;
1231
+ }
1232
+ case "round":
1233
+ out.push({ k: "circle", x: s * 0.72, y: -0.72, r: 0.36, fill: p.ears ?? p.body, ...ol });
1234
+ if (!p.ears) out.push({ k: "circle", x: s * 0.74, y: -0.74, r: 0.19, fill: p.accent });
1235
+ break;
1236
+ case "big-round":
1237
+ out.push({ k: "circle", x: s * 0.96, y: -0.46, r: 0.52, fill: p.body, ...ol });
1238
+ out.push({ k: "circle", x: s * 1, y: -0.44, r: 0.32, fill: p.accent });
1239
+ break;
1240
+ case "long":
1241
+ 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 });
1242
+ out.push({ k: "ellipse", x: s * 0.39, y: -1.36, rx: 0.12, ry: 0.48, rot: s * 0.14, fill: p.accent });
1243
+ break;
1244
+ case "side":
1245
+ out.push({ k: "circle", x: s * 1, y: 0.02, r: 0.34, fill: p.body, ...ol });
1246
+ out.push({ k: "circle", x: s * 1.02, y: 0.02, r: 0.2, fill: p.belly });
1247
+ break;
1248
+ case "tufts":
1249
+ 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 });
1250
+ out.push({ k: "line", pts: [s * 0.62, -0.78, s * 1, -0.98], stroke: p.accent, lw: 0.08 });
1251
+ break;
1252
+ case "antennae": {
1253
+ const tip = { x: s * 0.62, y: -1.42 };
1254
+ 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 });
1255
+ 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 });
1256
+ out.push({ k: "circle", x: tip.x, y: tip.y, r: 0.17, fill: p.accent, ...ol });
1257
+ break;
1258
+ }
1259
+ case "floppy":
1260
+ 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 });
1261
+ break;
1262
+ case "droopy":
1263
+ 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 });
1264
+ out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
1265
+ break;
1266
+ case "human":
1267
+ out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
1268
+ break;
1269
+ case "horns": {
1270
+ const pts2 = [
1271
+ { x: s * 0.3, y: -0.84 },
1272
+ { x: s * 0.6, y: -1.5 },
1273
+ { x: s * 0.66, y: -0.68 }
1274
+ ];
1275
+ out.push({ k: "path", d: roundedPolygon(pts2, 0.18), fill: p.accent, ...ol });
1276
+ break;
1277
+ }
1278
+ }
1279
+ }
1280
+ 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 });
1281
+ if (def.ears === "antenna") {
1282
+ out.push({ k: "line", pts: [0, -0.85, 0, -1.42], stroke: p.outline, lw: ow * 2.6 });
1283
+ out.push({ k: "line", pts: [0, -0.85, 0, -1.42], stroke: p.limbs, lw: ow * 1.2 });
1284
+ out.push({ k: "circle", x: 0, y: -1.5, r: 0.18, fill: p.accent, ...ol });
1285
+ }
1286
+ return out;
1287
+ }
1288
+ function eyes(def, ow) {
1289
+ const p = def.palette;
1290
+ const f2 = def.face;
1291
+ const out = [];
1292
+ const ink = p.outline;
1293
+ const ex = f2.eyesOnTop ? 0.46 : 0.4;
1294
+ const ey = f2.eyesOnTop ? -0.72 : -0.04;
1295
+ const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
1296
+ switch (f2.eyes) {
1297
+ case "dot":
1298
+ for (const s of SIDES) {
1299
+ out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.12, ry: 0.155, fill: ink });
1300
+ out.push(shine(s * ex + 0.04, ey - 0.06, 0.045));
1301
+ }
1302
+ break;
1303
+ case "bead":
1304
+ for (const s of SIDES) {
1305
+ out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.16, ry: 0.22, fill: ink });
1306
+ out.push(shine(s * ex + 0.05, ey - 0.08, 0.065));
1307
+ }
1308
+ break;
1309
+ case "big":
1310
+ for (const s of SIDES) {
1311
+ if (f2.eyeRing) out.push({ k: "circle", x: s * ex, y: ey, r: 0.36, fill: f2.eyeRing, stroke: ink, lw: ow * 0.8 });
1312
+ out.push({ k: "circle", x: s * ex, y: ey, r: 0.25, fill: WHITE, stroke: ink, lw: ow * 0.8 });
1313
+ out.push({ k: "circle", x: s * ex + s * -0.03, y: ey + 0.03, r: 0.14, fill: f2.eyeColor ?? ink });
1314
+ out.push(shine(s * ex + s * -0.03 + 0.05, ey - 0.03, 0.05));
1315
+ }
1316
+ break;
1317
+ case "mask":
1318
+ for (const s of SIDES) {
1319
+ out.push({ k: "ellipse", x: s * 0.4, y: 0, rx: 0.24, ry: 0.32, rot: s * -0.55, fill: p.limbs });
1320
+ out.push({ k: "circle", x: s * 0.4, y: -0.02, r: 0.11, fill: WHITE });
1321
+ out.push({ k: "circle", x: s * 0.39, y: -0, r: 0.065, fill: ink });
1322
+ }
1323
+ break;
1324
+ case "screen":
1325
+ 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 });
1326
+ for (const s of SIDES) {
1327
+ out.push({ k: "ellipse", x: s * 0.36, y: 0, rx: 0.2, ry: 0.24, fill: f2.eyeColor ?? "#7ff0ff", alpha: 0.3 });
1328
+ out.push({ k: "ellipse", x: s * 0.36, y: 0, rx: 0.12, ry: 0.16, fill: f2.eyeColor ?? "#7ff0ff" });
1329
+ }
1330
+ break;
1331
+ case "almond":
1332
+ for (const s of SIDES) {
1333
+ 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 });
1334
+ out.push(shine(s * 0.42 - s * 0.07, -0.08, 0.07));
1335
+ }
1336
+ break;
1337
+ case "calm":
1338
+ for (const s of SIDES) {
1339
+ out.push({ k: "ellipse", x: s * 0.38, y: 0.02, rx: 0.085, ry: 0.12, fill: ink });
1340
+ out.push(shine(s * 0.38 + 0.03, -0.02, 0.03));
1341
+ }
1342
+ break;
1343
+ case "slit":
1344
+ out.push({ k: "line", pts: [0, -0.98, 0, -0.2], stroke: ink, lw: ow * 0.8, alpha: 0.5 });
1345
+ 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 });
1346
+ 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 });
1347
+ for (const s of SIDES) {
1348
+ out.push({ k: "ellipse", x: s * 0.4, y: -0.04, rx: 0.12, ry: 0.07, fill: f2.eyeColor ?? WHITE });
1349
+ 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 });
1350
+ }
1351
+ break;
1352
+ case "goggle":
1353
+ 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 });
1354
+ 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 });
1355
+ 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 });
1356
+ 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 });
1357
+ 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 });
1358
+ break;
1359
+ case "visor":
1360
+ out.push({ k: "ellipse", x: 0, y: 0.08, rx: 0.8, ry: 0.64, fill: "#2a3f6b", stroke: ink, lw: ow * 0.8 });
1361
+ 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 });
1362
+ 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" });
1363
+ break;
1364
+ }
1365
+ return out;
1366
+ }
1367
+ function mouth(def, ow) {
1368
+ const p = def.palette;
1369
+ const f2 = def.face;
1370
+ const out = [];
1371
+ const dark = f2.eyes === "visor" ? "#cfeeff" : MOUTH;
1372
+ const lw = ow * 1.05;
1373
+ if (f2.nose && f2.mouth !== "beak") {
1374
+ 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 });
1375
+ else if (def.ears === "big-round") out.push({ k: "ellipse", x: 0, y: 0.22, rx: 0.2, ry: 0.25, fill: f2.nose });
1376
+ else out.push({ k: "ellipse", x: 0, y: 0.25, rx: 0.13, ry: 0.085, fill: f2.nose });
1377
+ }
1378
+ switch (f2.mouth) {
1379
+ case "smile":
1380
+ 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 });
1381
+ break;
1382
+ case "open":
1383
+ 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 });
1384
+ out.push({ k: "ellipse", x: 0, y: 0.6, rx: 0.1, ry: 0.06, fill: TONGUE });
1385
+ break;
1386
+ case "cat":
1387
+ 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 });
1388
+ break;
1389
+ case "beak":
1390
+ 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 });
1391
+ break;
1392
+ case "teeth":
1393
+ 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 });
1394
+ 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 });
1395
+ break;
1396
+ case "grill":
1397
+ 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 });
1398
+ 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 });
1399
+ break;
1400
+ case "tiny":
1401
+ out.push({ k: "ellipse", x: 0, y: 0.44, rx: 0.08, ry: 0.1, fill: dark });
1402
+ break;
1403
+ case "smirk":
1404
+ out.push({ k: "path", d: "M-0.16 0.46 Q0.06 0.54 0.22 0.38", stroke: dark, lw });
1405
+ break;
1406
+ case "bill":
1407
+ 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 });
1408
+ out.push({ k: "path", d: "M-0.36 0.38 Q0 0.46 0.36 0.38", stroke: p.outline, lw: ow * 0.7 });
1409
+ for (const s of SIDES) out.push({ k: "circle", x: s * 0.1, y: 0.28, r: 0.03, fill: p.outline });
1410
+ break;
1411
+ case "hook":
1412
+ out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
1413
+ 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 });
1414
+ break;
1415
+ case "jaws": {
1416
+ 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 });
1417
+ for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
1418
+ const y = 0.3 + 0.4 * (x + 0.5) * (0.5 - x) - 0.01;
1419
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.075, y, x + 0.075, y, x, y + 0.13), 0.15), fill: WHITE });
1420
+ }
1421
+ break;
1422
+ }
1423
+ }
1424
+ return out;
1425
+ }
1426
+ var polar = (a, r) => ({ x: Math.sin(a) * r, y: -Math.cos(a) * r });
1427
+
1428
+ // ../avatars/src/rig.ts
1429
+ var VISIBLE = 0.5;
1430
+ var FACE = { leftEye: 2, rightEye: 5, leftEar: 7, rightEar: 8 };
1431
+ var BAND_BODY = {
1432
+ "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 },
1433
+ "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 },
1434
+ "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 }
1435
+ };
1436
+ var DEFAULT_BAND = "6-8";
1437
+ var bodyBand = (def) => def.band in BAND_BODY ? def.band : DEFAULT_BAND;
1438
+ var HEAD = BAND_BODY[DEFAULT_BAND].head;
1439
+ var MAX_TILT = 0.3;
1440
+ var TILT_DEAD_ZONE = 0.12;
1441
+ var TILT_TRUSTED_TO = 0.9;
1442
+ var TILT_FADE = 0.5;
1443
+ var FACE_VISIBLE = 0.65;
1444
+ var NOSE_FOLLOW = 0.35;
1445
+ var NOSE_REACH = 0.2;
1446
+ var FORESHORTENED = 0.25;
1447
+ var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
1448
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
1449
+ var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
1450
+ var len = (a) => Math.hypot(a.x, a.y);
1451
+ var mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
1452
+ var norm = (a) => {
1453
+ const l = len(a);
1454
+ return l > 1e-9 ? { x: a.x / l, y: a.y / l } : { x: 0, y: 1 };
1455
+ };
1456
+ var finite = (...v) => v.every(Number.isFinite);
1457
+ var wrap = (a) => Math.atan2(Math.sin(a), Math.cos(a));
1458
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
1459
+ function joints(body, box, mirrored = false) {
1460
+ return (i) => {
1461
+ const x = body[i * 3];
1462
+ const y = body[i * 3 + 1];
1463
+ const v = body[i * 3 + 2];
1464
+ if (x === void 0 || y === void 0 || v === void 0 || !finite(x, y, v)) return { x: 0, y: 0, ok: false };
1465
+ return { x: box.x + (mirrored ? 1 - x : x) * box.w, y: box.y + y * box.h, ok: v >= VISIBLE };
1466
+ };
1467
+ }
1468
+ function frameOf(body, box, opts = {}) {
1469
+ const at = joints(body, box, opts.mirrored);
1470
+ const ls = at(P.leftShoulder);
1471
+ const rs = at(P.rightShoulder);
1472
+ if (!ls.ok || !rs.ok) return null;
1473
+ const lh = at(P.leftHip);
1474
+ const rh = at(P.rightHip);
1475
+ const S = mid(ls, rs);
1476
+ const sw = len(sub(rs, ls));
1477
+ const hipsOk = lh.ok && rh.ok;
1478
+ let across = norm(sub(rs, ls));
1479
+ if (sw < 1e-6) across = { x: 1, y: 0 };
1480
+ let d;
1481
+ let L;
1482
+ let U;
1483
+ if (hipsOk) {
1484
+ const H = mid(lh, rh);
1485
+ L = len(sub(H, S));
1486
+ if (L >= sw * FORESHORTENED) d = norm(sub(H, S));
1487
+ else {
1488
+ d = { x: -across.y, y: across.x };
1489
+ if (d.y < 0) d = mul(d, -1);
1490
+ }
1491
+ U = Math.max(L, sw * 1.15);
1492
+ } else {
1493
+ d = { x: -across.y, y: across.x };
1494
+ if (d.y < 0) d = mul(d, -1);
1495
+ U = sw * 1.15;
1496
+ L = U;
1497
+ }
1498
+ U = Math.max(U, 1e-6);
1499
+ const r = { x: d.y, y: -d.x };
1500
+ const angle = Math.atan2(r.y, r.x);
1501
+ const up = mul(d, -1);
1502
+ const bb = BAND_BODY[opts.band ?? DEFAULT_BAND] ?? BAND_BODY[DEFAULT_BAND];
1503
+ const headR = bb.head * U;
1504
+ let head = add(S, mul(up, headR * bb.neck));
1505
+ const nose = at(P.nose);
1506
+ if (nose.ok) {
1507
+ const want = add(nose, mul(up, headR * 0.15));
1508
+ const delta = mul(sub(want, head), NOSE_FOLLOW);
1509
+ const k = Math.min(1, headR * NOSE_REACH / Math.max(len(delta), 1e-6));
1510
+ head = add(head, mul(delta, k));
1511
+ }
1512
+ return { S, r, d, U, L, hipsOk, head, headR, headRot: angle + headTilt(body, at, angle, U), angle };
1513
+ }
1514
+ function headTilt(body, at, angle, U) {
1515
+ const pairs = [
1516
+ [FACE.leftEar, FACE.rightEar],
1517
+ [FACE.leftEye, FACE.rightEye]
1518
+ ];
1519
+ for (const [a, b] of pairs) {
1520
+ const p = at(a);
1521
+ const q = at(b);
1522
+ 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;
1523
+ const t = wrap(Math.atan2(q.y - p.y, q.x - p.x) - angle);
1524
+ const past = Math.max(Math.abs(t) - TILT_DEAD_ZONE, 0);
1525
+ const trust = clamp((TILT_TRUSTED_TO + TILT_FADE - Math.abs(t)) / TILT_FADE, 0, 1);
1526
+ return Math.sign(t) * Math.min(past, MAX_TILT) * trust;
1527
+ }
1528
+ return 0;
1529
+ }
1530
+ function avatarShapes(body, def, box, opts = {}) {
1531
+ const band = bodyBand(def);
1532
+ const bb = BAND_BODY[band];
1533
+ const F = frameOf(body, box, { ...opts, band });
1534
+ if (!F) return [];
1535
+ const at = joints(body, box, opts.mirrored);
1536
+ const p = def.palette;
1537
+ const { S, r, d, U, L } = F;
1538
+ const has = (x) => def.bodyExtras.includes(x);
1539
+ const ow = Math.max(1.2, U * 0.045);
1540
+ const ol = { stroke: p.outline, lw: ow };
1541
+ const T = (x, y) => ({ x: S.x + r.x * x + d.x * y, y: S.y + r.y * x + d.y * y });
1542
+ const TP = (pts2) => pts2.map(([x, y]) => T(x, y));
1543
+ const ls = at(P.leftShoulder);
1544
+ const rs = at(P.rightShoulder);
1545
+ const lh = at(P.leftHip);
1546
+ const rh = at(P.rightHip);
1547
+ const sw = len(sub(rs, ls));
1548
+ const hw = F.hipsOk ? len(sub(rh, lh)) : sw * 0.7;
1549
+ const ht = Math.max(sw / 2 + bb.shoulderPad * U, bb.shoulderMin * U);
1550
+ const hb = Math.max(hw / 2 + bb.hipPad * U, bb.hipMin * U);
1551
+ const y0 = -0.1 * U;
1552
+ const y1 = L + bb.bottom * U;
1553
+ const back = [];
1554
+ const legs = [];
1555
+ const torso = [];
1556
+ const arms = [];
1557
+ const front = [];
1558
+ const limb = (out, pts2, w, color) => {
1559
+ const flat = pts2.flatMap((q) => [q.x, q.y]);
1560
+ out.push({ k: "line", pts: flat, stroke: p.outline, lw: w + ow * 2 });
1561
+ out.push({ k: "line", pts: flat, stroke: color, lw: w });
1562
+ };
1563
+ const legW = bb.leg * U;
1564
+ const armW = bb.arm * U;
1565
+ const pad = Math.max(bb.arm / 0.22, 0.9);
1566
+ if (F.hipsOk) {
1567
+ for (const [hip, kneeI, ankleI, side] of [
1568
+ [lh, P.leftKnee, P.leftAnkle, -1],
1569
+ [rh, P.rightKnee, P.rightAnkle, 1]
1570
+ ]) {
1571
+ const knee = at(kneeI);
1572
+ const ankle = at(ankleI);
1573
+ if (!knee.ok) continue;
1574
+ const top = add(hip, mul(d, -0.12 * U));
1575
+ const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
1576
+ limb(legs, pts2, legW, p.limbs);
1577
+ if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
1578
+ if (ankle.ok) legs.push(foot(def, ankle, F, side, ow, bb.foot));
1579
+ }
1580
+ }
1581
+ for (const [sh, elbowI, wristI, side] of [
1582
+ [ls, P.leftElbow, P.leftWrist, -1],
1583
+ [rs, P.rightElbow, P.rightWrist, 1]
1584
+ ]) {
1585
+ const elbow = at(elbowI);
1586
+ const wrist = at(wristI);
1587
+ if (has("pauldrons")) {
1588
+ const c = add(sh, mul(d, 0.02 * U));
1589
+ 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 });
1590
+ const q = add(c, mul(d, 0.05 * U));
1591
+ 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 });
1592
+ }
1593
+ if (!elbow.ok) continue;
1594
+ const raised = bb.armsOverHead && (elbow.x - S.x) * d.x + (elbow.y - S.y) * d.y < -0.25 * U;
1595
+ const out = raised ? front : arms;
1596
+ const top = add(sh, mul(d, 0.04 * U));
1597
+ limb(out, wrist.ok ? [top, elbow, wrist] : [top, elbow], armW, p.limbs);
1598
+ if (has("joints")) out.push({ k: "circle", x: elbow.x, y: elbow.y, r: 0.13 * U * pad, fill: p.accent, ...ol });
1599
+ if (!wrist.ok) continue;
1600
+ const dir = norm(sub(wrist, elbow));
1601
+ const hand = add(wrist, mul(dir, 0.05 * U));
1602
+ if (has("feathers")) {
1603
+ for (const a of [-0.55, 0, 0.55]) {
1604
+ const c = Math.cos(a);
1605
+ const s = Math.sin(a) * side;
1606
+ const fd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
1607
+ const q = add(hand, mul(fd, 0.16 * U));
1608
+ 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 });
1609
+ }
1610
+ }
1611
+ out.push({ k: "circle", x: hand.x, y: hand.y, r: bb.hand * U, fill: p.hands ?? p.limbs, ...ol });
1612
+ }
1613
+ if (has("wings")) {
1614
+ for (const s of [-1, 1]) {
1615
+ const w = TP([
1616
+ [s * ht * 0.5, 0.05 * U],
1617
+ [s * (ht + 0.85 * U), -0.6 * U],
1618
+ [s * (ht + 0.72 * U), 0.05 * U],
1619
+ [s * (ht + 0.45 * U), -0.02 * U],
1620
+ [s * (ht + 0.3 * U), 0.32 * U],
1621
+ [s * ht * 0.6, 0.55 * U]
1622
+ ]);
1623
+ back.push({ k: "path", d: roundedPolygon(w, 0.18), fill: p.accent, ...ol });
1624
+ }
1625
+ }
1626
+ if (has("cape")) {
1627
+ const yb = L + 0.8 * U;
1628
+ const wb = hb + 0.36 * U;
1629
+ const cape = TP([
1630
+ [-ht * 0.92, y0 + 0.04 * U],
1631
+ [ht * 0.92, y0 + 0.04 * U],
1632
+ [ht + 0.16 * U, L * 0.5],
1633
+ [wb, yb],
1634
+ [wb * 0.5, yb - 0.1 * U],
1635
+ [0, yb],
1636
+ [-wb * 0.5, yb - 0.1 * U],
1637
+ [-wb, yb],
1638
+ [-ht - 0.16 * U, L * 0.5]
1639
+ ]);
1640
+ back.push({ k: "path", d: smoothClosed(cape, 0.6), fill: p.accent, ...ol });
1641
+ }
1642
+ if (has("backpack")) {
1643
+ const c = T(0, 0.35 * U);
1644
+ 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 });
1645
+ }
1646
+ if (has("tank")) {
1647
+ const c = T(ht * 0.95, 0.24 * U);
1648
+ const col = def.extraColors?.[0] ?? p.accent;
1649
+ const valve = T(ht * 0.95, -0.3 * U);
1650
+ 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 });
1651
+ 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 });
1652
+ }
1653
+ if (F.hipsOk && !opts.portrait) back.push(...tail(def, F, hb, ow));
1654
+ if (has("tentacles") && F.hipsOk) {
1655
+ for (const s of [-1, 1]) {
1656
+ const pts2 = TP([
1657
+ [s * hb * 0.7, L * 0.9],
1658
+ [s * (hb + 0.22 * U), L + 0.3 * U],
1659
+ [s * (hb + 0.48 * U), L + 0.22 * U],
1660
+ [s * (hb + 0.5 * U), L - 0.02 * U],
1661
+ [s * (hb + 0.34 * U), L - 0.06 * U]
1662
+ ]);
1663
+ back.push({ k: "path", d: smoothOpen(pts2), stroke: p.outline, lw: 0.2 * U + ow * 2 });
1664
+ back.push({ k: "path", d: smoothOpen(pts2), stroke: p.limbs, lw: 0.2 * U });
1665
+ }
1666
+ }
1667
+ const bulge = Math.max(ht, hb) * bb.bulge;
1668
+ if (has("wool")) {
1669
+ for (const [x, y] of [
1670
+ [ht * 0.7, y0 + 0.02 * U],
1671
+ [bulge, L * 0.3],
1672
+ [bulge * 1.02, L * 0.62],
1673
+ [(bulge + hb) / 2, L * 0.92],
1674
+ [hb * 0.55, y1]
1675
+ ])
1676
+ for (const s of [-1, 1]) {
1677
+ const q2 = T(s * x, y);
1678
+ torso.push({ k: "circle", x: q2.x, y: q2.y, r: 0.2 * U, fill: p.body, ...ol });
1679
+ }
1680
+ const q = T(0, y1 + 0.02 * U);
1681
+ torso.push({ k: "circle", x: q.x, y: q.y, r: 0.2 * U, fill: p.body, ...ol });
1682
+ }
1683
+ if (has("robe")) {
1684
+ const yb = L + 0.58 * U;
1685
+ const wb = hb * 1.4;
1686
+ torso.push({
1687
+ k: "path",
1688
+ d: smoothClosed(
1689
+ TP([
1690
+ [-ht, y0],
1691
+ [0, y0 - 0.04 * U],
1692
+ [ht, y0],
1693
+ [bulge, L * 0.55],
1694
+ [wb, yb],
1695
+ [0, yb + 0.06 * U],
1696
+ [-wb, yb],
1697
+ [-bulge, L * 0.55]
1698
+ ]),
1699
+ 0.7
1700
+ ),
1701
+ fill: p.body,
1702
+ ...ol
1703
+ });
1704
+ const hem = TP([
1705
+ [-wb * 0.94, yb - 0.1 * U],
1706
+ [0, yb - 0.04 * U],
1707
+ [wb * 0.94, yb - 0.1 * U]
1708
+ ]);
1709
+ torso.push({ k: "path", d: smoothOpen(hem), stroke: def.extraColors?.[0] ?? p.accent, lw: 0.07 * U });
1710
+ } else if (has("sheet")) {
1711
+ const yb = L + 0.78 * U;
1712
+ const wb = hb * 1.3;
1713
+ torso.push({
1714
+ k: "path",
1715
+ d: smoothClosed(
1716
+ TP([
1717
+ [-ht, y0],
1718
+ [ht, y0],
1719
+ [bulge, L * 0.55],
1720
+ [wb, yb],
1721
+ [wb * 0.5, yb - 0.2 * U],
1722
+ [0, yb],
1723
+ [-wb * 0.5, yb - 0.2 * U],
1724
+ [-wb, yb],
1725
+ [-bulge, L * 0.55]
1726
+ ]),
1727
+ 0.9
1728
+ ),
1729
+ fill: p.body,
1730
+ ...ol
1731
+ });
1732
+ } else {
1733
+ torso.push({
1734
+ k: "path",
1735
+ d: smoothClosed(
1736
+ TP([
1737
+ [-ht, y0],
1738
+ [0, y0 - 0.04 * U],
1739
+ [ht, y0],
1740
+ [bulge, L * 0.55],
1741
+ [hb, y1],
1742
+ [0, y1 + 0.03 * U],
1743
+ [-hb, y1],
1744
+ [-bulge, L * 0.55]
1745
+ ]),
1746
+ 0.85
1747
+ ),
1748
+ fill: p.body,
1749
+ ...ol
1750
+ });
1751
+ }
1752
+ const minW = Math.min(ht, hb);
1753
+ if (has("pocket")) {
1754
+ const c = T(0, L * 0.74);
1755
+ 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 });
1756
+ } else if (has("racing-stripe")) {
1757
+ for (const x of [-0.1, 0.1]) {
1758
+ const a = T(x * U, y0 + 0.02 * U);
1759
+ const b = T(x * U, y1 - 0.04 * U);
1760
+ torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: p.belly, lw: 0.1 * U });
1761
+ }
1762
+ } else if (p.belly && p.belly !== p.body && !has("panel") && !has("sheet") && !has("stripes")) {
1763
+ const c = T(0, L * 0.58);
1764
+ 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 });
1765
+ }
1766
+ if (has("spots")) {
1767
+ const c = def.extraColors?.[1] ?? p.belly;
1768
+ for (const [x, y, rr] of [
1769
+ [-0.55, 0.35, 0.09],
1770
+ [0.5, 0.62, 0.11],
1771
+ [-0.35, 0.86, 0.07]
1772
+ ]) {
1773
+ const q = T(x * minW, y * L);
1774
+ torso.push({ k: "circle", x: q.x, y: q.y, r: rr * U, fill: c });
1775
+ }
1776
+ }
1777
+ if (has("stripes")) {
1778
+ const c = def.extraColors?.[1] ?? p.accent;
1779
+ for (const y of [0.22, 0.44, 0.66]) {
1780
+ const a = T(-minW * 0.98, L * y);
1781
+ const b = T(minW * 0.98, L * y);
1782
+ torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.1 * U });
1783
+ }
1784
+ }
1785
+ if (has("straps")) {
1786
+ const c = def.extraColors?.[1] ?? p.outline;
1787
+ for (const s of [-1, 1]) {
1788
+ const a = T(s * ht * 0.62, y0 + 0.02 * U);
1789
+ const b = T(s * minW * 0.62, L * 0.72);
1790
+ torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: p.outline, lw: 0.13 * U + ow * 2 });
1791
+ torso.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.13 * U });
1792
+ }
1793
+ }
1794
+ if (has("belt")) {
1795
+ const c = T(0, L * 0.9);
1796
+ 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 });
1797
+ 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 });
1798
+ }
1799
+ if (has("emblem")) {
1800
+ const c = T(0, L * 0.4);
1801
+ const fill = def.extraColors?.[0] ?? p.accent;
1802
+ const shield = roundedPolygon(
1803
+ [
1804
+ { x: -0.22, y: -0.2 },
1805
+ { x: 0.22, y: -0.2 },
1806
+ { x: 0.22, y: 0.04 },
1807
+ { x: 0, y: 0.26 },
1808
+ { x: -0.22, y: 0.04 }
1809
+ ],
1810
+ 0.25
1811
+ );
1812
+ torso.push({
1813
+ k: "group",
1814
+ x: c.x,
1815
+ y: c.y,
1816
+ rot: F.angle,
1817
+ s: U,
1818
+ items: [
1819
+ { k: "path", d: shield, fill, stroke: p.outline, lw: ow / U },
1820
+ { k: "group", x: 0, y: 0.01, s: 0.12, items: [{ k: "path", d: STAR, fill: fill === p.accent ? "#ffffff" : p.accent }] }
1821
+ ]
1822
+ });
1823
+ }
1824
+ if (has("panel")) {
1825
+ const c = T(0, L * 0.5);
1826
+ 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 });
1827
+ const lights = def.extraColors ?? ["#ff7a6b", "#fbd46a", "#7ff0ff"];
1828
+ lights.slice(0, 3).forEach((col, i) => {
1829
+ const q = T((i - 1) * minW * 0.34, L * 0.5);
1830
+ torso.push({ k: "circle", x: q.x, y: q.y, r: 0.075 * U, fill: col });
1831
+ });
1832
+ }
1833
+ if (has("badge")) {
1834
+ const c = T(-minW * 0.42, L * 0.36);
1835
+ torso.push({ k: "circle", x: c.x, y: c.y, r: 0.15 * U, fill: p.accent, ...ol });
1836
+ torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle, s: 0.09 * U, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
1837
+ }
1838
+ if (has("star")) {
1839
+ const c = T(-minW * 0.5, L * 0.3);
1840
+ const k = 0.15 * U;
1841
+ 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 }] });
1842
+ }
1843
+ if (has("scarf")) {
1844
+ const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
1845
+ const c = T(0, y);
1846
+ const col = def.extraColors?.[0] ?? p.accent;
1847
+ const tailEnd = T(ht * 0.45, y + 0.28 * U);
1848
+ 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 });
1849
+ 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 });
1850
+ }
1851
+ const head = { k: "group", x: F.head.x, y: F.head.y, rot: F.headRot, s: F.headR, items: cachedHead(def, ow / F.headR) };
1852
+ return [...back, ...legs, ...torso, ...arms, head, ...front];
1853
+ }
1854
+ var headCache = /* @__PURE__ */ new WeakMap();
1855
+ var HEAD_CACHE_SIZE = 64;
1856
+ function cachedHead(def, ow) {
1857
+ const key = Number(ow.toPrecision(4));
1858
+ let byOutline = headCache.get(def);
1859
+ if (!byOutline) headCache.set(def, byOutline = /* @__PURE__ */ new Map());
1860
+ let shapes = byOutline.get(key);
1861
+ if (!shapes) {
1862
+ if (byOutline.size >= HEAD_CACHE_SIZE) byOutline.clear();
1863
+ byOutline.set(key, shapes = headShapes(def, key));
1864
+ }
1865
+ return shapes;
1866
+ }
1867
+ function foot(def, ankle, F, side, ow, k) {
1868
+ const p = def.palette;
1869
+ const { d, r } = F;
1870
+ const U = F.U * k;
1871
+ const c = add(add(ankle, mul(d, 0.07 * U)), mul(r, side * 0.06 * U));
1872
+ const ol = { stroke: p.outline, lw: ow };
1873
+ const fill = p.feet ?? p.limbs;
1874
+ if (def.bodyExtras.includes("fins")) {
1875
+ const fin = roundedPolygon(
1876
+ [
1877
+ { x: -0.13, y: -0.06 },
1878
+ { x: 0.13, y: -0.06 },
1879
+ { x: 0.24, y: 0.46 },
1880
+ { x: -0.24, y: 0.46 }
1881
+ ],
1882
+ 0.22
1883
+ );
1884
+ return {
1885
+ k: "group",
1886
+ x: c.x,
1887
+ y: c.y,
1888
+ rot: F.angle,
1889
+ s: U,
1890
+ items: [
1891
+ { k: "path", d: fin, fill, stroke: p.outline, lw: ow / U },
1892
+ { k: "line", pts: [0, 0.06, 0, 0.38], stroke: p.outline, lw: ow / U, alpha: 0.5 }
1893
+ ]
1894
+ };
1895
+ }
1896
+ 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 };
1897
+ 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 };
1898
+ return { k: "ellipse", x: c.x, y: c.y, rx: 0.2 * U, ry: 0.13 * U, rot: F.angle, fill, ...ol };
1899
+ }
1900
+ function tail(def, F, hb, ow) {
1901
+ const p = def.palette;
1902
+ const { U, L, S, r, d } = F;
1903
+ const T = (x, y) => ({ x: S.x + r.x * x + d.x * y, y: S.y + r.y * x + d.y * y });
1904
+ const TP = (pts2) => pts2.map(([x, y]) => T(x, y));
1905
+ const ol = { stroke: p.outline, lw: ow };
1906
+ const stroke = (pts2, w, color) => [
1907
+ { k: "path", d: smoothOpen(pts2), stroke: p.outline, lw: w + ow * 2 },
1908
+ { k: "path", d: smoothOpen(pts2), stroke: color, lw: w }
1909
+ ];
1910
+ const out = [];
1911
+ switch (def.tail) {
1912
+ case "none":
1913
+ break;
1914
+ case "fluffy": {
1915
+ const pts2 = TP([
1916
+ [hb * 0.5, L * 0.95],
1917
+ [hb + 0.28 * U, L * 1],
1918
+ [hb + 0.72 * U, L * 0.62],
1919
+ [hb + 0.78 * U, L * 0.12],
1920
+ [hb + 0.52 * U, L * 0.32],
1921
+ [hb + 0.3 * U, L * 0.6],
1922
+ [hb * 0.6, L * 0.7]
1923
+ ]);
1924
+ out.push({ k: "path", d: smoothClosed(pts2), fill: p.body, ...ol });
1925
+ const tip = T(hb + 0.72 * U, L * 0.2);
1926
+ 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 });
1927
+ break;
1928
+ }
1929
+ case "brush": {
1930
+ const pts2 = TP([
1931
+ [hb * 0.4, L * 0.98],
1932
+ [hb + 0.42 * U, L * 1.06],
1933
+ [hb + 0.98 * U, L * 0.66],
1934
+ [hb + 1.06 * U, L * 0.12],
1935
+ [hb + 0.86 * U, -0.04 * U],
1936
+ [hb + 0.66 * U, L * 0.36],
1937
+ [hb + 0.3 * U, L * 0.66],
1938
+ [hb * 0.5, L * 0.68]
1939
+ ]);
1940
+ out.push({ k: "path", d: smoothClosed(pts2), fill: p.body, ...ol });
1941
+ const tip = TP([
1942
+ [hb + 1.03 * U, L * 0.36],
1943
+ [hb + 1.06 * U, L * 0.12],
1944
+ [hb + 0.86 * U, -0.04 * U],
1945
+ [hb + 0.72 * U, L * 0.24],
1946
+ [hb + 0.86 * U, L * 0.34]
1947
+ ]);
1948
+ out.push({ k: "path", d: smoothClosed(tip), fill: p.belly, ...ol });
1949
+ break;
1950
+ }
1951
+ case "pom": {
1952
+ const c = T(hb + 0.06 * U, L * 0.88);
1953
+ out.push({ k: "circle", x: c.x, y: c.y, r: 0.2 * U, fill: p.belly, ...ol });
1954
+ break;
1955
+ }
1956
+ case "long":
1957
+ case "striped":
1958
+ case "tuft": {
1959
+ const pts2 = TP([
1960
+ [hb * 0.6, L * 0.9],
1961
+ [hb + 0.35 * U, L * 0.85],
1962
+ [hb + 0.58 * U, L * 0.4],
1963
+ [hb + 0.5 * U, -0.02 * U]
1964
+ ]);
1965
+ out.push(...stroke(pts2, 0.14 * U, p.body));
1966
+ if (def.tail === "tuft") {
1967
+ const end = pts2[pts2.length - 1];
1968
+ out.push({ k: "circle", x: end.x, y: end.y, r: 0.16 * U, fill: def.extraColors?.[0] ?? p.accent, ...ol });
1969
+ }
1970
+ if (def.tail === "striped") {
1971
+ for (const t of [0.35, 0.6, 0.85]) {
1972
+ 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));
1973
+ 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 });
1974
+ }
1975
+ const end = pts2[pts2.length - 1];
1976
+ out.push({ k: "circle", x: end.x, y: end.y, r: 0.07 * U, fill: def.extraColors?.[0] ?? p.outline });
1977
+ }
1978
+ break;
1979
+ }
1980
+ case "curl": {
1981
+ const pts2 = TP([
1982
+ [hb * 0.6, L * 0.9],
1983
+ [hb + 0.4 * U, L * 0.88],
1984
+ [hb + 0.62 * U, L * 0.45],
1985
+ [hb + 0.5 * U, L * 0.12],
1986
+ [hb + 0.3 * U, L * 0.24],
1987
+ [hb + 0.36 * U, L * 0.42]
1988
+ ]);
1989
+ out.push(...stroke(pts2, 0.12 * U, p.limbs));
1990
+ break;
1991
+ }
1992
+ case "fish": {
1993
+ const line = TP([
1994
+ [hb * 0.6, L * 0.9],
1995
+ [hb + 0.36 * U, L * 0.86],
1996
+ [hb + 0.6 * U, L * 0.5]
1997
+ ]);
1998
+ out.push(...stroke(line, 0.17 * U, p.body));
1999
+ const tip = line[line.length - 1];
2000
+ const fin = roundedPolygon(
2001
+ [
2002
+ { x: 0, y: 0.06 },
2003
+ { x: -0.26, y: -0.34 },
2004
+ { x: 0.02, y: -0.12 },
2005
+ { x: 0.32, y: -0.3 }
2006
+ ],
2007
+ 0.2
2008
+ );
2009
+ 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 }] });
2010
+ break;
2011
+ }
2012
+ case "plume": {
2013
+ const cols = def.extraColors ?? [p.accent];
2014
+ [0, 1, 2].forEach((i) => {
2015
+ const c = T(hb + (0.12 + i * 0.13) * U, L + (0.32 - i * 0.06) * U);
2016
+ 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 });
2017
+ });
2018
+ break;
2019
+ }
2020
+ case "spiky":
2021
+ case "arrow": {
2022
+ const pts2 = TP([
2023
+ [hb * 0.3, L * 0.55],
2024
+ [hb + 0.35 * U, L * 0.68],
2025
+ [hb + 0.95 * U, L * 0.86],
2026
+ [hb + 0.35 * U, L * 1.08],
2027
+ [hb * 0.3, L * 1.08]
2028
+ ]);
2029
+ if (def.tail === "spiky") {
2030
+ for (const t of [0.15, 0.45, 0.72]) {
2031
+ const x = hb + (0.2 + t * 0.7) * U;
2032
+ const y = L * (0.66 + t * 0.19);
2033
+ 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 });
2034
+ }
2035
+ out.push({ k: "path", d: smoothClosed(pts2, 0.7), fill: p.body, ...ol });
2036
+ } else {
2037
+ const line = TP([
2038
+ [hb * 0.6, L * 0.9],
2039
+ [hb + 0.4 * U, L * 0.92],
2040
+ [hb + 0.62 * U, L * 0.5],
2041
+ [hb + 0.62 * U, L * 0.18]
2042
+ ]);
2043
+ out.push(...stroke(line, 0.13 * U, p.body));
2044
+ const tip = line[line.length - 1];
2045
+ 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 }] });
2046
+ }
2047
+ break;
2048
+ }
2049
+ }
2050
+ return out;
2051
+ }
2052
+
2053
+ // ../avatars/src/poses.ts
2054
+ var STAND = {
2055
+ nose: [0, -0.62],
2056
+ leftShoulder: [-0.42, 0],
2057
+ rightShoulder: [0.42, 0],
2058
+ leftElbow: [-0.58, 0.5],
2059
+ rightElbow: [0.58, 0.5],
2060
+ leftWrist: [-0.66, 0.98],
2061
+ rightWrist: [0.66, 0.98],
2062
+ leftHip: [-0.27, 1],
2063
+ rightHip: [0.27, 1],
2064
+ leftKnee: [-0.31, 1.6],
2065
+ rightKnee: [0.31, 1.6],
2066
+ leftAnkle: [-0.33, 2.18],
2067
+ rightAnkle: [0.33, 2.18]
2068
+ };
2069
+ function poseBody(spec, { cx = 0.5, cy = 0.36, T = 0.18 } = {}) {
2070
+ const s = { ...STAND, ...spec };
2071
+ const b = new Array(LANDMARKS * 3).fill(0);
2072
+ const set = (i, [x, y], v = 0.99) => {
2073
+ b[i * 3] = cx + x * T;
2074
+ b[i * 3 + 1] = cy + y * T;
2075
+ b[i * 3 + 2] = v;
2076
+ };
2077
+ for (const key of Object.keys(P)) set(P[key], s[key]);
2078
+ const tilt = s.tilt ?? 0;
2079
+ 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)];
2080
+ set(FACE.leftEye, around(-0.1, -0.08));
2081
+ set(FACE.rightEye, around(0.1, -0.08));
2082
+ set(FACE.leftEar, around(-0.22, -0.02));
2083
+ set(FACE.rightEar, around(0.22, -0.02));
2084
+ return b;
2085
+ }
2086
+ var neutralPose = () => poseBody({});
2087
+
2088
+ // ../avatars/src/interpolate.ts
2089
+ var SEEN = 0.5;
2090
+ var JUMP = 0.15;
2091
+ var BodyInterpolator = class {
2092
+ constructor(o = {}) {
2093
+ __publicField(this, "tracks", /* @__PURE__ */ new Map());
2094
+ /** Page clock minus sender clock, at its smallest (the fastest delivery): NaN before the first body. */
2095
+ __publicField(this, "offset", NaN);
2096
+ __publicField(this, "lastArrival", NaN);
2097
+ /** The time between bodies (sender's clock), smoothed. */
2098
+ __publicField(this, "interval", 33);
2099
+ __publicField(this, "lastT", NaN);
2100
+ __publicField(this, "lostMs");
2101
+ __publicField(this, "maxExtrapolateMs");
2102
+ this.lostMs = o.lostMs ?? 250;
2103
+ this.maxExtrapolateMs = o.maxExtrapolateMs ?? 25;
2104
+ }
2105
+ /** The time between bodies the drawing is behind by, in ms. */
2106
+ get delayMs() {
2107
+ return this.interval;
2108
+ }
2109
+ /**
2110
+ * A new body for this player, taken at t (the sender's clock), arriving now (this page's clock, by default
2111
+ * performance.now()). Bodies for several players of one camera frame share t.
2112
+ */
2113
+ push(player, t, body, arrival = now()) {
2114
+ if (!Number.isFinite(t)) t = arrival;
2115
+ this.clock(t, arrival);
2116
+ const tr = this.tracks.get(player);
2117
+ if (!tr || t - tr.tb > this.lostMs || t < tr.tb - this.lostMs || body.length !== tr.b.length) {
2118
+ this.tracks.set(player, { a: null, ta: t, b: body, tb: t, out: null, outAt: NaN });
2119
+ return;
2120
+ }
2121
+ if (body === tr.b || t <= tr.tb) {
2122
+ if (t === tr.tb && body !== tr.b) tr.b = body, tr.out = null;
2123
+ return;
2124
+ }
2125
+ tr.a = tr.b;
2126
+ tr.ta = tr.tb;
2127
+ tr.b = body;
2128
+ tr.tb = t;
2129
+ tr.out = null;
2130
+ }
2131
+ /** The player's body to draw at time now (this page's clock); null for a player never pushed or forgotten. */
2132
+ at(player, at = now()) {
2133
+ const tr = this.tracks.get(player);
2134
+ if (!tr) return null;
2135
+ if (!tr.a) return tr.b;
2136
+ if (tr.out && tr.outAt === at) return tr.out;
2137
+ const rt = at - this.offset - this.interval;
2138
+ const span = tr.tb - tr.ta;
2139
+ const ahead = Math.min(span * 0.5, this.maxExtrapolateMs);
2140
+ const u = Math.min(Math.max((rt - tr.ta) / span, 0), 1 + ahead / span);
2141
+ tr.out = blend(tr.a, tr.b, u);
2142
+ tr.outAt = at;
2143
+ return tr.out;
2144
+ }
2145
+ /** A player gone: their next body starts afresh. */
2146
+ forget(player) {
2147
+ this.tracks.delete(player);
2148
+ }
2149
+ /** Every player gone (a new scene). */
2150
+ clear() {
2151
+ this.tracks.clear();
2152
+ }
2153
+ clock(t, arrival) {
2154
+ const o = arrival - t;
2155
+ if (!Number.isFinite(this.offset) || o < this.offset || o > this.offset + 1e3) this.offset = o;
2156
+ else if (Number.isFinite(this.lastArrival)) this.offset += Math.min(o - this.offset, (arrival - this.lastArrival) / 1e3);
2157
+ this.lastArrival = arrival;
2158
+ if (Number.isFinite(this.lastT) && t > this.lastT) {
2159
+ const dt = t - this.lastT;
2160
+ if (dt < this.interval * 3 + 20) this.interval += (Math.min(Math.max(dt, 8), 100) - this.interval) * 0.1;
2161
+ }
2162
+ if (!(t <= this.lastT)) this.lastT = t;
2163
+ }
2164
+ };
2165
+ var now = () => globalThis.performance?.now() ?? Date.now();
2166
+ function blend(a, b, u) {
2167
+ const out = b.slice();
2168
+ const near = u < 0.5 ? a : b;
2169
+ for (let i = 0; i + 2 < b.length; i += 3) {
2170
+ const ax = a[i];
2171
+ const ay = a[i + 1];
2172
+ const bx = b[i];
2173
+ const by = b[i + 1];
2174
+ if (a[i + 2] >= SEEN && b[i + 2] >= SEEN && Math.abs(bx - ax) + Math.abs(by - ay) < JUMP) {
2175
+ out[i] = ax + (bx - ax) * u;
2176
+ out[i + 1] = ay + (by - ay) * u;
2177
+ out[i + 2] = a[i + 2] + (b[i + 2] - a[i + 2]) * Math.min(u, 1);
2178
+ } else {
2179
+ out[i] = near[i];
2180
+ out[i + 1] = near[i + 1];
2181
+ out[i + 2] = near[i + 2];
2182
+ }
2183
+ }
2184
+ return out;
2185
+ }
2186
+
2187
+ // ../avatars/src/index.ts
2188
+ var AVATAR_IDS = AVATARS.map((a) => a.id);
2189
+ var BY_ID = new Map(AVATARS.map((a) => [a.id, a]));
2190
+ var DEFAULT_AVATAR = BY_ID.get("fox");
2191
+ var isAvatarId = (v) => typeof v === "string" && BY_ID.has(v);
2192
+ var avatarById = (id) => BY_ID.get(id);
2193
+ var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
2194
+ function drawAvatar(ctx, body, def, box, opts = {}) {
2195
+ const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored });
2196
+ if (!shapes.length) return false;
2197
+ ctx.save();
2198
+ if (opts.alpha !== void 0) ctx.globalAlpha *= Math.min(Math.max(opts.alpha, 0), 1);
2199
+ drawShapes(ctx, shapes);
2200
+ ctx.restore();
2201
+ return true;
2202
+ }
2203
+ function iconShapes(def, size) {
2204
+ const d = resolve(def);
2205
+ const body = neutralPose();
2206
+ const band = bodyBand(d);
2207
+ const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
2208
+ const top = -bounds(headShapes(d, 0.05)).minY;
2209
+ const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
2210
+ const k = headR / BAND_BODY[band].head / unit.U;
2211
+ const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
2212
+ const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2213
+ return avatarShapes(body, d, box, { portrait: true });
2214
+ }
2215
+ function smoothBody(prev, next, amount = 0.5) {
2216
+ if (!prev || prev.length !== next.length) return next.slice();
2217
+ const out = next.slice();
2218
+ for (let i = 0; i + 2 < next.length; i += 3) {
2219
+ const pv = prev[i + 2];
2220
+ const nv = next[i + 2];
2221
+ const wasSeen = pv >= 0.5;
2222
+ const isSeen = nv >= 0.5;
2223
+ if (wasSeen && nv >= 0.3) out[i + 2] = Math.max(nv, 0.5);
2224
+ if (wasSeen && isSeen && Math.hypot(next[i] - prev[i], next[i + 1] - prev[i + 1]) < 0.15) {
2225
+ out[i] = prev[i] + (next[i] - prev[i]) * amount;
2226
+ out[i + 1] = prev[i + 1] + (next[i + 1] - prev[i + 1]) * amount;
2227
+ } else if (wasSeen && !isSeen && out[i + 2] >= 0.5) {
2228
+ out[i] = prev[i];
2229
+ out[i + 1] = prev[i + 1];
2230
+ }
2231
+ }
2232
+ smoothFace(prev, next, out, amount * FACE_SMOOTHING);
2233
+ return out;
2234
+ }
2235
+ var FACE_SMOOTHING = 0.35;
2236
+ var FACE_POINTS = 11;
2237
+ function smoothFace(prev, next, out, amount) {
2238
+ const ls = P.leftShoulder * 3;
2239
+ const rs = P.rightShoulder * 3;
2240
+ const seen = (b, i) => b[i + 2] >= 0.5;
2241
+ if (!seen(prev, ls) || !seen(prev, rs) || !seen(next, ls) || !seen(next, rs)) return;
2242
+ const mid2 = (b, k) => (b[ls + k] + b[rs + k]) / 2;
2243
+ const prevS = [mid2(prev, 0), mid2(prev, 1)];
2244
+ const nextS = [mid2(next, 0), mid2(next, 1)];
2245
+ const outS = [mid2(out, 0), mid2(out, 1)];
2246
+ for (let p = 0; p < FACE_POINTS && p * 3 + 2 < next.length; p++) {
2247
+ const i = p * 3;
2248
+ if (!seen(prev, i) || !seen(next, i)) continue;
2249
+ const po = [prev[i] - prevS[0], prev[i + 1] - prevS[1]];
2250
+ const no = [next[i] - nextS[0], next[i + 1] - nextS[1]];
2251
+ if (Math.hypot(no[0] - po[0], no[1] - po[1]) >= 0.15) continue;
2252
+ out[i] = outS[0] + po[0] + (no[0] - po[0]) * amount;
2253
+ out[i + 1] = outS[1] + po[1] + (no[1] - po[1]) * amount;
2254
+ }
2255
+ }
2256
+ function bounds(shapes) {
2257
+ const b = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
2258
+ const addPt = (x, y) => {
2259
+ b.minX = Math.min(b.minX, x);
2260
+ b.minY = Math.min(b.minY, y);
2261
+ b.maxX = Math.max(b.maxX, x);
2262
+ b.maxY = Math.max(b.maxY, y);
2263
+ };
2264
+ for (const s of shapes) {
2265
+ const pad = "lw" in s && s.stroke ? (s.lw ?? 1) / 2 : 0;
2266
+ switch (s.k) {
2267
+ case "circle":
2268
+ addPt(s.x - s.r - pad, s.y - s.r - pad);
2269
+ addPt(s.x + s.r + pad, s.y + s.r + pad);
2270
+ break;
2271
+ case "ellipse":
2272
+ case "rect": {
2273
+ const rx = s.k === "ellipse" ? s.rx : s.w / 2;
2274
+ const ry = s.k === "ellipse" ? s.ry : s.h / 2;
2275
+ const r = Math.max(rx, ry) + pad;
2276
+ addPt(s.x - r, s.y - r);
2277
+ addPt(s.x + r, s.y + r);
2278
+ break;
2279
+ }
2280
+ case "path": {
2281
+ const n = pathPoints(s.d);
2282
+ 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);
2283
+ break;
2284
+ }
2285
+ case "line":
2286
+ 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);
2287
+ break;
2288
+ case "group": {
2289
+ const inner = bounds(s.items);
2290
+ if (!Number.isFinite(inner.minX)) break;
2291
+ const k = s.s ?? 1;
2292
+ const c = Math.cos(s.rot ?? 0);
2293
+ const sn = Math.sin(s.rot ?? 0);
2294
+ for (const [x, y] of [
2295
+ [inner.minX, inner.minY],
2296
+ [inner.maxX, inner.minY],
2297
+ [inner.minX, inner.maxY],
2298
+ [inner.maxX, inner.maxY]
2299
+ ])
2300
+ addPt(s.x + (x * c - y * sn) * k, s.y + (x * sn + y * c) * k);
2301
+ break;
2302
+ }
2303
+ }
2304
+ }
2305
+ return b;
2306
+ }
2307
+
2308
+ // src/avatars.ts
2309
+ var frameTime = () => {
2310
+ const t = typeof document !== "undefined" ? document.timeline?.currentTime : null;
2311
+ return typeof t === "number" && Number.isFinite(t) ? t : globalThis.performance?.now() ?? Date.now();
2312
+ };
2313
+ var clockNow = () => globalThis.performance?.now() ?? Date.now();
2314
+ var AvatarBook = class {
2315
+ /** now: this frame's time (tests). */
2316
+ constructor(now2 = frameTime) {
2317
+ __publicField(this, "now", now2);
2318
+ __publicField(this, "known", /* @__PURE__ */ new Map());
2319
+ __publicField(this, "smoothed", /* @__PURE__ */ new Map());
2320
+ /** When each body of the host's frames was taken (the frame's t) and arrived. */
2321
+ __publicField(this, "times", /* @__PURE__ */ new WeakMap());
2322
+ __publicField(this, "lerp", new BodyInterpolator());
2323
+ /** Animation frames something was drawn in, for stats. */
2324
+ __publicField(this, "drawnAt", NaN);
2325
+ __publicField(this, "drawn", 0);
2326
+ /**
2327
+ * A player's smoothed body (see Play.smoothedBody): a frame's body smoothed for its player and interpolated for
2328
+ * this animation frame, or a player id's (null if none yet).
2329
+ */
2330
+ __publicField(this, "smoothedBody", (who, options = {}) => {
2331
+ const interpolate = options.interpolate !== false;
2332
+ if (typeof who === "number") {
2333
+ const last = this.smoothed.get(who);
2334
+ if (!last) return null;
2335
+ return interpolate ? this.lerp.at(who, this.tick()) ?? last.body : last.body;
2336
+ }
2337
+ if (typeof who !== "object" || who === null || !Array.isArray(who.body)) return null;
2338
+ return this.posed(who.id, who.body, smoothAmount(options.smooth), interpolate);
2339
+ });
2340
+ }
2341
+ /** Every frame from the host: the latest avatar of each player, and when their bodies were taken (t). */
2342
+ note(players, t, arrival = clockNow()) {
2343
+ for (const p of players) {
2344
+ this.known.set(p.id, this.resolve(p.id, p.avatar));
2345
+ if (t !== void 0 && Number.isFinite(t) && Array.isArray(p.body)) this.times.set(p.body, { t, arrival });
2346
+ }
2347
+ }
2348
+ /**
2349
+ * The avatar id of a player (an id, 1-based, or a frame): the one the child picked, or the default of the slot
2350
+ * (fox, lion, dragon, unicorn: the 6–8 band's, as no age is known here) before the first frame, from an older host, or for an id this SDK doesn't know.
2351
+ */
2352
+ of(player) {
2353
+ if (typeof player === "object" && player !== null) return player.avatar !== void 0 ? this.resolve(player.id, player.avatar) : this.of(player.id);
2354
+ return this.known.get(player) ?? defaultAvatarFor(player - 1);
2355
+ }
2356
+ /**
2357
+ * The body to draw for this player now: body smoothed (amount below 1) against the player's previous one, then,
2358
+ * with interpolate, at this animation frame's time between the last two.
2359
+ */
2360
+ posed(player, body, amount, interpolate = true) {
2361
+ const sample = this.smooth(player, body, amount);
2362
+ return interpolate ? this.lerp.at(player, this.tick()) ?? sample : sample;
2363
+ }
2364
+ /** body smoothed against this player's previous one; the same body twice in a row is smoothed once. */
2365
+ smooth(player, body, amount) {
2366
+ const last = this.smoothed.get(player);
2367
+ if (last && last.raw === body && last.amount === amount) return last.body;
2368
+ if (last && last.amount !== amount) this.lerp.forget(player);
2369
+ const next = amount < 1 ? smoothBody(last?.body, body, amount) : body;
2370
+ this.smoothed.set(player, { raw: body, body: next, amount });
2371
+ const at = this.times.get(body);
2372
+ const arrival = at?.arrival ?? clockNow();
2373
+ this.lerp.push(player, at?.t ?? arrival, next, arrival);
2374
+ return next;
2375
+ }
2376
+ /**
2377
+ * Animation frames the avatars were drawn in, since the last call; for stats. Frames are
2378
+ * told apart by their time, so any number of avatars or calls in one frame count once.
2379
+ */
2380
+ takeDrawn() {
2381
+ const n = this.drawn;
2382
+ this.drawn = 0;
2383
+ return n;
2384
+ }
2385
+ tick() {
2386
+ const t = this.now();
2387
+ if (t !== this.drawnAt) {
2388
+ this.drawnAt = t;
2389
+ this.drawn++;
2390
+ }
2391
+ return t;
2392
+ }
2393
+ resolve(id, avatar) {
2394
+ return isAvatarId(avatar) ? avatar : defaultAvatarFor(id - 1);
2395
+ }
2396
+ };
2397
+ function defaultBox(ctx, view) {
2398
+ const t = typeof ctx.getTransform === "function" ? ctx.getTransform() : null;
2399
+ const sx = t ? Math.hypot(t.a, t.b) || 1 : 1;
2400
+ const sy = t ? Math.hypot(t.c, t.d) || 1 : 1;
2401
+ const w = (ctx.canvas?.width ?? 0) / sx;
2402
+ const h = (ctx.canvas?.height ?? 0) / sy;
2403
+ return view ? { x: view.x * w, y: view.y * h, w: view.w * w, h: view.h * h } : { x: 0, y: 0, w, h };
2404
+ }
2405
+ var smoothAmount = (smooth) => smooth === false ? 1 : typeof smooth === "number" ? Math.min(Math.max(smooth, 0.05), 1) : 0.5;
2406
+ function avatarDrawer(book, scene) {
2407
+ return (ctx, who, player, box, options = {}) => {
2408
+ const frame = Array.isArray(who) ? null : who;
2409
+ const body = frame ? frame.body : who;
2410
+ if (!Array.isArray(body)) return null;
2411
+ const avatar = typeof player === "string" ? player : book.of(typeof player === "number" ? player : frame ?? 1);
2412
+ const id = typeof player === "number" ? player : frame?.id;
2413
+ const posed = id !== void 0 ? book.posed(id, body, smoothAmount(options.smooth), options.interpolate !== false) : body;
2414
+ return drawAvatar(ctx, posed, avatar, box ?? defaultBox(ctx, scene().view), options.alpha !== void 0 ? { alpha: options.alpha } : {}) ? posed : null;
2415
+ };
2416
+ }
2417
+
2418
+ // src/easy.ts
2419
+ function easy(connecting) {
2420
+ const listeners = /* @__PURE__ */ new Map();
2421
+ const emit = (event, data) => listeners.get(event)?.forEach((l) => l(data));
2422
+ let play = null;
2423
+ const queue = [];
2424
+ const withPlay = (fn) => play ? fn(play) : queue.push(fn);
2425
+ const early = new AvatarBook();
2426
+ const drawEarly = avatarDrawer(early, () => ({ view: null }));
2427
+ void connecting.then((p) => {
2428
+ play = p;
2429
+ const shapes = /* @__PURE__ */ new Map();
2430
+ p.on("gesture", (g) => emit(g.gesture, { player: g.player }));
2431
+ p.on("pause", (m) => emit("pause", { reason: m.reason === "distance" ? "distance" : "user" }));
2432
+ p.on("resume", () => emit("resume", {}));
2433
+ p.on("break-start", () => emit("break-start", {}));
2434
+ p.on("break-done", (m) => emit("break-end", { shown: m.shown }));
2435
+ p.onBackdrop((backdrop, view) => emit("backdrop", { backdrop, view }));
2436
+ p.on("frame", (f2) => {
2437
+ emit("players", { players: f2.players });
2438
+ for (const pl of f2.players) {
2439
+ emit("hands", { player: pl.id, left: pl.leftHand, right: pl.rightHand });
2440
+ if (shapes.get(pl.id) !== pl.shape) {
2441
+ shapes.set(pl.id, pl.shape);
2442
+ emit("shape", { player: pl.id, shape: pl.shape });
2443
+ }
2444
+ }
2445
+ });
2446
+ emit("start", startInfo(p));
2447
+ queue.splice(0).forEach((fn) => fn(p));
2448
+ });
2449
+ return {
2450
+ on(event, listener) {
2451
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
2452
+ listeners.set(event, set);
2453
+ set.add(listener);
2454
+ if (event === "start" && play) listener(startInfo(play));
2455
+ return () => void set.delete(listener);
2456
+ },
2457
+ ready: () => withPlay((p) => p.ready()),
2458
+ score: (score, player = 1) => withPlay((p) => p.score(player, score)),
2459
+ break: (reason) => new Promise((resolve2) => withPlay((p) => void p.break(reason).then(resolve2))),
2460
+ end: (score) => withPlay(
2461
+ (p) => p.end(typeof score === "number" ? [{ player: 1, score, stars: 2 }] : score.map((s) => ({ player: s.player, score: s.score, stars: s.stars ?? 2 })))
2462
+ ),
2463
+ keys: (map, options) => withPlay((p) => void keyboard(p, map, options)),
2464
+ pointer: (options) => withPlay((p) => void pointer(p, options)),
2465
+ get view() {
2466
+ return play?.view ?? null;
2467
+ },
2468
+ get device() {
2469
+ return play?.device ?? null;
2470
+ },
2471
+ reportStats: (s) => play?.reportStats(s),
2472
+ toScreen: (p) => play ? play.toScreen(p) : toScreen(null, p),
2473
+ avatarOf: (player) => play ? play.avatarOf(player) : early.of(player),
2474
+ drawAvatar: (...args) => play ? play.drawAvatar(...args) : drawEarly(...args),
2475
+ smoothedBody: (...args) => play ? play.smoothedBody(...args) : early.smoothedBody(...args)
2476
+ };
2477
+ }
2478
+ var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, backdrop: p.backdrop, view: p.view, device: p.device });
2479
+
2480
+ // src/index.ts
2481
+ var windowChannel = (win = window) => ({
2482
+ post: (m) => win.parent.postMessage(m, "*"),
2483
+ // Only the screen that embeds the game speaks to it.
2484
+ listen: (l) => win.addEventListener("message", (e) => e.source === win.parent && l(e.data))
2485
+ });
2486
+ var INIT_TIMEOUT_MS = 1e4;
2487
+ var STATS_MS = 1e3;
2488
+ function connect(channel = windowChannel()) {
2489
+ return new Promise((resolve2, reject) => {
2490
+ const listeners = /* @__PURE__ */ new Map();
2491
+ const breaks = [];
2492
+ let started = false;
2493
+ let backdrop = "none";
2494
+ let view = null;
2495
+ const backdropListeners = /* @__PURE__ */ new Set();
2496
+ const avatars = new AvatarBook();
2497
+ const drawAvatar2 = avatarDrawer(avatars, () => ({ view }));
2498
+ const smoothedBody = avatars.smoothedBody;
2499
+ const timer = setTimeout(() => reject(new Error("Fabi Play: no host, is the game opened from Fabi Play?")), INIT_TIMEOUT_MS);
2500
+ channel.listen((data) => {
2501
+ if (typeof data !== "object" || data === null || data.ns !== NS) return;
2502
+ const m = data;
2503
+ if (m.type === "init" && !started) {
2504
+ if (m.v !== PROTOCOL_VERSION) {
2505
+ clearTimeout(timer);
2506
+ return reject(new Error(`Fabi Play: protocol ${m.v}, this SDK speaks ${PROTOCOL_VERSION}`));
2507
+ }
2508
+ started = true;
2509
+ clearTimeout(timer);
2510
+ backdrop = m.backdrop === "video" ? "video" : "none";
2511
+ view = parseView(m.view) ?? null;
2512
+ const device = parseDeviceHint(m.device) ?? null;
2513
+ let reported = {};
2514
+ if (m.stats === true) {
2515
+ let since = performance.now();
2516
+ const stats = setInterval(() => {
2517
+ const now2 = performance.now();
2518
+ channel.post(wire({ type: "stats", renderFps: Math.round(avatars.takeDrawn() * 1e3 / Math.max(now2 - since, 1)), ...reported }));
2519
+ since = now2;
2520
+ }, STATS_MS);
2521
+ stats.unref?.();
2522
+ }
2523
+ resolve2({
2524
+ locale: m.locale,
2525
+ mode: m.mode,
2526
+ players: m.players,
2527
+ muted: m.muted,
2528
+ ageRange: m.ageRange ?? "6-8",
2529
+ get backdrop() {
2530
+ return backdrop;
2531
+ },
2532
+ get view() {
2533
+ return view;
2534
+ },
2535
+ device,
2536
+ reportStats: (s) => {
2537
+ reported = parseGameStats(s);
2538
+ },
2539
+ toScreen: (p) => toScreen(view, p),
2540
+ avatarOf: (player) => avatars.of(player),
2541
+ drawAvatar: drawAvatar2,
2542
+ smoothedBody,
2543
+ onBackdrop(listener) {
2544
+ backdropListeners.add(listener);
2545
+ return () => void backdropListeners.delete(listener);
2546
+ },
2547
+ on(event, listener) {
2548
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
2549
+ listeners.set(event, set);
2550
+ set.add(listener);
2551
+ return () => void set.delete(listener);
2552
+ },
2553
+ ready: () => channel.post(wire({ type: "ready" })),
2554
+ score: (player, score) => channel.post(wire({ type: "score", player, score })),
2555
+ break: (reason = "level") => new Promise((done) => {
2556
+ breaks.push(done);
2557
+ channel.post(wire({ type: "break", reason }));
2558
+ }),
2559
+ end: (scores) => channel.post(wire({ type: "end", scores }))
2560
+ });
2561
+ } else if (!started) return;
2562
+ else {
2563
+ let message = m;
2564
+ if (m.type === "frame" && Array.isArray(m.players)) avatars.note(m.players, m.t);
2565
+ if (m.type === "backdrop") {
2566
+ if (m.backdrop !== "video" && m.backdrop !== "none") return;
2567
+ const next = parseView(m.view) ?? null;
2568
+ if (m.backdrop === backdrop && sameView(next, view)) return;
2569
+ backdrop = m.backdrop;
2570
+ view = next;
2571
+ message = { type: "backdrop", backdrop, ...view ? { view } : {} };
2572
+ for (const l of [...backdropListeners]) l(backdrop, view);
2573
+ }
2574
+ for (const l of listeners.get(m.type) ?? []) l(message);
2575
+ if (m.type === "break-done") breaks.shift()?.(m.shown);
2576
+ }
2577
+ });
2578
+ });
2579
+ }
2580
+ var sameView = (a, b) => a === b || a !== null && b !== null && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
2581
+
2582
+ // src/shared-avatars.ts
2583
+ var shared_avatars_exports = {};
2584
+ __export(shared_avatars_exports, {
2585
+ BAND_BODY: () => BAND_BODY,
2586
+ DEFAULT_AVATAR: () => DEFAULT_AVATAR,
2587
+ avatarById: () => avatarById,
2588
+ avatarShapes: () => avatarShapes,
2589
+ drawShapes: () => drawShapes,
2590
+ frameOf: () => frameOf,
2591
+ iconShapes: () => iconShapes
2592
+ });
2593
+
2594
+ // src/install.ts
2595
+ function install(script) {
2596
+ const connecting = connect();
2597
+ const api = easy(connecting);
2598
+ if (script?.dataset.mute !== "manual") muteDuringBreaks(connecting);
2599
+ const global = Object.assign(api, { avatars: { ...shared_avatars_exports } });
2600
+ window.FabiPlay = global;
2601
+ const keys = script?.dataset.keys;
2602
+ if (keys) api.keys(parseKeys(keys));
2603
+ const hand = script?.dataset.pointer;
2604
+ if (hand) api.pointer({ hand: hand === "left" || hand === "right" ? hand : "any" });
2605
+ if (script?.dataset.ready !== "manual") {
2606
+ if (document.readyState === "complete") api.ready();
2607
+ else window.addEventListener("load", () => api.ready(), { once: true });
2608
+ }
2609
+ return global;
2610
+ }
2611
+ return module.exports.install(script);
2612
+ });