mochi-avatar 1.0.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 (96) hide show
  1. package/BRAND.md +85 -0
  2. package/LICENSE.md +104 -0
  3. package/README.md +135 -0
  4. package/dist/canvas2d/avatar.d.ts +264 -0
  5. package/dist/canvas2d/avatar.d.ts.map +1 -0
  6. package/dist/canvas2d/avatar.js +811 -0
  7. package/dist/canvas2d/avatar.js.map +1 -0
  8. package/dist/canvas2d/face.d.ts +74 -0
  9. package/dist/canvas2d/face.d.ts.map +1 -0
  10. package/dist/canvas2d/face.js +298 -0
  11. package/dist/canvas2d/face.js.map +1 -0
  12. package/dist/canvas2d/paths.d.ts +13 -0
  13. package/dist/canvas2d/paths.d.ts.map +1 -0
  14. package/dist/canvas2d/paths.js +24 -0
  15. package/dist/canvas2d/paths.js.map +1 -0
  16. package/dist/characters/colourways.d.ts +78 -0
  17. package/dist/characters/colourways.d.ts.map +1 -0
  18. package/dist/characters/colourways.js +76 -0
  19. package/dist/characters/colourways.js.map +1 -0
  20. package/dist/characters/index.d.ts +8 -0
  21. package/dist/characters/index.d.ts.map +1 -0
  22. package/dist/characters/index.js +8 -0
  23. package/dist/characters/index.js.map +1 -0
  24. package/dist/characters/mochi.d.ts +19 -0
  25. package/dist/characters/mochi.d.ts.map +1 -0
  26. package/dist/characters/mochi.js +66 -0
  27. package/dist/characters/mochi.js.map +1 -0
  28. package/dist/core/colour.d.ts +25 -0
  29. package/dist/core/colour.d.ts.map +1 -0
  30. package/dist/core/colour.js +37 -0
  31. package/dist/core/colour.js.map +1 -0
  32. package/dist/core/envelope.d.ts +153 -0
  33. package/dist/core/envelope.d.ts.map +1 -0
  34. package/dist/core/envelope.js +141 -0
  35. package/dist/core/envelope.js.map +1 -0
  36. package/dist/core/geometry.d.ts +106 -0
  37. package/dist/core/geometry.d.ts.map +1 -0
  38. package/dist/core/geometry.js +157 -0
  39. package/dist/core/geometry.js.map +1 -0
  40. package/dist/core/idle.d.ts +163 -0
  41. package/dist/core/idle.d.ts.map +1 -0
  42. package/dist/core/idle.js +262 -0
  43. package/dist/core/idle.js.map +1 -0
  44. package/dist/core/layout.d.ts +142 -0
  45. package/dist/core/layout.d.ts.map +1 -0
  46. package/dist/core/layout.js +172 -0
  47. package/dist/core/layout.js.map +1 -0
  48. package/dist/core/lens.d.ts +47 -0
  49. package/dist/core/lens.d.ts.map +1 -0
  50. package/dist/core/lens.js +67 -0
  51. package/dist/core/lens.js.map +1 -0
  52. package/dist/core/looks.d.ts +77 -0
  53. package/dist/core/looks.d.ts.map +1 -0
  54. package/dist/core/looks.js +168 -0
  55. package/dist/core/looks.js.map +1 -0
  56. package/dist/core/motion.d.ts +168 -0
  57. package/dist/core/motion.d.ts.map +1 -0
  58. package/dist/core/motion.js +527 -0
  59. package/dist/core/motion.js.map +1 -0
  60. package/dist/core/mouth.d.ts +93 -0
  61. package/dist/core/mouth.d.ts.map +1 -0
  62. package/dist/core/mouth.js +92 -0
  63. package/dist/core/mouth.js.map +1 -0
  64. package/dist/core/plain.d.ts +23 -0
  65. package/dist/core/plain.d.ts.map +1 -0
  66. package/dist/core/plain.js +63 -0
  67. package/dist/core/plain.js.map +1 -0
  68. package/dist/core/spec.d.ts +156 -0
  69. package/dist/core/spec.d.ts.map +1 -0
  70. package/dist/core/spec.js +236 -0
  71. package/dist/core/spec.js.map +1 -0
  72. package/dist/core/spring.d.ts +53 -0
  73. package/dist/core/spring.d.ts.map +1 -0
  74. package/dist/core/spring.js +68 -0
  75. package/dist/core/spring.js.map +1 -0
  76. package/dist/core/vocabulary.d.ts +182 -0
  77. package/dist/core/vocabulary.d.ts.map +1 -0
  78. package/dist/core/vocabulary.js +66 -0
  79. package/dist/core/vocabulary.js.map +1 -0
  80. package/dist/element/dough-avatar.d.ts +34 -0
  81. package/dist/element/dough-avatar.d.ts.map +1 -0
  82. package/dist/element/dough-avatar.js +189 -0
  83. package/dist/element/dough-avatar.js.map +1 -0
  84. package/dist/element/index.d.ts +3 -0
  85. package/dist/element/index.d.ts.map +1 -0
  86. package/dist/element/index.js +35 -0
  87. package/dist/element/index.js.map +1 -0
  88. package/dist/index.d.ts +38 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +42 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/svg/silhouette.d.ts +97 -0
  93. package/dist/svg/silhouette.d.ts.map +1 -0
  94. package/dist/svg/silhouette.js +143 -0
  95. package/dist/svg/silhouette.js.map +1 -0
  96. package/package.json +90 -0
@@ -0,0 +1,527 @@
1
+ /**
2
+ * A motion: the layer between idle and expression.
3
+ *
4
+ * ## Why this format exists now, having been refused twice
5
+ *
6
+ * The argument for not writing it was that a format designed against ONE
7
+ * motion set comes out shaped like that set. That was right, and it made the
8
+ * missing input sound external when it was not: the answer is to author a
9
+ * second motion, not to wait for one. So there are two below, deliberately
10
+ * unalike — `nod` is short, one-shot and postural; `sway` loops and moves her
11
+ * gaze. Everything here is a thing both of them needed.
12
+ *
13
+ * What that shook out, which one clip would not have:
14
+ *
15
+ * - `loop`, because one of them does and one must not
16
+ * - keys with OPTIONAL channels, because `nod` never touches gaze and a
17
+ * format forcing it to write zeros would make every clip claim every
18
+ * channel, and then "unset" and "zero" would be the same thing
19
+ * - normalised time, because the two differ in duration by 6x and keys
20
+ * written in milliseconds cannot be retimed without rewriting them all
21
+ *
22
+ * ## Four channels, and the mouth is not among them
23
+ *
24
+ * The layer order is idle -> motion -> expression -> mouth, and no layer may
25
+ * overwrite the mouth. A motion that could write `mouthOpen` would be able to
26
+ * hold her jaw shut while audio is playing, which reads as broken. The
27
+ * channels here are POSTURAL -- squash, lean, and where she is looking -- so
28
+ * the rule is a property of the vocabulary rather than something each clip has
29
+ * to respect.
30
+ */
31
+ import { DRIFT } from './idle.js';
32
+ /**
33
+ * Everything a motion is allowed to move. See the header.
34
+ *
35
+ * ## The three that were added, and why they are not more
36
+ *
37
+ * `squash`, `lean`, `gazeX`, `gazeY` are postural and could not express a
38
+ * character who goes anywhere: a hop with no vertical channel is a crouch, and
39
+ * a wander with no horizontal one is a lean. So:
40
+ *
41
+ * - `lift` — up, in fractions of her body HEIGHT. Positive leaves the ground.
42
+ * - `shift` — right, in fractions of her body WIDTH.
43
+ * - `turn` — -1..1, how far her features have slid toward one side.
44
+ *
45
+ * `turn` is NOT a rotation and cannot become one. One front-facing silhouette
46
+ * is drawn; there is no second view and no yaw. What it does is slide the
47
+ * features across the body, and everything is clipped to the silhouette
48
+ * already, so the far eye passes out of sight around her edge. That reads as
49
+ * turning to look at something and does not read as turning around. Anyone
50
+ * wanting the second thing has to draw her from behind, which is art rather
51
+ * than a channel.
52
+ *
53
+ * The mouth is still not among these, for the reason the header gives.
54
+ *
55
+ * ## Translation is a channel, not a window move
56
+ *
57
+ * `lift` and `shift` move her INSIDE her own window, and `paint` applies them
58
+ * by offsetting the origin every other coordinate is derived from -- so the
59
+ * outline, the features and the hit-test silhouette all travel together, and
60
+ * click-through keeps following her painted pixels for free.
61
+ *
62
+ * Moving the WINDOW was the alternative and is worse: `setPosition` from main
63
+ * on every frame of a renderer-driven animation is two processes disagreeing
64
+ * about where she is sixty times a second, which is the problem `drag.ts`
65
+ * already needs a cursor poll to solve.
66
+ */
67
+ export const MOTION_CHANNELS = [
68
+ 'squash',
69
+ 'lean',
70
+ 'gazeX',
71
+ 'gazeY',
72
+ 'lift',
73
+ 'shift',
74
+ 'turn',
75
+ ];
76
+ /**
77
+ * How far through a clip we are, or null once a one-shot has finished.
78
+ *
79
+ * Null rather than 1: a finished one-shot contributes NOTHING, and holding its
80
+ * last key would leave her permanently leaning if a clip happened to end away
81
+ * from neutral. The caller stops mixing the layer in at all.
82
+ */
83
+ export function progress(clip, elapsedMs) {
84
+ if (elapsedMs < 0)
85
+ return 0;
86
+ const raw = elapsedMs / clip.durationMs;
87
+ if (raw < 1)
88
+ return raw;
89
+ return clip.loop ? raw % 1 : null;
90
+ }
91
+ /**
92
+ * The pose at a moment, interpolated between the keys around it.
93
+ *
94
+ * Per CHANNEL, not per key. A key that omits `gazeX` is not saying "zero", it
95
+ * is saying nothing -- so the interpolation for that channel runs between the
96
+ * nearest keys that DO mention it, and a channel no key mentions is absent
97
+ * from the result. Blending toward an implied zero instead would make every
98
+ * clip fight every other layer over channels it never meant to touch.
99
+ */
100
+ export function poseAt(clip, at) {
101
+ const pose = {};
102
+ for (const channel of MOTION_CHANNELS) {
103
+ const value = channelAt(clip.keys, channel, at, clip.loop);
104
+ if (value !== null)
105
+ pose[channel] = value;
106
+ }
107
+ return pose;
108
+ }
109
+ function channelAt(keys, channel, at, loop) {
110
+ const stated = keys.filter((key) => key[channel] !== undefined);
111
+ if (stated.length === 0)
112
+ return null;
113
+ const first = stated[0];
114
+ const last = stated[stated.length - 1];
115
+ if (first === undefined || last === undefined)
116
+ return null;
117
+ // HELD outside the stated range rather than fading to zero. A clip that
118
+ // mentions `lean` only in its second half is upright until then, not
119
+ // drifting toward upright from somewhere unspecified.
120
+ if (at <= first.t)
121
+ return first[channel] ?? null;
122
+ if (at >= last.t)
123
+ return last[channel] ?? null;
124
+ for (let i = 1; i < stated.length; i += 1) {
125
+ const before = stated[i - 1];
126
+ const after = stated[i];
127
+ if (before === undefined || after === undefined || after.t < at)
128
+ continue;
129
+ return hermite(stated, i, channel, at, loop);
130
+ }
131
+ return last[channel] ?? null;
132
+ }
133
+ /**
134
+ * A cubic through the keys, rather than a straight line between them.
135
+ *
136
+ * ## What linear interpolation actually looks like
137
+ *
138
+ * It was `a*(1-k) + b*k`, which is exact and correct and moves like a machine.
139
+ * Every key is a VELOCITY DISCONTINUITY: she travels at a constant speed to the
140
+ * key, changes speed instantaneously, and travels at another constant speed
141
+ * away from it. Nothing physical does that, and the eye reads the corners
142
+ * immediately -- reported, in as many words, as the motions being crude.
143
+ *
144
+ * This is Catmull-Rom, expressed as a Hermite with finite-difference tangents:
145
+ * the speed at each key is estimated from its NEIGHBOURS, so velocity carries
146
+ * through a key instead of restarting at it. The curve still passes exactly
147
+ * through every authored value, which is what keeps a clip an authored thing
148
+ * rather than a suggestion.
149
+ *
150
+ * ## Why not ease each segment instead
151
+ *
152
+ * A smoothstep per segment is one line and is wrong for half the library. It
153
+ * forces velocity to ZERO at every key, so `sway` -- which crosses zero at its
154
+ * midpoint on the way from one extreme to the other -- would stop dead in the
155
+ * middle of a continuous lean. Easing is a property of a segment; smoothness is
156
+ * a property of the curve, and the complaint was about the curve.
157
+ *
158
+ * ## Time is NON-UNIFORM, so the tangents are scaled by it
159
+ *
160
+ * Keys are placed where the motion needs them, not on a grid: `hop` has eight
161
+ * keys with gaps from 0.12 to 0.16, and `wander` has eleven. The textbook
162
+ * uniform Catmull-Rom assumes even spacing and produces a visible surge either
163
+ * side of a short segment. Dividing each difference by the real time it spans
164
+ * is what makes the curve independent of where the keys happen to sit.
165
+ *
166
+ * ## Loops wrap; one-shots do not
167
+ *
168
+ * A looping clip takes its neighbours from the other end, so the seam is as
169
+ * smooth as everywhere else -- otherwise `sway` and `wander` would kink once
170
+ * per cycle, forever, which is the defect the loop-closure check exists to
171
+ * prevent in its cruder form. A one-shot uses a one-sided difference at its
172
+ * ends, so it starts and finishes without being dragged by a key that is not
173
+ * there.
174
+ *
175
+ * Overshoot between keys is possible and is deliberate: it is what gives a
176
+ * landing its weight. `squashed` clamps the body scale, so the one channel
177
+ * where an overshoot could be structural is already bounded.
178
+ */
179
+ function hermite(stated, index, channel, at, loop) {
180
+ const n = stated.length;
181
+ const value = (key) => key?.[channel] ?? 0;
182
+ const p1 = stated[index - 1];
183
+ const p2 = stated[index];
184
+ if (p1 === undefined || p2 === undefined)
185
+ return value(p2 ?? p1);
186
+ const span = p2.t - p1.t;
187
+ if (span <= 0)
188
+ return value(p2);
189
+ /*
190
+ The neighbour outside the segment, and how far away in time it is.
191
+
192
+ For a loop that is the key at the other end, one period away — the first and
193
+ last keys hold the same value (the closure check enforces it), so the wrap
194
+ skips one of them to avoid a zero-length span that would make the tangent
195
+ infinite.
196
+ */
197
+ const before = index >= 2
198
+ ? { key: stated[index - 2], dt: p1.t - (stated[index - 2]?.t ?? 0) }
199
+ : loop && n >= 3
200
+ ? { key: stated[n - 2], dt: p1.t + (1 - (stated[n - 2]?.t ?? 1)) }
201
+ : null;
202
+ const after = index + 1 < n
203
+ ? { key: stated[index + 1], dt: (stated[index + 1]?.t ?? 1) - p2.t }
204
+ : loop && n >= 3
205
+ ? { key: stated[1], dt: 1 - p2.t + (stated[1]?.t ?? 0) }
206
+ : null;
207
+ // Finite differences over real time. With no neighbour the difference is
208
+ // one-sided, which is the segment's own slope — an end that neither
209
+ // accelerates into nothing nor is pulled by a key that does not exist.
210
+ const m1 = before === null || before.dt <= 0
211
+ ? (value(p2) - value(p1)) / span
212
+ : (value(p2) - value(before.key)) / (span + before.dt);
213
+ const m2 = after === null || after.dt <= 0
214
+ ? (value(p2) - value(p1)) / span
215
+ : (value(after.key) - value(p1)) / (span + after.dt);
216
+ const u = (at - p1.t) / span;
217
+ const uu = u * u;
218
+ const uuu = uu * u;
219
+ return ((2 * uuu - 3 * uu + 1) * value(p1) +
220
+ (uuu - 2 * uu + u) * span * m1 +
221
+ (-2 * uuu + 3 * uu) * value(p2) +
222
+ (uuu - uu) * span * m2);
223
+ }
224
+ /**
225
+ * A short agreement, and a long one that repeats.
226
+ *
227
+ * Two, and unalike on purpose -- see the header. They are also the only
228
+ * motions this app has, which is why `caps.supportsMotions` can finally be
229
+ * true: a backend claiming the capability with an empty library would be the
230
+ * flag lying, and `playMotion` would report success over a face that never
231
+ * moved.
232
+ */
233
+ export const BUILT_IN_MOTIONS = {
234
+ // Down, past centre, and settle. The overshoot is what makes it read as a
235
+ // nod rather than as a lurch -- the same reason `spring.ts` is second order.
236
+ nod: {
237
+ durationMs: 620,
238
+ loop: false,
239
+ keys: [
240
+ { t: 0, squash: 0, lean: 0 },
241
+ { t: 0.28, squash: 0.1, lean: 0.05 },
242
+ { t: 0.62, squash: -0.04, lean: -0.02 },
243
+ { t: 1, squash: 0, lean: 0 },
244
+ ],
245
+ },
246
+ // A slow lean with her gaze trailing it, repeating. Nothing about her body
247
+ // returns to centre at the ends, because a looping clip whose first and last
248
+ // keys differ jumps every cycle.
249
+ sway: {
250
+ durationMs: 3800,
251
+ loop: true,
252
+ keys: [
253
+ { t: 0, lean: 0, gazeX: 0 },
254
+ { t: 0.25, lean: 0.06, gazeX: 0.18 },
255
+ { t: 0.5, lean: 0, gazeX: 0 },
256
+ { t: 0.75, lean: -0.06, gazeX: -0.18 },
257
+ { t: 1, lean: 0, gazeX: 0 },
258
+ ],
259
+ },
260
+ /**
261
+ * A small hop: crouch, launch, hang, land, settle.
262
+ *
263
+ * The ANTICIPATION is what makes it read as a jump rather than a twitch --
264
+ * she compresses before she leaves the ground, and lands compressed again.
265
+ * Squash and lift are deliberately out of phase: at the apex she is stretched
266
+ * (negative squash) and highest, and the two return to zero at different
267
+ * times so the landing has weight.
268
+ *
269
+ * `lift: 0.16` is about eleven pixels at her default size. Small on purpose
270
+ * -- this is a gesture, not a jump, and the room for it has to be reserved in
271
+ * her window before she can use it.
272
+ */
273
+ hop: {
274
+ // Quick and SHALLOW. This is a gesture, not a jump: she leaves the ground
275
+ // by about eight pixels at her drawn size, which is enough to read as a
276
+ // hop and not enough to read as her going anywhere.
277
+ durationMs: 460,
278
+ loop: false,
279
+ keys: [
280
+ { t: 0, squash: 0, lift: 0 },
281
+ // The crouch is FAST and the launch is faster: most of the clip is air.
282
+ { t: 0.14, squash: 0.1, lift: 0 },
283
+ { t: 0.26, squash: -0.05, lift: 0.045 },
284
+ { t: 0.44, squash: -0.02, lift: 0.085 },
285
+ { t: 0.62, squash: 0.015, lift: 0.042 },
286
+ { t: 0.72, squash: 0.085, lift: 0 },
287
+ { t: 0.86, squash: -0.025, lift: 0 },
288
+ { t: 1, squash: 0, lift: 0 },
289
+ ],
290
+ },
291
+ /**
292
+ * A pendulum: her body goes where it leans, repeating.
293
+ *
294
+ * The difference from `sway`, which is worth saying because they are easy to
295
+ * confuse: `sway` moves her GAZE and leaves her body where it is, and reads
296
+ * as attention wandering. This moves her body and leaves her gaze alone, and
297
+ * reads as contentment. Lean and shift are IN PHASE -- a pendulum travels the
298
+ * way it tilts, and putting them out of phase produces a body sliding out
299
+ * from under its own head.
300
+ */
301
+ swing: {
302
+ // Slow and shallow. A pendulum you notice is a pendulum you watch; this is
303
+ // meant to be seen out of the corner of an eye, so the travel is about four
304
+ // pixels either way on her drawn body.
305
+ durationMs: 3400,
306
+ loop: true,
307
+ keys: [
308
+ { t: 0, lean: 0, shift: 0 },
309
+ { t: 0.25, lean: 0.032, shift: 0.045 },
310
+ { t: 0.5, lean: 0, shift: 0 },
311
+ { t: 0.75, lean: -0.032, shift: -0.045 },
312
+ { t: 1, lean: 0, shift: 0 },
313
+ ],
314
+ },
315
+ /**
316
+ * Turning to look at something beside her, and coming back.
317
+ *
318
+ * Not a rotation -- see the note on `MOTION_CHANNELS`. The features slide
319
+ * toward one edge and the silhouette clips the far one, which is what a flat
320
+ * character turning looks like. The lean and the gaze go with it because a
321
+ * head that turns without the body reads as a glance, and a glance is what
322
+ * the gaze channel alone already does.
323
+ *
324
+ * One-shot, and it returns to zero: a turn that stayed turned would leave her
325
+ * facing away for the rest of the session, and there is no view of her from
326
+ * that angle to make it worth looking at.
327
+ */
328
+ turn: {
329
+ // A head turn is quick and the RETURN is slower than the going, which is
330
+ // what makes it read as looking at something rather than as scanning.
331
+ durationMs: 950,
332
+ loop: false,
333
+ keys: [
334
+ // Her face LEADS and TRAILS her body, which is how a head turn actually
335
+ // goes: the features start moving before the shoulders commit, and they
336
+ // are the last thing to come back. It also leaves a moment near the end
337
+ // where the lean is exactly zero and the turn is not, which is the only
338
+ // instant at which this channel can be measured on its own -- a clip
339
+ // whose channels all move together can only ever be tested as a whole.
340
+ { t: 0, turn: 0, lean: 0, gazeX: 0 },
341
+ { t: 0.12, turn: 0.22, lean: 0.012, gazeX: 0.12 },
342
+ { t: 0.26, turn: 0.4, lean: 0.03, gazeX: 0.26 },
343
+ { t: 0.58, turn: 0.39, lean: 0.034, gazeX: 0.28 },
344
+ { t: 0.82, turn: 0.24, lean: 0, gazeX: 0 },
345
+ { t: 1, turn: 0, lean: 0, gazeX: 0 },
346
+ ],
347
+ },
348
+ /**
349
+ * Wandering: a slow drift one way, a pause, and back.
350
+ *
351
+ * LONG and slow on purpose. Everything else in this app is built so she can
352
+ * be ignored -- click-through by default, a halo rather than a spinner -- and
353
+ * a companion who crosses your screen quickly is one you have to attend to.
354
+ * Eleven seconds for less than half a body width each way is movement you
355
+ * notice only if you look.
356
+ *
357
+ * The bob is on `lift` at twice the drift's frequency, so her step and her
358
+ * travel are not the same beat. Both return to zero at `t: 1`, which the
359
+ * loop-closure check enforces for every channel rather than for the one the
360
+ * author was thinking about.
361
+ *
362
+ * She leans INTO the direction of travel at the start of each leg and out of
363
+ * it at the end, which is what a body does when it starts and stops.
364
+ */
365
+ wander: {
366
+ durationMs: 9_000,
367
+ loop: true,
368
+ keys: [
369
+ { t: 0, shift: 0, lean: 0, lift: 0 },
370
+ { t: 0.1, shift: 0.03, lean: 0.018, lift: 0.006 },
371
+ { t: 0.2, shift: 0.065, lean: 0.012, lift: 0 },
372
+ { t: 0.3, shift: 0.085, lean: -0.01, lift: 0.006 },
373
+ { t: 0.38, shift: 0.09, lean: 0, lift: 0 },
374
+ { t: 0.5, shift: 0.072, lean: -0.018, lift: 0.006 },
375
+ { t: 0.62, shift: 0, lean: -0.018, lift: 0 },
376
+ { t: 0.74, shift: -0.065, lean: -0.012, lift: 0.006 },
377
+ { t: 0.84, shift: -0.078, lean: 0.014, lift: 0 },
378
+ { t: 0.94, shift: -0.03, lean: 0.018, lift: 0.005 },
379
+ { t: 1, shift: 0, lean: 0, lift: 0 },
380
+ ],
381
+ },
382
+ };
383
+ /**
384
+ * How far a clip takes her from where she stands, as fractions of her body.
385
+ *
386
+ * The room for a translation has to exist in her window BEFORE she uses it, or
387
+ * she walks into the edge of a transparent rectangle and is clipped by it. This
388
+ * is what `face.ts` reserves against, and it is derived from the keys rather
389
+ * than declared beside them: a number an author has to keep in step with the
390
+ * clip is a number that goes stale the first time somebody retimes it.
391
+ *
392
+ * `up` only, for `lift`. She hops; she does not sink through the floor, and
393
+ * reserving room below her would push her down inside her own window for a
394
+ * clearance nothing uses.
395
+ */
396
+ export function motionReach(clip) {
397
+ let x = 0;
398
+ let up = 0;
399
+ for (const key of clip.keys) {
400
+ x = Math.max(x, Math.abs(key.shift ?? 0));
401
+ up = Math.max(up, key.lift ?? 0);
402
+ }
403
+ return { x, up };
404
+ }
405
+ /**
406
+ * The worst case across every built-in, PLUS the drift, which is what a window
407
+ * has to hold.
408
+ *
409
+ * The drift is continuous and additive -- she is always a little off her spot,
410
+ * and a clip starts from wherever that is rather than from centre. So the two
411
+ * reaches SUM: reserving only the clip's would clip her by a couple of pixels
412
+ * at whichever extreme the two happened to agree on, intermittently, which is
413
+ * the least debuggable kind of wrong.
414
+ */
415
+ export function builtInReach() {
416
+ let x = 0;
417
+ let up = 0;
418
+ for (const clip of Object.values(BUILT_IN_MOTIONS)) {
419
+ const reach = motionReach(clip);
420
+ x = Math.max(x, reach.x);
421
+ up = Math.max(up, reach.up);
422
+ }
423
+ return { x: x + DRIFT.shift, up: up + DRIFT.lift };
424
+ }
425
+ /**
426
+ * Turn something off disk into a clip, or say what is wrong with all of it.
427
+ *
428
+ * A package may carry motions, so this is a trust boundary and takes the same
429
+ * treatment `parseFaceSpec` gets: every problem reported at once
430
+ * rather than the first, unknown keys refused rather than dropped, and every
431
+ * number range-checked -- `t: 5` and `squash: 400` are both valid JSON numbers
432
+ * that throw nothing and produce a mochi somewhere off the screen.
433
+ *
434
+ * The ordering and loop-closure rules are checked HERE rather than trusted,
435
+ * because `poseAt` is written against them: keys out of order make the
436
+ * interpolation pick the wrong pair, and a loop whose ends differ jumps once
437
+ * per cycle for as long as it plays.
438
+ */
439
+ export function parseMotionClip(value) {
440
+ const problems = [];
441
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
442
+ return { ok: false, problems: ['a motion must be a JSON object'] };
443
+ }
444
+ const source = value;
445
+ const durationMs = source['durationMs'];
446
+ if (typeof durationMs !== 'number' || !Number.isFinite(durationMs) || durationMs <= 0) {
447
+ problems.push('durationMs must be a positive number of milliseconds');
448
+ }
449
+ const loop = source['loop'];
450
+ if (typeof loop !== 'boolean')
451
+ problems.push('loop must be true or false');
452
+ for (const key of Object.keys(source)) {
453
+ if (!['durationMs', 'loop', 'keys'].includes(key)) {
454
+ problems.push(`${key} is not a field of a motion`);
455
+ }
456
+ }
457
+ const rawKeys = source['keys'];
458
+ if (!Array.isArray(rawKeys)) {
459
+ problems.push('keys must be a list');
460
+ return { ok: false, problems };
461
+ }
462
+ // TWO, not one. A single key is a pose rather than a motion, and it would
463
+ // interpolate against nothing -- which reads as the clip having failed.
464
+ if (rawKeys.length < 2)
465
+ problems.push('a motion needs at least two keys');
466
+ const keys = [];
467
+ for (const [index, raw] of rawKeys.entries()) {
468
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
469
+ problems.push(`keys[${String(index)}] must be an object`);
470
+ continue;
471
+ }
472
+ const entry = raw;
473
+ const t = entry['t'];
474
+ if (typeof t !== 'number' || !Number.isFinite(t) || t < 0 || t > 1) {
475
+ problems.push(`keys[${String(index)}].t must be between 0 and 1`);
476
+ continue;
477
+ }
478
+ const key = { t };
479
+ for (const [name, channelValue] of Object.entries(entry)) {
480
+ if (name === 't')
481
+ continue;
482
+ if (!MOTION_CHANNELS.includes(name)) {
483
+ // Refused, not dropped: `squahs` silently ignored shows its author a
484
+ // motion that plays and does nothing they asked for.
485
+ problems.push(`keys[${String(index)}].${name} is not a channel a motion may move`);
486
+ continue;
487
+ }
488
+ if (typeof channelValue !== 'number' || !Number.isFinite(channelValue)) {
489
+ problems.push(`keys[${String(index)}].${name} must be a finite number`);
490
+ continue;
491
+ }
492
+ if (channelValue < -1 || channelValue > 1) {
493
+ // Bounded because these feed body scale and gaze. 400 is a legal
494
+ // number that draws a mochi nobody can see.
495
+ problems.push(`keys[${String(index)}].${name} must be between -1 and 1`);
496
+ continue;
497
+ }
498
+ key[name] = channelValue;
499
+ }
500
+ keys.push(key);
501
+ }
502
+ for (let i = 1; i < keys.length; i += 1) {
503
+ if ((keys[i]?.t ?? 0) < (keys[i - 1]?.t ?? 0)) {
504
+ problems.push('keys must be in order of t');
505
+ break;
506
+ }
507
+ }
508
+ if (loop === true && keys.length >= 2) {
509
+ const first = keys[0];
510
+ const last = keys[keys.length - 1];
511
+ if (first !== undefined && last !== undefined) {
512
+ for (const channel of MOTION_CHANNELS) {
513
+ if ((first[channel] ?? 0) !== (last[channel] ?? 0)) {
514
+ // A looping clip whose ends differ jumps once per cycle, forever.
515
+ problems.push(`a looping motion must end where it began (${channel} differs)`);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ if (problems.length > 0)
521
+ return { ok: false, problems };
522
+ return {
523
+ ok: true,
524
+ clip: { durationMs: durationMs, loop: loop, keys },
525
+ };
526
+ }
527
+ //# sourceMappingURL=motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion.js","sourceRoot":"","sources":["../../src/core/motion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The single writer to her mouth.
3
+ *
4
+ * One object owns `setMouthOpen`, and that is the whole point rather than a
5
+ * tidiness preference. The mouth is layer 4 and is written last, so nothing
6
+ * above it can arbitrate between two sources -- two writers produce a mouth
7
+ * flickering between them at whatever rate they happen to disagree, and no
8
+ * layer ordering can fix it.
9
+ *
10
+ * ## Where the viseme path goes
11
+ *
12
+ * The mouth has two paths and they are not a ladder: cloud speech-to-speech
13
+ * carries no phoneme timings, so the envelope is the only thing available
14
+ * there, not a fallback. The five-vowel path belongs to a local TTS front end,
15
+ * whose G2P stage produces timings for free.
16
+ *
17
+ * There is deliberately no second implementation and no factory to choose
18
+ * between them YET. Nothing emits phoneme timings on the current road and
19
+ * `MochiAvatar.caps.visemes` is false, so a `VisemeMouth` would be an
20
+ * abstraction over one caller and a capability nobody can satisfy. When a
21
+ * provider reports `phonemeTimings`, it arrives as a second class behind this
22
+ * same interface and the choice is made ONCE -- by picking an object, never by
23
+ * a flag read inside a callback, which would be a branch in every frame and a
24
+ * place for the wrong path to be taken.
25
+ *
26
+ * ## The gate is THREE conditions, not one
27
+ *
28
+ * When that day comes, the precise path requires all of:
29
+ *
30
+ * 1. the provider emits phoneme timings,
31
+ * 2. the backend can render visemes (`caps.visemes`), and
32
+ * 3. THE LOADED MODEL carries every one of the five vowel presets.
33
+ *
34
+ * The third is the one that gets dropped, and it is the expensive one. VRM's
35
+ * vowel presets are optional per model, so a capable backend can load a model
36
+ * carrying none: routing on caps alone sends weights nowhere and leaves the
37
+ * mouth shut mid-word, intermittently, with every flag reporting success. RMS
38
+ * over the whole utterance is visibly better than four fifths of a mouth.
39
+ *
40
+ * mochi locked all three into `canUseVisemes()` in shared/avatar.ts. It is not
41
+ * ported here because there is nothing yet to call it -- port it together with
42
+ * the second driver, not before.
43
+ */
44
+ import type { AvatarBackend } from './vocabulary.js';
45
+ import { type EnvelopeSettings } from './envelope.js';
46
+ /** Only the part of the backend a mouth driver is allowed to touch. */
47
+ export type MouthSink = Pick<AvatarBackend, 'setMouthOpen'>;
48
+ export interface MouthDriver {
49
+ /**
50
+ * The loudness of what is sounding right now, and how long since the last
51
+ * call. Driven from the render loop, after every other layer.
52
+ */
53
+ observe(level: number, dtSeconds: number): void;
54
+ /** The turn ended, or she was interrupted. Shuts the mouth immediately. */
55
+ end(): void;
56
+ }
57
+ /**
58
+ * The RMS path.
59
+ *
60
+ * Driven once per rendered frame rather than per audio callback. Under a peer
61
+ * connection her voice is a remote MediaStream the browser plays itself -- no
62
+ * PCM reaches this process at all -- so the level arrives by sampling an
63
+ * analyser, which is naturally a per-frame operation. At 60Hz that is a 16ms
64
+ * sampling interval against a ~4Hz syllable rate, which is ample.
65
+ */
66
+ export declare class EnvelopeMouth implements MouthDriver {
67
+ private readonly avatar;
68
+ private readonly settings;
69
+ private state;
70
+ constructor(avatar: MouthSink, settings?: EnvelopeSettings);
71
+ observe(level: number, dtSeconds: number): void;
72
+ /**
73
+ * Whether the envelope currently judges the signal to be speech.
74
+ *
75
+ * The one place anything outside the rig should ask that question. Reading it
76
+ * here rather than re-deriving it from a level means the mouth and the
77
+ * "is she still talking" decision can never disagree -- and they did, when
78
+ * the caller compared a raw RMS against a constant while this compared a peak
79
+ * against a learned floor.
80
+ */
81
+ get speaking(): boolean;
82
+ /**
83
+ * Shut, and forget the level -- but KEEP the learned floor and peak.
84
+ *
85
+ * The references are what make the envelope level-independent, and they are
86
+ * properties of this voice on this connection, not of this turn. Discarding
87
+ * them at every turn boundary would make her first syllable back a
88
+ * recalibration, which is visible: the mouth either slams or sits closed
89
+ * while the estimate catches up.
90
+ */
91
+ end(): void;
92
+ }
93
+ //# sourceMappingURL=mouth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mouth.d.ts","sourceRoot":"","sources":["../../src/core/mouth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAA6C,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhG,uEAAuE;AACvE,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC,CAAA;AAE3D,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/C,2EAA2E;IAC3E,GAAG,IAAI,IAAI,CAAA;CACZ;AAED;;;;;;;;GAQG;AACH,qBAAa,aAAc,YAAW,WAAW;IAI7C,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ3B,OAAO,CAAC,KAAK,CAAS;IAEtB,YACmB,MAAM,EAAE,SAAS,EACjB,QAAQ,GAAE,gBAAmC,EAC5D;IAEJ,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAG9C;IAED;;;;;;;;OAQG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;;;;;;OAQG;IACH,GAAG,IAAI,IAAI,CAGV;CACF"}