@voqalize/avatar 0.4.2 → 0.4.4

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 (191) hide show
  1. package/LICENSE-APACHE-2.0 +201 -0
  2. package/README.md +11 -98
  3. package/assets/README.md +28 -17
  4. package/assets/tanvi.glb +0 -0
  5. package/assets/tanya.glb +0 -0
  6. package/assets/tara.glb +0 -0
  7. package/assets/tess.glb +0 -0
  8. package/assets/tushar.glb +0 -0
  9. package/client/internal.ts +4 -0
  10. package/client/supports.ts +6 -7
  11. package/client/three/assets.ts +2 -0
  12. package/client/three/budgets.ts +1 -1
  13. package/client/three/{tara-rig.ts → character-rig.ts} +708 -547
  14. package/client/three/createCharacter.ts +109 -0
  15. package/client/three/holds.ts +1 -1
  16. package/client/three/internal.ts +4 -3
  17. package/client/three/motion-limits.json +5 -1
  18. package/client/three/tanvi-asset.ts +6 -0
  19. package/client/three/tanvi.ts +30 -0
  20. package/client/three/tanya.ts +18 -58
  21. package/client/three/tara.ts +19 -60
  22. package/client/three/tess.ts +17 -59
  23. package/client/three/tushar.ts +18 -55
  24. package/dist/internal.d.ts +1 -1
  25. package/dist/internal.d.ts.map +1 -1
  26. package/dist/internal.js +5 -1
  27. package/dist/internal.js.map +1 -1
  28. package/dist/supports.d.ts +6 -7
  29. package/dist/supports.d.ts.map +1 -1
  30. package/dist/supports.js +6 -7
  31. package/dist/supports.js.map +1 -1
  32. package/dist/three/assets.d.ts +1 -0
  33. package/dist/three/assets.d.ts.map +1 -1
  34. package/dist/three/assets.js +2 -0
  35. package/dist/three/assets.js.map +1 -1
  36. package/dist/three/budgets.d.ts +1 -1
  37. package/dist/three/budgets.js +1 -1
  38. package/dist/three/character-rig.d.ts +366 -0
  39. package/dist/three/character-rig.d.ts.map +1 -0
  40. package/dist/three/{tara-rig.js → character-rig.js} +664 -525
  41. package/dist/three/character-rig.js.map +1 -0
  42. package/dist/three/createCharacter.d.ts +60 -0
  43. package/dist/three/createCharacter.d.ts.map +1 -0
  44. package/dist/three/createCharacter.js +84 -0
  45. package/dist/three/createCharacter.js.map +1 -0
  46. package/dist/three/holds.js +1 -1
  47. package/dist/three/holds.js.map +1 -1
  48. package/dist/three/internal.d.ts +3 -3
  49. package/dist/three/internal.d.ts.map +1 -1
  50. package/dist/three/internal.js +2 -2
  51. package/dist/three/internal.js.map +1 -1
  52. package/dist/three/motion-limits.json +5 -1
  53. package/dist/three/tanvi-asset.d.ts +7 -0
  54. package/dist/three/tanvi-asset.d.ts.map +1 -0
  55. package/dist/three/tanvi-asset.js +7 -0
  56. package/dist/three/tanvi-asset.js.map +1 -0
  57. package/dist/three/tanvi.d.ts +24 -0
  58. package/dist/three/tanvi.d.ts.map +1 -0
  59. package/dist/three/tanvi.js +24 -0
  60. package/dist/three/tanvi.js.map +1 -0
  61. package/dist/three/tanya.d.ts +16 -28
  62. package/dist/three/tanya.d.ts.map +1 -1
  63. package/dist/three/tanya.js +15 -43
  64. package/dist/three/tanya.js.map +1 -1
  65. package/dist/three/tara.d.ts +17 -22
  66. package/dist/three/tara.d.ts.map +1 -1
  67. package/dist/three/tara.js +16 -45
  68. package/dist/three/tara.js.map +1 -1
  69. package/dist/three/tess.d.ts +15 -29
  70. package/dist/three/tess.d.ts.map +1 -1
  71. package/dist/three/tess.js +14 -44
  72. package/dist/three/tess.js.map +1 -1
  73. package/dist/three/tushar.d.ts +16 -25
  74. package/dist/three/tushar.d.ts.map +1 -1
  75. package/dist/three/tushar.js +15 -40
  76. package/dist/three/tushar.js.map +1 -1
  77. package/package.json +7 -50
  78. package/src/avatar.d.ts +2 -0
  79. package/src/avatar.js +99 -220
  80. package/src/gaze.js +1 -3
  81. package/src/idle.js +10 -2
  82. package/src/prosody.js +7 -3
  83. package/src/speech-timing.js +28 -0
  84. package/src/visemes.js +107 -13
  85. package/client/arjun.ts +0 -26
  86. package/client/createCanvasAvatar.ts +0 -72
  87. package/client/interviewer-female.ts +0 -4
  88. package/client/interviewer-male.ts +0 -4
  89. package/client/ishita.ts +0 -26
  90. package/client/kabir.ts +0 -26
  91. package/client/meera.ts +0 -26
  92. package/client/naina.ts +0 -26
  93. package/client/professional-female-a.ts +0 -4
  94. package/client/professional-female-b.ts +0 -4
  95. package/client/professional-male-a.ts +0 -4
  96. package/client/professional-male-b.ts +0 -4
  97. package/client/vikram.ts +0 -26
  98. package/dist/arjun.d.ts +0 -7
  99. package/dist/arjun.d.ts.map +0 -1
  100. package/dist/arjun.js +0 -20
  101. package/dist/arjun.js.map +0 -1
  102. package/dist/createCanvasAvatar.d.ts +0 -22
  103. package/dist/createCanvasAvatar.d.ts.map +0 -1
  104. package/dist/createCanvasAvatar.js +0 -47
  105. package/dist/createCanvasAvatar.js.map +0 -1
  106. package/dist/interviewer-female.d.ts +0 -4
  107. package/dist/interviewer-female.d.ts.map +0 -1
  108. package/dist/interviewer-female.js +0 -3
  109. package/dist/interviewer-female.js.map +0 -1
  110. package/dist/interviewer-male.d.ts +0 -4
  111. package/dist/interviewer-male.d.ts.map +0 -1
  112. package/dist/interviewer-male.js +0 -3
  113. package/dist/interviewer-male.js.map +0 -1
  114. package/dist/ishita.d.ts +0 -7
  115. package/dist/ishita.d.ts.map +0 -1
  116. package/dist/ishita.js +0 -20
  117. package/dist/ishita.js.map +0 -1
  118. package/dist/kabir.d.ts +0 -7
  119. package/dist/kabir.d.ts.map +0 -1
  120. package/dist/kabir.js +0 -20
  121. package/dist/kabir.js.map +0 -1
  122. package/dist/meera.d.ts +0 -7
  123. package/dist/meera.d.ts.map +0 -1
  124. package/dist/meera.js +0 -20
  125. package/dist/meera.js.map +0 -1
  126. package/dist/naina.d.ts +0 -7
  127. package/dist/naina.d.ts.map +0 -1
  128. package/dist/naina.js +0 -20
  129. package/dist/naina.js.map +0 -1
  130. package/dist/professional-female-a.d.ts +0 -4
  131. package/dist/professional-female-a.d.ts.map +0 -1
  132. package/dist/professional-female-a.js +0 -3
  133. package/dist/professional-female-a.js.map +0 -1
  134. package/dist/professional-female-b.d.ts +0 -4
  135. package/dist/professional-female-b.d.ts.map +0 -1
  136. package/dist/professional-female-b.js +0 -3
  137. package/dist/professional-female-b.js.map +0 -1
  138. package/dist/professional-male-a.d.ts +0 -4
  139. package/dist/professional-male-a.d.ts.map +0 -1
  140. package/dist/professional-male-a.js +0 -3
  141. package/dist/professional-male-a.js.map +0 -1
  142. package/dist/professional-male-b.d.ts +0 -4
  143. package/dist/professional-male-b.d.ts.map +0 -1
  144. package/dist/professional-male-b.js +0 -3
  145. package/dist/professional-male-b.js.map +0 -1
  146. package/dist/three/tara-rig.d.ts +0 -423
  147. package/dist/three/tara-rig.d.ts.map +0 -1
  148. package/dist/three/tara-rig.js.map +0 -1
  149. package/dist/vikram.d.ts +0 -7
  150. package/dist/vikram.d.ts.map +0 -1
  151. package/dist/vikram.js +0 -20
  152. package/dist/vikram.js.map +0 -1
  153. package/src/canvas/author/parts/eye.mjs +0 -722
  154. package/src/canvas/author/parts/hand.mjs +0 -1156
  155. package/src/canvas/author/parts/mouth.mjs +0 -741
  156. package/src/canvas/author/parts/nose.mjs +0 -100
  157. package/src/canvas/author/parts/skin-detail.mjs +0 -67
  158. package/src/canvas/author/path.mjs +0 -283
  159. package/src/canvas/author/rig.mjs +0 -405
  160. package/src/canvas/avatars/round/face.d.mts +0 -3
  161. package/src/canvas/avatars/round/face.mjs +0 -1307
  162. package/src/canvas/create-rig.d.ts +0 -15
  163. package/src/canvas/create-rig.js +0 -100
  164. package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
  165. package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
  166. package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
  167. package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
  168. package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
  169. package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
  170. package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
  171. package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
  172. package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
  173. package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
  174. package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
  175. package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
  176. package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
  177. package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
  178. package/src/canvas/data/img/round-m3-top-body.webp +0 -0
  179. package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
  180. package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
  181. package/src/canvas/data/img/round-w1-top-body.webp +0 -0
  182. package/src/canvas/data/interviewer-female.rig.json +0 -1
  183. package/src/canvas/data/interviewer-male.rig.json +0 -1
  184. package/src/canvas/data/professional-female-a.rig.json +0 -1
  185. package/src/canvas/data/professional-female-b.rig.json +0 -1
  186. package/src/canvas/data/professional-male-a.rig.json +0 -1
  187. package/src/canvas/data/professional-male-b.rig.json +0 -1
  188. package/src/canvas/src/live.js +0 -508
  189. package/src/canvas/src/render2d.js +0 -218
  190. package/src/canvas/src/rig.js +0 -297
  191. package/src/canvas/src/vocab.js +0 -96
@@ -1,218 +0,0 @@
1
- // Canvas2D renderer for the blended display list.
2
- //
3
- // Rive's own canvas runtime paints vector art with Path2D and warps textured
4
- // meshes separately; this does the same. Path2D objects are cached per draw and
5
- // only rebuilt when that draw's geometry actually moved this frame, which keeps
6
- // a viseme change to ~30 rebuilds out of 813 paths.
7
-
8
- const mul = (A, B) => [
9
- A[0] * B[0] + A[2] * B[1],
10
- A[1] * B[0] + A[3] * B[1],
11
- A[0] * B[2] + A[2] * B[3],
12
- A[1] * B[2] + A[3] * B[3],
13
- A[0] * B[4] + A[2] * B[5] + A[4],
14
- A[1] * B[4] + A[3] * B[5] + A[5],
15
- ];
16
-
17
- const invSimilarity = ([s, , , , tx, ty]) => [1 / s, 0, 0, 1 / s, -tx / s, -ty / s];
18
-
19
- export function buildPath(cmds) {
20
- const p = new Path2D();
21
- for (let i = 0; i < cmds.length;) {
22
- const c = cmds[i];
23
- if (c === 0) { p.moveTo(cmds[i + 1], cmds[i + 2]); i += 3; }
24
- else if (c === 1) { p.lineTo(cmds[i + 1], cmds[i + 2]); i += 3; }
25
- else if (c === 2) { p.bezierCurveTo(cmds[i + 1], cmds[i + 2], cmds[i + 3], cmds[i + 4], cmds[i + 5], cmds[i + 6]); i += 7; }
26
- else if (c === 3) { p.closePath(); i += 1; }
27
- else if (c === -1) {
28
- // A sub-path placed under its own matrix; Rive emits these for shapes
29
- // whose contours live in a different space than the shape itself.
30
- const n = cmds[i + 7];
31
- const sub = buildPath(cmds.subarray(i + 8, i + 8 + n));
32
- p.addPath(sub, new DOMMatrix([cmds[i + 1], cmds[i + 2], cmds[i + 3], cmds[i + 4], cmds[i + 5], cmds[i + 6]]));
33
- i += 8 + n;
34
- } else break;
35
- }
36
- return p;
37
- }
38
-
39
- const cssOf = (c) => `rgba(${c[0]},${c[1]},${c[2]},${c[3]})`;
40
-
41
- export class Renderer2D {
42
- constructor(canvas, rig) {
43
- this.canvas = canvas;
44
- this.rig = rig;
45
- this.ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
46
- this.unalign = invSimilarity(rig.meta.align || [1, 0, 0, 1, 0, 0]);
47
- this.cache = rig.out.map(() => ({ gen: -1, path: null }));
48
- this.clipCache = new Map();
49
- this.gradCache = new Map();
50
- this.global = [1, 0, 0, 1, 0, 0];
51
- // Only 11 of the 824 draws are image-backed -- the hair, brows, suit,
52
- // glasses and a freckle texture. 'off' drops them so what is left is the
53
- // pure vector rig; 'ghost' leaves a flat silhouette in their place so you
54
- // can see the holes they would fill.
55
- this.bitmaps = 'on'; // on | off | ghost
56
- this.stats = { paths: 0, rebuilt: 0, tris: 0, images: 0 };
57
- this.resize();
58
- }
59
-
60
- resize(dpr = window.devicePixelRatio || 1) {
61
- const { clientWidth: w, clientHeight: h } = this.canvas;
62
- this.canvas.width = Math.max(1, Math.round(w * dpr));
63
- this.canvas.height = Math.max(1, Math.round(h * dpr));
64
- const { w: aw, h: ah } = this.rig.meta.artboard;
65
- const s = Math.min(this.canvas.width / aw, this.canvas.height / ah);
66
- // contain + centre, then divide out the fit transform the extractor baked in
67
- this.global = mul([s, 0, 0, s, (this.canvas.width - aw * s) / 2, (this.canvas.height - ah * s) / 2], this.unalign);
68
- this.scale = s;
69
- }
70
-
71
- _path(d) {
72
- const c = this.cache[d.i];
73
- if (c.gen !== d.gen || !c.path) { c.path = buildPath(d.cmds); c.gen = d.gen; this.stats.rebuilt++; }
74
- return c.path;
75
- }
76
-
77
- // Masks deform along with the body, so their Path2D is cached on the same
78
- // gen counter the draws use rather than built once and kept forever.
79
- _clipPath(c) {
80
- let e = this.clipCache.get(c.i);
81
- if (!e || e.gen !== c.gen) { e = { gen: c.gen, path: buildPath(c.c) }; this.clipCache.set(c.i, e); }
82
- return e.path;
83
- }
84
-
85
- _paint(d) {
86
- if (d.colour) return d.colour;
87
- const p = this.rig.paints[d.paint];
88
- if (p.t === 'solid') return cssOf(p.c);
89
- let g = this.gradCache.get(d.paint);
90
- if (!g) {
91
- const ctx = this.ctx;
92
- const [x0, y0, x1, y1] = p.p;
93
- g = p.t === 'linear'
94
- ? ctx.createLinearGradient(x0, y0, x1, y1)
95
- : ctx.createRadialGradient(x0, y0, 0, x0, y0, Math.hypot(x1 - x0, y1 - y0));
96
- for (const [o, c] of p.stops) g.addColorStop(Math.min(1, Math.max(0, o)), cssOf(c));
97
- this.gradCache.set(d.paint, g);
98
- }
99
- return g;
100
- }
101
-
102
- draw() {
103
- const ctx = this.ctx, rig = this.rig, G = this.global;
104
- this.stats.paths = 0; this.stats.rebuilt = 0; this.stats.tris = 0; this.stats.images = 0;
105
- ctx.setTransform(1, 0, 0, 1, 0, 0);
106
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
107
-
108
- for (const d of rig.out) {
109
- if (!(d.a > 0.003)) continue;
110
- const set = rig.clipsets[d.clip];
111
- const clipped = set && set.length;
112
- if (clipped) {
113
- ctx.save();
114
- for (const id of set) {
115
- const c = rig.clipOut[id];
116
- const m = mul(G, c.m);
117
- ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
118
- ctx.clip(this._clipPath(c), c.r || 'nonzero');
119
- }
120
- }
121
- const m = mul(G, d.m);
122
- ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
123
- ctx.globalAlpha = d.a > 1 ? 1 : d.a;
124
- ctx.globalCompositeOperation = d.blend || 'source-over';
125
-
126
- if (d.k === 'path') {
127
- this.stats.paths++;
128
- const path = this._path(d);
129
- if (d.stroke) {
130
- ctx.strokeStyle = this._paint(d);
131
- ctx.lineWidth = d.stroke.w;
132
- ctx.lineCap = d.stroke.cap;
133
- ctx.lineJoin = d.stroke.join;
134
- ctx.stroke(path);
135
- } else {
136
- ctx.fillStyle = this._paint(d);
137
- ctx.fill(path, d.rule || 'nonzero');
138
- }
139
- } else if (this.bitmaps === 'off') {
140
- // nothing to draw: the image is what this slot is made of
141
- } else if (this.bitmaps === 'ghost') {
142
- this.stats.images++;
143
- this._ghost(ctx, d);
144
- } else if (d.k === 'bitmap') {
145
- this.stats.images++;
146
- ctx.drawImage(rig.images[d.src], 0, 0);
147
- } else if (d.k === 'mesh') {
148
- this.stats.images++;
149
- this._mesh(ctx, d, m);
150
- }
151
-
152
- if (clipped) ctx.restore();
153
- }
154
- ctx.setTransform(1, 0, 0, 1, 0, 0);
155
- ctx.globalAlpha = 1;
156
- ctx.globalCompositeOperation = 'source-over';
157
- }
158
-
159
- // The footprint an image-backed draw occupies, as a flat fill: the mesh's own
160
- // triangles, or the image's rect for a plain blit. Useful for seeing exactly
161
- // which parts of the character a new persona would have to supply art for.
162
- _ghost(ctx, d) {
163
- ctx.globalAlpha = 0.4;
164
- ctx.fillStyle = '#7c5cff';
165
- ctx.beginPath();
166
- if (d.k === 'mesh') {
167
- const V = d.verts, I = d.idx;
168
- for (let t = 0; t < I.length; t += 3) {
169
- const a = I[t] * 2, b = I[t + 1] * 2, c = I[t + 2] * 2;
170
- ctx.moveTo(V[a], V[a + 1]); ctx.lineTo(V[b], V[b + 1]); ctx.lineTo(V[c], V[c + 1]);
171
- ctx.closePath();
172
- }
173
- } else ctx.rect(0, 0, d.w, d.h);
174
- ctx.fill();
175
- }
176
-
177
- // Deformable image mesh: for each triangle, solve the affine that carries its
178
- // UV triangle onto its deformed vertex triangle, clip to the triangle, and
179
- // blit the whole texture through it. Triangles are nudged outward from their
180
- // centroid by half a device pixel so shared edges do not show seams.
181
- _mesh(ctx, d, m) {
182
- const img = this.rig.images[d.src];
183
- if (!img) return;
184
- const V = d.verts, U = d.uvs, I = d.idx, W = img.width, H = img.height;
185
- const grow = 0.5 / (Math.hypot(m[0], m[1]) || 1);
186
- for (let t = 0; t < I.length; t += 3) {
187
- const i0 = I[t] * 2, i1 = I[t + 1] * 2, i2 = I[t + 2] * 2;
188
- let x0 = V[i0], y0 = V[i0 + 1], x1 = V[i1], y1 = V[i1 + 1], x2 = V[i2], y2 = V[i2 + 1];
189
- const s0x = U[i0] * W, s0y = U[i0 + 1] * H;
190
- const s1x = U[i1] * W, s1y = U[i1 + 1] * H;
191
- const s2x = U[i2] * W, s2y = U[i2 + 1] * H;
192
- const det = (s1x - s0x) * (s2y - s0y) - (s2x - s0x) * (s1y - s0y);
193
- if (!det) continue;
194
- const a = ((x1 - x0) * (s2y - s0y) - (x2 - x0) * (s1y - s0y)) / det;
195
- const c = ((x2 - x0) * (s1x - s0x) - (x1 - x0) * (s2x - s0x)) / det;
196
- const b = ((y1 - y0) * (s2y - s0y) - (y2 - y0) * (s1y - s0y)) / det;
197
- const dd = ((y2 - y0) * (s1x - s0x) - (y1 - y0) * (s2x - s0x)) / det;
198
- const e = x0 - a * s0x - c * s0y;
199
- const f = y0 - b * s0x - dd * s0y;
200
-
201
- const cx = (x0 + x1 + x2) / 3, cy = (y0 + y1 + y2) / 3;
202
- const push = (x, y) => {
203
- const dx = x - cx, dy = y - cy, l = Math.hypot(dx, dy) || 1;
204
- return [x + (dx / l) * grow, y + (dy / l) * grow];
205
- };
206
- [x0, y0] = push(x0, y0); [x1, y1] = push(x1, y1); [x2, y2] = push(x2, y2);
207
-
208
- ctx.save();
209
- ctx.beginPath();
210
- ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.closePath();
211
- ctx.clip();
212
- ctx.transform(a, b, c, dd, e, f);
213
- ctx.drawImage(img, 0, 0);
214
- ctx.restore();
215
- this.stats.tris++;
216
- }
217
- }
218
- }
@@ -1,297 +0,0 @@
1
- // The rig: a base display list plus a library of poses stored as differences
2
- // from it. Blending is additive — v = base + Σ wᵢ·(poseᵢ − base) — so two
3
- // weights of 0.5 give a true half-way blend regardless of the order they are
4
- // listed in. That is the one place we deliberately differ from Rive, which
5
- // lerps sequentially and is therefore order-dependent.
6
-
7
- const EPS = 1e-4;
8
-
9
- // Deltas arrive keyed by draw index. Flatten them into tuple lists once, and
10
- // record the union of touched indices so a frame only rebuilds what moved.
11
- function prepare(delta) {
12
- const touch = new Set();
13
- const pairs = (src, cast) => {
14
- if (!src) return null;
15
- const out = [];
16
- for (const k of Object.keys(src)) { const i = +k; touch.add(i); out.push([i, cast(src[k])]); }
17
- return out.length ? out : null;
18
- };
19
- const F = (a) => Float32Array.from(a);
20
- const id = (v) => v;
21
- const p = {
22
- mat: pairs(delta.mat, F),
23
- geo: pairs(delta.geo, F),
24
- verts: pairs(delta.verts, F),
25
- alpha: pairs(delta.alpha, id),
26
- paint: pairs(delta.paint, id),
27
- clip: pairs(delta.clip, id),
28
- src: pairs(delta.src, id),
29
- hide: null,
30
- };
31
- if (delta.hide && delta.hide.length) {
32
- p.hide = delta.hide.slice();
33
- for (const i of p.hide) touch.add(i);
34
- }
35
- p.touch = Int32Array.from(touch);
36
-
37
- // Masks are indexed by clip slot rather than draw index, so they need their
38
- // own dirty set. A mask moving is the common case -- it rides the body -- and
39
- // it must interpolate, not snap, or its edge pops while the art glides.
40
- const ctouch = new Set();
41
- const cpairs = (src, cast) => {
42
- if (!src) return null;
43
- const out = [];
44
- for (const k of Object.keys(src)) { const i = +k; ctouch.add(i); out.push([i, cast(src[k])]); }
45
- return out.length ? out : null;
46
- };
47
- p.cmat = cpairs(delta.cmat, F);
48
- p.cgeo = cpairs(delta.cgeo, F);
49
- p.ctouch = Int32Array.from(ctouch);
50
- return p;
51
- }
52
-
53
- export class Rig {
54
- constructor(data, images) {
55
- this.data = data;
56
- this.images = images;
57
- this.meta = data.meta;
58
- this.paints = data.paints;
59
- this.clipsets = data.clipsets; // arrays of clip-slot ids
60
-
61
- // Live mask state, mirroring the draw list: base kept for restore, an out
62
- // buffer that poses blend into, and a gen counter the renderer caches on.
63
- this.clipBase = data.clipSlots.map((c) => ({
64
- m: Float32Array.from(c.m), c: Float32Array.from(c.c), r: c.r,
65
- }));
66
- this.clipOut = this.clipBase.map((b, i) => ({
67
- i, r: b.r, m: Float32Array.from(b.m), c: Float32Array.from(b.c), gen: 0,
68
- }));
69
- this.clipOut.forEach((o) => { o._c = o.c; });
70
-
71
- this.base = data.draws.map((d) => ({
72
- k: d.k, m: Float32Array.from(d.m), a: d.a, blend: d.blend, clip: d.clip,
73
- rule: d.rule, stroke: d.stroke, paint: d.paint, src: d.src, w: d.w, h: d.h,
74
- cmds: d.cmds ? Float32Array.from(d.cmds) : null,
75
- verts: d.verts ? Float32Array.from(d.verts) : null,
76
- uvs: d.uvs ? Float32Array.from(d.uvs) : null,
77
- idx: d.idx ? Uint16Array.from(d.idx) : null,
78
- }));
79
-
80
- // Live display list. cmds/verts get their own scratch buffers so a pose
81
- // that has to snap (topology change) can swap its array in and back out.
82
- this.out = this.base.map((b, i) => {
83
- const o = {
84
- i, k: b.k, blend: b.blend, rule: b.rule, stroke: b.stroke,
85
- src: b.src, w: b.w, h: b.h, uvs: b.uvs, idx: b.idx,
86
- m: Float32Array.from(b.m), a: b.a, clip: b.clip, paint: b.paint,
87
- colour: null, // set when paint blending produced a plain colour
88
- gen: 0,
89
- };
90
- o._cmds = b.cmds ? Float32Array.from(b.cmds) : null;
91
- o._verts = b.verts ? Float32Array.from(b.verts) : null;
92
- o.cmds = o._cmds;
93
- o.verts = o._verts;
94
- o._geoDirty = false;
95
- return o;
96
- });
97
-
98
- this.poses = {};
99
- for (const [name, d] of Object.entries(data.poses)) this.poses[name] = prepare(d);
100
-
101
- // A sampled timeline is just an ordered set of poses; playing it is a
102
- // two-weight blend between the frames either side of the cursor. Reusing
103
- // the pose machinery means ambient motion costs nothing extra.
104
- this.tracks = {};
105
- for (const [name, t] of Object.entries(data.tracks || {})) {
106
- const keys = t.frames.map((f, j) => {
107
- const key = `@${name}/${j}`;
108
- this.poses[key] = prepare(f);
109
- return key;
110
- });
111
- // `at` holds each key's normalised position in the clip. Keys are placed
112
- // where the motion is, so they are not evenly spaced and playback has to
113
- // search rather than divide.
114
- this.tracks[name] = { period: t.period, loop: t.loop, at: t.at, keys };
115
- }
116
-
117
- this.dirty = new Set();
118
- this.cdirty = new Set();
119
- this.frame = 0;
120
- this._acc = new Map(); // draw index -> paint accumulator, reused each frame
121
- }
122
-
123
- restore(i) {
124
- const o = this.out[i], b = this.base[i];
125
- o.m.set(b.m);
126
- o.a = b.a;
127
- o.clip = b.clip;
128
- o.paint = b.paint;
129
- o.src = b.src;
130
- o.colour = null;
131
- // Only touch geometry if a pose actually moved it last frame. Ambient
132
- // motion is almost pure matrix work, so this keeps the renderer's Path2D
133
- // cache warm for ~800 of the 813 paths.
134
- if (o._geoDirty) {
135
- if (b.cmds) { o.cmds = o._cmds; o.cmds.set(b.cmds); }
136
- if (b.verts) { o.verts = o._verts; o.verts.set(b.verts); }
137
- o.gen = this.frame;
138
- o._geoDirty = false;
139
- }
140
- }
141
-
142
- restoreClip(i) {
143
- const o = this.clipOut[i], b = this.clipBase[i];
144
- o.m.set(b.m);
145
- o.c = o._c;
146
- o.c.set(b.c);
147
- o.gen = this.frame;
148
- }
149
-
150
- // weights: { poseName: 0..1 }. Anything absent is zero.
151
- evaluate(weights) {
152
- this.frame++;
153
- const out = this.out, base = this.base;
154
-
155
- for (const i of this.dirty) this.restore(i);
156
- this.dirty.clear();
157
- for (const i of this.cdirty) this.restoreClip(i);
158
- this.cdirty.clear();
159
-
160
- const act = [];
161
- for (const name of Object.keys(weights)) {
162
- const w = weights[name];
163
- if (!(w > EPS || w < -EPS)) continue; // spline weights may go negative
164
- const p = this.poses[name];
165
- if (!p) continue;
166
- act.push(p, w);
167
- const t = p.touch;
168
- for (let j = 0; j < t.length; j++) this.dirty.add(t[j]);
169
- const ct = p.ctouch;
170
- for (let j = 0; j < ct.length; j++) this.cdirty.add(ct[j]);
171
- }
172
-
173
- const acc = this._acc;
174
- acc.clear();
175
- const slot = (i) => {
176
- let s = acc.get(i);
177
- if (!s) { s = { paint: -1, pw: 0, clip: -1, cw: 0, geoSnap: null, gw: 0, src: -1, sw: 0 }; acc.set(i, s); }
178
- return s;
179
- };
180
- const cacc = new Map();
181
- const cslot = (i) => {
182
- let s = cacc.get(i);
183
- if (!s) { s = { snap: null, gw: 0 }; cacc.set(i, s); }
184
- return s;
185
- };
186
-
187
- for (let a = 0; a < act.length; a += 2) {
188
- const p = act[a], w = act[a + 1];
189
-
190
- if (p.mat) for (const [i, v] of p.mat) {
191
- const o = out[i].m, b = base[i].m;
192
- for (let j = 0; j < 6; j++) o[j] += w * (v[j] - b[j]);
193
- }
194
-
195
- if (p.geo) for (const [i, v] of p.geo) {
196
- const d = out[i], b = base[i].cmds;
197
- d.gen = this.frame; d._geoDirty = true;
198
- if (v.length !== b.length) {
199
- // Topology changed, so there is nothing to interpolate between.
200
- // Snap to whichever competing pose is pulling hardest.
201
- const s = slot(i);
202
- if (w > s.gw) { s.gw = w; s.geoSnap = v; }
203
- } else {
204
- const o = d.cmds === d._cmds ? d.cmds : d._cmds;
205
- for (let j = 0; j < v.length; j++) o[j] += w * (v[j] - b[j]);
206
- }
207
- }
208
-
209
- if (p.verts) for (const [i, v] of p.verts) {
210
- const d = out[i], b = base[i].verts, o = d.verts;
211
- d.gen = this.frame; d._geoDirty = true;
212
- for (let j = 0; j < v.length; j++) o[j] += w * (v[j] - b[j]);
213
- }
214
-
215
- if (p.alpha) for (const [i, v] of p.alpha) out[i].a += w * (v - base[i].a);
216
- if (p.hide) for (const i of p.hide) out[i].a += w * (0 - base[i].a);
217
-
218
- if (p.paint) for (const [i, v] of p.paint) {
219
- const s = slot(i);
220
- s.list = s.list || [];
221
- s.list.push(v, w);
222
- }
223
- if (p.clip) for (const [i, v] of p.clip) {
224
- const s = slot(i);
225
- if (w > s.cw) { s.cw = w; s.clip = v; }
226
- }
227
- if (p.cmat) for (const [i, v] of p.cmat) {
228
- const o = this.clipOut[i].m, b = this.clipBase[i].m;
229
- for (let j = 0; j < 6; j++) o[j] += w * (v[j] - b[j]);
230
- this.clipOut[i].gen = this.frame;
231
- }
232
-
233
- if (p.cgeo) for (const [i, v] of p.cgeo) {
234
- const d = this.clipOut[i], b = this.clipBase[i].c;
235
- d.gen = this.frame;
236
- if (v.length !== b.length) {
237
- const s = cslot(i);
238
- if (w > s.gw) { s.gw = w; s.snap = v; }
239
- } else {
240
- const o = d.c === d._c ? d.c : d._c;
241
- for (let j = 0; j < v.length; j++) o[j] += w * (v[j] - b[j]);
242
- }
243
- }
244
-
245
- // A texture swap has no midpoint, so it flips with the dominant pose.
246
- if (p.src) for (const [i, v] of p.src) {
247
- const s = slot(i);
248
- if (w > s.sw) { s.sw = w; s.src = v; }
249
- }
250
- }
251
-
252
- for (const [i, s] of cacc) if (s.snap && s.gw > 0.5) this.clipOut[i].c = s.snap;
253
-
254
- for (const [i, s] of acc) {
255
- const d = out[i];
256
- if (s.geoSnap && s.gw > 0.5) d.cmds = s.geoSnap;
257
- if (s.clip >= 0 && s.cw > 0.5) d.clip = s.clip;
258
- if (s.src >= 0 && s.sw > 0.5) d.src = s.src;
259
- if (s.list) this._resolvePaint(d, base[i].paint, s.list);
260
- }
261
-
262
- return out;
263
- }
264
-
265
- // Solid-to-solid paint changes blend in RGBA, which is what the eye-hue
266
- // ladder and the viseme lip tints need. Anything involving a gradient has no
267
- // meaningful midpoint, so it snaps at the halfway mark instead.
268
- _resolvePaint(d, basePaint, list) {
269
- const P = this.paints;
270
- const b = P[basePaint];
271
- let allSolid = b.t === 'solid';
272
- let best = -1, bw = 0;
273
- for (let j = 0; j < list.length; j += 2) {
274
- if (P[list[j]].t !== 'solid') allSolid = false;
275
- if (list[j + 1] > bw) { bw = list[j + 1]; best = list[j]; }
276
- }
277
- if (!allSolid) { if (bw > 0.5) { d.paint = best; d.colour = null; } return; }
278
- const c = [b.c[0], b.c[1], b.c[2], b.c[3]];
279
- for (let j = 0; j < list.length; j += 2) {
280
- const v = P[list[j]].c, w = list[j + 1];
281
- for (let k = 0; k < 4; k++) c[k] += w * (v[k] - b.c[k]);
282
- }
283
- const q = (v) => (v < 0 ? 0 : v > 255 ? 255 : Math.round(v));
284
- d.paint = basePaint;
285
- d.colour = `rgba(${q(c[0])},${q(c[1])},${q(c[2])},${Math.min(1, Math.max(0, c[3])).toFixed(3)})`;
286
- }
287
- }
288
-
289
- export async function loadRig(url) {
290
- const base = new URL(url, location.href);
291
- const data = await (await fetch(base)).json();
292
- const images = await Promise.all(data.images.map(async (im) => {
293
- const r = await fetch(new URL('img/' + im.file, base));
294
- return createImageBitmap(await r.blob());
295
- }));
296
- return new Rig(data, images);
297
- }
@@ -1,96 +0,0 @@
1
- // The driver's pose vocabulary, as data.
2
- //
3
- // `drivers.js` emits { poseName: weight } maps and nothing else; a name it asks
4
- // for that the rig does not have is silently dropped by `rig.js`, so a typo
5
- // shows up as a feature that simply never animates. That failure mode is why
6
- // this file exists: the driver builds its names from these lists, and the
7
- // authoring-side checker (`author/validate.mjs`) validates against the same
8
- // lists. One source, so the two cannot drift.
9
- //
10
- // 73 required pose names + 3 optional `wardrobe/*` + 9 optional `rhubarb/*`
11
- // + 4 optional `gaze/*`, and 6 track names.
12
-
13
- export const VISEMES = [
14
- 'viseme/100 - silence', 'viseme/101 - A,AI', 'viseme/102 - a', 'viseme/103 - ae',
15
- 'viseme/104 - O', 'viseme/105 - i', 'viseme/107 - p,b,m', 'viseme/108 - L',
16
- 'viseme/109 - v,f', 'viseme/110 - U', 'viseme/112 - o', 'viseme/113 - s,z',
17
- 'viseme/114 - ol', 'viseme/115 - sh,tsh,zh, dzh', 'viseme/116 - th', 'viseme/118 - kgn',
18
- ];
19
-
20
- // The six eye states the expression x blink cross-fade can reach.
21
- export const eyePose = (n) => `eye/eyes-${n}`;
22
- export const EYE_STATES = ['idle_closed', 'idle_wide', 'happy', 'happy_closed', 'sad', 'sad_closed'];
23
- export const EYE_POSES = EYE_STATES.map(eyePose);
24
-
25
- // Six feature axes, each with a +100 and a -100 extreme.
26
- export const MORPH_AXES = ['head', 'lips', 'nose', 'brows', 'eyes', 'distance'];
27
- export const MORPH_POSES = MORPH_AXES.flatMap((a) => [`morph/${a}_100`, `morph/${a}_-100`]);
28
-
29
- // The iris hue ladder: 37 steps of 10 degrees, zero-padded to three digits.
30
- // The driver holds one or two neighbouring rungs and the runtime crossfades
31
- // them solid-to-solid in RGBA.
32
- export const hueKey = (n) => `hue/${String(n).padStart(3, '0')}`;
33
- export const HUE_STEP = 10;
34
- export const HUE_LADDER = Array.from({ length: 360 / HUE_STEP + 1 }, (_, i) => hueKey(i * HUE_STEP));
35
-
36
- // Two overlays on top of whatever hue is showing. They pull in opposite
37
- // directions: saturation-0 drains colour, brightness-0 lifts a black overlay.
38
- export const IRIS_SATURATION = 'iris/eyes-saturation-0';
39
- export const IRIS_BRIGHTNESS = 'iris/eyes-brightness-0';
40
- export const IRIS_POSES = [IRIS_SATURATION, IRIS_BRIGHTNESS];
41
-
42
- // Optional by design: these three only fire when state.hair / suit / glassesOff
43
- // are raised, and they exist for the bitmap-backed mascot. A hand-authored
44
- // vector avatar that covers 73 of 76 names is complete, not incomplete.
45
- export const WARDROBE_HAIR = 'wardrobe/hair-blonde';
46
- export const WARDROBE_SUIT = 'wardrobe/suit-black';
47
- export const WARDROBE_GLASSES = 'wardrobe/glasses-off';
48
- export const WARDROBE_POSES = [WARDROBE_HAIR, WARDROBE_SUIT, WARDROBE_GLASSES];
49
-
50
- // Optional the same way: GAZE. The four names are the extremes of the driver's
51
- // `pupilX`/`pupilY` channels (-1..1, + x = the viewer's right, + y = down), so
52
- // a mixer reaches an arbitrary gaze by holding ONE of each pair at |channel|
53
- // and the two axes compose. They are named for what a VIEWER sees, not for
54
- // whose left it is.
55
- //
56
- // Optional because an eye that cannot look anywhere but forward is still a
57
- // complete face — ink and facet do not implement these and are not incomplete
58
- // — and because a rig that does implement them costs four poses to say so.
59
- export const GAZE_DIRS = ['left', 'right', 'up', 'down'];
60
- export const GAZE_POSES = GAZE_DIRS.map((d) => `gaze/${d}`);
61
-
62
- // The driver's boot state, as data — and the only three numbers in it that any
63
- // AUTHORING-time code has to know. `drivers.js` sets these three on
64
- // `this.state` (drivers.js:34-36); it deliberately does NOT import them, so
65
- // this list is a mirror and has to be kept in step by hand.
66
- //
67
- // They are here because they are load-bearing on the far side of the pipeline:
68
- // the base iris paint in a rig file is solved backwards from where these
69
- // defaults should land (see `author/rig.mjs` solveIrisBase), so changing one of
70
- // them silently changes the colour of every avatar's eyes.
71
- export const DRIVER_DEFAULTS = { hue: 200, saturation: 0.15, brightness: 0.5 };
72
-
73
- // Tracks, not poses. `breathing` and `subtle` run forever under the ambient
74
- // amplitude; the four idle one-shots are shuffled, so every one of them must
75
- // start and end on the exact rest pose.
76
- export const AMBIENT_TRACKS = ['breathing', 'subtle'];
77
- export const IDLE_CLIPS = ['idle 1', 'idle 2', 'idle 3', 'idle 4'];
78
- export const TRACKS = [...AMBIENT_TRACKS, ...IDLE_CLIPS];
79
-
80
- // --- mouth part: the Rhubarb letters ---------------------------------------
81
- // The nine-letter Rhubarb alphabet (A–H plus X for silence), which is the
82
- // alphabet voqalize's lip-sync speaks and the condensation of the Preston Blair
83
- // set that every open-source aligner already targets. A rig gets these when its
84
- // mouth is `author/parts/mouth.mjs` — they are baked straight from that
85
- // contract's own `VISEME_SHAPES` (author/parts/mouth-tables.mjs) with no
86
- // hand-tuning, so they are a PROOF that the channel mapping lands on the right
87
- // nine shapes as much as they are a vocabulary.
88
- //
89
- // OPTIONAL, and they stay optional: `ink` and `facet` still hold their mouths
90
- // inline against the pre-channel keys and have no way to bake them, and the
91
- // 16-code mascot set above remains the required one for every rig.
92
- export const RHUBARB_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'X'];
93
- export const RHUBARB_POSES = RHUBARB_LETTERS.map((l) => 'rhubarb/' + l);
94
-
95
- export const REQUIRED_POSES = [...VISEMES, ...EYE_POSES, ...MORPH_POSES, ...HUE_LADDER, ...IRIS_POSES];
96
- export const OPTIONAL_POSES = [...WARDROBE_POSES, ...RHUBARB_POSES, ...GAZE_POSES];