@voqalize/avatar 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +21 -661
  2. package/README.md +144 -663
  3. package/client/Avatar.tsx +57 -0
  4. package/client/AvatarClient.ts +523 -0
  5. package/client/arjun.ts +24 -0
  6. package/client/createAvatar.ts +139 -0
  7. package/client/createCanvasAvatar.ts +72 -0
  8. package/client/index.ts +39 -0
  9. package/client/internal.ts +80 -0
  10. package/client/interviewer-female.ts +4 -0
  11. package/client/interviewer-male.ts +4 -0
  12. package/client/ishita.ts +24 -0
  13. package/client/kabir.ts +24 -0
  14. package/client/meera.ts +24 -0
  15. package/client/naina.ts +24 -0
  16. package/client/professional-female-a.ts +4 -0
  17. package/client/professional-female-b.ts +4 -0
  18. package/client/professional-male-a.ts +4 -0
  19. package/client/professional-male-b.ts +4 -0
  20. package/client/react.ts +13 -0
  21. package/client/types.ts +146 -0
  22. package/client/useAvatar.ts +74 -0
  23. package/client/vikram.ts +24 -0
  24. package/dist/Avatar.d.ts +44 -0
  25. package/dist/Avatar.d.ts.map +1 -0
  26. package/{client/dist → dist}/Avatar.js +2 -2
  27. package/dist/Avatar.js.map +1 -0
  28. package/dist/AvatarClient.d.ts +196 -0
  29. package/dist/AvatarClient.d.ts.map +1 -0
  30. package/dist/AvatarClient.js +450 -0
  31. package/dist/AvatarClient.js.map +1 -0
  32. package/dist/arjun.d.ts +6 -0
  33. package/dist/arjun.d.ts.map +1 -0
  34. package/dist/arjun.js +18 -0
  35. package/dist/arjun.js.map +1 -0
  36. package/dist/createAvatar.d.ts +99 -0
  37. package/dist/createAvatar.d.ts.map +1 -0
  38. package/dist/createAvatar.js +75 -0
  39. package/dist/createAvatar.js.map +1 -0
  40. package/dist/createCanvasAvatar.d.ts +22 -0
  41. package/dist/createCanvasAvatar.d.ts.map +1 -0
  42. package/dist/createCanvasAvatar.js +47 -0
  43. package/dist/createCanvasAvatar.js.map +1 -0
  44. package/dist/index.d.ts +30 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +29 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/internal.d.ts +30 -0
  49. package/dist/internal.d.ts.map +1 -0
  50. package/dist/internal.js +33 -0
  51. package/dist/internal.js.map +1 -0
  52. package/dist/interviewer-female.d.ts +4 -0
  53. package/dist/interviewer-female.d.ts.map +1 -0
  54. package/dist/interviewer-female.js +3 -0
  55. package/dist/interviewer-female.js.map +1 -0
  56. package/dist/interviewer-male.d.ts +4 -0
  57. package/dist/interviewer-male.d.ts.map +1 -0
  58. package/dist/interviewer-male.js +3 -0
  59. package/dist/interviewer-male.js.map +1 -0
  60. package/dist/ishita.d.ts +6 -0
  61. package/dist/ishita.d.ts.map +1 -0
  62. package/dist/ishita.js +18 -0
  63. package/dist/ishita.js.map +1 -0
  64. package/dist/kabir.d.ts +6 -0
  65. package/dist/kabir.d.ts.map +1 -0
  66. package/dist/kabir.js +18 -0
  67. package/dist/kabir.js.map +1 -0
  68. package/dist/meera.d.ts +6 -0
  69. package/dist/meera.d.ts.map +1 -0
  70. package/dist/meera.js +18 -0
  71. package/dist/meera.js.map +1 -0
  72. package/dist/naina.d.ts +6 -0
  73. package/dist/naina.d.ts.map +1 -0
  74. package/dist/naina.js +18 -0
  75. package/dist/naina.js.map +1 -0
  76. package/dist/professional-female-a.d.ts +4 -0
  77. package/dist/professional-female-a.d.ts.map +1 -0
  78. package/dist/professional-female-a.js +3 -0
  79. package/dist/professional-female-a.js.map +1 -0
  80. package/dist/professional-female-b.d.ts +4 -0
  81. package/dist/professional-female-b.d.ts.map +1 -0
  82. package/dist/professional-female-b.js +3 -0
  83. package/dist/professional-female-b.js.map +1 -0
  84. package/dist/professional-male-a.d.ts +4 -0
  85. package/dist/professional-male-a.d.ts.map +1 -0
  86. package/dist/professional-male-a.js +3 -0
  87. package/dist/professional-male-a.js.map +1 -0
  88. package/dist/professional-male-b.d.ts +4 -0
  89. package/dist/professional-male-b.d.ts.map +1 -0
  90. package/dist/professional-male-b.js +3 -0
  91. package/dist/professional-male-b.js.map +1 -0
  92. package/dist/react.d.ts +13 -0
  93. package/dist/react.d.ts.map +1 -0
  94. package/dist/react.js +13 -0
  95. package/dist/react.js.map +1 -0
  96. package/dist/types.d.ts +94 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +93 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/useAvatar.d.ts +39 -0
  101. package/dist/useAvatar.d.ts.map +1 -0
  102. package/dist/useAvatar.js +35 -0
  103. package/dist/useAvatar.js.map +1 -0
  104. package/dist/vikram.d.ts +6 -0
  105. package/dist/vikram.d.ts.map +1 -0
  106. package/dist/vikram.js +18 -0
  107. package/dist/vikram.js.map +1 -0
  108. package/package.json +81 -24
  109. package/src/avatar.d.ts +161 -120
  110. package/src/avatar.js +234 -146
  111. package/src/behavior.d.ts +26 -0
  112. package/src/behavior.js +102 -0
  113. package/src/camera.js +29 -0
  114. package/src/canvas/author/parts/eye.mjs +722 -0
  115. package/src/canvas/author/parts/hand.mjs +1156 -0
  116. package/src/canvas/author/parts/mouth.mjs +741 -0
  117. package/src/canvas/author/parts/nose.mjs +100 -0
  118. package/src/canvas/author/parts/skin-detail.mjs +67 -0
  119. package/src/canvas/author/path.mjs +283 -0
  120. package/src/canvas/author/rig.mjs +405 -0
  121. package/src/canvas/avatars/round/face.d.mts +3 -0
  122. package/src/canvas/avatars/round/face.mjs +1307 -0
  123. package/src/canvas/create-rig.d.ts +15 -0
  124. package/src/canvas/create-rig.js +100 -0
  125. package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
  126. package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
  127. package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
  128. package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
  129. package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
  130. package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
  131. package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
  132. package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
  133. package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
  134. package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
  135. package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
  136. package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
  137. package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
  138. package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
  139. package/src/canvas/data/img/round-m3-top-body.webp +0 -0
  140. package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
  141. package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
  142. package/src/canvas/data/img/round-w1-top-body.webp +0 -0
  143. package/src/canvas/data/interviewer-female.rig.json +1 -0
  144. package/src/canvas/data/interviewer-male.rig.json +1 -0
  145. package/src/canvas/data/professional-female-a.rig.json +1 -0
  146. package/src/canvas/data/professional-female-b.rig.json +1 -0
  147. package/src/canvas/data/professional-male-a.rig.json +1 -0
  148. package/src/canvas/data/professional-male-b.rig.json +1 -0
  149. package/src/canvas/src/live.js +508 -0
  150. package/src/canvas/src/render2d.js +218 -0
  151. package/src/canvas/src/rig.js +297 -0
  152. package/src/canvas/src/vocab.js +96 -0
  153. package/src/clips.js +18 -7
  154. package/src/conformance.js +119 -0
  155. package/src/emotions.js +4 -3
  156. package/src/face-core.js +27 -1
  157. package/src/face-myna.d.ts +7 -0
  158. package/src/face-myna.js +192 -131
  159. package/src/face-peep-control-plane.js +167 -0
  160. package/src/face-peep.d.ts +7 -0
  161. package/src/face-peep.js +274 -116
  162. package/src/face-wren.d.ts +7 -0
  163. package/src/face-wren.js +17 -19
  164. package/src/faces.d.ts +12 -0
  165. package/src/faces.js +53 -0
  166. package/src/gaze.js +119 -4
  167. package/src/hand.js +120 -91
  168. package/src/idle.js +18 -101
  169. package/src/interjections.js +156 -31
  170. package/src/params.js +8 -4
  171. package/src/perform.js +7 -9
  172. package/src/rig.d.ts +24 -0
  173. package/src/rig.js +32 -0
  174. package/src/speech-timing.js +23 -0
  175. package/src/visemes.js +28 -12
  176. package/client/dist/Avatar.d.ts +0 -27
  177. package/client/dist/Avatar.d.ts.map +0 -1
  178. package/client/dist/Avatar.js.map +0 -1
  179. package/client/dist/AvatarClient.d.ts +0 -139
  180. package/client/dist/AvatarClient.d.ts.map +0 -1
  181. package/client/dist/AvatarClient.js +0 -223
  182. package/client/dist/AvatarClient.js.map +0 -1
  183. package/client/dist/index.d.ts +0 -22
  184. package/client/dist/index.d.ts.map +0 -1
  185. package/client/dist/index.js +0 -22
  186. package/client/dist/index.js.map +0 -1
  187. package/client/dist/types.d.ts +0 -86
  188. package/client/dist/types.d.ts.map +0 -1
  189. package/client/dist/types.js +0 -31
  190. package/client/dist/types.js.map +0 -1
  191. package/client/dist/useAvatar.d.ts +0 -42
  192. package/client/dist/useAvatar.d.ts.map +0 -1
  193. package/client/dist/useAvatar.js +0 -51
  194. package/client/dist/useAvatar.js.map +0 -1
  195. package/client/src/Avatar.tsx +0 -33
  196. package/client/src/AvatarClient.ts +0 -270
  197. package/client/src/index.ts +0 -22
  198. package/client/src/types.ts +0 -104
  199. package/client/src/useAvatar.ts +0 -85
  200. package/docs/contract-avatar.md +0 -371
  201. package/docs/contract-protocol.md +0 -461
@@ -0,0 +1,405 @@
1
+ // ---------------------------------------------------------------------------
2
+ // author/rig.mjs — the authoring plumbing that survived a change of style.
3
+ //
4
+ // Two generators (avatars/facet, avatars/round) were written from scratch and
5
+ // then cloned across a style change. Exactly one layer came through
6
+ // byte-identical, and it is all in here: the paint registry, the control-vector
7
+ // machinery, the rebuild-and-diff pose harness, the small colour maths and the
8
+ // serialiser's contract defaults. Everything about the *character* — landmarks,
9
+ // geometry tables, the draw-builder body, palettes and ramps, control-channel
10
+ // names, lighting — deliberately stayed in each avatar's own build.mjs.
11
+ //
12
+ // Authoring-time only in intent — but PURE, and that is now load-bearing: it
13
+ // imports nothing from `node:`, so `avatars/round/face.mjs` (and the two parts
14
+ // under `parts/`, which import `clamp`/`lerp`/`drawPusher` back out of here)
15
+ // can be imported by a BROWSER. `src/live.js` evaluates the face at runtime and
16
+ // that is the whole chain it drags in. The one function that had to touch the
17
+ // filesystem, `writeRig`, moved to `finish.mjs` — its only caller — for exactly
18
+ // that reason.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ import { r2, rotMat } from './path.mjs';
22
+ import { DRIVER_DEFAULTS } from '../src/vocab.js';
23
+ import { eyeRest } from './parts/eye.mjs';
24
+ import { mouthRest } from './parts/mouth.mjs';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Small maths. Fifteen lines, copied byte-for-byte between both generators.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
31
+ export const sstep = (e0, e1, x) => { const t = clamp((x - e0) / (e1 - e0), 0, 1); return t * t * (3 - 2 * t); };
32
+ export const lerp = (a, b, t) => a + (b - a) * t;
33
+
34
+ export function hsl(h, s, l) {
35
+ h = ((h % 360) + 360) % 360;
36
+ const c = (1 - Math.abs(2 * l - 1)) * s;
37
+ const hp = h / 60;
38
+ const x = c * (1 - Math.abs((hp % 2) - 1));
39
+ let r = 0, g = 0, b = 0;
40
+ if (hp < 1) [r, g, b] = [c, x, 0];
41
+ else if (hp < 2) [r, g, b] = [x, c, 0];
42
+ else if (hp < 3) [r, g, b] = [0, c, x];
43
+ else if (hp < 4) [r, g, b] = [0, x, c];
44
+ else if (hp < 5) [r, g, b] = [x, 0, c];
45
+ else [r, g, b] = [c, 0, x];
46
+ const m = l - c / 2;
47
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), 1];
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Paint registry. Solids are deduped on a 3dp key, so a builder can call
52
+ // solid() freely from inside a loop without spraying near-identical entries.
53
+ // `paints` is the live array the serialiser wants; hold onto the reference.
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export function paintRegistry() {
57
+ const paints = [];
58
+ const paintKey = new Map();
59
+ function solid(c) {
60
+ const k = c.map((v) => Math.round(v * 1000) / 1000).join(',');
61
+ if (paintKey.has(k)) return paintKey.get(k);
62
+ const i = paints.length;
63
+ paints.push({ t: 'solid', c: [c[0], c[1], c[2], c[3]] });
64
+ paintKey.set(k, i);
65
+ return i;
66
+ }
67
+ return { paints, solid };
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // The draw pusher, and the per-draw override hook.
72
+ //
73
+ // All three builders open with the same line: an `out` array and a `push` that
74
+ // appends `{ slot, group, cmds, paint, a }`. Two of them then spread a sixth
75
+ // argument over it, because `toRig` passes four more fields through verbatim
76
+ // (`stroke`, `rule`, `blend`, `m` — see author/README.md) and nothing offered a
77
+ // place to put them. This is that place:
78
+ //
79
+ // push('limbal', HEAD, ring(...), solid(P.limbal), 1, { rule: 'evenodd' });
80
+ // push('toothSh', HEAD, band(...), solid(P.toothSh), tA, { blend: 'multiply' });
81
+ // push('faceInk', HEAD, spline(pts, 1), INK, 1, { stroke: STROKE(W_SIL) });
82
+ //
83
+ // `a` stays a POSITIONAL argument rather than joining the bag, and that is the
84
+ // distinction the hook is really drawing. `a` is a POSE CHANNEL: `rig.js`
85
+ // blends it frame by frame, so a shape can fade with the thing it belongs to
86
+ // (a lash that vanishes as the eye shuts, a seam that vanishes as the mouth
87
+ // opens) and the builder sets it from the control vector on nearly every
88
+ // interesting draw. The four in the bag are CONSTANTS of the draw — a rule, a
89
+ // composite op, a stroke width and a base matrix are copied from base to out
90
+ // and never interpolated — so they are rare, and they read better named.
91
+ //
92
+ // `extra` may be undefined or null; both spread to nothing.
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export const drawPusher = (out) => (slot, group, cmds, paint, a = 1, extra) =>
96
+ out.push({ slot, group, cmds, paint, a, ...extra });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // The control vector. REST is the character's own business — its channel names
100
+ // are one rigger's opinion about what one face can do — but the machinery that
101
+ // clones it and applies a shallow-with-one-level-of-nesting patch is not.
102
+ // ---------------------------------------------------------------------------
103
+
104
+ const clone = (o) => JSON.parse(JSON.stringify(o));
105
+
106
+ // The rest control vector. This started out as the one thing most obviously
107
+ // NOT library — "one rigger's opinion about what one face can do" — and then
108
+ // three independently-styled avatars turned out to hold it byte-for-byte
109
+ // identically, because it is not really an opinion about a face: it is the set
110
+ // of channels the DRIVER's vocabulary needs somebody to implement. Six identity
111
+ // morphs (one per `MORPH_AXES` entry), a jaw and a cheek for the visemes, an
112
+ // eye block for the six eye states, a mouth block for the sixteen visemes.
113
+ //
114
+ // It is a default, not a law: `makeCtrl` takes whatever rest vector it is
115
+ // given, and an avatar that grows a channel adds it to its own copy.
116
+ //
117
+ // The `eye:` and `mouth:` blocks are no longer written out here: they are the
118
+ // rest blocks of `parts/eye.mjs` and `parts/mouth.mjs`, so the channels a part
119
+ // implements and the channels the vector offers are defined in one place and
120
+ // cannot drift. Both are FUNCTIONS rather than consts on purpose — a part
121
+ // imports `clamp`/`lerp`/`drawPusher` back out of this file, and a function
122
+ // declaration is initialised before any module body runs, so the cycle is
123
+ // harmless whichever end of it a process happens to import first.
124
+ export const REST_CONTROLS = {
125
+ headW: 0, eyeSize: 0, eyeSpace: 0, noseW: 0, lipFull: 0, browH: 0,
126
+ // Sex axes. Unlike the six above these are NOT animated and NOT exposed as
127
+ // morph poses: they are baked into the rest vector an avatar is built at
128
+ // (`face.restFor(persona)` -> `poseHarness(builder, rest)`), so a man and a
129
+ // woman of the same family are two builds of one skull rather than two
130
+ // skulls. 0 is the family's neutral (feminine) read; 1 is fully masculine.
131
+ // jawWidth — mandible width at the gonial corner, and how square it turns
132
+ // neckWidth — the neck, its cast shadow, and the collar it comes out of
133
+ jawWidth: 0, neckWidth: 0,
134
+ jaw: 0, cheekRaise: 0,
135
+ eye: eyeRest(),
136
+ mouth: mouthRest(),
137
+ };
138
+
139
+ // Which channel of that vector each `morph/<axis>_±100` pose name drives. The
140
+ // axis names are contract (src/vocab.js MORPH_AXES, straight out of the state
141
+ // machine); the channel names are this rest vector's. The map is the join
142
+ // between them, and it is the reason the morph poses can be enumerated by a
143
+ // library instead of retyped per avatar.
144
+ export const MORPH_CHANNELS = {
145
+ head: 'headW', lips: 'lipFull', nose: 'noseW',
146
+ brows: 'browH', eyes: 'eyeSize', distance: 'eyeSpace',
147
+ };
148
+
149
+ export function makeCtrl(REST) {
150
+ return function ctrl(patch = {}) {
151
+ const c = clone(REST);
152
+ for (const [k, v] of Object.entries(patch)) {
153
+ if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(c[k], v);
154
+ else c[k] = v;
155
+ }
156
+ return c;
157
+ };
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // The pose harness — the single most valuable thing either generator had.
162
+ //
163
+ // A pose is "run the whole builder again with a different control vector and
164
+ // keep whatever moved". Because both runs go through the same code path, the
165
+ // cmds arrays are emitted by the same emitters in the same order, so the opcode
166
+ // sequence is identical by construction — which is the runtime's precondition
167
+ // for interpolating instead of snapping. Nothing else about the format makes
168
+ // that safety free.
169
+ //
170
+ // buildDraws(c) must return an array of { slot, group, cmds, a, paint }.
171
+ // ---------------------------------------------------------------------------
172
+
173
+ function same(a, b) {
174
+ if (a.length !== b.length) return false;
175
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
176
+ return true;
177
+ }
178
+
179
+ export function poseHarness(buildDraws, rest) {
180
+ const BASE = buildDraws(rest);
181
+ const N = BASE.length;
182
+ const IDX = Object.fromEntries(BASE.map((d, i) => [d.slot, i]));
183
+ const group = (g) => BASE.map((d, i) => (d.group === g ? i : -1)).filter((i) => i >= 0);
184
+
185
+ function poseOf(c) {
186
+ const cur = buildDraws(c);
187
+ const geo = {}, alpha = {};
188
+ for (let i = 0; i < N; i++) {
189
+ if (!same(cur[i].cmds, BASE[i].cmds)) geo[i] = cur[i].cmds;
190
+ if (cur[i].a !== BASE[i].a) alpha[i] = cur[i].a;
191
+ }
192
+ const p = {};
193
+ if (Object.keys(geo).length) p.geo = geo;
194
+ if (Object.keys(alpha).length) p.alpha = alpha;
195
+ return p;
196
+ }
197
+
198
+ return { BASE, N, IDX, group, poseOf };
199
+ }
200
+
201
+ // Poses are plain objects keyed by { geo, alpha, paint, mat }; stacking two of
202
+ // them is a per-section Object.assign, not a top-level one.
203
+ export const merge = (...ps) => {
204
+ const o = {};
205
+ for (const p of ps) for (const [k, v] of Object.entries(p)) o[k] = Object.assign(o[k] || {}, v);
206
+ return o;
207
+ };
208
+
209
+ // The missing transform hierarchy. There is no parent/child in the format, so
210
+ // "move this group" means writing the same 6 numbers onto every draw index in
211
+ // it. Each argument is [drawIndices, matrix].
212
+ //
213
+ // A null matrix is skipped, and so is an IDENTITY one: `toRig` gives every draw
214
+ // base matrix [1,0,0,1,0,0], so writing identity onto a group says "these 119
215
+ // draws move by exactly nothing" in 119 entries the runtime still has to
216
+ // prepare, dirty and blend every frame. The rest key of a cyclic clip
217
+ // (`headMat(0, 0, 0, 0)`) is the case that hits it, and it was the whole of
218
+ // every zero-delta warning the checker used to emit. An empty result drops the
219
+ // section entirely, which is what makes a rest key `{}` — the one shape
220
+ // `validate.mjs` calls a legitimate whole-frame rest.
221
+ const isIdentity = (m) => m.length === 6
222
+ && m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
223
+
224
+ export const groupMat = (...pairs) => {
225
+ const mat = {};
226
+ for (const [indices, m] of pairs) {
227
+ if (!m || isIdentity(m)) continue;
228
+ for (const i of indices) mat[i] = m;
229
+ }
230
+ return Object.keys(mat).length ? { mat } : {};
231
+ };
232
+
233
+ // A body translate is the common second half of a head matrix.
234
+ export const translateMat = (ty) => [1, 0, 0, 1, 0, r2(ty)];
235
+
236
+ // ...and this is the whole of the first half. All three avatars had the same
237
+ // six lines: rotate the head group about the neck pivot, translate the body
238
+ // group, skip the body when it does not move. `head` and `body` are draw-index
239
+ // arrays from the harness's `group()`; the returned function is the one every
240
+ // track frame is written in terms of.
241
+ //
242
+ // It is plumbing, not performance: WHICH draws are in the head, where the
243
+ // pivot is, and how many degrees a given key turns are all the avatar's.
244
+ export function headMatFactory({ head, body, pivot }) {
245
+ return (deg, tx = 0, ty = 0, bodyTy = 0) => groupMat(
246
+ [head, rotMat(deg, pivot, tx, ty)],
247
+ [body, bodyTy ? translateMat(bodyTy) : null],
248
+ );
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // The weight-table deformer. "Move every landmark by its share of one control"
253
+ // — a jaw drop, a cheek raise — written once instead of three times.
254
+ //
255
+ // The `L`/`R` rule is the part worth owning: a table is written in UNSUFFIXED
256
+ // landmark names because a weight is a property of the feature, not of which
257
+ // side of the face it is on, so `chin` covers `chinL` and `chinR` at once. The
258
+ // `?? table[n]` fallback then lets a table name one specific point that happens
259
+ // to end in L or R anyway, which is exactly the case one of the three copies
260
+ // had quietly dropped.
261
+ //
262
+ // fn(pt, w, name) mutates the point in place; what it does with the weight is
263
+ // the character's business (jaw drop also narrows, cheek raise only lifts).
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export function applyWeights(pts, table, fn) {
267
+ for (const n of Object.keys(pts)) {
268
+ const w = table[n.replace(/[LR]$/, '')] ?? table[n];
269
+ if (w) fn(pts[n], w, n);
270
+ }
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // The iris base solve.
275
+ //
276
+ // The driver never shows the base iris paint. It holds one or two `hue/NNN`
277
+ // rungs at a combined weight of 1 and layers `iris/eyes-saturation-0` and
278
+ // `iris/eyes-brightness-0` on top, and `rig.js:278-285` blends all of them in
279
+ // RGBA *against the base*:
280
+ //
281
+ // shown = base + Σ wᵢ·(cᵢ − base) => base = (Σ wᵢ·cᵢ − shown) / (Σ wᵢ − 1)
282
+ //
283
+ // At the driver's boot state the weights are 1, 1 − saturation and brightness,
284
+ // so Σ wᵢ = 2.35 and the divisor is 1.35. Which means the base paint an artist
285
+ // writes into the file is NOT a colour anyone picked — it is an answer to an
286
+ // equation whose coefficients live in `drivers.js`. That coupling, not the fact
287
+ // that three files would otherwise hold the same six lines, is why this is
288
+ // library: change the driver's defaults and every avatar's base iris is wrong,
289
+ // silently, and this is the one place that says so.
290
+ //
291
+ // `ladder.hue(h)` is the avatar's own hue ramp; `grey` and `bright` are the two
292
+ // overlay colours it puts in the two iris poses; `target` is the colour the
293
+ // eye should actually be on screen at boot.
294
+ // ---------------------------------------------------------------------------
295
+
296
+ export function solveIrisBase(target, ladder, defaults = DRIVER_DEFAULTS) {
297
+ const H = ladder.hue(defaults.hue), G = ladder.grey, B = ladder.bright;
298
+ const wH = 1, wS = 1 - defaults.saturation, wB = defaults.brightness;
299
+ const div = wH + wS + wB - 1;
300
+ return H.map((_, i) => Math.round((wH * H[i] + wS * G[i] + wB * B[i] - target[i]) / div));
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // The camera.
305
+ //
306
+ // `render2d.js:60-68` computes, once per resize:
307
+ //
308
+ // global = fit(meta.artboard onto the canvas) · inverse(meta.align)
309
+ //
310
+ // and `invSimilarity` inverts align as a similarity, so an align of
311
+ // [1/k, 0, 0, 1/k, cx, cy] takes a design-space point p to (p − c)·k. Set
312
+ // meta.artboard to the OUTPUT FRAME rather than to the space the art was drawn
313
+ // in and the pair becomes a camera: "crop this rectangle of design space and
314
+ // blow it up to fill this frame". Nothing else in the file moves — not a draw,
315
+ // not a pose, not a stroke width, not a bitmap. It is metadata, and it is the
316
+ // only kind of framing change that cannot introduce a fidelity bug.
317
+ //
318
+ // A camera is three numbers plus the frame:
319
+ //
320
+ // camera: { frame: { w, h }, window: { cx, y, h } }
321
+ //
322
+ // where `frame` is the output raster's aspect (its absolute size only sets the
323
+ // units the player fits to) and `window` is the crop in DESIGN space: where it
324
+ // is centred horizontally, where its top edge is, how tall it is. The crop's
325
+ // WIDTH is derived from the frame's aspect and is not an input, because the one
326
+ // way to get this wrong is to write a window whose aspect disagrees with the
327
+ // frame's — the renderer would then letterbox inside the crop and every number
328
+ // the author computed would be off by that ratio. Three numbers cannot say it.
329
+ //
330
+ // Vertical-first is also how the framing is actually reasoned about: the crop
331
+ // is chosen by how much of frame height belongs above, across and below the
332
+ // head. All three measurements are y.
333
+ // ---------------------------------------------------------------------------
334
+
335
+ const round6 = (v) => Math.round(v * 1e6) / 1e6;
336
+
337
+ export function cameraMeta({ frame, window: win }) {
338
+ const bad = (m) => { throw new Error(`camera: ${m}`); };
339
+ for (const [o, ks, n] of [[frame, ['w', 'h'], 'frame'], [win, ['cx', 'y', 'h'], 'window']]) {
340
+ if (!o) bad(`${n} is required`);
341
+ for (const k of ks) if (!Number.isFinite(o[k])) bad(`${n}.${k} must be a finite number, got ${o[k]}`);
342
+ }
343
+ if (frame.w <= 0 || frame.h <= 0) bad(`frame must be positive, got ${frame.w}x${frame.h}`);
344
+ if (win.h <= 0) bad(`window.h must be positive, got ${win.h}`);
345
+ const w = win.h * (frame.w / frame.h); // the crop's width, never an input
346
+ const k = frame.h / win.h; // design units -> frame pixels
347
+ return {
348
+ artboard: { w: frame.w, h: frame.h },
349
+ align: [round6(1 / k), 0, 0, round6(1 / k), round6(win.cx - w / 2), round6(win.y)],
350
+ window: { x: round6(win.cx - w / 2), y: round6(win.y), w: round6(w), h: round6(win.h), k: round6(k) },
351
+ };
352
+ }
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // Serialiser. Three of the four contract defaults below are load-bearing and
356
+ // none of them is checked by the runtime, which is the entire argument for a
357
+ // writer: clipsets[0] is the "no mask" index every draw points at, images must
358
+ // be present because loadRig maps over it unconditionally, and meta.align is
359
+ // divided out rather than applied, so a hand-authored rig with no camera wants
360
+ // identity and artboard-space coordinates.
361
+ // ---------------------------------------------------------------------------
362
+
363
+ export function toRig({ artboard, paints, draws, poses, tracks, camera, images = [] }) {
364
+ // A bitmap draw is a different shape from a path one, not a path with extra
365
+ // fields: `render2d.js:141` reads `src`, `m`, `a`, `blend` and `clip` and
366
+ // nothing else, and a `cmds` on it would never be drawn. `images` is the
367
+ // table `loadRig` maps over to fetch `img/<file>` next to the rig JSON, so an
368
+ // entry's `id` is its own index and its `file` is a bare filename. Both are
369
+ // empty for a vector-only avatar, which is what keeps those rigs' bytes.
370
+ const d = draws.map((x) => (x.k === 'bitmap' ? {
371
+ k: 'bitmap', slot: x.slot, m: x.m, a: x.a,
372
+ blend: x.blend || 'source-over', clip: x.clip || 0, src: x.src, w: x.w, h: x.h,
373
+ } : {
374
+ // Four draw-level fields the two flat-fill avatars never set, so all four
375
+ // were hard-coded. They are contract, not style: `m` is the draw's own
376
+ // matrix (a pose's `mat` section overwrites it, so a non-identity base
377
+ // matrix is how a shape sits somewhere its points do not), `blend` is the
378
+ // canvas composite op (`render2d.js:124` reads `d.blend || 'source-over'`,
379
+ // so 'multiply' is a legal shadow), and `rule` picks the fill rule
380
+ // (`render2d.js:137`, `ctx.fill(path, d.rule)`) — the only way to say
381
+ // "this contour is a hole". Each is passed through when a builder supplies
382
+ // one and is byte-identical to the old constant when it does not.
383
+ k: 'path', slot: x.slot, m: x.m || [1, 0, 0, 1, 0, 0], a: x.a,
384
+ blend: x.blend || 'source-over', clip: 0, cmds: x.cmds, rule: x.rule || 'nonzero',
385
+ // `stroke` is the one draw-level field the two flat-fill avatars never set,
386
+ // so it was hard-coded to null. It is not style-agnostic to omit it:
387
+ // `render2d.js` reads `if (d.stroke) { ...ctx.stroke() } else { ctx.fill() }`,
388
+ // i.e. a draw is EITHER stroked or filled, and an outlined style needs the
389
+ // stroked half. Passed straight through when a builder supplies one,
390
+ // `{ w, cap, join }`; still exactly `null` when it does not, which is what
391
+ // keeps facet's and round's bytes identical. Width is deliberately not a
392
+ // pose channel — `rig.js` copies `stroke` from base to out and never blends
393
+ // it, and there is no `stroke` section in a pose.
394
+ stroke: x.stroke || null, paint: x.paint,
395
+ }));
396
+ // No camera: the frame IS the artboard and align is identity, which is what
397
+ // every rig here emitted before cameras existed and what keeps those bytes.
398
+ const cam = camera ? cameraMeta(camera) : null;
399
+ return {
400
+ meta: cam
401
+ ? { artboard: cam.artboard, align: cam.align, drawCount: d.length }
402
+ : { artboard, align: [1, 0, 0, 1, 0, 0], drawCount: d.length },
403
+ paints, clipSlots: [], clipsets: [[]], draws: d, images, poses, tracks,
404
+ };
405
+ }
@@ -0,0 +1,3 @@
1
+ export const P: Readonly<Record<string, unknown>>;
2
+ export function makeSpec(persona?: Readonly<Record<string, unknown>>): unknown;
3
+ export function buildDraws(...args: unknown[]): unknown[];