@diffenderfer-games/hub 2.0.0-next.5 → 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 (170) hide show
  1. package/CHANGELOG.md +54 -19
  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 +503 -14
  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 +2020 -144
  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 +8 -0
  137. package/src/core/errors.ts +7 -1
  138. package/src/core/hub-progress.ts +269 -238
  139. package/src/core/hub.ts +174 -6
  140. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  141. package/src/core/offline/hub-kinds/index.ts +9 -0
  142. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  143. package/src/core/page-account.ts +121 -0
  144. package/src/core/rest/routes.gen.ts +31 -1
  145. package/src/core/resume/hub-resume.ts +39 -5
  146. package/src/core/resume/resume-types.ts +2 -2
  147. package/src/core/saved/daily-saves.ts +196 -0
  148. package/src/core/saved/daily-seed.ts +16 -0
  149. package/src/core/saved/hub-saved.ts +221 -0
  150. package/src/core/saved/local-saved.ts +122 -0
  151. package/src/core/saved/saved-list.ts +129 -0
  152. package/src/core/saved/saved-open.ts +104 -0
  153. package/src/core/saved/saved-order.ts +49 -0
  154. package/src/core/saved/saved-types.ts +24 -0
  155. package/src/core/turns/hub-turns.ts +140 -0
  156. package/src/core/turns/turn-game.ts +388 -0
  157. package/src/core/version/client-version.gen.ts +2 -2
  158. package/src/daily/daily-system.ts +112 -13
  159. package/src/daily/page-daily.ts +2 -2
  160. package/src/daily/types.ts +46 -9
  161. package/src/doctor/hub-version-fetch.ts +43 -43
  162. package/src/index.ts +10 -1
  163. package/src/input/input-system.ts +10 -1
  164. package/src/input/navigator.ts +11 -0
  165. package/src/input.ts +1 -0
  166. package/src/overlay/overlay.ts +25 -1
  167. package/src/realtime/runtime-api.ts +14 -3
  168. package/src/social.ts +1 -0
  169. package/src/turns-kit/check-rules.ts +182 -0
  170. package/src/turns.ts +13 -0
@@ -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,68 @@
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
+ /** Glasses, drawn per lens so they follow the eyes through a head turn. */
52
+ export interface GlassesPart {
53
+ readonly lens: Path;
54
+ readonly bridge: Path;
55
+ }
56
+
57
+ /**
58
+ * Headwear. `hides`: `'all'` hides the hair (hijab, headwrap); a number
59
+ * cuts the hair above that y (hats, §14.2).
60
+ */
61
+ export interface HeadwearPart {
62
+ readonly back?: readonly Shape[];
63
+ readonly front: readonly Shape[];
64
+ readonly rear: readonly Shape[];
65
+ readonly hides: 'all' | number;
66
+ readonly hidesEars?: boolean;
67
+ readonly top: number;
68
+ }
@@ -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
+ }