@diffenderfer-games/hub 2.0.0-next.4 → 2.0.0-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +54 -11
  2. package/README.md +3 -6
  3. package/dist/auto.d.ts +5 -5
  4. package/dist/auto.js +2 -2
  5. package/dist/avatar-pixi.d.ts +147 -0
  6. package/dist/avatar-pixi.d.ts.map +1 -0
  7. package/dist/avatar-pixi.js +280 -0
  8. package/dist/avatar-pixi.js.map +1 -0
  9. package/dist/avatar-svg.d.ts +4 -0
  10. package/dist/avatar-svg.js +5 -0
  11. package/dist/avatar-three.d.ts +69 -0
  12. package/dist/avatar-three.d.ts.map +1 -0
  13. package/dist/avatar-three.js +49 -0
  14. package/dist/avatar-three.js.map +1 -0
  15. package/dist/avatar.d.ts +7 -0
  16. package/dist/avatar.js +487 -0
  17. package/dist/avatar.js.map +1 -0
  18. package/dist/daily.d.ts +2 -2
  19. package/dist/doctor/cli.mjs +1 -1
  20. package/dist/index.d.ts +5 -5
  21. package/dist/index.js +2 -2
  22. package/dist/input.d.ts +2 -2
  23. package/dist/shared/animator.d.ts +104 -0
  24. package/dist/shared/animator.d.ts.map +1 -0
  25. package/dist/shared/art.js +1804 -0
  26. package/dist/shared/art.js.map +1 -0
  27. package/dist/shared/atlas.d.ts +134 -0
  28. package/dist/shared/atlas.d.ts.map +1 -0
  29. package/dist/shared/avatar.d.ts +117 -0
  30. package/dist/shared/avatar.d.ts.map +1 -0
  31. package/dist/shared/bridge.js +13 -5
  32. package/dist/shared/bridge.js.map +1 -1
  33. package/dist/shared/client-version.gen.js +1 -1
  34. package/dist/shared/daily.d.ts +129 -18
  35. package/dist/shared/daily.d.ts.map +1 -1
  36. package/dist/shared/daily.js +120 -26
  37. package/dist/shared/daily.js.map +1 -1
  38. package/dist/shared/defineProperty.js +39 -0
  39. package/dist/shared/describe.d.ts +32 -0
  40. package/dist/shared/describe.d.ts.map +1 -0
  41. package/dist/shared/describe.js +101 -0
  42. package/dist/shared/describe.js.map +1 -0
  43. package/dist/shared/globals.js +1 -39
  44. package/dist/shared/globals.js.map +1 -1
  45. package/dist/shared/index.d.ts +537 -17
  46. package/dist/shared/index.d.ts.map +1 -1
  47. package/dist/shared/input.d.ts +31 -13
  48. package/dist/shared/input.d.ts.map +1 -1
  49. package/dist/shared/input.js +40 -2
  50. package/dist/shared/input.js.map +1 -1
  51. package/dist/shared/json-guards.js +48 -0
  52. package/dist/shared/json-guards.js.map +1 -0
  53. package/dist/shared/live-canvas.js +62 -0
  54. package/dist/shared/live-canvas.js.map +1 -0
  55. package/dist/shared/mp.js +1 -1
  56. package/dist/shared/overlays.js +800 -0
  57. package/dist/shared/overlays.js.map +1 -0
  58. package/dist/shared/page-stack.js +2 -1
  59. package/dist/shared/page-stack.js.map +1 -1
  60. package/dist/shared/pose-key.js +249 -0
  61. package/dist/shared/pose-key.js.map +1 -0
  62. package/dist/shared/race.js +1 -1
  63. package/dist/shared/rooms.js +1 -1
  64. package/dist/shared/scene.d.ts +90 -0
  65. package/dist/shared/scene.d.ts.map +1 -0
  66. package/dist/shared/social.d.ts +55 -4
  67. package/dist/shared/social.d.ts.map +1 -1
  68. package/dist/shared/src.js +2217 -199
  69. package/dist/shared/src.js.map +1 -1
  70. package/dist/shared/svg-markup.js +1860 -0
  71. package/dist/shared/svg-markup.js.map +1 -0
  72. package/dist/shared/svg.js +27 -0
  73. package/dist/shared/svg.js.map +1 -0
  74. package/dist/shared/types.d.ts +166 -0
  75. package/dist/shared/types.d.ts.map +1 -0
  76. package/dist/social.d.ts +2 -2
  77. package/dist/social.js +2 -2
  78. package/dist/turns.d.ts +39 -0
  79. package/dist/turns.d.ts.map +1 -0
  80. package/dist/turns.js +117 -0
  81. package/dist/turns.js.map +1 -0
  82. package/package.json +33 -3
  83. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  84. package/src/avatar/adapters/pixi-crowd.ts +302 -0
  85. package/src/avatar/adapters/pixi-types.ts +84 -0
  86. package/src/avatar/adapters/three.ts +126 -0
  87. package/src/avatar/animator/animator.ts +285 -0
  88. package/src/avatar/animator/reactions.ts +44 -0
  89. package/src/avatar/animator/schedule.ts +73 -0
  90. package/src/avatar/art/beard.ts +49 -0
  91. package/src/avatar/art/glasses.ts +9 -0
  92. package/src/avatar/art/hair.ts +254 -0
  93. package/src/avatar/art/headwear.ts +75 -0
  94. package/src/avatar/art/index.ts +21 -0
  95. package/src/avatar/art/shape.ts +68 -0
  96. package/src/avatar/art-store.ts +36 -0
  97. package/src/avatar/atlas.ts +236 -0
  98. package/src/avatar/catalog-view.ts +14 -0
  99. package/src/avatar/colour.ts +109 -0
  100. package/src/avatar/describe.ts +76 -0
  101. package/src/avatar/face.ts +187 -0
  102. package/src/avatar/geom.ts +168 -0
  103. package/src/avatar/layout.ts +47 -0
  104. package/src/avatar/look-like.ts +52 -0
  105. package/src/avatar/outputs/bitmap.ts +75 -0
  106. package/src/avatar/outputs/canvas-paint.ts +177 -0
  107. package/src/avatar/outputs/canvas.ts +62 -0
  108. package/src/avatar/outputs/custom-element.ts +140 -0
  109. package/src/avatar/outputs/element.ts +58 -0
  110. package/src/avatar/outputs/live-canvas.ts +57 -0
  111. package/src/avatar/outputs/overlay-svg.ts +22 -0
  112. package/src/avatar/outputs/pose-key.ts +30 -0
  113. package/src/avatar/outputs/svg-markup.ts +113 -0
  114. package/src/avatar/outputs/svg.ts +41 -0
  115. package/src/avatar/overlays.ts +128 -0
  116. package/src/avatar/render/body.ts +116 -0
  117. package/src/avatar/render/colours.ts +81 -0
  118. package/src/avatar/render/compose.ts +258 -0
  119. package/src/avatar/render/context.ts +117 -0
  120. package/src/avatar/render/eyes.ts +78 -0
  121. package/src/avatar/render/eyewear.ts +56 -0
  122. package/src/avatar/render/frame.ts +60 -0
  123. package/src/avatar/render/head.ts +81 -0
  124. package/src/avatar/render/mouth.ts +86 -0
  125. package/src/avatar/render/props.ts +91 -0
  126. package/src/avatar/render/types.ts +84 -0
  127. package/src/avatar/scene.ts +119 -0
  128. package/src/avatar-pixi.ts +20 -0
  129. package/src/avatar-svg.ts +21 -0
  130. package/src/avatar-three.ts +18 -0
  131. package/src/avatar.ts +62 -0
  132. package/src/core/avatar/avatar-cache.ts +95 -0
  133. package/src/core/avatar/avatar-ref.ts +34 -0
  134. package/src/core/avatar/avatar-types.ts +43 -0
  135. package/src/core/avatar/hub-avatar.ts +308 -0
  136. package/src/core/connection.ts +9 -0
  137. package/src/core/errors.ts +15 -2
  138. package/src/core/hub-progress.ts +269 -202
  139. package/src/core/hub.ts +174 -6
  140. package/src/core/offline/cloud-saves.ts +62 -0
  141. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  142. package/src/core/offline/hub-kinds/index.ts +14 -3
  143. package/src/core/offline/hub-kinds/progress-kinds.ts +21 -4
  144. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  145. package/src/core/offline/local-store.ts +46 -1
  146. package/src/core/page-account.ts +121 -0
  147. package/src/core/rest/routes.gen.ts +31 -1
  148. package/src/core/resume/hub-resume.ts +39 -5
  149. package/src/core/resume/resume-types.ts +2 -2
  150. package/src/core/saved/daily-saves.ts +196 -0
  151. package/src/core/saved/daily-seed.ts +16 -0
  152. package/src/core/saved/hub-saved.ts +221 -0
  153. package/src/core/saved/local-saved.ts +122 -0
  154. package/src/core/saved/saved-list.ts +129 -0
  155. package/src/core/saved/saved-open.ts +104 -0
  156. package/src/core/saved/saved-order.ts +49 -0
  157. package/src/core/saved/saved-types.ts +24 -0
  158. package/src/core/turns/hub-turns.ts +140 -0
  159. package/src/core/turns/turn-game.ts +388 -0
  160. package/src/core/version/client-version.gen.ts +2 -2
  161. package/src/daily/daily-system.ts +112 -13
  162. package/src/daily/page-daily.ts +2 -2
  163. package/src/daily/types.ts +46 -9
  164. package/src/doctor/hub-version-fetch.ts +43 -43
  165. package/src/index.ts +11 -1
  166. package/src/input/input-system.ts +10 -1
  167. package/src/input/navigator.ts +11 -0
  168. package/src/input.ts +1 -0
  169. package/src/overlay/overlay.ts +25 -1
  170. package/src/realtime/runtime-api.ts +14 -3
  171. package/src/social.ts +1 -0
  172. package/src/turns-kit/check-rules.ts +182 -0
  173. package/src/turns.ts +13 -0
@@ -0,0 +1,1860 @@
1
+ import { t as _defineProperty } from "./defineProperty.js";
2
+ import { decodeLook, encodeLook, normaliseHex, partName, resolveColour } from "@diffenderfer-games/hub-contract/avatar";
3
+ //#region src/avatar/look-like.ts
4
+ /** Turns anything a render accepts into a look (or `null` for the silhouette), caching decoded codes. */
5
+ const decoded = /* @__PURE__ */ new Map();
6
+ const DECODE_CACHE = 512;
7
+ /** Decodes a code once (an LRU of 512). */
8
+ function fromCode(code) {
9
+ const hit = decoded.get(code);
10
+ if (hit !== void 0 || decoded.has(code)) return hit ?? null;
11
+ const look = decodeLook(code)?.look ?? null;
12
+ if (decoded.size >= DECODE_CACHE) {
13
+ const oldest = decoded.keys().next().value;
14
+ if (oldest !== void 0) decoded.delete(oldest);
15
+ }
16
+ decoded.set(code, look);
17
+ return look;
18
+ }
19
+ /** The look a {@link LookLike} names, or `null` (hidden, malformed, missing). */
20
+ function toLook(like) {
21
+ if (like === null || like === void 0 || like === "") return null;
22
+ if (typeof like === "string") return fromCode(like);
23
+ if ("v" in like) return like;
24
+ if ("look" in like) return like.look;
25
+ return fromCode(like.code);
26
+ }
27
+ /** The code a {@link LookLike} names (`''` for none). */
28
+ function toCode(like) {
29
+ if (typeof like === "string") return like;
30
+ if (like && "code" in like) return like.code;
31
+ const look = toLook(like);
32
+ return look ? encodeLook(look) : "";
33
+ }
34
+ //#endregion
35
+ //#region src/avatar/colour.ts
36
+ const toLin = (c) => {
37
+ const v = c / 255;
38
+ return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
39
+ };
40
+ const fromLin = (c) => {
41
+ const v = c <= .0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - .055;
42
+ return Math.round(Math.min(1, Math.max(0, v)) * 255);
43
+ };
44
+ /** A `#rrggbb` colour in OKLab. */
45
+ function hexToLab(hex) {
46
+ const r = toLin(parseInt(hex.slice(1, 3), 16));
47
+ const g = toLin(parseInt(hex.slice(3, 5), 16));
48
+ const bl = toLin(parseInt(hex.slice(5, 7), 16));
49
+ const l = Math.cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * bl);
50
+ const m = Math.cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * bl);
51
+ const s = Math.cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * bl);
52
+ return {
53
+ L: .2104542553 * l + .793617785 * m - .0040720468 * s,
54
+ a: 1.9779984951 * l - 2.428592205 * m + .4505937099 * s,
55
+ b: .0259040371 * l + .7827717662 * m - .808675766 * s
56
+ };
57
+ }
58
+ /** An OKLab colour as `#rrggbb` (clipped to sRGB). */
59
+ function labToHex(lab) {
60
+ const { a, b } = lab;
61
+ const l = (lab.L + .3963377774 * a + .2158037573 * b) ** 3;
62
+ const m = (lab.L - .1055613458 * a - .0638541728 * b) ** 3;
63
+ const s = (lab.L - .0894841775 * a - 1.291485548 * b) ** 3;
64
+ return `#${[
65
+ fromLin(4.0767416621 * l - 3.3077115913 * m + .2309699292 * s),
66
+ fromLin(-1.2684380046 * l + 2.6097574011 * m - .3413193965 * s),
67
+ fromLin(-.0041960863 * l - .7034186147 * m + 1.707614701 * s)
68
+ ].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
69
+ }
70
+ /** Shifts lightness by `dL` (OKLab L units) and scales chroma. */
71
+ function adjust(hex, dL, chroma = 1) {
72
+ const c = hexToLab(hex);
73
+ return labToHex({
74
+ L: Math.min(.98, Math.max(.03, c.L + dL)),
75
+ a: c.a * chroma,
76
+ b: c.b * chroma
77
+ });
78
+ }
79
+ /** The one shade tone of a fill. */
80
+ const shade = (hex) => adjust(hex, -.1, 1.08);
81
+ /** The one highlight tone of a fill. */
82
+ const light = (hex) => adjust(hex, .09, .9);
83
+ /** Style C's tonal outline: a darker shade of the fill's own colour (§14.1). */
84
+ const outline = (hex) => adjust(hex, -.24, 1.1);
85
+ /** A dark ink in the fill's own hue (status props keep a strong edge). */
86
+ function ink(hex) {
87
+ const c = hexToLab(hex);
88
+ return labToHex({
89
+ L: Math.min(.24, c.L * .42),
90
+ a: c.a * .6,
91
+ b: c.b * .6
92
+ });
93
+ }
94
+ /** Linear mix in OKLab. */
95
+ function mix$1(a, b, t) {
96
+ const lhs = hexToLab(a);
97
+ const rhs = hexToLab(b);
98
+ return labToHex({
99
+ L: lhs.L + (rhs.L - lhs.L) * t,
100
+ a: lhs.a + (rhs.a - lhs.a) * t,
101
+ b: lhs.b + (rhs.b - lhs.b) * t
102
+ });
103
+ }
104
+ /** OKLab lightness, 0 … 1. */
105
+ const lightness = (hex) => hexToLab(hex).L;
106
+ /** Perceptual distance (OKLab ΔE × 100). */
107
+ function deltaE(a, b) {
108
+ const lhs = hexToLab(a);
109
+ const rhs = hexToLab(b);
110
+ return Math.hypot(lhs.L - rhs.L, lhs.a - rhs.a, lhs.b - rhs.b) * 100;
111
+ }
112
+ /**
113
+ * Keeps brows and facial hair visible on the skin (§14.2): a colour within
114
+ * ΔE 12 of the skin is darkened by 0.18 L.
115
+ */
116
+ function keepApart(colour, skin) {
117
+ return deltaE(colour, skin) >= 12 ? colour : adjust(colour, -.18);
118
+ }
119
+ /** WCAG relative luminance of a `#rrggbb`. */
120
+ function luminance(hex) {
121
+ const r = toLin(parseInt(hex.slice(1, 3), 16));
122
+ const g = toLin(parseInt(hex.slice(3, 5), 16));
123
+ const b = toLin(parseInt(hex.slice(5, 7), 16));
124
+ return .2126 * r + .7152 * g + .0722 * b;
125
+ }
126
+ /** WCAG contrast ratio between two colours. */
127
+ function contrast(a, b) {
128
+ const lhs = luminance(a);
129
+ const rhs = luminance(b);
130
+ return (Math.max(lhs, rhs) + .05) / (Math.min(lhs, rhs) + .05);
131
+ }
132
+ //#endregion
133
+ //#region src/avatar/scene.ts
134
+ /** A group helper that drops empty groups' wrapping. */
135
+ function g(children, opts = {}) {
136
+ return {
137
+ k: "g",
138
+ ...opts,
139
+ children
140
+ };
141
+ }
142
+ /** A path helper. */
143
+ function path(d, paint, tf) {
144
+ return {
145
+ k: "path",
146
+ d,
147
+ ...paint,
148
+ ...tf ? { tf } : {}
149
+ };
150
+ }
151
+ /** An ellipse helper. */
152
+ function ellipse(cx, cy, rx, ry, paint, tf) {
153
+ return {
154
+ k: "ellipse",
155
+ cx,
156
+ cy,
157
+ rx,
158
+ ry,
159
+ ...paint,
160
+ ...tf ? { tf } : {}
161
+ };
162
+ }
163
+ /** A rect helper. */
164
+ function rect(left, top, w, h, paint, r) {
165
+ return {
166
+ k: "rect",
167
+ x: left,
168
+ y: top,
169
+ w,
170
+ h,
171
+ ...paint,
172
+ ...r === void 0 ? {} : { r }
173
+ };
174
+ }
175
+ /** A translate step. */
176
+ const tr = (dx, dy) => ({
177
+ k: "t",
178
+ x: dx,
179
+ y: dy
180
+ });
181
+ /** A scale step (uniform when `sy` is omitted). */
182
+ const sc = (sx, sy = sx) => ({
183
+ k: "s",
184
+ x: sx,
185
+ y: sy
186
+ });
187
+ /** A rotate step, degrees. */
188
+ const rot = (a) => ({
189
+ k: "r",
190
+ a
191
+ });
192
+ /** A horizontal skew step, degrees. */
193
+ const skewX = (a) => ({
194
+ k: "skx",
195
+ a
196
+ });
197
+ /** A round-joined stroke paint. */
198
+ function line(colour, width, cap = "round", opacity) {
199
+ return {
200
+ fill: "none",
201
+ stroke: colour,
202
+ sw: width,
203
+ cap,
204
+ ...opacity === void 0 ? {} : { opacity }
205
+ };
206
+ }
207
+ //#endregion
208
+ //#region src/avatar/render/frame.ts
209
+ /**
210
+ * Frames (§3.4): which part of rig space an output shows, and the badge
211
+ * background (the site's rounded panel with a ring that lightens on dark
212
+ * skin, §3.9).
213
+ */
214
+ /** The three frames' boxes. */
215
+ const FRAMES = {
216
+ head: [
217
+ -66,
218
+ -164,
219
+ 132,
220
+ 132
221
+ ],
222
+ bust: [
223
+ -84,
224
+ -188,
225
+ 168,
226
+ 168
227
+ ],
228
+ full: [
229
+ -86,
230
+ -196,
231
+ 172,
232
+ 206
233
+ ]
234
+ };
235
+ /** The site panel colour behind badges. */
236
+ const PANEL = "#11141c";
237
+ /** The frame an options object asks for (default `head`). */
238
+ function frameOf(opts) {
239
+ return opts.frame ?? "head";
240
+ }
241
+ /** Output pixel size for a frame (the longer edge is `size`). */
242
+ function pixelSize(opts) {
243
+ const [, , w, h] = FRAMES[frameOf(opts)];
244
+ const scale = (opts.size ?? 48) / Math.max(w, h);
245
+ return {
246
+ width: w * scale,
247
+ height: h * scale,
248
+ scale
249
+ };
250
+ }
251
+ /** The badge ring colour: lighter when the skin is darker than the panel, so every skin has an edge. */
252
+ function ringColour(look) {
253
+ return look && lightness(resolveColour(look.skin)) < .42 ? "#6f7787" : "#2a3142";
254
+ }
255
+ /** Wraps the avatar's nodes in the frame's background (a badge clips to the rounded panel). */
256
+ function framed(nodes, look, opts) {
257
+ const [left, top, w, h] = FRAMES[frameOf(opts)];
258
+ const background = opts.background ?? (frameOf(opts) === "head" ? "badge" : "none");
259
+ if (background === "badge") {
260
+ const r = w * .24;
261
+ return [
262
+ rect(left, top, w, h, { fill: PANEL }, r),
263
+ g(nodes, { clip: { shapes: [rect(left, top, w, h, {}, r)] } }),
264
+ rect(left + 1.5, top + 1.5, w - 3, h - 3, {
265
+ fill: "none",
266
+ stroke: ringColour(look),
267
+ sw: 3
268
+ }, r - 1.5)
269
+ ];
270
+ }
271
+ if (background === "none") return [...nodes];
272
+ const fill = normaliseHex(background);
273
+ return fill ? [rect(left, top, w, h, { fill }), ...nodes] : [...nodes];
274
+ }
275
+ //#endregion
276
+ //#region src/avatar/art-store.ts
277
+ let art = null;
278
+ let loading = null;
279
+ const waiters = /* @__PURE__ */ new Set();
280
+ /** The loaded art, or `null` before {@link ready} resolves. */
281
+ function currentArt() {
282
+ return art;
283
+ }
284
+ /** Loads the art chunk (once). Resolves when rendering is synchronous. */
285
+ function ready() {
286
+ loading ?? (loading = import("./art.js").then((mod) => {
287
+ art = mod.ART;
288
+ for (const waiter of waiters) waiter();
289
+ waiters.clear();
290
+ }));
291
+ return loading;
292
+ }
293
+ /** Calls `fn` once the art has loaded (now, when it already has). */
294
+ function onArtReady(fn) {
295
+ if (art) {
296
+ fn();
297
+ return;
298
+ }
299
+ waiters.add(fn);
300
+ }
301
+ //#endregion
302
+ //#region src/avatar/face.ts
303
+ const brow$1 = (dy = 0, tilt = 0, arch = 2.5) => ({
304
+ dy,
305
+ tilt,
306
+ arch
307
+ });
308
+ const mouth$1 = (m) => ({
309
+ w: 13,
310
+ sL: 1.5,
311
+ sR: 1.5,
312
+ open: 0,
313
+ round: 0,
314
+ teeth: 0,
315
+ tongue: 0,
316
+ dx: 0,
317
+ ...m
318
+ });
319
+ const BASE = {
320
+ browL: brow$1(),
321
+ browR: brow$1(),
322
+ lidL: .12,
323
+ lidR: .12,
324
+ lowL: 0,
325
+ lowR: 0,
326
+ lidTilt: 0,
327
+ closedCurveL: 1,
328
+ closedCurveR: 1,
329
+ pupil: 1,
330
+ mouth: mouth$1({}),
331
+ cheeks: .25,
332
+ headTilt: 0,
333
+ squash: 0,
334
+ extras: []
335
+ };
336
+ const face = (f) => ({
337
+ ...BASE,
338
+ ...f
339
+ });
340
+ /** The launch set of 16 expressions (§3.5). Kid-friendly: angry and shocked stay cartoon-cute, cry is one tear. */
341
+ const EXPRESSIONS = {
342
+ neutral: BASE,
343
+ happy: face({
344
+ browL: brow$1(-2, 0, 3.5),
345
+ browR: brow$1(-2, 0, 3.5),
346
+ lowL: .22,
347
+ lowR: .22,
348
+ mouth: mouth$1({
349
+ w: 19,
350
+ sL: 5,
351
+ sR: 5,
352
+ open: 7,
353
+ teeth: .5,
354
+ tongue: .6
355
+ }),
356
+ cheeks: .65
357
+ }),
358
+ laugh: face({
359
+ browL: brow$1(-4, -1, 4),
360
+ browR: brow$1(-4, -1, 4),
361
+ lidL: 1,
362
+ lidR: 1,
363
+ mouth: mouth$1({
364
+ w: 22,
365
+ sL: 6,
366
+ sR: 6,
367
+ open: 13,
368
+ teeth: .45,
369
+ tongue: .9
370
+ }),
371
+ cheeks: .9,
372
+ headTilt: -6,
373
+ squash: .03,
374
+ extras: ["sparkle"]
375
+ }),
376
+ grin: face({
377
+ browL: brow$1(-2.5, 0, 3.5),
378
+ browR: brow$1(-2.5, 0, 3.5),
379
+ lowL: .3,
380
+ lowR: .3,
381
+ mouth: mouth$1({
382
+ w: 21,
383
+ sL: 5.5,
384
+ sR: 5.5,
385
+ open: 4.5,
386
+ teeth: 1
387
+ }),
388
+ cheeks: .7
389
+ }),
390
+ sad: face({
391
+ browL: brow$1(1, -4.5, 1),
392
+ browR: brow$1(1, -4.5, 1),
393
+ lidL: .3,
394
+ lidR: .3,
395
+ lidTilt: -3,
396
+ mouth: mouth$1({
397
+ w: 12,
398
+ sL: -4,
399
+ sR: -4
400
+ }),
401
+ cheeks: .1,
402
+ headTilt: 4
403
+ }),
404
+ cry: face({
405
+ browL: brow$1(1, -5.5, 1),
406
+ browR: brow$1(1, -5.5, 1),
407
+ lidL: .4,
408
+ lidR: .4,
409
+ lidTilt: -3,
410
+ lowL: .15,
411
+ lowR: .15,
412
+ mouth: mouth$1({
413
+ w: 13,
414
+ sL: -4.5,
415
+ sR: -4.5,
416
+ open: 3
417
+ }),
418
+ cheeks: .4,
419
+ headTilt: 5,
420
+ extras: ["tear"]
421
+ }),
422
+ surprised: face({
423
+ browL: brow$1(-7, -1, 4),
424
+ browR: brow$1(-7, -1, 4),
425
+ lidL: 0,
426
+ lidR: 0,
427
+ pupil: .8,
428
+ mouth: mouth$1({
429
+ w: 9,
430
+ sL: 0,
431
+ sR: 0,
432
+ open: 11,
433
+ round: 1,
434
+ tongue: .3
435
+ }),
436
+ cheeks: .2,
437
+ squash: -.05
438
+ }),
439
+ shocked: face({
440
+ browL: brow$1(-9, -2, 4.5),
441
+ browR: brow$1(-9, -2, 4.5),
442
+ lidL: 0,
443
+ lidR: 0,
444
+ pupil: .7,
445
+ mouth: mouth$1({
446
+ w: 11,
447
+ sL: -1,
448
+ sR: -1,
449
+ open: 14,
450
+ round: .8,
451
+ tongue: .4
452
+ }),
453
+ cheeks: .15,
454
+ squash: -.07,
455
+ extras: ["sweat"]
456
+ }),
457
+ angry: face({
458
+ browL: brow$1(2, 5.5, 0),
459
+ browR: brow$1(2, 5.5, 0),
460
+ lidL: .32,
461
+ lidR: .32,
462
+ lidTilt: 4,
463
+ mouth: mouth$1({
464
+ w: 15,
465
+ sL: -2.5,
466
+ sR: -2.5,
467
+ open: 3.5,
468
+ teeth: .9
469
+ }),
470
+ cheeks: .35,
471
+ extras: ["anger"]
472
+ }),
473
+ grumpy: face({
474
+ browL: brow$1(2, 3.5, .5),
475
+ browR: brow$1(2, 3.5, .5),
476
+ lidL: .42,
477
+ lidR: .42,
478
+ lidTilt: 2,
479
+ mouth: mouth$1({
480
+ w: 12,
481
+ sL: -2,
482
+ sR: -2.5,
483
+ dx: -1
484
+ }),
485
+ cheeks: .1,
486
+ headTilt: -3
487
+ }),
488
+ thinking: face({
489
+ browL: brow$1(-5, -1.5, 3.5),
490
+ browR: brow$1(1.5, 3, .5),
491
+ lidL: .1,
492
+ lidR: .3,
493
+ mouth: mouth$1({
494
+ w: 9,
495
+ sL: -.5,
496
+ sR: 1.5,
497
+ dx: 4
498
+ }),
499
+ cheeks: .15,
500
+ headTilt: -5,
501
+ extras: ["question"]
502
+ }),
503
+ wink: face({
504
+ browL: brow$1(-2, 0, 3.5),
505
+ browR: brow$1(1.5, 1.5, 1.5),
506
+ lidR: 1,
507
+ lowL: .15,
508
+ mouth: mouth$1({
509
+ w: 16,
510
+ sL: 2,
511
+ sR: 5.5,
512
+ dx: 1.5
513
+ }),
514
+ cheeks: .6,
515
+ headTilt: 3,
516
+ extras: ["sparkle"]
517
+ }),
518
+ smug: face({
519
+ browL: brow$1(0, 1, 1.5),
520
+ browR: brow$1(-4, -1, 3.5),
521
+ lidL: .45,
522
+ lidR: .4,
523
+ lowL: .1,
524
+ lowR: .1,
525
+ mouth: mouth$1({
526
+ w: 14,
527
+ sL: 1,
528
+ sR: 5,
529
+ dx: 3
530
+ }),
531
+ cheeks: .45,
532
+ headTilt: 4
533
+ }),
534
+ worried: face({
535
+ browL: brow$1(-2, -5, 2),
536
+ browR: brow$1(-2, -5, 2),
537
+ lidL: .05,
538
+ lidR: .05,
539
+ pupil: .9,
540
+ mouth: mouth$1({
541
+ w: 11,
542
+ sL: -1.5,
543
+ sR: -1,
544
+ open: 1.5
545
+ }),
546
+ cheeks: .1,
547
+ extras: ["sweat"]
548
+ }),
549
+ sleepy: face({
550
+ browL: brow$1(1, -1, 1.5),
551
+ browR: brow$1(1, -1, 1.5),
552
+ lidL: .72,
553
+ lidR: .72,
554
+ closedCurveL: -1,
555
+ closedCurveR: -1,
556
+ mouth: mouth$1({
557
+ w: 7,
558
+ sL: 0,
559
+ sR: 0,
560
+ open: 2.5,
561
+ round: 1
562
+ }),
563
+ cheeks: .2,
564
+ headTilt: 7,
565
+ extras: ["zzz"]
566
+ }),
567
+ determined: face({
568
+ browL: brow$1(1, 3.5, .5),
569
+ browR: brow$1(1, 3.5, .5),
570
+ lidL: .22,
571
+ lidR: .22,
572
+ lidTilt: 2,
573
+ mouth: mouth$1({
574
+ w: 12,
575
+ sL: 1,
576
+ sR: 1
577
+ }),
578
+ cheeks: .25
579
+ })
580
+ };
581
+ /** Every expression name, in the launch order. */
582
+ const EXPRESSION_NAMES = [
583
+ "neutral",
584
+ "happy",
585
+ "laugh",
586
+ "grin",
587
+ "sad",
588
+ "cry",
589
+ "surprised",
590
+ "shocked",
591
+ "angry",
592
+ "grumpy",
593
+ "thinking",
594
+ "wink",
595
+ "smug",
596
+ "worried",
597
+ "sleepy",
598
+ "determined"
599
+ ];
600
+ /** True for a known expression name. */
601
+ function isExpression(name) {
602
+ return Object.prototype.hasOwnProperty.call(EXPRESSIONS, name);
603
+ }
604
+ const lerp = (a, b, t) => a + (b - a) * t;
605
+ const mixBrow = (a, b, t) => ({
606
+ dy: lerp(a.dy, b.dy, t),
607
+ tilt: lerp(a.tilt, b.tilt, t),
608
+ arch: lerp(a.arch, b.arch, t)
609
+ });
610
+ const mixMouth = (a, b, t) => ({
611
+ w: lerp(a.w, b.w, t),
612
+ sL: lerp(a.sL, b.sL, t),
613
+ sR: lerp(a.sR, b.sR, t),
614
+ open: lerp(a.open, b.open, t),
615
+ round: lerp(a.round, b.round, t),
616
+ teeth: lerp(a.teeth, b.teeth, t),
617
+ tongue: lerp(a.tongue, b.tongue, t),
618
+ dx: lerp(a.dx, b.dx, t)
619
+ });
620
+ /** An expression's parameters (a name or parameters pass through). */
621
+ function faceOf(e) {
622
+ return typeof e === "string" ? EXPRESSIONS[e] : e;
623
+ }
624
+ /** Blends two expressions: `mix('happy', 'surprised', 0.4)`. */
625
+ function mix(a, b, t) {
626
+ const from = faceOf(a);
627
+ const to = faceOf(b);
628
+ return {
629
+ browL: mixBrow(from.browL, to.browL, t),
630
+ browR: mixBrow(from.browR, to.browR, t),
631
+ lidL: lerp(from.lidL, to.lidL, t),
632
+ lidR: lerp(from.lidR, to.lidR, t),
633
+ lowL: lerp(from.lowL, to.lowL, t),
634
+ lowR: lerp(from.lowR, to.lowR, t),
635
+ lidTilt: lerp(from.lidTilt, to.lidTilt, t),
636
+ closedCurveL: lerp(from.closedCurveL, to.closedCurveL, t),
637
+ closedCurveR: lerp(from.closedCurveR, to.closedCurveR, t),
638
+ pupil: lerp(from.pupil, to.pupil, t),
639
+ mouth: mixMouth(from.mouth, to.mouth, t),
640
+ cheeks: lerp(from.cheeks, to.cheeks, t),
641
+ headTilt: lerp(from.headTilt, to.headTilt, t),
642
+ squash: lerp(from.squash, to.squash, t),
643
+ extras: t < .5 ? from.extras : to.extras
644
+ };
645
+ }
646
+ /** The hub's quick-chat emotes (`e:<emoji>`, the safety package's EMOTES) mapped to faces (§3.7), so an emote can also play on the sender's face. */
647
+ const EMOTE_FACES = {
648
+ "😀": "happy",
649
+ "😂": "laugh",
650
+ "😮": "surprised",
651
+ "😢": "sad",
652
+ "😎": "smug",
653
+ "🤔": "thinking",
654
+ "👍": "grin",
655
+ "👏": "happy",
656
+ "🎉": "laugh",
657
+ "❤️": "happy",
658
+ "🔥": "determined",
659
+ "⭐": "grin"
660
+ };
661
+ /** The face for a quick-chat emote (`e:😂` or the bare emoji), or `null`. */
662
+ function expressionFromEmote(emoteId) {
663
+ return EMOTE_FACES[emoteId.startsWith("e:") ? emoteId.slice(2) : emoteId] ?? null;
664
+ }
665
+ //#endregion
666
+ //#region src/avatar/geom.ts
667
+ /** Rounds to one decimal and drops a trailing `.0` and negative zero. */
668
+ function n(v) {
669
+ const r = Math.round(v * 10) / 10;
670
+ return Object.is(r, -0) ? "0" : String(r);
671
+ }
672
+ /** A path of absolute M/L/C/Q/Z commands that can be warped as a whole. */
673
+ var Path = class {
674
+ constructor() {
675
+ _defineProperty(this, "cmds", []);
676
+ }
677
+ m(px, py) {
678
+ this.cmds.push({
679
+ c: "M",
680
+ p: [px, py]
681
+ });
682
+ return this;
683
+ }
684
+ l(px, py) {
685
+ this.cmds.push({
686
+ c: "L",
687
+ p: [px, py]
688
+ });
689
+ return this;
690
+ }
691
+ c(x1, y1, x2, y2, px, py) {
692
+ this.cmds.push({
693
+ c: "C",
694
+ p: [
695
+ x1,
696
+ y1,
697
+ x2,
698
+ y2,
699
+ px,
700
+ py
701
+ ]
702
+ });
703
+ return this;
704
+ }
705
+ q(x1, y1, px, py) {
706
+ this.cmds.push({
707
+ c: "Q",
708
+ p: [
709
+ x1,
710
+ y1,
711
+ px,
712
+ py
713
+ ]
714
+ });
715
+ return this;
716
+ }
717
+ z() {
718
+ this.cmds.push({
719
+ c: "Z",
720
+ p: []
721
+ });
722
+ return this;
723
+ }
724
+ /** Appends every command of another path. */
725
+ add(other) {
726
+ this.cmds.push(...other.cmds);
727
+ return this;
728
+ }
729
+ /** The SVG `d` string, optionally warping every point. */
730
+ d(warp) {
731
+ return this.cmds.map(({ c, p }) => {
732
+ if (c === "Z") return "Z";
733
+ const out = [];
734
+ for (let i = 0; i < p.length; i += 2) {
735
+ const px = p[i] ?? 0;
736
+ const py = p[i + 1] ?? 0;
737
+ const [wx, wy] = warp ? warp(px, py) : [px, py];
738
+ out.push(`${n(wx)} ${n(wy)}`);
739
+ }
740
+ return c + out.join(" ");
741
+ }).join("");
742
+ }
743
+ };
744
+ /** A point by index, or the origin. */
745
+ const at = (pts, i) => pts[i] ?? [0, 0];
746
+ /**
747
+ * A smooth Catmull-Rom curve through the points (cubic segments). Points
748
+ * flagged `'c'` become sharp corners.
749
+ */
750
+ function blob(pts, isClosed = true, tension = 1, into = new Path()) {
751
+ const count = pts.length;
752
+ if (count === 0) return into;
753
+ const wrap = (i) => isClosed ? at(pts, (i + count) % count) : at(pts, Math.max(0, Math.min(count - 1, i)));
754
+ const tangent = (i) => {
755
+ if (wrap(i)[2] === "c" || !isClosed && (i === 0 || i === count - 1)) return [0, 0];
756
+ const a = wrap(i - 1);
757
+ const b = wrap(i + 1);
758
+ return [(b[0] - a[0]) * tension / 6, (b[1] - a[1]) * tension / 6];
759
+ };
760
+ const first = at(pts, 0);
761
+ into.m(first[0], first[1]);
762
+ const segs = isClosed ? count : count - 1;
763
+ for (let i = 0; i < segs; i += 1) {
764
+ const p1 = wrap(i);
765
+ const p2 = wrap(i + 1);
766
+ const t1 = tangent(i);
767
+ const t2 = tangent(i + 1);
768
+ into.c(p1[0] + t1[0], p1[1] + t1[1], p2[0] - t2[0], p2[1] - t2[1], p2[0], p2[1]);
769
+ }
770
+ if (isClosed) into.z();
771
+ return into;
772
+ }
773
+ /** Points along an ellipse arc; angles in degrees, 0 = right, 90 = down. */
774
+ function arc(cx, cy, rx, ry, a0, a1, steps) {
775
+ const out = [];
776
+ for (let i = 0; i <= steps; i += 1) {
777
+ const a = (a0 + (a1 - a0) * i / steps) * Math.PI / 180;
778
+ out.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
779
+ }
780
+ return out;
781
+ }
782
+ /**
783
+ * A scalloped edge: one outward bump between each pair of points (quadratic
784
+ * curves), for curls and clouds. `side` 1 bumps to the left of travel.
785
+ */
786
+ function scallop(pts, bump, isClosed, side = 1, into = new Path()) {
787
+ const count = pts.length;
788
+ const first = at(pts, 0);
789
+ into.m(first[0], first[1]);
790
+ const segs = isClosed ? count : count - 1;
791
+ for (let i = 0; i < segs; i += 1) {
792
+ const a = at(pts, i);
793
+ const b = at(pts, (i + 1) % count);
794
+ const dx = b[0] - a[0];
795
+ const dy = b[1] - a[1];
796
+ const len = Math.hypot(dx, dy) || 1;
797
+ into.q((a[0] + b[0]) / 2 + dy / len * bump * side, (a[1] + b[1]) / 2 + -dx / len * bump * side, b[0], b[1]);
798
+ }
799
+ if (isClosed) into.z();
800
+ return into;
801
+ }
802
+ /** A rounded rectangle path. */
803
+ function roundedRect(px, py, w, h, r) {
804
+ return new Path().m(px + r, py).l(px + w - r, py).q(px + w, py, px + w, py + r).l(px + w, py + h - r).q(px + w, py + h, px + w - r, py + h).l(px + r, py + h).q(px, py + h, px, py + h - r).l(px, py + r).q(px, py, px + r, py).z();
805
+ }
806
+ /** Clamps `v` to [lo, hi]. */
807
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
808
+ //#endregion
809
+ //#region src/avatar/render/colours.ts
810
+ /**
811
+ * Resolves a look's colours into every tint slot the art uses (§3.2): each
812
+ * fill gets its shade, highlight and tonal outline (style C, §14.1), brows
813
+ * and beards are kept apart from the skin (§14.2), and accessories default
814
+ * to the body colour.
815
+ */
816
+ /** The soft dark-brown ink for lids, mouths and closed eyes (§14.1). */
817
+ const SOFT_INK = "#3a2a2a";
818
+ /** The fixed colours every look shares. */
819
+ const FIXED = {
820
+ ink: SOFT_INK,
821
+ white: "#fbfaf6",
822
+ mouth: "#5a1d2a",
823
+ tongue: "#ff7d8e",
824
+ blush: "#ff7b8f",
825
+ pupil: "#140f0d"
826
+ };
827
+ /** A fill and its tones. */
828
+ function tones(hex) {
829
+ return {
830
+ base: hex,
831
+ shade: shade(hex),
832
+ light: light(hex),
833
+ ink: outline(hex)
834
+ };
835
+ }
836
+ /** The colours of a look. */
837
+ function lookColours(look) {
838
+ const skin = resolveColour(look.skin);
839
+ const hair = resolveColour(look.hair.colour);
840
+ const browBase = look.brows.colour === void 0 ? lightness(hair) > .78 ? adjust(hair, -.22) : adjust(hair, -.05) : resolveColour(look.brows.colour);
841
+ const beard = keepApart(look.facialHair.colour === void 0 ? hair : resolveColour(look.facialHair.colour), skin);
842
+ const eye = resolveColour(look.eyes.colour);
843
+ const body = resolveColour(look.body.colour);
844
+ return {
845
+ skin,
846
+ skinShade: shade(skin),
847
+ skinLight: light(skin),
848
+ skinInk: outline(skin),
849
+ eyeRim: adjust(skin, -.2, 1.1),
850
+ hair,
851
+ hairShade: shade(hair),
852
+ hairLight: light(hair),
853
+ hairInk: outline(hair),
854
+ hairBuzz: mix$1(hair, skin, .42),
855
+ brow: keepApart(browBase, skin),
856
+ beard,
857
+ beardShade: shade(beard),
858
+ beardInk: outline(beard),
859
+ stubble: mix$1(beard, skin, .35),
860
+ eye,
861
+ eyeDark: adjust(eye, -.18),
862
+ body,
863
+ bodyShade: shade(body),
864
+ bodyLight: light(body),
865
+ bodyInk: outline(body)
866
+ };
867
+ }
868
+ /** An accessory's colour: its own, else the body colour. */
869
+ function accessoryColour(look, accessory) {
870
+ return accessory?.colour === void 0 ? resolveColour(look.body.colour) : resolveColour(accessory.colour);
871
+ }
872
+ /** Style C's outline width at the 100-unit head height (§14.1). */
873
+ const LINE = 2.4;
874
+ /** Part fills are outlined a little thinner than lines (the POC's style C). */
875
+ const FILL_OUTLINE = .8;
876
+ /** The head outline (centre 0,0; 100 units tall). */
877
+ const HEAD_PATH = new Path().m(0, -50).c(27, -50, 46, -32, 46, -4).c(46, 24, 30, 50, 0, 50).c(-30, 50, -46, 24, -46, -4).c(-46, -32, -27, -50, 0, -50).z();
878
+ /** The head as a clip region. */
879
+ const HEAD_CLIP = { shapes: [path(HEAD_PATH.d(), {})] };
880
+ /** Where a face feature centred at `x0` lands after the head turn. */
881
+ function feat(ctx, x0) {
882
+ const t0 = Math.asin(clamp(x0 / 46, -1, 1));
883
+ const t = t0 + ctx.phi;
884
+ return {
885
+ x: 46 * Math.sin(t),
886
+ sx: Math.max(.05, Math.cos(t) / Math.cos(t0)),
887
+ vis: Math.cos(t) > .22
888
+ };
889
+ }
890
+ /**
891
+ * A warp that wraps a part around the skull by `phi`. Unlike a strict
892
+ * cylinder, the far side is squeezed toward the silhouette instead of
893
+ * folding behind it, so outlines never collapse into wedges (§14.2).
894
+ */
895
+ function turnWarp(phi, dy = 0) {
896
+ if (phi === 0 && dy === 0) return;
897
+ const half = Math.PI / 2;
898
+ return (px, py) => {
899
+ const ax = Math.abs(px);
900
+ const sg = Math.sign(px) || 1;
901
+ const t0 = Math.asin(Math.min(ax, 46) / 46) * sg;
902
+ const t = t0 + phi * (phi >= 0 ? t0 <= 0 ? 1 : 1 - t0 / half : t0 >= 0 ? 1 : 1 + t0 / half);
903
+ const over = Math.max(0, ax - 46) * Math.max(.25, Math.abs(Math.sin(t)));
904
+ return [46 * Math.sin(t) + sg * over, py + dy];
905
+ };
906
+ }
907
+ /** Resolves a tint slot. `acc` is the accessory (or hair-tie) colour for this part. */
908
+ function slotColour(c, acc, slot) {
909
+ switch (slot) {
910
+ case "none": return "none";
911
+ case "acc": return acc.base;
912
+ case "accShade": return acc.shade;
913
+ case "accLight": return acc.light;
914
+ case "accInk": return acc.ink;
915
+ case "ink": return FIXED.ink;
916
+ case "white": return FIXED.white;
917
+ default: return c[slot];
918
+ }
919
+ }
920
+ /** The tonal outline slot for a fill slot. */
921
+ function outlineOf(slot) {
922
+ if (slot.startsWith("hair")) return "hairInk";
923
+ if (slot === "beard" || slot === "beardShade" || slot === "stubble") return "beardInk";
924
+ if (slot.startsWith("acc")) return "accInk";
925
+ if (slot.startsWith("skin")) return "skinInk";
926
+ return "ink";
927
+ }
928
+ /** Paints catalog shapes with an optional warp; `clip: 'head'` shapes are clipped to the head. */
929
+ function paintShapes(c, acc, shapes, warp) {
930
+ if (!shapes) return [];
931
+ return shapes.map((s) => {
932
+ const d = s.d.d(warp);
933
+ const opacity = s.opacity === void 0 ? {} : { opacity: s.opacity };
934
+ if (s.line) return path(d, {
935
+ fill: "none",
936
+ stroke: slotColour(c, acc, s.stroke ?? "ink"),
937
+ sw: s.sw ?? 2.4,
938
+ cap: s.cap ?? "round",
939
+ ...opacity
940
+ });
941
+ const strokeSlot = s.stroke ?? outlineOf(s.fill);
942
+ const stroke = strokeSlot === "none" ? {} : {
943
+ stroke: slotColour(c, acc, strokeSlot),
944
+ sw: (s.sw ?? 2.4) * FILL_OUTLINE
945
+ };
946
+ const node = path(d, {
947
+ fill: slotColour(c, acc, s.fill),
948
+ ...s.evenOdd ? { evenOdd: true } : {},
949
+ ...stroke,
950
+ ...opacity
951
+ });
952
+ return s.clip === "head" ? g([node], { clip: HEAD_CLIP }) : node;
953
+ });
954
+ }
955
+ //#endregion
956
+ //#region src/avatar/render/body.ts
957
+ /**
958
+ * The body blob and its poses (§3.11). Each pose is a transform of the same
959
+ * gumdrop plus where the head sits; a future limbed body re-implements the
960
+ * same names. The cycle poses (`walk`, `run`, `dance`, `jump`, `cheer`) read
961
+ * `phase` (0 … 1).
962
+ */
963
+ const BODY = blob([
964
+ [-35, -2],
965
+ [-40, -24],
966
+ [-30, -50],
967
+ [0, -60],
968
+ [30, -50],
969
+ [40, -24],
970
+ [35, -2],
971
+ [0, 2]
972
+ ]).d();
973
+ const STOOL = [
974
+ rect(-27, -12, 6, 12, {
975
+ fill: "#2a3142",
976
+ stroke: "#0d0f14",
977
+ sw: 2
978
+ }),
979
+ rect(21, -12, 6, 12, {
980
+ fill: "#2a3142",
981
+ stroke: "#0d0f14",
982
+ sw: 2
983
+ }),
984
+ rect(-38, -22, 76, 11, {
985
+ fill: "#4a5470",
986
+ stroke: "#0d0f14",
987
+ sw: 3
988
+ }, 5.5)
989
+ ];
990
+ const hand = (c, cx, cy) => ellipse(cx, cy, 8, 8, {
991
+ fill: c.body,
992
+ stroke: c.bodyInk,
993
+ sw: LINE
994
+ });
995
+ /** Lays out the body for a pose; `phi` is the body turn (shading shifts with it). */
996
+ function bodyLayout(c, pose, phi, phase, hasStool) {
997
+ const wave = Math.sin(phase * Math.PI * 2);
998
+ const hop = Math.abs(Math.sin(phase * Math.PI));
999
+ let tf = [];
1000
+ let headDx = 0;
1001
+ let headDy = 0;
1002
+ let tilt = 0;
1003
+ let lift = 0;
1004
+ const behind = [];
1005
+ const front = [];
1006
+ switch (pose) {
1007
+ case "sit":
1008
+ case "play": {
1009
+ const up = hasStool ? -18 : 0;
1010
+ tf = [tr(0, up), sc(1.12, .8)];
1011
+ headDy = 12 + up;
1012
+ if (hasStool) behind.push(...STOOL);
1013
+ break;
1014
+ }
1015
+ case "walk":
1016
+ case "run": {
1017
+ const k = pose === "run" ? 1.6 : 1;
1018
+ tf = [
1019
+ tr(0, -10 * k),
1020
+ skewX(-10 * k),
1021
+ sc(1 - wave * .04, 1 + wave * .06)
1022
+ ];
1023
+ headDy = -14 * k + wave * 3;
1024
+ headDx = 7 * k;
1025
+ tilt = 6 * k;
1026
+ behind.push(path("M-52 -30h-14M-50 -18h-20M-54 -6h-10", line("#6f7787", 3, "round", .8)));
1027
+ break;
1028
+ }
1029
+ case "wave":
1030
+ tf = [rot(-6)];
1031
+ headDx = -5;
1032
+ tilt = -7;
1033
+ front.push(hand(c, 44, -58), path("M56 -70Q61 -64 58 -56M62 -76Q69 -66 64 -54", line("#e8e6df", 2.2, "round", .8)));
1034
+ break;
1035
+ case "cheer":
1036
+ lift = hop * 10;
1037
+ tf = [sc(1 - hop * .04, 1 + hop * .05)];
1038
+ headDy = -4 * hop;
1039
+ front.push(hand(c, -42, -66 - hop * 4), hand(c, 42, -66 - hop * 4));
1040
+ break;
1041
+ case "jump":
1042
+ lift = hop * 18;
1043
+ tf = [sc(1 - hop * .06, 1 + hop * .08)];
1044
+ headDy = -6 * hop;
1045
+ break;
1046
+ case "dance":
1047
+ tf = [rot(wave * 8), sc(1 + hop * .03, 1 - hop * .03)];
1048
+ headDx = wave * 5;
1049
+ tilt = wave * 7;
1050
+ break;
1051
+ case "away":
1052
+ tf = [sc(1.04, .95)];
1053
+ headDy = 3;
1054
+ tilt = 5;
1055
+ }
1056
+ const hl = -16 - Math.sin(phi) * 8;
1057
+ const body = [g([
1058
+ path(BODY, { fill: c.bodyShade }),
1059
+ path(BODY, { fill: c.body }, [tr(-3 - Math.sin(phi) * 2, -3), sc(.9)]),
1060
+ ellipse(0, -54, 20, 6, { fill: c.bodyShade }),
1061
+ ellipse(0, 0, 6, 4, { fill: c.bodyLight }, [tr(hl, -36), rot(-30)]),
1062
+ path(BODY, {
1063
+ fill: "none",
1064
+ stroke: c.bodyInk,
1065
+ sw: LINE
1066
+ })
1067
+ ], { tf })];
1068
+ const isSitting = pose === "sit" || pose === "play";
1069
+ return {
1070
+ behind,
1071
+ body,
1072
+ front,
1073
+ headDx,
1074
+ headDy,
1075
+ tilt,
1076
+ lift,
1077
+ prop: {
1078
+ x: headDx * .5,
1079
+ y: isSitting ? -34 + (hasStool ? -18 : 0) * .8 : -30
1080
+ }
1081
+ };
1082
+ }
1083
+ /** The soft ground shadow at the feet (shrinks while lifted). */
1084
+ function groundShadow(lift) {
1085
+ const k = Math.max(.5, 1 - lift / 40);
1086
+ return ellipse(0, 0, 36 * k, 8 * k, {
1087
+ fill: "#000000",
1088
+ opacity: .3
1089
+ });
1090
+ }
1091
+ //#endregion
1092
+ //#region src/avatar/render/eyewear.ts
1093
+ /** Glasses: arms toward the ears, a lens at each eye, and the bridge. */
1094
+ function glasses(ctx, part, frame) {
1095
+ const out = [];
1096
+ const warp = turnWarp(ctx.phi, ctx.dy);
1097
+ for (const side of [-1, 1]) if (feat(ctx, 30 * side).vis) out.push(path(new Path().m(30 * side, 2).l(45 * side, 0).d(warp), line(frame, 2.6)));
1098
+ for (const side of [-1, 1]) {
1099
+ const f = feat(ctx, 17 * side);
1100
+ if (!f.vis) continue;
1101
+ out.push(g([path(part.lens.d(), {
1102
+ fill: "#ffffff",
1103
+ fillOpacity: .16,
1104
+ stroke: frame,
1105
+ sw: 3.2
1106
+ }), path("M-6 -4L-2 -7", line("#ffffff", 1.8, "round", .7))], { tf: [tr(f.x, 4 + ctx.dy), sc(f.sx, 1)] }));
1107
+ }
1108
+ const fb = feat(ctx, 0);
1109
+ out.push(g([path(part.bridge.d(), line(frame, 2.8))], { tf: [tr(fb.x, 4 + ctx.dy), sc(fb.sx, 1)] }));
1110
+ return out;
1111
+ }
1112
+ const STRAP_FRONT = new Path().m(-47, -2).c(-30, -6, 30, -6, 47, -2);
1113
+ const STRAP_BACK = new Path().m(-47, -2).c(-30, 4, 30, 4, 47, -2);
1114
+ const SHELL = blob([
1115
+ [-31, -9],
1116
+ [-4, -10],
1117
+ [0, -6],
1118
+ [4, -10],
1119
+ [31, -9],
1120
+ [34, 4],
1121
+ [26, 16],
1122
+ [7, 16],
1123
+ [0, 11],
1124
+ [-7, 16],
1125
+ [-26, 16],
1126
+ [-34, 4]
1127
+ ]);
1128
+ const GLASS_L = blob([
1129
+ [-27, -5],
1130
+ [-7, -6],
1131
+ [-5, 8],
1132
+ [-9, 12],
1133
+ [-24, 12],
1134
+ [-29, 4]
1135
+ ]);
1136
+ const GLASS_R = blob([
1137
+ [27, -5],
1138
+ [7, -6],
1139
+ [5, 8],
1140
+ [9, 12],
1141
+ [24, 12],
1142
+ [29, 4]
1143
+ ]);
1144
+ /** Play goggles over the eyes (they replace the eyes and any glasses); the strap is the body colour. */
1145
+ function goggles(ctx) {
1146
+ const w = turnWarp(ctx.phi, ctx.dy);
1147
+ const band = ctx.c.body;
1148
+ const strap = STRAP_FRONT.d(w);
1149
+ return [
1150
+ path(strap, line(ink(band), 9)),
1151
+ path(strap, line(band, 6)),
1152
+ path(SHELL.d(w), {
1153
+ fill: "#2a3142",
1154
+ stroke: "#0d0f14",
1155
+ sw: LINE
1156
+ }),
1157
+ path(GLASS_L.d(w), { fill: "#5fe3f2" }),
1158
+ path(GLASS_R.d(w), { fill: "#5fe3f2" }),
1159
+ path(new Path().m(-22, 6).l(-14, -2).d(w), line("#e8fbff", 3)),
1160
+ path(new Path().m(12, 6).l(20, -2).d(w), line("#e8fbff", 3))
1161
+ ];
1162
+ }
1163
+ /** The goggles strap seen from behind. */
1164
+ function gogglesStrapBack(ctx, phi) {
1165
+ const d = STRAP_BACK.d(turnWarp(phi));
1166
+ return [path(d, line(ink(ctx.c.body), 9)), path(d, line(ctx.c.body, 6))];
1167
+ }
1168
+ //#endregion
1169
+ //#region src/avatar/render/eyes.ts
1170
+ /**
1171
+ * Eyes and brows in style C (§14.1): whites 7% larger than style A's with a
1172
+ * 1.6-unit rim in a darker shade of the skin, an iris with a darker ring, a
1173
+ * pupil, two glints, and a soft dark-brown lid line. No dark outline.
1174
+ */
1175
+ /** Eye-white radii: style A's 9.6 × 11.8, 7% larger. */
1176
+ const RX = 10.272;
1177
+ const RY = 11.8 * 1.07;
1178
+ /** One eye; `side` −1 is the viewer's left. */
1179
+ function eye(ctx, side) {
1180
+ const f = feat(ctx, 17 * side);
1181
+ if (!f.vis) return [];
1182
+ const fp = ctx.face;
1183
+ const lidBase = side < 0 ? fp.lidL : fp.lidR;
1184
+ const lid = lidBase + (1 - lidBase) * ctx.blink;
1185
+ const low = side < 0 ? fp.lowL : fp.lowR;
1186
+ const curve = side < 0 ? fp.closedCurveL : fp.closedCurveR;
1187
+ const place = [tr(f.x, 4 + ctx.dy), sc(f.sx, 1)];
1188
+ if (lid >= .9) return [g([path(`M-8 1.5Q0 ${n(1.5 - 9 * curve)} 8 1.5`, line(SOFT_INK, LINE * 1.05))], { tf: place })];
1189
+ const top = -12.626000000000001 + lid * 2 * RY;
1190
+ const edgeY = (px) => top + fp.lidTilt * (px * -side) / 9;
1191
+ const bot = RY - low * 2 * RY;
1192
+ const gx = ctx.gaze.x * 2.8;
1193
+ const gy = ctx.gaze.y * 3.4;
1194
+ const clip = { shapes: [ellipse(0, 0, RX, RY, {})] };
1195
+ const inner = [g([
1196
+ ellipse(0, 0, 7, 7, { fill: ctx.c.eye }),
1197
+ ellipse(0, 0, 6.4, 6.4, line(ctx.c.eyeDark, 1.2)),
1198
+ ellipse(0, 0, 3.6 * fp.pupil, 3.6 * fp.pupil, { fill: FIXED.pupil }),
1199
+ ellipse(-2.4, -2.8, 2.2, 2.2, { fill: "#ffffff" }),
1200
+ ellipse(2.1, 2, .9, .9, { fill: "#ffffff" })
1201
+ ], { tf: [tr(gx, gy)] })];
1202
+ if (lid > .02) inner.push(path(`M-12 -14L12 -14L12 ${n(edgeY(12))}Q0 ${n(top + 1.5)} -12 ${n(edgeY(-12))}Z`, { fill: ctx.c.skin }));
1203
+ if (low > .02) inner.push(path(`M-12 14L12 14L12 ${n(bot)}Q0 ${n(bot - 3)} -12 ${n(bot)}Z`, { fill: ctx.c.skin }));
1204
+ const out = [ellipse(0, 0, RX, RY, { fill: FIXED.white }), g(inner, { clip })];
1205
+ if (lid > .06) out.push(g([path(`M${n(-9.772)} ${n(edgeY(-9.772))}Q0 ${n(top + 1.5)} ${n(9.772)} ${n(edgeY(9.772))}`, line(SOFT_INK, 1.9))], { clip }));
1206
+ out.push(ellipse(0, 0, RX, RY, line(ctx.c.eyeRim, 1.6)));
1207
+ return [g(out, { tf: place })];
1208
+ }
1209
+ const BROW_W = {
1210
+ soft: 3.8,
1211
+ thin: 2.4,
1212
+ thick: 5.6,
1213
+ arched: 3.6,
1214
+ straight: 4.2
1215
+ };
1216
+ const BROW_ARCH = {
1217
+ soft: 1,
1218
+ thin: 1,
1219
+ thick: .8,
1220
+ arched: 1.9,
1221
+ straight: .2
1222
+ };
1223
+ /** One brow in a style (`soft`, `thin`, …). */
1224
+ function brow(ctx, side, style) {
1225
+ const f = feat(ctx, 17 * side);
1226
+ if (!f.vis) return [];
1227
+ const p = side < 0 ? ctx.face.browL : ctx.face.browR;
1228
+ const by = -14 + p.dy + ctx.dy * 1.1;
1229
+ const xi = -7.5 * side;
1230
+ const xo = 7.5 * side;
1231
+ const yi = by + p.tilt * .8;
1232
+ const yo = by - p.tilt * .35;
1233
+ const arch = p.arch * (BROW_ARCH[style] ?? 1);
1234
+ const width = (BROW_W[style] ?? 3.8) * 1.15;
1235
+ return [g([path(`M${n(xi)} ${n(yi)}Q0 ${n(Math.min(yi, yo) - arch * 2)} ${n(xo)} ${n(yo)}`, line(ctx.c.brow, width))], { tf: [tr(f.x, 0), sc(f.sx, 1)] })];
1236
+ }
1237
+ //#endregion
1238
+ //#region src/avatar/render/head.ts
1239
+ /** The head, ears, cheeks and the small face and ear extras. */
1240
+ const EAR = "M-2 -10C6 -12 11 -6 10 1C9 8 5 11 -1 10Z";
1241
+ const EAR_FOLD = "M1.5 -5C5.5 -5 6.5 0 5 3.5";
1242
+ /** Ears behind the head (`behind`) or in front of it (`front`), hidden when turned away. */
1243
+ function ears(ctx, which, extra) {
1244
+ const out = [];
1245
+ for (const side of [-1, 1]) {
1246
+ const t = Math.PI / 2 * side + ctx.phi;
1247
+ const cos = Math.cos(t);
1248
+ if (cos < -.15) continue;
1249
+ const isInFront = cos > .3;
1250
+ if (which === "front" !== isInFront) continue;
1251
+ const earX = clamp(46 * Math.sin(t), -44, 44);
1252
+ const w = isInFront ? .75 + .25 * cos : 1;
1253
+ const flip = isInFront ? -Math.sign(earX || side) : side;
1254
+ const parts = [path(EAR, {
1255
+ fill: ctx.c.skin,
1256
+ stroke: ctx.c.skinInk,
1257
+ sw: LINE
1258
+ }), path(EAR_FOLD, line(ctx.c.skinShade, 2.2))];
1259
+ if (extra?.kind === "studs") parts.push(ellipse(3, 7, 2.2, 2.2, {
1260
+ fill: extra.colour,
1261
+ stroke: "#3a2a2a",
1262
+ sw: .8
1263
+ }));
1264
+ if (extra?.kind === "hoops") parts.push(ellipse(3, 12, 3.6, 4.4, line(extra.colour, 1.8)));
1265
+ out.push(g(parts, { tf: [tr(earX, 6 + ctx.dy * .3), sc(flip * w, 1)] }));
1266
+ }
1267
+ return out;
1268
+ }
1269
+ /** The head: a shade base, the lit face, and the tonal outline. */
1270
+ function head(ctx) {
1271
+ const d = HEAD_PATH.d();
1272
+ const lx = -2.5 + Math.sin(ctx.phi) * 3;
1273
+ return [
1274
+ path(d, { fill: ctx.c.skinShade }),
1275
+ path(d, { fill: ctx.c.skin }, [tr(lx, -3), sc(.95)]),
1276
+ path(d, {
1277
+ fill: "none",
1278
+ stroke: ctx.c.skinInk,
1279
+ sw: LINE
1280
+ })
1281
+ ];
1282
+ }
1283
+ const FRECKLES = [
1284
+ [-3.5, -1.5],
1285
+ [0, .8],
1286
+ [3.5, -1.2],
1287
+ [1.5, -3.5]
1288
+ ];
1289
+ /** Cheek blush (from the expression, plus `rosy`) and freckles. */
1290
+ function cheeks(ctx, hasFreckles, isRosy) {
1291
+ const out = [];
1292
+ const blush = Math.min(1, ctx.face.cheeks + (isRosy ? .45 : 0));
1293
+ for (const side of [-1, 1]) {
1294
+ const f = feat(ctx, 25 * side);
1295
+ if (!f.vis) continue;
1296
+ const parts = [];
1297
+ if (blush > .02) parts.push(ellipse(0, 0, 6.5, 3.8, {
1298
+ fill: FIXED.blush,
1299
+ opacity: Math.round(blush * 5) / 10
1300
+ }));
1301
+ if (hasFreckles) parts.push(...FRECKLES.map(([fx, fy]) => ellipse(fx, fy, 1.05, 1.05, {
1302
+ fill: ctx.c.skinInk,
1303
+ opacity: .45
1304
+ })));
1305
+ if (parts.length > 0) out.push(g(parts, { tf: [tr(f.x, 18 + ctx.dy), sc(f.sx, 1)] }));
1306
+ }
1307
+ return out;
1308
+ }
1309
+ //#endregion
1310
+ //#region src/avatar/render/mouth.ts
1311
+ /**
1312
+ * The nose, the mouth morph (one cubic structure for every shape, so any two
1313
+ * expressions tween, §3.5) and the small FX glyphs.
1314
+ */
1315
+ /** The nose: a soft shade with a highlight. */
1316
+ function nose(ctx) {
1317
+ const f = feat(ctx, 0);
1318
+ return [g([ellipse(0, 0, 4.4, 3, { fill: ctx.c.skinShade }), ellipse(-1.5, -1, 1.2, 1.2, {
1319
+ fill: ctx.c.skinLight,
1320
+ opacity: .9
1321
+ })], { tf: [tr(f.x, 16 + ctx.dy), sc(Math.max(.6, f.sx), 1)] })];
1322
+ }
1323
+ /** The mouth: a line when closed, a filled shape with teeth and tongue when open. Talking adds opening. */
1324
+ function mouth(ctx) {
1325
+ const base = ctx.face.mouth;
1326
+ const m = {
1327
+ ...base,
1328
+ open: base.open + ctx.talk * 9 * (1 - base.round * .4)
1329
+ };
1330
+ const f = feat(ctx, m.dx);
1331
+ const baseY = 27;
1332
+ const lx = -m.w / 2;
1333
+ const ly = baseY - m.sL;
1334
+ const rx = m.w / 2;
1335
+ const ry = baseY - m.sR;
1336
+ const s = (m.sL + m.sR) / 2;
1337
+ const corner = baseY - s;
1338
+ const ctrl = (target) => (target - .25 * corner) / .75;
1339
+ const place = [tr(f.x, ctx.dy * .8), sc(Math.max(.35, f.sx), 1)];
1340
+ if (m.open < 1) {
1341
+ const c = ctrl(baseY + s * .55);
1342
+ return [g([path(`M${n(lx)} ${n(ly)}C${n(-m.w * .18)} ${n(c)} ${n(m.w * .18)} ${n(c)} ${n(rx)} ${n(ry)}`, line(SOFT_INK, LINE * .95))], { tf: place })];
1343
+ }
1344
+ const up = baseY + s * .3 - m.round * m.open * .45;
1345
+ const lo = baseY + s * .55 + m.open * (1 - m.round * .45);
1346
+ const kx = m.w / 2 * (.55 + m.round * .8);
1347
+ const d = `M${n(lx)} ${n(ly)}C${n(-kx)} ${n(ctrl(up))} ${n(kx)} ${n(ctrl(up))} ${n(rx)} ${n(ry)}C${n(kx)} ${n(ctrl(lo))} ${n(-kx)} ${n(ctrl(lo))} ${n(lx)} ${n(ly)}Z`;
1348
+ const inside = [];
1349
+ if (m.tongue > .05) inside.push(ellipse(0, lo + 1, m.w * .32, m.open * .42 * m.tongue + 1, { fill: FIXED.tongue }));
1350
+ if (m.teeth > .05) inside.push(rect(-m.w / 2, up - 6, m.w, 6 + m.open * .22 * m.teeth, { fill: FIXED.white }));
1351
+ return [g([
1352
+ path(d, { fill: FIXED.mouth }),
1353
+ g(inside, { clip: { shapes: [path(d, {})] } }),
1354
+ path(d, line(SOFT_INK, LINE * .9))
1355
+ ], { tf: place })];
1356
+ }
1357
+ /** FX glyphs beside the head (sparkle, tear, sweat, anger, question, zzz). */
1358
+ function extras(ctx) {
1359
+ const out = [];
1360
+ const f = feat(ctx, 0);
1361
+ for (const e of ctx.face.extras) {
1362
+ if (e === "sparkle") out.push(path("M0 -9Q1.4 -1.4 9 0Q1.4 1.4 0 9Q-1.4 1.4 -9 0Q-1.4 -1.4 0 -9Z", {
1363
+ fill: "#ffd23f",
1364
+ stroke: "#0d0f14",
1365
+ sw: 1.6
1366
+ }, [tr(48 + f.x * .2, -44)]));
1367
+ if (e === "tear") {
1368
+ const t = feat(ctx, -16);
1369
+ if (t.vis) out.push(path("M0 -5Q4.5 2 0 5Q-4.5 2 0 -5Z", {
1370
+ fill: "#7cc7ff",
1371
+ stroke: "#1f3b5c",
1372
+ sw: 1.4
1373
+ }, [tr(t.x - 5, 16 + ctx.dy)]));
1374
+ }
1375
+ if (e === "sweat") out.push(path("M0 -6Q5 2 0 6Q-5 2 0 -6Z", {
1376
+ fill: "#9fdcff",
1377
+ stroke: "#1f3b5c",
1378
+ sw: 1.4
1379
+ }, [tr(40 + f.x * .2, -26)]));
1380
+ if (e === "anger") out.push(g([
1381
+ path("M-6 -2Q-2 -2 -2 -6", line("#ff4d4d", 2.6)),
1382
+ path("M2 -6Q2 -2 6 -2", line("#ff4d4d", 2.6)),
1383
+ path("M6 2Q2 2 2 6", line("#ff4d4d", 2.6)),
1384
+ path("M-2 6Q-2 2 -6 2", line("#ff4d4d", 2.6))
1385
+ ], { tf: [tr(30 + f.x * .3, -36)] }));
1386
+ if (e === "question") out.push({
1387
+ k: "text",
1388
+ x: 50 + f.x * .2,
1389
+ y: -40,
1390
+ text: "?",
1391
+ size: 26,
1392
+ font: "pixel",
1393
+ fill: "#ffd23f",
1394
+ stroke: "#0d0f14",
1395
+ sw: 1.2
1396
+ });
1397
+ if (e === "zzz") out.push({
1398
+ k: "text",
1399
+ x: 44 + f.x * .2,
1400
+ y: -42,
1401
+ text: "z z",
1402
+ size: 20,
1403
+ font: "pixel",
1404
+ fill: "#8ab4ff",
1405
+ stroke: "#0d0f14",
1406
+ sw: 1.2
1407
+ });
1408
+ }
1409
+ return out;
1410
+ }
1411
+ //#endregion
1412
+ //#region src/avatar/render/props.ts
1413
+ /**
1414
+ * Status props (§3.11, §14.3): runtime-only parts the world (or a preview)
1415
+ * puts on a player. `goggles` lives in eyewear.ts; the head props here sit on
1416
+ * the head, the hand props at the `prop` anchor in front of the body.
1417
+ */
1418
+ const DARK = "#0d0f14";
1419
+ /** Hand props are drawn at this scale so they read at small sizes. */
1420
+ const PROP_SCALE = 1.45;
1421
+ /** A headset: a band over the head, ear cups and a mic (head space). */
1422
+ function headset(ctx, isBack) {
1423
+ const w = turnWarp(isBack ? 0 : ctx.phi);
1424
+ const band = blob(arc(0, -4, 51, 51, 196, 344, 8), false);
1425
+ const nodes = [path(band.d(w), line(DARK, 8)), path(band.d(w), line("#4a5470", 5))];
1426
+ const phi = isBack ? 0 : ctx.phi;
1427
+ for (const side of [-1, 1]) {
1428
+ const t = Math.PI / 2 * side + phi;
1429
+ if (Math.cos(t) < -.15) continue;
1430
+ const cupX = Math.max(-47, Math.min(47, 46 * Math.sin(t)));
1431
+ const wide = 6 + 3 * Math.max(0, Math.cos(t));
1432
+ nodes.push(path(roundedRect(cupX - wide, -6, wide * 2, 20, 5).d(), {
1433
+ fill: "#2a3142",
1434
+ stroke: DARK,
1435
+ sw: LINE
1436
+ }));
1437
+ }
1438
+ if (!isBack && Math.abs(phi) < .7) nodes.push(path(new Path().m(-46, 12).q(-40, 30, -18, 30).d(w), line("#4a5470", 3)), ellipse(-17, 30, 3, 3, {
1439
+ fill: "#ff5f3b",
1440
+ stroke: DARK,
1441
+ sw: 1.2
1442
+ }));
1443
+ return nodes;
1444
+ }
1445
+ /** A small crown on top of the head (head space; `top` is the hair or hat top). */
1446
+ function crown(top) {
1447
+ return [g([path("M-16 0L-18 -16L-8 -8L0 -20L8 -8L18 -16L16 0Z", {
1448
+ fill: "#ffd23f",
1449
+ stroke: "#8a5a00",
1450
+ sw: LINE
1451
+ }), ellipse(0, -5, 2.4, 2.4, { fill: "#ff5f3b" })], { tf: [tr(0, -50 - top + 4)] })];
1452
+ }
1453
+ const card = (colour) => rect(-6, -9, 12, 17, {
1454
+ fill: colour,
1455
+ stroke: DARK,
1456
+ sw: 1.6
1457
+ }, 2.5);
1458
+ /** Nub hands holding something at the prop anchor (body space). */
1459
+ function nubs(body, ink, spread, atY) {
1460
+ return [-1, 1].map((s) => ellipse(spread * s, atY, 7, 6, {
1461
+ fill: body,
1462
+ stroke: ink,
1463
+ sw: LINE
1464
+ }));
1465
+ }
1466
+ /** A hand prop drawn at the `prop` anchor (x, y) in front of the body. */
1467
+ function handProp(prop, atX, atY, body, bodyInk) {
1468
+ switch (prop) {
1469
+ case "cards": return [g([
1470
+ g([card("#ff5f3b")], { tf: [tr(-8, 0), rot(-18)] }),
1471
+ g([card("#ffd23f")], { tf: [tr(0, -3)] }),
1472
+ g([card("#3d7bff")], { tf: [tr(8, 0), rot(18)] }),
1473
+ ...nubs(body, bodyInk, 13, 8)
1474
+ ], { tf: [tr(atX, atY), sc(PROP_SCALE)] })];
1475
+ case "controller": return [g([
1476
+ path(new Path().m(-20, -4).c(-20, -12, 20, -12, 20, -4).c(24, 10, 14, 14, 8, 6).l(-8, 6).c(-14, 14, -24, 10, -20, -4).z().d(), {
1477
+ fill: "#2a3142",
1478
+ stroke: DARK,
1479
+ sw: LINE
1480
+ }),
1481
+ ellipse(-9, -2, 2.6, 2.6, { fill: "#8ab4ff" }),
1482
+ ellipse(10, -4, 2, 2, { fill: "#ff5f3b" }),
1483
+ ellipse(13, 0, 2, 2, { fill: "#4ade80" }),
1484
+ ...nubs(body, bodyInk, 20, 4)
1485
+ ], { tf: [tr(atX, atY), sc(PROP_SCALE)] })];
1486
+ case "book": return [g([
1487
+ path("M0 -2C-8 -8 -18 -8 -24 -5L-24 12C-18 9 -8 9 0 14C8 9 18 9 24 12L24 -5C18 -8 8 -8 0 -2Z", {
1488
+ fill: "#f4f1ea",
1489
+ stroke: DARK,
1490
+ sw: LINE
1491
+ }),
1492
+ path("M0 -2L0 14", line("#c9c2b4", 1.6)),
1493
+ path("M-19 0L-5 3M-19 5L-5 8M5 3L19 0M5 8L19 5", line("#9a9384", 1.2)),
1494
+ path("M-24 12C-18 9 -8 9 0 14C8 9 18 9 24 12L24 15C18 12 8 12 0 17C-8 12 -18 12 -24 15Z", {
1495
+ fill: "#ff5f3b",
1496
+ stroke: DARK,
1497
+ sw: 1.4
1498
+ }),
1499
+ ...nubs(body, bodyInk, 25, 6)
1500
+ ], { tf: [tr(atX, atY), sc(PROP_SCALE)] })];
1501
+ case "trophy": return [g([
1502
+ path("M-11 -14L11 -14L9 0C8 6 -8 6 -9 0Z", {
1503
+ fill: "#ffd23f",
1504
+ stroke: "#8a5a00",
1505
+ sw: LINE
1506
+ }),
1507
+ path("M-11 -11C-18 -11 -18 -2 -9 -1M11 -11C18 -11 18 -2 9 -1", line("#8a5a00", 2.2)),
1508
+ rect(-3, 4, 6, 6, { fill: "#e0a800" }),
1509
+ rect(-9, 10, 18, 5, {
1510
+ fill: "#6b4f12",
1511
+ stroke: DARK,
1512
+ sw: 1.2
1513
+ }, 1.5),
1514
+ ellipse(13, 6, 7, 6, {
1515
+ fill: body,
1516
+ stroke: bodyInk,
1517
+ sw: LINE
1518
+ })
1519
+ ], { tf: [tr(atX + 24, atY - 10), sc(PROP_SCALE)] })];
1520
+ default: return [];
1521
+ }
1522
+ }
1523
+ //#endregion
1524
+ //#region src/avatar/render/compose.ts
1525
+ /**
1526
+ * Composes a look and a pose into a scene in rig space: the feet at (0, 0),
1527
+ * y down, the head centre near (0, −96) (§14.2). Layers go back to front as
1528
+ * in §3.2, with facial hair split into beard and moustache layers.
1529
+ */
1530
+ /** The head centre's height above the feet. */
1531
+ const HEAD_Y = -96;
1532
+ const DEG = Math.PI / 180;
1533
+ const FACING_ANGLE = {
1534
+ S: 0,
1535
+ SE: 34,
1536
+ E: 70,
1537
+ NE: 145,
1538
+ N: 180
1539
+ };
1540
+ const MIRROR = {
1541
+ SW: "SE",
1542
+ W: "E",
1543
+ NW: "NE"
1544
+ };
1545
+ const VIEW_FACING = {
1546
+ front: "S",
1547
+ back: "N",
1548
+ left: "W",
1549
+ right: "E"
1550
+ };
1551
+ /** The facing an options object asks for. */
1552
+ function facingOf(opts) {
1553
+ return opts.facing ?? (opts.view ? VIEW_FACING[opts.view] : "S");
1554
+ }
1555
+ function resolveParts(look, art) {
1556
+ const slot = (name) => look.accessories.find((a) => a.slot === name);
1557
+ const hatAcc = slot("headwear");
1558
+ const glassAcc = slot("glasses");
1559
+ const earAcc = slot("ears");
1560
+ const face = slot("face");
1561
+ return {
1562
+ c: lookColours(look),
1563
+ hair: art.hair[partName(look.hair.style)],
1564
+ hat: hatAcc ? art.headwear[partName(hatAcc.id)] : void 0,
1565
+ hatColour: accessoryColour(look, hatAcc),
1566
+ beard: art.beard[partName(look.facialHair.style)],
1567
+ glasses: glassAcc ? art.glasses[partName(glassAcc.id)] : void 0,
1568
+ glassesColour: glassAcc?.colour === void 0 ? "#1d1a1e" : accessoryColour(look, glassAcc),
1569
+ freckles: face?.id === "face.freckles",
1570
+ rosy: face?.id === "face.rosy",
1571
+ earExtra: earAcc ? {
1572
+ kind: earAcc.id === "ears.hoops" ? "hoops" : "studs",
1573
+ colour: earAcc.colour === void 0 ? "#f5c542" : accessoryColour(look, earAcc)
1574
+ } : void 0,
1575
+ browStyle: partName(look.brows.style)
1576
+ };
1577
+ }
1578
+ /** The far-side clip for a turned head's rear art (a back-of-skull crescent). */
1579
+ function backOfHead(phi) {
1580
+ const left = -46 * Math.cos(phi) - 3;
1581
+ const cx = (left + 46 + 12) / 2;
1582
+ const rx = (58 - left) / 2;
1583
+ return {
1584
+ crescent: {
1585
+ shapes: [path(`M-200 -200H200V200H-200Z M${n(cx - rx)} 8A${n(rx)} 64 0 1 0 ${n(cx + rx)} 8A${n(rx)} 64 0 1 0 ${n(cx - rx)} 8Z`, {})],
1586
+ evenOdd: true
1587
+ },
1588
+ half: { shapes: [rect(-200, -200, 200 + cx, 400, {})] }
1589
+ };
1590
+ }
1591
+ /** The front and side views of the head. */
1592
+ function frontHead(ctx, p, isPlaying, status, angle, gazeX) {
1593
+ const hatCut = p.hat && typeof p.hat.hides === "number" ? { shapes: [rect(-120, p.hat.hides, 240, 260, {})] } : void 0;
1594
+ const isHairShown = !(p.hat && p.hat.hides === "all");
1595
+ const cut = (nodes) => hatCut && nodes.length > 0 ? [g(nodes, { clip: hatCut })] : nodes;
1596
+ const hairTie = tones(ctx.c.body);
1597
+ const hatTones = tones(p.hatColour);
1598
+ const warpFront = turnWarp((angle + gazeX * 6) * DEG);
1599
+ const warpBack = ctx.phi === 0 ? void 0 : (px, py) => [px - Math.sin(ctx.phi) * 8, py];
1600
+ const out = [];
1601
+ if (p.hat?.back) out.push(...paintShapes(ctx.c, hatTones, p.hat.back, warpBack));
1602
+ if (isHairShown) out.push(...cut(paintShapes(ctx.c, hairTie, p.hair?.back, warpBack)));
1603
+ if (!p.hat?.hidesEars) out.push(...ears(ctx, "behind", p.earExtra));
1604
+ out.push(...head(ctx));
1605
+ const turnS = Math.sin(ctx.phi);
1606
+ if (Math.abs(turnS) > .3) {
1607
+ const co = Math.cos(ctx.phi);
1608
+ const shift = (px, py) => [px * (.92 + .08 * co) - turnS * 5, py];
1609
+ const rear = [...isHairShown ? cut(paintShapes(ctx.c, hairTie, p.hair?.rear, shift)) : [], ...p.hat ? paintShapes(ctx.c, hatTones, p.hat.rear, shift) : []];
1610
+ const clips = backOfHead(ctx.phi);
1611
+ if (rear.length > 0) out.push(g([g(rear, { clip: clips.crescent })], { clip: clips.half }));
1612
+ }
1613
+ if (!p.hat?.hidesEars) out.push(...ears(ctx, "front", p.earExtra));
1614
+ out.push(...cheeks(ctx, p.freckles, p.rosy));
1615
+ const faceWarp = turnWarp(ctx.phi, ctx.dy * .8);
1616
+ out.push(...paintShapes(ctx.c, hairTie, p.beard?.beard, faceWarp));
1617
+ if (!isPlaying) out.push(...eye(ctx, -1), ...eye(ctx, 1));
1618
+ out.push(...brow(ctx, -1, p.browStyle), ...brow(ctx, 1, p.browStyle));
1619
+ if (turnS > .8) out.push(path("M43 9C50 11 51 18 43.5 20", {
1620
+ fill: ctx.c.skin,
1621
+ stroke: ctx.c.skinInk,
1622
+ sw: LINE
1623
+ }));
1624
+ out.push(...nose(ctx), ...mouth(ctx));
1625
+ out.push(...paintShapes(ctx.c, hairTie, p.beard?.moustache, faceWarp));
1626
+ if (isHairShown) {
1627
+ out.push(...cut(paintShapes(ctx.c, hairTie, p.hair?.front, warpFront)));
1628
+ if (p.hair?.front?.some((s) => s.clip === "head")) out.push(path(HEAD_PATH.d(), {
1629
+ fill: "none",
1630
+ stroke: ctx.c.skinInk,
1631
+ sw: LINE
1632
+ }));
1633
+ }
1634
+ if (isPlaying) out.push(...goggles(ctx));
1635
+ else if (p.glasses) out.push(...glasses(ctx, p.glasses, p.glassesColour));
1636
+ if (p.hat) out.push(...paintShapes(ctx.c, hatTones, p.hat.front, warpFront));
1637
+ if (status === "headset") out.push(...headset(ctx, false));
1638
+ out.push(...extras(ctx));
1639
+ return out;
1640
+ }
1641
+ /** The back views (N, NE): the rear art wrapped past 180°. */
1642
+ function backHead(ctx, p, isPlaying, status, angle) {
1643
+ const phi = (angle - 180) * DEG;
1644
+ const warp = turnWarp(phi);
1645
+ const isHairShown = !(p.hat && p.hat.hides === "all");
1646
+ const hatCut = p.hat && typeof p.hat.hides === "number" ? { shapes: [rect(-120, p.hat.hides, 240, 260, {})] } : void 0;
1647
+ const out = [];
1648
+ if (!p.hat?.hidesEars) out.push(...ears({
1649
+ ...ctx,
1650
+ phi
1651
+ }, "behind", p.earExtra));
1652
+ out.push(...head({
1653
+ ...ctx,
1654
+ phi: 0
1655
+ }));
1656
+ if (isHairShown) {
1657
+ const rear = paintShapes(ctx.c, tones(ctx.c.body), p.hair?.rear, warp);
1658
+ out.push(...hatCut && rear.length > 0 ? [g(rear, { clip: hatCut })] : rear);
1659
+ if (p.hair?.rear.some((s) => s.clip === "head")) out.push(path(HEAD_PATH.d(), {
1660
+ fill: "none",
1661
+ stroke: ctx.c.skinInk,
1662
+ sw: LINE
1663
+ }));
1664
+ }
1665
+ if (isPlaying) out.push(...gogglesStrapBack(ctx, phi));
1666
+ if (p.hat) out.push(...paintShapes(ctx.c, tones(p.hatColour), p.hat.rear, warp));
1667
+ if (status === "headset") out.push(...headset(ctx, true));
1668
+ return out;
1669
+ }
1670
+ /** The grey silhouette: a malformed code, a hidden player, or before the art loads. */
1671
+ function silhouette(hasShadow) {
1672
+ const grey = "#6f7787";
1673
+ return [
1674
+ ...hasShadow ? [groundShadow(0)] : [],
1675
+ path("M-35 -2C-42 -20 -38 -46 -22 -56C-8 -62 8 -62 22 -56C38 -46 42 -20 35 -2C20 3 -20 3 -35 -2Z", { fill: grey }),
1676
+ path(HEAD_PATH.d(), { fill: grey }, [tr(0, -96), sc(.94)])
1677
+ ];
1678
+ }
1679
+ function anchorsFor(headX, headY, top, prop, mirror) {
1680
+ const at = (px, py) => ({
1681
+ x: px * mirror,
1682
+ y: py
1683
+ });
1684
+ return {
1685
+ feet: at(0, 0),
1686
+ body: at(0, -30),
1687
+ neck: at(headX, headY + 46),
1688
+ head: at(headX, headY),
1689
+ eyeLine: at(headX, headY + 4),
1690
+ mouth: at(headX, headY + 27),
1691
+ hatTop: at(headX, top),
1692
+ nameplate: at(0, top - 10),
1693
+ bubble: at(0, top - 14),
1694
+ seat: at(0, 0),
1695
+ prop: at(prop.x, prop.y),
1696
+ shadow: at(0, 0),
1697
+ handL: at(-40, -30),
1698
+ handR: at(40, -30),
1699
+ pod: at(0, 14)
1700
+ };
1701
+ }
1702
+ /** Composes a look (or `null` for the silhouette) with a pose. */
1703
+ function composeAvatar(look, opts = {}) {
1704
+ const art = currentArt();
1705
+ const facingIn = facingOf(opts);
1706
+ const isMirrored = MIRROR[facingIn] !== void 0;
1707
+ const facing = MIRROR[facingIn] ?? (facingIn === "S" || facingIn === "SE" || facingIn === "E" || facingIn === "NE" ? facingIn : "N");
1708
+ const hasShadow = opts.shadow ?? opts.frame === "full";
1709
+ if (!look || !art) return {
1710
+ nodes: silhouette(hasShadow),
1711
+ anchors: anchorsFor(0, -96, -156, {
1712
+ x: 0,
1713
+ y: -30
1714
+ }, 1)
1715
+ };
1716
+ const pose = opts.pose ?? {};
1717
+ const bodyPose = pose.body ?? "idle";
1718
+ const status = pose.status ?? (bodyPose === "play" ? "goggles" : null);
1719
+ const isPlaying = status === "goggles";
1720
+ const face = pose.face ?? EXPRESSIONS[pose.expression ?? (bodyPose === "play" ? "determined" : bodyPose === "away" ? "sleepy" : "neutral")];
1721
+ const gaze = pose.look ?? {
1722
+ x: 0,
1723
+ y: 0
1724
+ };
1725
+ const angle = FACING_ANGLE[facing];
1726
+ const isBack = angle > 90;
1727
+ const parts = resolveParts(look, art);
1728
+ const ctx = {
1729
+ c: parts.c,
1730
+ face,
1731
+ gaze,
1732
+ phi: (angle + gaze.x * 11) * DEG,
1733
+ dy: gaze.y * 3,
1734
+ blink: pose.blink ?? 0,
1735
+ talk: pose.talk ?? 0
1736
+ };
1737
+ const b = bodyLayout(parts.c, bodyPose, ctx.phi, pose.phase ?? .25, opts.stool === true);
1738
+ const headX = b.headDx + (pose.bob?.x ?? 0);
1739
+ const headY = -96 + b.headDy + (pose.bob?.y ?? 0);
1740
+ const tilt = face.headTilt + b.tilt + (pose.tilt ?? 0);
1741
+ const sq = face.squash + (pose.squash ?? 0);
1742
+ const headNodes = isBack ? backHead(ctx, parts, isPlaying, status, angle) : frontHead(ctx, parts, isPlaying, status, angle, gaze.x);
1743
+ const top = headY - 50 - Math.max(parts.hair?.top ?? 0, parts.hat?.top ?? 0);
1744
+ if (status === "crown") headNodes.push(...crown(Math.max(parts.hair?.top ?? 0, parts.hat?.top ?? 0)));
1745
+ const handProps = status && !isBack ? handProp(status, b.prop.x, b.prop.y, parts.c.body, parts.c.bodyInk) : [];
1746
+ const character = [
1747
+ ...b.behind,
1748
+ ...b.body,
1749
+ g(headNodes, { tf: [
1750
+ tr(headX, headY),
1751
+ rot(tilt),
1752
+ sc(1 + sq, 1 - sq)
1753
+ ] }),
1754
+ ...b.front,
1755
+ ...handProps
1756
+ ];
1757
+ const lifted = b.lift > 0 ? [g(character, { tf: [tr(0, -b.lift)] })] : character;
1758
+ const rimmed = opts.rim ? [g(lifted, { rim: true })] : lifted;
1759
+ const all = [...hasShadow ? [groundShadow(b.lift)] : [], ...rimmed];
1760
+ const mirror = isMirrored ? -1 : 1;
1761
+ return {
1762
+ nodes: isMirrored ? [g(all, { tf: [sc(-1, 1)] })] : all,
1763
+ anchors: anchorsFor(headX, headY - b.lift, top - b.lift, {
1764
+ x: b.prop.x,
1765
+ y: b.prop.y - b.lift
1766
+ }, mirror)
1767
+ };
1768
+ }
1769
+ //#endregion
1770
+ //#region src/avatar/outputs/svg-markup.ts
1771
+ /**
1772
+ * Scene → SVG markup. Ids for clips and the rim filter are prefixed per call,
1773
+ * so many avatars can be inlined on one page. The output holds only numbers,
1774
+ * derived colours and escaped overlay text, so it is safe to insert as HTML.
1775
+ */
1776
+ /** The rim filter: a dilated, soft light edge behind the silhouette. */
1777
+ const RIM_COLOUR = "#aab3c7";
1778
+ const FONTS = {
1779
+ mono: "'Space Mono', monospace",
1780
+ pixel: "'VT323', monospace"
1781
+ };
1782
+ /** Three decimals, for scales and opacities. */
1783
+ const fine = (v) => String(Math.round(v * 1e3) / 1e3);
1784
+ /** Escapes text for SVG content. */
1785
+ function escapeXml(text) {
1786
+ return text.replace(/[&<>"']/g, (c) => `&#${String(c.charCodeAt(0))};`);
1787
+ }
1788
+ /** A transform list as an SVG `transform` attribute (empty for none). */
1789
+ function tfAttr(tf) {
1790
+ if (!tf || tf.length === 0) return "";
1791
+ return ` transform="${tf.map((op) => {
1792
+ switch (op.k) {
1793
+ case "t": return `translate(${n(op.x)} ${n(op.y)})`;
1794
+ case "s": return `scale(${fine(op.x)} ${fine(op.y)})`;
1795
+ case "r": return `rotate(${n(op.a)})`;
1796
+ case "skx": return `skewX(${n(op.a)})`;
1797
+ }
1798
+ }).join(" ")}"`;
1799
+ }
1800
+ function paintAttrs(p) {
1801
+ let out = ` fill="${p.fill ?? "none"}"`;
1802
+ if (p.evenOdd) out += " fill-rule=\"evenodd\"";
1803
+ if (p.fillOpacity !== void 0) out += ` fill-opacity="${fine(p.fillOpacity)}"`;
1804
+ if (p.stroke && p.stroke !== "none") out += ` stroke="${p.stroke}" stroke-width="${n(p.sw ?? 1)}" stroke-linejoin="round" stroke-linecap="${p.cap ?? "round"}"`;
1805
+ if (p.opacity !== void 0 && p.opacity < 1) out += ` opacity="${fine(p.opacity)}"`;
1806
+ return out;
1807
+ }
1808
+ /** Collects `<defs>` while serialising. */
1809
+ var Writer = class {
1810
+ constructor(prefix) {
1811
+ _defineProperty(this, "defs", []);
1812
+ _defineProperty(this, "count", 0);
1813
+ _defineProperty(this, "usesRim", false);
1814
+ _defineProperty(this, "prefix", void 0);
1815
+ this.prefix = prefix;
1816
+ }
1817
+ id(kind) {
1818
+ this.count += 1;
1819
+ return `${this.prefix}${kind}${this.count.toString(36)}`;
1820
+ }
1821
+ clip(clip) {
1822
+ const id = this.id("c");
1823
+ const rule = clip.evenOdd ? " clip-rule=\"evenodd\"" : "";
1824
+ this.defs.push(`<clipPath id="${id}">${clip.shapes.map((s) => this.node(s).replace("<path ", `<path${rule} `)).join("")}</clipPath>`);
1825
+ return id;
1826
+ }
1827
+ node(node) {
1828
+ switch (node.k) {
1829
+ case "g": {
1830
+ let attrs = tfAttr(node.tf);
1831
+ if (node.clip) attrs += ` clip-path="url(#${this.clip(node.clip)})"`;
1832
+ if (node.opacity !== void 0 && node.opacity < 1) attrs += ` opacity="${fine(node.opacity)}"`;
1833
+ if (node.rim) {
1834
+ this.usesRim = true;
1835
+ attrs += ` filter="url(#${this.prefix}rim)"`;
1836
+ }
1837
+ const inner = node.children.map((child) => this.node(child)).join("");
1838
+ return attrs === "" ? inner : `<g${attrs}>${inner}</g>`;
1839
+ }
1840
+ case "path": return `<path d="${node.d}"${tfAttr(node.tf)}${paintAttrs(node)}/>`;
1841
+ case "ellipse": return node.rx === node.ry ? `<circle cx="${n(node.cx)}" cy="${n(node.cy)}" r="${n(node.rx)}"${tfAttr(node.tf)}${paintAttrs(node)}/>` : `<ellipse cx="${n(node.cx)}" cy="${n(node.cy)}" rx="${n(node.rx)}" ry="${n(node.ry)}"${tfAttr(node.tf)}${paintAttrs(node)}/>`;
1842
+ case "rect": return `<rect x="${n(node.x)}" y="${n(node.y)}" width="${n(node.w)}" height="${n(node.h)}"${node.r ? ` rx="${n(node.r)}"` : ""}${tfAttr(node.tf)}${paintAttrs(node)}/>`;
1843
+ case "text": return `<text x="${n(node.x)}" y="${n(node.y)}" font-family="${FONTS[node.font]}" font-size="${n(node.size)}"${node.bold ? " font-weight=\"700\"" : ""}${node.anchor === "middle" ? " text-anchor=\"middle\"" : ""} paint-order="stroke"${paintAttrs(node)}>${escapeXml(node.text)}</text>`;
1844
+ }
1845
+ }
1846
+ };
1847
+ /** A scene as SVG markup plus the `<defs>` it needs. */
1848
+ function sceneToSvg(nodes, prefix) {
1849
+ const writer = new Writer(prefix);
1850
+ const body = nodes.map((node) => writer.node(node)).join("");
1851
+ if (writer.usesRim) writer.defs.push(`<filter id="${prefix}rim" x="-20%" y="-20%" width="140%" height="140%"><feMorphology in="SourceAlpha" operator="dilate" radius="2.8" result="d"/><feFlood flood-color="${RIM_COLOUR}" flood-opacity=".48"/><feComposite in2="d" operator="in"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>`);
1852
+ return {
1853
+ body,
1854
+ defs: writer.defs.join("")
1855
+ };
1856
+ }
1857
+ //#endregion
1858
+ export { contrast as A, ellipse as C, rect as D, path as E, lightness as M, toCode as N, rot as O, toLook as P, pixelSize as S, line as T, onArtReady as _, composeAvatar as a, frameOf as b, n as c, EXPRESSIONS as d, EXPRESSION_NAMES as f, mix as g, isExpression as h, HEAD_Y as i, deltaE as j, tr as k, roundedRect as l, faceOf as m, escapeXml as n, arc as o, expressionFromEmote as p, sceneToSvg as r, blob as s, RIM_COLOUR as t, scallop as u, ready as v, g as w, framed as x, FRAMES as y };
1859
+
1860
+ //# sourceMappingURL=svg-markup.js.map