@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10

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 (293) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/LICENSE +21 -21
  3. package/README.md +203 -139
  4. package/dist/auto.d.ts +8 -8
  5. package/dist/auto.js +3 -3
  6. package/dist/avatar-pixi.d.ts +165 -0
  7. package/dist/avatar-pixi.d.ts.map +1 -0
  8. package/dist/avatar-pixi.js +299 -0
  9. package/dist/avatar-pixi.js.map +1 -0
  10. package/dist/avatar-svg.d.ts +4 -0
  11. package/dist/avatar-svg.js +5 -0
  12. package/dist/avatar-three.d.ts +69 -0
  13. package/dist/avatar-three.d.ts.map +1 -0
  14. package/dist/avatar-three.js +49 -0
  15. package/dist/avatar-three.js.map +1 -0
  16. package/dist/avatar.d.ts +7 -0
  17. package/dist/avatar.js +500 -0
  18. package/dist/avatar.js.map +1 -0
  19. package/dist/daily.d.ts +2 -2
  20. package/dist/doctor/cli.mjs +119 -28
  21. package/dist/doctor/cli.mjs.map +1 -1
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +3 -3
  24. package/dist/input.d.ts +2 -2
  25. package/dist/input.js +2 -2
  26. package/dist/mp.d.ts +3 -3
  27. package/dist/race.d.ts +1 -1
  28. package/dist/rooms.d.ts +3 -3
  29. package/dist/rooms.js +1 -1
  30. package/dist/shared/animator.d.ts +104 -0
  31. package/dist/shared/animator.d.ts.map +1 -0
  32. package/dist/shared/art.js +1946 -0
  33. package/dist/shared/art.js.map +1 -0
  34. package/dist/shared/atlas.d.ts +138 -0
  35. package/dist/shared/atlas.d.ts.map +1 -0
  36. package/dist/shared/avatar.d.ts +123 -0
  37. package/dist/shared/avatar.d.ts.map +1 -0
  38. package/dist/shared/bridge.js +18 -4
  39. package/dist/shared/bridge.js.map +1 -1
  40. package/dist/shared/client-version.gen.js +1 -1
  41. package/dist/shared/daily.d.ts +145 -19
  42. package/dist/shared/daily.d.ts.map +1 -1
  43. package/dist/shared/daily.js +138 -25
  44. package/dist/shared/daily.js.map +1 -1
  45. package/dist/shared/defineProperty.js +39 -0
  46. package/dist/shared/describe.d.ts +32 -0
  47. package/dist/shared/describe.d.ts.map +1 -0
  48. package/dist/shared/describe.js +109 -0
  49. package/dist/shared/describe.js.map +1 -0
  50. package/dist/shared/globals.js +9 -42
  51. package/dist/shared/globals.js.map +1 -1
  52. package/dist/shared/index.d.ts +1122 -46
  53. package/dist/shared/index.d.ts.map +1 -1
  54. package/dist/shared/input.d.ts +74 -14
  55. package/dist/shared/input.d.ts.map +1 -1
  56. package/dist/shared/input.js +179 -25
  57. package/dist/shared/input.js.map +1 -1
  58. package/dist/shared/json-guards.js +48 -0
  59. package/dist/shared/json-guards.js.map +1 -0
  60. package/dist/shared/latest-state.js +1 -1
  61. package/dist/shared/live-canvas.js +62 -0
  62. package/dist/shared/live-canvas.js.map +1 -0
  63. package/dist/shared/mp.js +10 -2
  64. package/dist/shared/mp.js.map +1 -1
  65. package/dist/shared/multiplayer-types.d.ts +15 -2
  66. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  67. package/dist/shared/overlays.js +800 -0
  68. package/dist/shared/overlays.js.map +1 -0
  69. package/dist/shared/page-stack.js +2 -1
  70. package/dist/shared/page-stack.js.map +1 -1
  71. package/dist/shared/pose-key.js +249 -0
  72. package/dist/shared/pose-key.js.map +1 -0
  73. package/dist/shared/race-types.d.ts +1 -1
  74. package/dist/shared/race.js +1 -1
  75. package/dist/shared/room-types.d.ts +28 -3
  76. package/dist/shared/room-types.d.ts.map +1 -1
  77. package/dist/shared/rooms.d.ts.map +1 -1
  78. package/dist/shared/rooms.js +23 -4
  79. package/dist/shared/rooms.js.map +1 -1
  80. package/dist/shared/runtime-opener.js +1 -1
  81. package/dist/shared/scene.d.ts +90 -0
  82. package/dist/shared/scene.d.ts.map +1 -0
  83. package/dist/shared/social.d.ts +90 -9
  84. package/dist/shared/social.d.ts.map +1 -1
  85. package/dist/shared/social.js +6 -1
  86. package/dist/shared/social.js.map +1 -1
  87. package/dist/shared/src.js +4563 -667
  88. package/dist/shared/src.js.map +1 -1
  89. package/dist/shared/svg-markup.js +2354 -0
  90. package/dist/shared/svg-markup.js.map +1 -0
  91. package/dist/shared/svg.js +27 -0
  92. package/dist/shared/svg.js.map +1 -0
  93. package/dist/shared/types.d.ts +174 -0
  94. package/dist/shared/types.d.ts.map +1 -0
  95. package/dist/shared/wire-types.d.ts +6 -2
  96. package/dist/shared/wire-types.d.ts.map +1 -1
  97. package/dist/social.d.ts +5 -5
  98. package/dist/social.js +2 -2
  99. package/dist/turns-rules.d.ts +2 -0
  100. package/dist/turns-rules.js +1 -0
  101. package/dist/turns.d.ts +61 -0
  102. package/dist/turns.d.ts.map +1 -0
  103. package/dist/turns.js +183 -0
  104. package/dist/turns.js.map +1 -0
  105. package/package.json +125 -89
  106. package/src/auto.ts +39 -39
  107. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  108. package/src/avatar/adapters/pixi-crowd.ts +329 -0
  109. package/src/avatar/adapters/pixi-types.ts +84 -0
  110. package/src/avatar/adapters/three.ts +126 -0
  111. package/src/avatar/animator/animator.ts +285 -0
  112. package/src/avatar/animator/reactions.ts +44 -0
  113. package/src/avatar/animator/schedule.ts +73 -0
  114. package/src/avatar/art/beard.ts +49 -0
  115. package/src/avatar/art/glasses.ts +67 -0
  116. package/src/avatar/art/hair.ts +254 -0
  117. package/src/avatar/art/headwear.ts +75 -0
  118. package/src/avatar/art/index.ts +21 -0
  119. package/src/avatar/art/shape.ts +87 -0
  120. package/src/avatar/art-store.ts +36 -0
  121. package/src/avatar/atlas.ts +246 -0
  122. package/src/avatar/catalog-view.ts +14 -0
  123. package/src/avatar/colour.ts +109 -0
  124. package/src/avatar/describe.ts +79 -0
  125. package/src/avatar/face.ts +187 -0
  126. package/src/avatar/geom.ts +173 -0
  127. package/src/avatar/layout.ts +47 -0
  128. package/src/avatar/look-like.ts +52 -0
  129. package/src/avatar/outputs/bitmap.ts +75 -0
  130. package/src/avatar/outputs/canvas-paint.ts +177 -0
  131. package/src/avatar/outputs/canvas.ts +62 -0
  132. package/src/avatar/outputs/custom-element.ts +140 -0
  133. package/src/avatar/outputs/element.ts +58 -0
  134. package/src/avatar/outputs/live-canvas.ts +57 -0
  135. package/src/avatar/outputs/overlay-svg.ts +22 -0
  136. package/src/avatar/outputs/pose-key.ts +30 -0
  137. package/src/avatar/outputs/svg-markup.ts +113 -0
  138. package/src/avatar/outputs/svg.ts +41 -0
  139. package/src/avatar/overlays.ts +128 -0
  140. package/src/avatar/render/body.ts +116 -0
  141. package/src/avatar/render/colours.ts +81 -0
  142. package/src/avatar/render/compose.ts +320 -0
  143. package/src/avatar/render/context.ts +117 -0
  144. package/src/avatar/render/eyes.ts +78 -0
  145. package/src/avatar/render/eyewear.ts +118 -0
  146. package/src/avatar/render/frame.ts +73 -0
  147. package/src/avatar/render/head.ts +81 -0
  148. package/src/avatar/render/mouth.ts +86 -0
  149. package/src/avatar/render/props.ts +91 -0
  150. package/src/avatar/render/types.ts +93 -0
  151. package/src/avatar/render/walker-parts.ts +121 -0
  152. package/src/avatar/render/walker.ts +199 -0
  153. package/src/avatar/scene.ts +119 -0
  154. package/src/avatar-pixi.ts +20 -0
  155. package/src/avatar-svg.ts +21 -0
  156. package/src/avatar-three.ts +18 -0
  157. package/src/avatar.ts +63 -0
  158. package/src/core/avatar/avatar-cache.ts +95 -0
  159. package/src/core/avatar/avatar-ref.ts +34 -0
  160. package/src/core/avatar/avatar-types.ts +43 -0
  161. package/src/core/avatar/hub-avatar.ts +308 -0
  162. package/src/core/connection.ts +408 -299
  163. package/src/core/errors.ts +160 -147
  164. package/src/core/game-data.ts +40 -38
  165. package/src/core/hub-economy.ts +216 -179
  166. package/src/core/hub-progress.ts +269 -205
  167. package/src/core/hub.ts +352 -131
  168. package/src/core/menu-control.ts +61 -61
  169. package/src/core/offline/cloud-saves.ts +62 -0
  170. package/src/core/offline/events.ts +50 -50
  171. package/src/core/offline/favorites.ts +21 -21
  172. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  173. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  174. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  175. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  176. package/src/core/offline/hub-kinds/index.ts +52 -0
  177. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  178. package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
  179. package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
  180. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  181. package/src/core/offline/kind-adapters.ts +48 -0
  182. package/src/core/offline/local-inventory.ts +64 -0
  183. package/src/core/offline/local-store.ts +304 -261
  184. package/src/core/offline/offline-api.ts +65 -0
  185. package/src/core/offline/outbox-entry.ts +59 -0
  186. package/src/core/offline/outbox-kinds.ts +78 -0
  187. package/src/core/offline/outbox-merge.ts +39 -0
  188. package/src/core/offline/outbox-replayer.ts +141 -0
  189. package/src/core/offline/outbox-store.ts +85 -0
  190. package/src/core/offline/outbox.ts +232 -0
  191. package/src/core/offline/queue.ts +126 -171
  192. package/src/core/offline/replay-lease.ts +87 -0
  193. package/src/core/offline/replay-policy.ts +52 -0
  194. package/src/core/page-account.ts +121 -0
  195. package/src/core/page.ts +87 -74
  196. package/src/core/rest/endpoint-types.ts +22 -22
  197. package/src/core/rest/rest-client.ts +163 -152
  198. package/src/core/rest/routes.gen.ts +144 -88
  199. package/src/core/resume/hub-resume.ts +286 -0
  200. package/src/core/resume/local-resume.ts +130 -0
  201. package/src/core/resume/resume-list.ts +65 -0
  202. package/src/core/resume/resume-request.ts +52 -0
  203. package/src/core/resume/resume-types.ts +64 -0
  204. package/src/core/saved/daily-saves.ts +196 -0
  205. package/src/core/saved/daily-seed.ts +16 -0
  206. package/src/core/saved/hub-saved.ts +221 -0
  207. package/src/core/saved/local-saved.ts +122 -0
  208. package/src/core/saved/saved-list.ts +129 -0
  209. package/src/core/saved/saved-open.ts +104 -0
  210. package/src/core/saved/saved-order.ts +49 -0
  211. package/src/core/saved/saved-types.ts +24 -0
  212. package/src/core/storage/json-guards.ts +80 -74
  213. package/src/core/storage/key-value.ts +66 -66
  214. package/src/core/ticket.ts +43 -43
  215. package/src/core/turns/hub-turns.ts +161 -0
  216. package/src/core/turns/turn-game.ts +510 -0
  217. package/src/core/turns/your-turn-buzz.ts +69 -0
  218. package/src/core/types.ts +134 -134
  219. package/src/core/version/client-version.gen.ts +2 -2
  220. package/src/core/version/hub-version.ts +62 -62
  221. package/src/core/version/semver.ts +97 -97
  222. package/src/daily/daily-system.ts +276 -161
  223. package/src/daily/page-daily.ts +36 -36
  224. package/src/daily/types.ts +105 -67
  225. package/src/daily.ts +10 -10
  226. package/src/doctor/checks.ts +86 -76
  227. package/src/doctor/cli.ts +101 -106
  228. package/src/doctor/hub-version-fetch.ts +43 -0
  229. package/src/doctor/tsconfig.json +12 -12
  230. package/src/doctor/world-check.ts +61 -0
  231. package/src/globals.ts +115 -110
  232. package/src/index.ts +68 -50
  233. package/src/input/device-state.ts +146 -146
  234. package/src/input/edges.ts +55 -55
  235. package/src/input/focus-ring.ts +13 -0
  236. package/src/input/gamepad.ts +61 -61
  237. package/src/input/input-events.ts +32 -32
  238. package/src/input/input-registry.ts +171 -164
  239. package/src/input/input-system.ts +406 -393
  240. package/src/input/legacy-controls.ts +64 -64
  241. package/src/input/modality.ts +122 -0
  242. package/src/input/nav-state.ts +200 -191
  243. package/src/input/navigator.ts +182 -171
  244. package/src/input/on-screen-controls.ts +101 -101
  245. package/src/input/page-control.ts +43 -43
  246. package/src/input/page-input.ts +193 -193
  247. package/src/input/player-remaps.ts +77 -77
  248. package/src/input/source-values.ts +116 -116
  249. package/src/input/spatial.ts +47 -47
  250. package/src/input/types.ts +331 -331
  251. package/src/input/zero-config.ts +164 -177
  252. package/src/input.ts +26 -22
  253. package/src/mp.ts +10 -10
  254. package/src/overlay/controls-panel.ts +163 -163
  255. package/src/overlay/dom.ts +46 -46
  256. package/src/overlay/layout-clusters.ts +162 -162
  257. package/src/overlay/layout-geometry.ts +136 -136
  258. package/src/overlay/layout.ts +203 -203
  259. package/src/overlay/listener-scope.ts +23 -23
  260. package/src/overlay/on-screen-keyboard.ts +137 -137
  261. package/src/overlay/overlay.ts +234 -208
  262. package/src/overlay/styles.ts +13 -12
  263. package/src/overlay/virtual-controls.ts +262 -262
  264. package/src/race.ts +10 -10
  265. package/src/realtime/bridge.ts +126 -121
  266. package/src/realtime/cross-bundle.ts +29 -29
  267. package/src/realtime/fallback-notice.ts +48 -48
  268. package/src/realtime/hub-link.ts +65 -65
  269. package/src/realtime/latest-state.ts +33 -33
  270. package/src/realtime/multiplayer-forwarder.ts +40 -28
  271. package/src/realtime/multiplayer-types.ts +66 -52
  272. package/src/realtime/page-runtime.ts +60 -60
  273. package/src/realtime/race-forwarder.ts +59 -59
  274. package/src/realtime/race-types.ts +132 -132
  275. package/src/realtime/room-types.ts +151 -125
  276. package/src/realtime/rooms-forwarder.ts +104 -79
  277. package/src/realtime/runtime-api.ts +74 -49
  278. package/src/realtime/runtime-opener.ts +61 -61
  279. package/src/realtime/social-forwarder.ts +77 -72
  280. package/src/realtime/social-types.ts +86 -78
  281. package/src/realtime/types.ts +54 -53
  282. package/src/realtime/wire-types.ts +79 -72
  283. package/src/realtime/world-types.ts +20 -0
  284. package/src/rooms.ts +10 -10
  285. package/src/social.ts +19 -18
  286. package/src/turns-kit/check-rules.ts +281 -0
  287. package/src/turns-rules.ts +12 -0
  288. package/src/turns.ts +14 -0
  289. package/src/types.ts +13 -13
  290. package/src/uistack/page-stack.ts +45 -45
  291. package/src/uistack/play-suspension.ts +82 -82
  292. package/src/uistack/types.ts +76 -76
  293. package/src/uistack/ui-stack.ts +144 -144
@@ -0,0 +1,254 @@
1
+ /** Hair styles (catalog `hair.*`), keyed by the part name. */
2
+ import type { Path} from '../geom';
3
+ import { arc, blob, scallop, type Pt } from '../geom';
4
+ import type { HairPart, Shape, Slot } from './shape';
5
+
6
+ // ---- helpers -------------------------------------------------------------
7
+
8
+ /** The skull arc (centre 0,−4) from the left temple over the top to the right. */
9
+ const skull = (vol: number, a0 = 183, a1 = 357, steps = 8): Pt[] => arc(0, -4, 46 + vol, 46 + vol, a0, a1, steps);
10
+
11
+ /** A cap: the skull arc outset by `vol`, closed by a hairline (right → left). */
12
+ const cap = (vol: number, hairline: readonly Pt[], a0 = 183, a1 = 357): Path => blob([...skull(vol, a0, a1), ...hairline]);
13
+
14
+ const shine = (r = 40, a0 = 212, a1 = 244): Shape => ({
15
+ d: blob(arc(0, -4, r, r, a0, a1, 3), false), fill: 'hairLight', line: true, sw: 3.4, stroke: 'hairLight', opacity: 0.85,
16
+ });
17
+
18
+ /** A smooth back-view cap: down to the nape at `nape`. */
19
+ const rearCap = (vol: number, nape: number, napeW = 40): Path =>
20
+ blob([...arc(0, -4, 46 + vol, 46 + vol, 162, 378, 10), [napeW + vol * 0.4, nape], [0, nape + 4], [-napeW - vol * 0.4, nape]]);
21
+
22
+ /** A round, scalloped cloud (afro, puffs, buns). */
23
+ const cloud = (cx: number, cy: number, rx: number, ry: number, count: number, bump: number, phase = 0): Path =>
24
+ scallop(arc(cx, cy, rx, ry, phase, phase + 360 - 360 / count, count - 1), bump, true);
25
+
26
+ /** A hanging strand (locs, braids): an outlined thick round line. */
27
+ const strand = (pts: readonly Pt[], w: number, slot: Slot = 'hair'): Shape[] => {
28
+ const d = blob(pts, false);
29
+ return [
30
+ { d, fill: 'none', line: true, stroke: 'hairInk', sw: w + 3 },
31
+ { d, fill: 'none', line: true, stroke: slot, sw: w },
32
+ ];
33
+ };
34
+
35
+ const smoothHairline: Pt[] = [[45, -8], [38, -24], [22, -33], [0, -36], [-22, -33], [-38, -24], [-45, -8]];
36
+
37
+ // ---- hair ----------------------------------------------------------------
38
+
39
+ /** Every hair style by name. */
40
+ export const HAIR_ART: Readonly<Record<string, HairPart>> = {
41
+ bald: {
42
+ front: [{ d: blob(arc(0, -4, 38, 38, 218, 240, 2), false), fill: 'none', line: true, stroke: 'white', sw: 3.2, opacity: 0.35 }],
43
+ rear: [],
44
+ top: 0,
45
+ },
46
+
47
+ buzz: {
48
+ front: [{
49
+ d: cap(3, [[46, -10], [40, -22], [26, -29], [0, -32], [-26, -29], [-40, -22], [-46, -10]]),
50
+ fill: 'hairBuzz', clip: 'head', stroke: 'none',
51
+ }],
52
+ rear: [{ d: rearCap(2, 22), fill: 'hairBuzz', clip: 'head', stroke: 'none' }],
53
+ top: 0,
54
+ },
55
+
56
+ crop: {
57
+ front: [
58
+ { d: cap(5, [[45, -8], [41, -21], [29, -29], [15, -27], [2, -33], [-12, -27], [-26, -31], [-39, -22], [-46, -8]]), fill: 'hair' },
59
+ shine(42),
60
+ ],
61
+ rear: [{ d: rearCap(5, 18), fill: 'hair' }],
62
+ top: 5,
63
+ },
64
+
65
+ swoop: {
66
+ front: [
67
+ {
68
+ d: blob([[-48, -4], [-52, -28], [-42, -50], [-18, -63], [10, -64], [34, -55], [49, -35], [51, -8],
69
+ [45, -6], [41, -20], [30, -26], [16, -25], [0, -29], [-16, -36], [-30, -40], [-41, -34], [-46, -18]]),
70
+ fill: 'hair',
71
+ },
72
+ { d: blob([[24, -60], [14, -46], [6, -30]], false), fill: 'none', line: true, stroke: 'hairShade', sw: 2.4 },
73
+ shine(46, 222, 250),
74
+ ],
75
+ rear: [{ d: rearCap(7, 16), fill: 'hair' }],
76
+ top: 14,
77
+ },
78
+
79
+ spiky: {
80
+ front: [{
81
+ d: blob([
82
+ [-48, -4], [-58, -22, 'c'], [-47, -30], [-54, -48, 'c'], [-36, -48], [-30, -66, 'c'], [-16, -55], [-2, -72, 'c'],
83
+ [8, -56], [26, -68, 'c'], [30, -50], [50, -54, 'c'], [46, -36], [60, -24, 'c'], [48, -4],
84
+ [44, -10], [38, -26, 'c'], [30, -21], [22, -34, 'c'], [12, -25], [2, -36, 'c'], [-8, -25], [-18, -34, 'c'], [-28, -22], [-38, -29, 'c'], [-45, -10],
85
+ ]),
86
+ fill: 'hair',
87
+ }, shine(40)],
88
+ rear: [{
89
+ d: blob([[-48, 14], [-58, -6, 'c'], [-50, -20], [-56, -40, 'c'], [-38, -44], [-30, -64, 'c'], [-14, -54], [0, -70, 'c'], [14, -54], [30, -64, 'c'],
90
+ [38, -44], [56, -40, 'c'], [50, -20], [58, -6, 'c'], [48, 14], [24, 18, 'c'], [12, 26, 'c'], [0, 20, 'c'], [-12, 26, 'c'], [-24, 18, 'c']]),
91
+ fill: 'hair',
92
+ }],
93
+ top: 20,
94
+ },
95
+
96
+ curly: {
97
+ back: [{ d: cloud(0, -8, 58, 54, 13, 6, 135), fill: 'hairShade' }],
98
+ front: [{
99
+ d: scallop([...skull(9, 180, 360, 9), [48, -6], [40, -20], [28, -28], [14, -30], [0, -29], [-14, -30], [-28, -28], [-40, -20], [-48, -6]], 4.5, true),
100
+ fill: 'hair',
101
+ }, shine(44, 214, 238)],
102
+ rear: [{ d: cloud(0, -6, 58, 58, 15, 6, 0), fill: 'hair' }],
103
+ top: 16,
104
+ },
105
+
106
+ coily: {
107
+ front: [{
108
+ d: scallop([...skull(7, 178, 362, 14), [46, -10], [40, -22], [30, -29], [20, -31], [10, -32], [0, -32], [-10, -32], [-20, -31], [-30, -29], [-40, -22], [-46, -10]], 2.6, true),
109
+ fill: 'hair',
110
+ }, shine(42, 218, 240)],
111
+ rear: [{ d: scallop([...arc(0, -4, 53, 53, 160, 380, 18), [44, 20], [30, 24], [15, 26], [0, 27], [-15, 26], [-30, 24], [-44, 20]], 2.6, true), fill: 'hair' }],
112
+ top: 10,
113
+ },
114
+
115
+ afro: {
116
+ back: [{ d: cloud(0, -16, 72, 64, 16, 6.5, 90), fill: 'hair' }],
117
+ front: [
118
+ { d: scallop([...skull(4, 186, 354, 6), [44, -12], [36, -24], [24, -30], [12, -31], [0, -31], [-12, -31], [-24, -30], [-36, -24], [-44, -12]], 3.5, true), fill: 'hair', stroke: 'none' },
119
+ { d: scallop([[44, -12], [36, -24], [24, -30], [12, -31], [0, -31], [-12, -31], [-24, -30], [-36, -24], [-44, -12]], 3.5, false, -1), fill: 'none', line: true, stroke: 'hairInk', sw: 3 },
120
+ shine(62, 214, 236),
121
+ ],
122
+ rear: [{ d: cloud(0, -16, 72, 64, 16, 6.5, 90), fill: 'hair' }],
123
+ top: 30,
124
+ coversEars: true,
125
+ },
126
+
127
+ bob: {
128
+ back: [{ d: blob([[-52, 40, 'c'], [-56, 6], [-56, -28], [-42, -52], [0, -60], [42, -52], [56, -28], [56, 6], [52, 40, 'c']]), fill: 'hairShade' }],
129
+ front: [{
130
+ d: blob([[-54, 38, 'c'], [-56, 4], [-53, -28], [-38, -51], [0, -58], [38, -51], [53, -28], [56, 4], [54, 38, 'c'],
131
+ [43, 36], [41, 10], [42, -14, 'c'], [30, -20], [15, -19], [0, -21], [-15, -19], [-30, -20], [-42, -14, 'c'], [-41, 10], [-43, 36]]),
132
+ fill: 'hair',
133
+ }, shine(46, 220, 246)],
134
+ rear: [{ d: blob([[-54, 40, 'c'], [-57, 4], [-54, -28], [-40, -52], [0, -60], [40, -52], [54, -28], [57, 4], [54, 40, 'c'], [27, 44], [0, 42], [-27, 44]]), fill: 'hair' }],
135
+ top: 10,
136
+ coversEars: true,
137
+ },
138
+
139
+ long: {
140
+ back: [{
141
+ d: blob([[-50, -10], [-54, -36], [-36, -52], [0, -56], [36, -52], [54, -36], [50, -10], [56, 34], [60, 74, 'c'], [30, 78], [0, 72], [-30, 78], [-60, 74, 'c'], [-56, 34]]),
142
+ fill: 'hairShade',
143
+ }],
144
+ front: [{
145
+ d: blob([[0, -58, 'c'], [26, -55], [46, -40], [53, -12], [53, 30], [52, 70, 'c'], [43, 62], [41, 30], [41, 0], [35, -20], [20, -31], [7, -38], [0, -46, 'c'],
146
+ [-7, -38], [-20, -31], [-35, -20], [-41, 0], [-41, 30], [-43, 62], [-52, 70, 'c'], [-53, 30], [-53, -12], [-46, -40], [-26, -55]]),
147
+ fill: 'hair',
148
+ }, shine(44, 220, 248)],
149
+ rear: [{
150
+ d: blob([[-52, -10], [-58, -40], [-40, -60], [0, -64], [40, -60], [58, -40], [52, -10], [56, 34], [60, 74, 'c'], [30, 78], [0, 74], [-30, 78], [-60, 74, 'c'], [-56, 34]]),
151
+ fill: 'hair',
152
+ }, { d: blob([[0, -58], [0, -30], [2, 10]], false), fill: 'none', line: true, stroke: 'hairShade', sw: 2.4 }],
153
+ top: 14,
154
+ coversEars: true,
155
+ },
156
+
157
+ ponytail: {
158
+ back: [
159
+ { d: blob([[18, -48], [44, -64], [66, -52], [72, -22], [66, 14], [58, 44, 'c'], [52, 12], [48, -18], [34, -40]]), fill: 'hairShade' },
160
+ ],
161
+ front: [
162
+ { d: cap(4, smoothHairline), fill: 'hair' },
163
+ { d: blob([[34, -52], [40, -44]], false), fill: 'none', line: true, stroke: 'acc', sw: 7, cap: 'round' },
164
+ shine(42),
165
+ ],
166
+ rear: [
167
+ { d: rearCap(4, 14), fill: 'hair' },
168
+ { d: blob([[0, -30], [14, -10], [12, 30], [0, 62, 'c'], [-12, 30], [-14, -10]]), fill: 'hair' },
169
+ { d: blob([[-7, -28], [7, -28]], false), fill: 'none', line: true, stroke: 'acc', sw: 7 },
170
+ ],
171
+ top: 14,
172
+ },
173
+
174
+ bun: {
175
+ back: [{ d: blob(arc(0, -60, 20, 18, 0, 345, 11)), fill: 'hair' }, { d: blob(arc(0, -60, 12, 10, 200, 250, 2), false), fill: 'none', line: true, stroke: 'hairLight', sw: 3 }],
176
+ front: [
177
+ { d: cap(4, smoothHairline), fill: 'hair' },
178
+ { d: blob([[0, -36], [-2, -48]], false), fill: 'none', line: true, stroke: 'hairShade', sw: 2.2 },
179
+ shine(42, 222, 246),
180
+ ],
181
+ rear: [{ d: rearCap(4, 14), fill: 'hair' }, { d: blob(arc(0, -52, 20, 18, 0, 345, 11)), fill: 'hair' }],
182
+ top: 30,
183
+ },
184
+
185
+ puffs: {
186
+ back: [{ d: cloud(-46, -40, 20, 20, 9, 4, 20), fill: 'hair' }, { d: cloud(46, -40, 20, 20, 9, 4, 0), fill: 'hair' }],
187
+ front: [
188
+ { d: cap(4, smoothHairline), fill: 'hair' },
189
+ { d: blob([[0, -36], [0, -50]], false), fill: 'none', line: true, stroke: 'hairShade', sw: 2.2 },
190
+ { d: blob([[-36, -44], [-30, -52]], false), fill: 'none', line: true, stroke: 'acc', sw: 6 },
191
+ { d: blob([[36, -44], [30, -52]], false), fill: 'none', line: true, stroke: 'acc', sw: 6 },
192
+ ],
193
+ rear: [{ d: rearCap(4, 14), fill: 'hair' }, { d: cloud(-46, -40, 20, 20, 9, 4, 20), fill: 'hair' }, { d: cloud(46, -40, 20, 20, 9, 4, 0), fill: 'hair' }],
194
+ top: 14,
195
+ },
196
+
197
+ mohawk: {
198
+ front: [
199
+ { d: cap(2, [[46, -10], [40, -22], [26, -29], [0, -32], [-26, -29], [-40, -22], [-46, -10]]), fill: 'hairBuzz', clip: 'head', stroke: 'none', opacity: 0.55 },
200
+ {
201
+ d: blob([[-13, -28], [-18, -50], [-24, -72, 'c'], [-8, -62], [0, -86, 'c'], [8, -62], [24, -72, 'c'], [18, -50], [13, -28], [0, -32]]),
202
+ fill: 'hair',
203
+ },
204
+ ],
205
+ rear: [
206
+ { d: rearCap(2, 22), fill: 'hairBuzz', clip: 'head', stroke: 'none', opacity: 0.55 },
207
+ { d: blob([[-11, 18], [-13, -40], [-14, -60, 'c'], [-6, -54], [0, -72, 'c'], [6, -54], [14, -60, 'c'], [13, -40], [11, 18]]), fill: 'hair' },
208
+ ],
209
+ top: 30,
210
+ },
211
+
212
+ locs: {
213
+ back: [
214
+ { d: blob([[-50, -10], [-54, -36], [-36, -50], [0, -52], [36, -50], [54, -36], [50, -10], [56, 30], [58, 62, 'c'], [0, 58], [-58, 62, 'c'], [-56, 30]]), fill: 'hairShade' },
215
+ ...[-44, -30, 30, 44].flatMap((at) => strand([[at, 0], [at * 1.12, 30], [at * 1.12, 60]], 7, 'hairShade')),
216
+ ],
217
+ front: [
218
+ { d: cap(6, [[46, -2], [42, -20], [28, -30], [14, -26], [0, -32], [-14, -26], [-28, -30], [-42, -20], [-46, -2]]), fill: 'hair' },
219
+ ...strand([[-46, -10], [-50, 14], [-50, 40]], 8),
220
+ ...strand([[46, -10], [50, 14], [50, 40]], 8),
221
+ ...strand([[-38, -14], [-41, 8], [-41, 24]], 7),
222
+ ...strand([[38, -14], [41, 8], [41, 24]], 7),
223
+ shine(44),
224
+ ],
225
+ rear: [
226
+ { d: rearCap(6, 10), fill: 'hair' },
227
+ ...[-40, -27, -14, 0, 14, 27, 40].flatMap((at) => strand([[at * 1.05, -6], [at * 1.12, 26], [at * 1.12, 60]], 9)),
228
+ ],
229
+ top: 8,
230
+ coversEars: true,
231
+ },
232
+
233
+ braids: {
234
+ back: [
235
+ { d: blob([[-50, -10], [-54, -36], [-36, -50], [0, -52], [36, -50], [54, -36], [50, -10], [54, 30], [54, 70, 'c'], [0, 64], [-54, 70, 'c'], [-54, 30]]), fill: 'hairShade' },
236
+ ...[-46, -34, 34, 46].flatMap((at) => strand([[at, 0], [at * 1.08, 70]], 6, 'hairShade')),
237
+ ],
238
+ front: [
239
+ { d: cap(4, [[45, -6], [36, -24], [22, -30], [0, -34], [-22, -30], [-36, -24], [-45, -6]]), fill: 'hair' },
240
+ { d: blob([[0, -34], [0, -52]], false), fill: 'none', line: true, stroke: 'hairShade', sw: 2.2 },
241
+ ...[-20, -10, 10, 20].map((at): Shape => ({ d: blob([[at, -32], [at * 1.5, -52]], false), fill: 'none', line: true, stroke: 'hairShade', sw: 1.8 })),
242
+ ...strand([[-46, -8], [-50, 30], [-50, 66]], 6),
243
+ ...strand([[46, -8], [50, 30], [50, 66]], 6),
244
+ { d: blob([[-54, 60], [-46, 60]], false), fill: 'none', line: true, stroke: 'acc', sw: 4, cap: 'butt' },
245
+ { d: blob([[46, 60], [54, 60]], false), fill: 'none', line: true, stroke: 'acc', sw: 4, cap: 'butt' },
246
+ ],
247
+ rear: [
248
+ { d: rearCap(4, 10), fill: 'hair' },
249
+ ...[-36, -24, -12, 0, 12, 24, 36].flatMap((at) => strand([[at * 1.05, -10], [at * 1.08, 30], [at * 1.08, 66]], 6.5)),
250
+ ],
251
+ top: 6,
252
+ coversEars: true,
253
+ },
254
+ };
@@ -0,0 +1,75 @@
1
+ /** Headwear (catalog `headwear.*`). Head coverings are drawn with care and hide hair and ears (§14.2). */
2
+ import { arc, blob, roundedRect } from '../geom';
3
+ import type { HeadwearPart, Shape } from './shape';
4
+
5
+ /** Every headwear part by name. */
6
+ export const HEADWEAR_ART: Readonly<Record<string, HeadwearPart>> = {
7
+ hijab: {
8
+ // The drape sits over the body; the front frames the face (even-odd hole).
9
+ back: [{
10
+ d: blob([[-54, -10], [-56, -40], [-36, -60], [0, -64], [36, -60], [56, -40], [54, -10], [58, 30], [62, 66, 'c'], [30, 74], [0, 72], [-30, 74], [-62, 66, 'c'], [-58, 30]]),
11
+ fill: 'accShade',
12
+ }],
13
+ front: [{
14
+ d: blob([[-55, -8], [-55, -38], [-36, -58], [0, -62], [36, -58], [55, -38], [55, -8], [54, 28], [44, 56], [20, 66], [0, 66], [-20, 66], [-44, 56], [-54, 28]])
15
+ .add(blob([[0, -38], [24, -34], [37, -14], [38, 14], [30, 38], [14, 52], [0, 54], [-14, 52], [-30, 38], [-38, 14], [-37, -14], [-24, -34]])),
16
+ fill: 'acc', evenOdd: true,
17
+ }, { d: blob([[-30, 60], [-12, 52], [8, 58]], false), fill: 'none', line: true, stroke: 'accShade', sw: 2.4 },
18
+ { d: blob([[-49, -6], [-46, -30], [-30, -48]], false), fill: 'none', line: true, stroke: 'accLight', sw: 3, opacity: 0.8 }],
19
+ rear: [{
20
+ d: blob([[-54, -10], [-56, -40], [-36, -60], [0, -64], [36, -60], [56, -40], [54, -10], [58, 30], [62, 66, 'c'], [30, 74], [0, 72], [-30, 74], [-62, 66, 'c'], [-58, 30]]),
21
+ fill: 'acc',
22
+ }, { d: blob([[-20, 20], [0, 30], [22, 22]], false), fill: 'none', line: true, stroke: 'accShade', sw: 2.4 }],
23
+ hides: 'all',
24
+ hidesEars: true,
25
+ top: 14,
26
+ },
27
+ headwrap: {
28
+ back: [
29
+ { d: blob([[-12, -56], [-34, -80], [-14, -88], [0, -64]]), fill: 'accShade' },
30
+ { d: blob([[12, -56], [34, -82], [14, -90], [0, -64]]), fill: 'accShade' },
31
+ ],
32
+ front: [
33
+ { d: blob([[-49, -12], [-54, -38], [-38, -62], [0, -70], [38, -62], [54, -38], [49, -12], [36, -24], [18, -30], [0, -31], [-18, -30], [-36, -24]]), fill: 'acc' },
34
+ { d: blob([[-44, -22], [-14, -48], [10, -64]], false), fill: 'none', line: true, stroke: 'accShade', sw: 2.4 },
35
+ { d: blob([[44, -22], [20, -44], [2, -50]], false), fill: 'none', line: true, stroke: 'accShade', sw: 2.4 },
36
+ { d: blob(arc(0, -64, 9, 7, 0, 330, 11)), fill: 'acc' },
37
+ ],
38
+ rear: [
39
+ { d: blob([[-49, 4], [-56, -30], [-40, -60], [0, -70], [40, -60], [56, -30], [49, 4], [24, 0], [0, -2], [-24, 0]]), fill: 'acc' },
40
+ { d: blob([[-40, -8], [-6, -44], [16, -64]], false), fill: 'none', line: true, stroke: 'accShade', sw: 2.4 },
41
+ { d: blob(arc(0, -64, 9, 7, 0, 330, 11)), fill: 'acc' },
42
+ ],
43
+ hides: 'all',
44
+ top: 36,
45
+ },
46
+ beanie: {
47
+ front: [
48
+ { d: blob([[-50, -22, 'c'], [-50, -40], [-34, -60], [0, -66], [34, -60], [50, -40], [50, -22, 'c']]), fill: 'acc' },
49
+ { d: roundedRect(-52, -30, 104, 15, 6), fill: 'accShade' },
50
+ ...[-36, -18, 0, 18, 36].map((at): Shape => ({ d: blob([[at, -27], [at, -18]], false), fill: 'none', line: true, stroke: 'acc', sw: 2.4 })),
51
+ { d: blob(arc(0, -70, 10, 9, 0, 330, 11)), fill: 'accLight' },
52
+ ],
53
+ rear: [
54
+ { d: blob([[-50, -22, 'c'], [-50, -40], [-34, -60], [0, -66], [34, -60], [50, -40], [50, -22, 'c']]), fill: 'acc' },
55
+ { d: roundedRect(-52, -30, 104, 15, 6), fill: 'accShade' },
56
+ { d: blob(arc(0, -70, 10, 9, 0, 330, 11)), fill: 'accLight' },
57
+ ],
58
+ hides: -24,
59
+ top: 28,
60
+ },
61
+ cap: {
62
+ front: [
63
+ { d: blob([[-49, -22, 'c'], [-48, -42], [-30, -60], [0, -64], [30, -60], [48, -42], [49, -22, 'c'], [0, -26]]), fill: 'acc' },
64
+ { d: blob([[-6, -63], [-4, -40], [0, -27]], false), fill: 'none', line: true, stroke: 'accShade', sw: 2.2 },
65
+ { d: blob([[-46, -24, 'c'], [-24, -21], [0, -20], [24, -21], [46, -24, 'c'], [40, -14], [20, -6], [0, -4], [-20, -6], [-40, -14]]), fill: 'accShade' },
66
+ { d: blob(arc(0, -63, 4, 3, 0, 330, 7)), fill: 'accShade' },
67
+ ],
68
+ rear: [
69
+ { d: blob([[-49, -18, 'c'], [-48, -42], [-30, -60], [0, -64], [30, -60], [48, -42], [49, -18, 'c'], [12, -20], [12, -30, 'c'], [-12, -30, 'c'], [-12, -20]]), fill: 'acc' },
70
+ { d: roundedRect(-12, -26, 24, 5, 2), fill: 'accShade' },
71
+ ],
72
+ hides: -22,
73
+ top: 18,
74
+ },
75
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The art chunk: every part's path data, keyed by catalog name. Loaded
3
+ * lazily by `ready()` so `encode`, `decode` and `defaultLook` never pull it
4
+ * in (avatars.md §3.1).
5
+ */
6
+ import { BEARD_ART } from './beard';
7
+ import { GLASSES_ART } from './glasses';
8
+ import { HAIR_ART } from './hair';
9
+ import { HEADWEAR_ART } from './headwear';
10
+ import type { FacialHairPart, GlassesPart, HairPart, HeadwearPart } from './shape';
11
+
12
+ /** Every category's art. */
13
+ export interface ArtSet {
14
+ readonly hair: Readonly<Record<string, HairPart>>;
15
+ readonly beard: Readonly<Record<string, FacialHairPart>>;
16
+ readonly glasses: Readonly<Record<string, GlassesPart>>;
17
+ readonly headwear: Readonly<Record<string, HeadwearPart>>;
18
+ }
19
+
20
+ /** The launch art. */
21
+ export const ART: ArtSet = { hair: HAIR_ART, beard: BEARD_ART, glasses: GLASSES_ART, headwear: HEADWEAR_ART };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Part art is authored as point lists on the 100-unit rig (head centre at
3
+ * 0,0; skull circle centre 0,−4, r 46; eye line y 4; mouth y 27). Every
4
+ * shape fills a tint slot, never a colour, so any colour works with no new
5
+ * art. Hair and headwear parts ship a `rear` layer (the back of the head),
6
+ * used by the back and side facings (avatars.md §3.3, §14.2).
7
+ */
8
+ import type { Path } from '../geom';
9
+
10
+ /** A tint slot: the renderer resolves it from the look. */
11
+ export type Slot =
12
+ | 'hair' | 'hairShade' | 'hairLight' | 'hairBuzz' | 'hairInk'
13
+ | 'beard' | 'beardShade' | 'beardInk' | 'stubble'
14
+ | 'acc' | 'accShade' | 'accLight' | 'accInk'
15
+ | 'skin' | 'skinShade' | 'skinInk' | 'ink' | 'white' | 'none';
16
+
17
+ /** One painted shape of a part. */
18
+ export interface Shape {
19
+ readonly d: Path;
20
+ readonly fill: Slot;
21
+ /** Outline slot; defaults to the fill's tonal outline. `'none'` for no outline. */
22
+ readonly stroke?: Slot;
23
+ readonly sw?: number;
24
+ /** Clip to the head silhouette (buzz cuts, stubble). */
25
+ readonly clip?: 'head';
26
+ readonly opacity?: number;
27
+ /** A stroke-only line (the fill is ignored). */
28
+ readonly line?: boolean;
29
+ readonly cap?: 'round' | 'butt';
30
+ /** Fill with the even-odd rule (shapes with holes: the hijab's face opening). */
31
+ readonly evenOdd?: boolean;
32
+ }
33
+
34
+ /** A hair style: `back` (behind the head), `front` (over the face), `rear` (the back view). */
35
+ export interface HairPart {
36
+ readonly back?: readonly Shape[];
37
+ readonly front?: readonly Shape[];
38
+ readonly rear: readonly Shape[];
39
+ /** How far the style rises above the skull top (the name-tag anchor). */
40
+ readonly top: number;
41
+ /** The style covers the ears. */
42
+ readonly coversEars?: boolean;
43
+ }
44
+
45
+ /** Facial hair, as two layers (§14.2): `beard` below the mouth, `moustache` above it. */
46
+ export interface FacialHairPart {
47
+ readonly beard?: readonly Shape[];
48
+ readonly moustache?: readonly Shape[];
49
+ }
50
+
51
+ /**
52
+ * Glasses, drawn per lens so they follow the eyes through a head turn. Lens
53
+ * paths are centred on the eye and drawn for the viewer's right eye.
54
+ */
55
+ export interface GlassesPart {
56
+ /** One lens (absent for a visor). */
57
+ readonly lens?: Path;
58
+ /** The bridge over the nose, centred between the eyes. */
59
+ readonly bridge?: Path;
60
+ /** The frame's line width (default 3.2). */
61
+ readonly frame?: number;
62
+ /** A heavier bar along each lens's top (half-rims). */
63
+ readonly bar?: Path;
64
+ /** Mirror the lens for the other eye (shapes with an outer and inner side). */
65
+ readonly isMirrored?: true;
66
+ /** One visor across both eyes in head space instead of lenses (sport). */
67
+ readonly visor?: Path;
68
+ /** Only the right-hand lens, with no arms or bridge (the monocle). */
69
+ readonly isSingle?: true;
70
+ /** The monocle's chain, in head space. */
71
+ readonly chain?: Path;
72
+ /** The lens tint when the look has none (tinted glasses only). */
73
+ readonly tint?: string;
74
+ }
75
+
76
+ /**
77
+ * Headwear. `hides`: `'all'` hides the hair (hijab, headwrap); a number
78
+ * cuts the hair above that y (hats, §14.2).
79
+ */
80
+ export interface HeadwearPart {
81
+ readonly back?: readonly Shape[];
82
+ readonly front: readonly Shape[];
83
+ readonly rear: readonly Shape[];
84
+ readonly hides: 'all' | number;
85
+ readonly hidesEars?: boolean;
86
+ readonly top: number;
87
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The art chunk is loaded lazily (avatars.md §3.1): `ready()` imports it
3
+ * once, and every render is synchronous after that. Before it lands, renders
4
+ * draw the silhouette and `<dg-avatar>` upgrades itself when it arrives.
5
+ */
6
+ import type { ArtSet } from './art';
7
+
8
+ let art: ArtSet | null = null;
9
+ let loading: Promise<void> | null = null;
10
+ const waiters = new Set<() => void>();
11
+
12
+ /** The loaded art, or `null` before {@link ready} resolves. */
13
+ export function currentArt(): ArtSet | null {
14
+ return art;
15
+ }
16
+
17
+ /** Loads the art chunk (once). Resolves when rendering is synchronous. */
18
+ export function ready(): Promise<void> {
19
+ loading ??= import('./art').then((mod) => {
20
+ art = mod.ART;
21
+ for (const waiter of waiters) {
22
+ waiter();
23
+ }
24
+ waiters.clear();
25
+ });
26
+ return loading;
27
+ }
28
+
29
+ /** Calls `fn` once the art has loaded (now, when it already has). */
30
+ export function onArtReady(fn: () => void): void {
31
+ if (art) {
32
+ fn();
33
+ return;
34
+ }
35
+ waiters.add(fn);
36
+ }