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,68 @@
1
+ /**
2
+ * A second-order spring.
3
+ *
4
+ * The reason this exists rather than the one-pole filter used elsewhere: a
5
+ * first-order system approaches its target and stops. It CANNOT overshoot, and
6
+ * overshoot is what reads as mass. A mochi that gets squashed should wobble
7
+ * past centre and settle; one that eases back looks like a scaling picture.
8
+ *
9
+ * Pure arithmetic, no clock. The caller owns time, which is what lets the same
10
+ * code run in a render loop and in a test that advances a number.
11
+ */
12
+ export const DEFAULT_SPRING = { stiffness: 190, damping: 20 };
13
+ /**
14
+ * Longest step the integrator will take, in seconds.
15
+ *
16
+ * Semi-implicit Euler is only conditionally stable: past roughly 2/sqrt(k) the
17
+ * correction overshoots further than the error it was correcting and the value
18
+ * diverges to infinity within a few frames. A backgrounded window, a breakpoint
19
+ * or a laptop waking from sleep all deliver exactly that -- a dt of seconds.
20
+ *
21
+ * Clamping means a long gap is under-integrated (she resumes from where she
22
+ * was, a little behind) rather than exploding. Under-integration is invisible;
23
+ * divergence paints a mochi the size of the screen and then NaN, after which
24
+ * every subsequent frame is blank with nothing in the log to say why.
25
+ */
26
+ export const MAX_STEP_SECONDS = 1 / 30;
27
+ export class Spring {
28
+ position;
29
+ velocity = 0;
30
+ constructor(initial = 0) {
31
+ this.position = Number.isFinite(initial) ? initial : 0;
32
+ }
33
+ /** The current value, without advancing time. */
34
+ get value() {
35
+ return this.position;
36
+ }
37
+ /**
38
+ * Advance by `dtSeconds` toward `target` and return the new value.
39
+ *
40
+ * Non-finite inputs are ignored rather than propagated: one NaN reaching the
41
+ * velocity would poison the spring permanently, and the symptom -- she simply
42
+ * stops moving, forever, from an arbitrary later frame -- points nowhere near
43
+ * the frame that caused it.
44
+ */
45
+ step(dtSeconds, target, config = DEFAULT_SPRING) {
46
+ if (!Number.isFinite(dtSeconds) || dtSeconds <= 0)
47
+ return this.position;
48
+ if (!Number.isFinite(target))
49
+ return this.position;
50
+ const dt = Math.min(dtSeconds, MAX_STEP_SECONDS);
51
+ const acceleration = -config.stiffness * (this.position - target) - config.damping * this.velocity;
52
+ this.velocity += acceleration * dt;
53
+ this.position += this.velocity * dt;
54
+ return this.position;
55
+ }
56
+ /** Jump to a value and kill the velocity. For teleports, not for animation. */
57
+ snap(value) {
58
+ if (!Number.isFinite(value))
59
+ return;
60
+ this.position = value;
61
+ this.velocity = 0;
62
+ }
63
+ /** True once the spring has effectively stopped, so a caller can skip work. */
64
+ isAtRest(target, epsilon = 1e-3) {
65
+ return Math.abs(this.position - target) < epsilon && Math.abs(this.velocity) < epsilon;
66
+ }
67
+ }
68
+ //# sourceMappingURL=spring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spring.js","sourceRoot":"","sources":["../../src/core/spring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,MAAM,CAAC,MAAM,cAAc,GAAiB,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;AAE3E;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,CAAA;AAEtC,MAAM,OAAO,MAAM;IACT,QAAQ,CAAQ;IAChB,QAAQ,GAAG,CAAC,CAAA;IAEpB,YAAY,OAAO,GAAG,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IACxD,CAAC;IAED,iDAAiD;IACjD,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,IAAI,CAAC,SAAiB,EAAE,MAAc,EAAE,MAAM,GAAiB,cAAc;QAC3E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAA;QAElD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAA;QAChD,MAAM,YAAY,GAChB,CAAC,MAAM,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAA;QAE/E,IAAI,CAAC,QAAQ,IAAI,YAAY,GAAG,EAAE,CAAA;QAClC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAA;QACnC,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;IAED,+EAA+E;IAC/E,IAAI,CAAC,KAAa;QAChB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAM;QACnC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAA;QACrB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;IACnB,CAAC;IAED,+EAA+E;IAC/E,QAAQ,CAAC,MAAc,EAAE,OAAO,GAAG,IAAI;QACrC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,GAAG,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAA;IACxF,CAAC;CACF"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The rendering contract.
3
+ *
4
+ * Implementations live in the renderer. There is exactly one, and the enum says
5
+ * so: a value nothing will ever return is a promise the type system keeps
6
+ * forcing call sites to honour. mochi used that argument to refuse a `live2d`
7
+ * member, then shipped a `vrm` one with no VRM backend behind it -- and this
8
+ * file inherited it. Removed. It comes back the day something returns it.
9
+ *
10
+ * There is no `placeholder` member either, and that absence is the decision.
11
+ * In the previous codebase the placeholder outlived the "real"
12
+ * backend it stood in for, because a third-party VRM cannot be redistributed
13
+ * and so no model ever shipped. A stand-in that is the only thing anyone ever
14
+ * sees is not a stand-in; naming it `mochi` is what stops it being treated as
15
+ * disposable.
16
+ */
17
+ export type AvatarKind = 'mochi';
18
+ /**
19
+ * The canonical emotion list.
20
+ *
21
+ * A tuple, not a bare union, because the tool schema in voice.ts needs the same
22
+ * values at runtime. Deriving both from here means adding an emotion cannot
23
+ * leave the schema stale -- which a hand-written array plus `satisfies` would
24
+ * allow, since `satisfies` rejects wrong entries but never notices missing ones.
25
+ */
26
+ export declare const EMOTIONS: readonly ['neutral', 'happy', 'shy', 'sad', 'angry', 'surprised', 'thinking', 'sleepy'];
27
+ export type Emotion = (typeof EMOTIONS)[number];
28
+ /**
29
+ * The canonical viseme set: VRM 1.0's five vowel presets.
30
+ *
31
+ * A tuple for the same reason EMOTIONS is one -- both the avatar contract and
32
+ * the provider's VisemeFrame need these values, and a bare string union would
33
+ * let a provider emit `ah` or `A` and have it silently ignored while every
34
+ * capability flag still claimed the precise lip-sync path was working.
35
+ *
36
+ * Consonants are deliberately absent. VRM defines no consonant presets, and
37
+ * inventing some would produce a set no model can render.
38
+ */
39
+ declare const VISEMES: readonly ['aa', 'ih', 'ou', 'ee', 'oh'];
40
+ export type Viseme = (typeof VISEMES)[number];
41
+ /** A complete snapshot. Coarticulation genuinely overlaps vowels, so these do not sum to 1. */
42
+ export type VisemeWeights = Readonly<Record<Viseme, number>>;
43
+ export interface EmotionSignal {
44
+ readonly emotion: Emotion;
45
+ /** 0..1, normalised through clamp01 -- non-finite becomes 0. */
46
+ readonly intensity: number;
47
+ /**
48
+ * Drift back to neutral after this many milliseconds. Finite and >= 0.
49
+ * Omitted means hold until the next signal.
50
+ *
51
+ * Each setEmotion() supersedes any pending reset, so a newer signal is never
52
+ * cut short by an older signal's timer.
53
+ */
54
+ readonly holdMs?: number;
55
+ }
56
+ /** Clamp to 0..1 with an explicit non-finite policy, so every backend agrees. */
57
+ export declare function clamp01(value: number): number;
58
+ /**
59
+ * Clamp to -1..1, for a signed OFFSET rather than a magnitude.
60
+ *
61
+ * Beside `clamp01` and deliberately not derived from it: the two answer
62
+ * different questions, and using the 0..1 one on a position is the whole of the
63
+ * gaze bug `AvatarBackend.lookAt` describes — centre came out as the top-left
64
+ * corner and half the input range collapsed onto it.
65
+ *
66
+ * Non-finite returns 0 here, which is CENTRE for a signed offset. Callers that
67
+ * must not move on a bad sample check finiteness before calling, as `lookAt`
68
+ * does; this is the floor rather than that policy.
69
+ */
70
+ export declare function clampSigned(value: number): number;
71
+ /**
72
+ * Differences a call site would otherwise have to branch on. Nothing goes in
73
+ * here speculatively -- each field marks a divergence that is already known.
74
+ *
75
+ * These describe the BACKEND, not whichever model happens to be loaded.
76
+ */
77
+ export interface AvatarBackendCaps {
78
+ /** Fixed expression names the format defines (VRM's Joy/Angry/...). */
79
+ readonly presetExpressions: boolean;
80
+ /** Arbitrary expression identifiers authored into the model. */
81
+ readonly customExpressions: boolean;
82
+ /** Whether load() can return non-null licence metadata. */
83
+ readonly licenseMetadata: boolean;
84
+ /** Whether the backend can run physics at all; a given model may still carry none. */
85
+ readonly supportsPhysics: boolean;
86
+ /** Whether playMotion() does anything. A backend with no motion library reports false. */
87
+ readonly supportsMotions: boolean;
88
+ /**
89
+ * Whether setVisemes() can drive anything at all.
90
+ *
91
+ * Independent of the provider's caps.phonemeTimings: timings are useless
92
+ * without a backend that can render them, and a backend that can render them
93
+ * sits idle behind a provider that emits none.
94
+ */
95
+ readonly visemes: boolean;
96
+ }
97
+ /**
98
+ * Milliseconds from a monotonic clock with the animation-frame time origin --
99
+ * i.e. what requestAnimationFrame passes and performance.now() returns. Never
100
+ * Date.now(), which can jump backwards.
101
+ */
102
+ export type MonotonicMs = number;
103
+ export interface AvatarBackend {
104
+ readonly kind: AvatarKind;
105
+ readonly caps: AvatarBackendCaps;
106
+ /**
107
+ * 0 = closed, 1 = fully open. Called every frame. Non-finite input is
108
+ * treated as 0 (see clamp01) rather than left to each backend to invent.
109
+ *
110
+ * The coarse path, and the only one available behind cloud
111
+ * speech-to-speech: an audio stream carries no phoneme timings, so the RMS
112
+ * envelope is not a shortcut there.
113
+ *
114
+ * Supersedes any viseme weights still applied. The two paths write the same
115
+ * mouth, so the last caller wins outright rather than blending -- a provider
116
+ * switch mid-session must not leave a stale vowel held open underneath.
117
+ */
118
+ setMouthOpen(value: number): void;
119
+ /**
120
+ * The precise path: a complete weight per canonical viseme, each clamped
121
+ * through clamp01.
122
+ *
123
+ * Total rather than partial: an omitted key would have to mean either "hold
124
+ * the previous value" or "zero", and a caller cannot tell which from the
125
+ * type. No-op when caps.visemes is false.
126
+ */
127
+ setVisemes(weights: VisemeWeights): void;
128
+ setEmotion(signal: EmotionSignal): void;
129
+ /** No-op when caps.supportsMotions is false. */
130
+ playMotion(name: string): void;
131
+ /**
132
+ * Gaze target as a SIGNED offset from centre: -1..1 on each axis, origin in
133
+ * the middle. Finite values are clamped to that range.
134
+ *
135
+ * ## The range is -1..1 because that is what every caller has always sent
136
+ *
137
+ * This said 0..1 with the origin at the top-left, and `MochiAvatar.lookAt`
138
+ * implemented it — `(clamp01(n) - 0.5) * 2` — while `face.ts` passed
139
+ * `(clientX / width) * 2 - 1` from every `mousemove`, `lookAt(0, 0)` to
140
+ * recentre on `mouseleave`, and `lookAt(0.35, -0.5)` for the thinking pose.
141
+ *
142
+ * Measured against the implementation, the whole left and top half of the
143
+ * pointer range collapsed onto one point:
144
+ *
145
+ * | caller meant | backend stored |
146
+ * | --- | --- |
147
+ * | centre `0` | **-1** — hard up-left |
148
+ * | left edge `-1` | -1, identical to centre |
149
+ * | right edge `1` | 1 |
150
+ *
151
+ * So she tracked the cursor only across the right and bottom half, and the
152
+ * `mouseleave` recentre — whose entire job is to put her eyes back to the
153
+ * middle — pinned them to the corner instead.
154
+ *
155
+ * The interface moved rather than the callers, because -1..1 is what the rest
156
+ * of the backend already used: `setAsleep` and the reset path both assign
157
+ * `gazeTarget = { x: 0, y: 0 }` DIRECTLY and mean centre by it, and the
158
+ * per-frame sum adds `look.gazeX` offsets that are signed. 0..1 was the only
159
+ * dissenting statement, and it was in the one place nothing executed.
160
+ *
161
+ * A non-finite coordinate leaves the previous target in place rather than
162
+ * being coerced to 0 -- clamp01's policy is right for a magnitude like mouth
163
+ * openness, but applying it to a position would snap her gaze to the corner
164
+ * on a single bad sample.
165
+ */
166
+ lookAt(nx: number, ny: number): void;
167
+ /** Idle animation: breathing, blinking, micro-motion. All of it stops when off. */
168
+ setIdle(on: boolean): void;
169
+ /**
170
+ * Is this point on the avatar? CSS pixels from the window's top-left.
171
+ *
172
+ * Drives per-region click-through, so it must agree with what is actually
173
+ * drawn. A backend that reports hits on pixels it does not paint makes the
174
+ * window swallow clicks over apparently empty space.
175
+ */
176
+ hitTest(x: number, y: number): boolean;
177
+ render(now: MonotonicMs): void;
178
+ /** Idempotent. Clears what was drawn and stops responding to hit tests. */
179
+ dispose(): void;
180
+ }
181
+ export {};
182
+ //# sourceMappingURL=vocabulary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vocabulary.d.ts","sourceRoot":"","sources":["../../src/core/vocabulary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,UAAU,GAAG,OAAO,CAAA;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,YACnB,SAAS,EACT,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,EACV,QAAQ,CACA,CAAA;AAEV,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/C;;;;;;;;;;GAUG;AACH,QAAA,MAAM,OAAO,YAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAEvD,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7C,+FAA+F;AAC/F,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAA;IACnC,gEAAgE;IAChE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAA;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,sFAAsF;IACtF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,0FAA0F;IAC1F,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAA;AAEhC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;IAEhC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAEjC;;;;;;;OAOG;IACH,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAExC,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAEvC,gDAAgD;IAChD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IAEpC,mFAAmF;IACnF,OAAO,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAE1B;;;;;;OAMG;IACH,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAEtC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAA;IAE9B,2EAA2E;IAC3E,OAAO,IAAI,IAAI,CAAA;CAChB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The rendering contract.
3
+ *
4
+ * Implementations live in the renderer. There is exactly one, and the enum says
5
+ * so: a value nothing will ever return is a promise the type system keeps
6
+ * forcing call sites to honour. mochi used that argument to refuse a `live2d`
7
+ * member, then shipped a `vrm` one with no VRM backend behind it -- and this
8
+ * file inherited it. Removed. It comes back the day something returns it.
9
+ *
10
+ * There is no `placeholder` member either, and that absence is the decision.
11
+ * In the previous codebase the placeholder outlived the "real"
12
+ * backend it stood in for, because a third-party VRM cannot be redistributed
13
+ * and so no model ever shipped. A stand-in that is the only thing anyone ever
14
+ * sees is not a stand-in; naming it `mochi` is what stops it being treated as
15
+ * disposable.
16
+ */
17
+ /**
18
+ * The canonical emotion list.
19
+ *
20
+ * A tuple, not a bare union, because the tool schema in voice.ts needs the same
21
+ * values at runtime. Deriving both from here means adding an emotion cannot
22
+ * leave the schema stale -- which a hand-written array plus `satisfies` would
23
+ * allow, since `satisfies` rejects wrong entries but never notices missing ones.
24
+ */
25
+ export const EMOTIONS = [
26
+ 'neutral',
27
+ 'happy',
28
+ 'shy',
29
+ 'sad',
30
+ 'angry',
31
+ 'surprised',
32
+ 'thinking',
33
+ 'sleepy',
34
+ ];
35
+ /**
36
+ * The canonical viseme set: VRM 1.0's five vowel presets.
37
+ *
38
+ * A tuple for the same reason EMOTIONS is one -- both the avatar contract and
39
+ * the provider's VisemeFrame need these values, and a bare string union would
40
+ * let a provider emit `ah` or `A` and have it silently ignored while every
41
+ * capability flag still claimed the precise lip-sync path was working.
42
+ *
43
+ * Consonants are deliberately absent. VRM defines no consonant presets, and
44
+ * inventing some would produce a set no model can render.
45
+ */
46
+ const VISEMES = ['aa', 'ih', 'ou', 'ee', 'oh'];
47
+ /** Clamp to 0..1 with an explicit non-finite policy, so every backend agrees. */
48
+ export function clamp01(value) {
49
+ return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
50
+ }
51
+ /**
52
+ * Clamp to -1..1, for a signed OFFSET rather than a magnitude.
53
+ *
54
+ * Beside `clamp01` and deliberately not derived from it: the two answer
55
+ * different questions, and using the 0..1 one on a position is the whole of the
56
+ * gaze bug `AvatarBackend.lookAt` describes — centre came out as the top-left
57
+ * corner and half the input range collapsed onto it.
58
+ *
59
+ * Non-finite returns 0 here, which is CENTRE for a signed offset. Callers that
60
+ * must not move on a bad sample check finiteness before calling, as `lookAt`
61
+ * does; this is the floor rather than that policy.
62
+ */
63
+ export function clampSigned(value) {
64
+ return Number.isFinite(value) ? Math.min(1, Math.max(-1, value)) : 0;
65
+ }
66
+ //# sourceMappingURL=vocabulary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/core/vocabulary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,SAAS;IACT,OAAO;IACP,KAAK;IACL,KAAK;IACL,OAAO;IACP,WAAW;IACX,UAAU;IACV,QAAQ;CACA,CAAA;AAIV;;;;;;;;;;GAUG;AACH,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAqBvD,iFAAiF;AACjF,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACrE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACtE,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `<dough-avatar>` — the engine as a custom element.
3
+ *
4
+ * The point of this file is that using the character engine from a plain HTML
5
+ * page should not require a bundler, a framework, or knowing that a render loop
6
+ * exists. Everything below is the boilerplate a host would otherwise write:
7
+ * a canvas sized to the element at device resolution, a rAF loop that stops
8
+ * when the element leaves the document, and attributes wired to the backend.
9
+ *
10
+ * **Browser only.** Importing this module evaluates `class ... extends
11
+ * HTMLElement`, which throws in a bare Node process — so it is a SEPARATE entry
12
+ * point (`@hando/dough/element`) that server-side code must not import. The
13
+ * core is meant to run under Node and does; this is not.
14
+ */
15
+ import { type FaceSpec } from '../core/spec.js';
16
+ export declare class DoughAvatarElement extends HTMLElement {
17
+ static observedAttributes: string[];
18
+ private canvas;
19
+ private avatar;
20
+ private frame;
21
+ private observer;
22
+ private face;
23
+ connectedCallback(): void;
24
+ disconnectedCallback(): void;
25
+ attributeChangedCallback(name: string): void;
26
+ /** The face, as a `FaceSpec`. Set this instead of the attribute when you have one. */
27
+ set faceSpec(face: FaceSpec | undefined);
28
+ get faceSpec(): FaceSpec | undefined;
29
+ private applyFace;
30
+ private applyEmotion;
31
+ private applySize;
32
+ private fitToElement;
33
+ }
34
+ //# sourceMappingURL=dough-avatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dough-avatar.d.ts","sourceRoot":"","sources":["../../src/element/dough-avatar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAmB9D,qBAAa,kBAAmB,SAAQ,WAAW;IACjD,MAAM,CAAC,kBAAkB,WAA8B;IAEvD,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,IAAI,CAAsB;IAElC,iBAAiB,IAAI,IAAI,CAsDxB;IAED,oBAAoB,IAAI,IAAI,CAS3B;IAED,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAO3C;IAED,sFAAsF;IACtF,IAAI,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAQtC;IAED,IAAI,QAAQ,IAAI,QAAQ,GAAG,SAAS,CAEnC;IAED,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,YAAY;CA2BrB"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `<dough-avatar>` — the engine as a custom element.
3
+ *
4
+ * The point of this file is that using the character engine from a plain HTML
5
+ * page should not require a bundler, a framework, or knowing that a render loop
6
+ * exists. Everything below is the boilerplate a host would otherwise write:
7
+ * a canvas sized to the element at device resolution, a rAF loop that stops
8
+ * when the element leaves the document, and attributes wired to the backend.
9
+ *
10
+ * **Browser only.** Importing this module evaluates `class ... extends
11
+ * HTMLElement`, which throws in a bare Node process — so it is a SEPARATE entry
12
+ * point (`@hando/dough/element`) that server-side code must not import. The
13
+ * core is meant to run under Node and does; this is not.
14
+ */
15
+ import { DoughAvatar } from '../canvas2d/avatar.js';
16
+ import { parseFaceSpec } from '../core/spec.js';
17
+ import { PLAIN } from '../core/plain.js';
18
+ import { EMOTIONS } from '../core/vocabulary.js';
19
+ function isEmotion(value) {
20
+ return value !== null && EMOTIONS.includes(value);
21
+ }
22
+ /**
23
+ * `size`, as the backend takes it: a percentage, or the string that means
24
+ * "fill whatever canvas you were given". Anything unparseable is the latter,
25
+ * which is also what an absent attribute means.
26
+ */
27
+ function sizeFrom(raw) {
28
+ if (raw === null || raw.trim() === '' || raw.trim() === 'fit-canvas')
29
+ return 'fit-canvas';
30
+ const percent = Number(raw);
31
+ return Number.isFinite(percent) ? percent : 'fit-canvas';
32
+ }
33
+ export class DoughAvatarElement extends HTMLElement {
34
+ static observedAttributes = ['emotion', 'size', 'face'];
35
+ canvas = null;
36
+ avatar = null;
37
+ frame = 0;
38
+ observer = null;
39
+ face;
40
+ connectedCallback() {
41
+ /*
42
+ Capabilities FIRST, before anything is created or appended.
43
+
44
+ Checked up front rather than discovered halfway through: initialising in
45
+ order meant a missing `ResizeObserver` threw after the avatar existed and
46
+ a missing `requestAnimationFrame` threw after the observer was attached,
47
+ each leaving a half-built element with no teardown path. Nothing is built
48
+ until everything needed to tear it down is known to exist.
49
+ */
50
+ if (typeof ResizeObserver === 'undefined' ||
51
+ typeof requestAnimationFrame !== 'function' ||
52
+ typeof cancelAnimationFrame !== 'function') {
53
+ return;
54
+ }
55
+ if (this.canvas === null) {
56
+ const canvas = document.createElement('canvas');
57
+ canvas.style.width = '100%';
58
+ canvas.style.height = '100%';
59
+ canvas.style.display = 'block';
60
+ this.canvas = canvas;
61
+ }
62
+ // A 2D context can genuinely be refused — a lost GPU, or too many live
63
+ // canvases. Returning before the canvas is appended leaves the element
64
+ // empty rather than showing a blank rectangle that looks like a bug.
65
+ const ctx = this.canvas.getContext('2d');
66
+ if (ctx === null)
67
+ return;
68
+ if (this.style.display === '')
69
+ this.style.display = 'block';
70
+ if (!this.canvas.isConnected)
71
+ this.append(this.canvas);
72
+ this.avatar = new DoughAvatar(ctx, {
73
+ // Spread rather than `face: this.face`: under `exactOptionalPropertyTypes`
74
+ // an explicit `undefined` is not the same as an absent key, and the
75
+ // engine's default only applies to the absent one.
76
+ ...(this.face === undefined ? {} : { face: this.face }),
77
+ size: sizeFrom(this.getAttribute('size')),
78
+ });
79
+ this.applyEmotion();
80
+ this.fitToElement();
81
+ // Re-measure on layout change rather than on window resize: the element can be
82
+ // resized by its container without the window changing at all.
83
+ this.observer = new ResizeObserver(() => this.fitToElement());
84
+ this.observer.observe(this);
85
+ const tick = (now) => {
86
+ this.avatar?.render(now);
87
+ this.frame = requestAnimationFrame(tick);
88
+ };
89
+ this.frame = requestAnimationFrame(tick);
90
+ }
91
+ disconnectedCallback() {
92
+ // Both, and neither is optional: a live rAF loop keeps the element and its
93
+ // canvas reachable, so an element removed from the document would go on
94
+ // painting into a detached canvas forever.
95
+ if (typeof cancelAnimationFrame === 'function')
96
+ cancelAnimationFrame(this.frame);
97
+ this.frame = 0;
98
+ this.observer?.disconnect();
99
+ this.observer = null;
100
+ this.avatar = null;
101
+ }
102
+ attributeChangedCallback(name) {
103
+ // Read back through `getAttribute` rather than taking the new value: that
104
+ // makes removal — which arrives as null — the same code path as a change,
105
+ // and every one of these has a defined meaning for "absent".
106
+ if (name === 'emotion')
107
+ this.applyEmotion();
108
+ if (name === 'face')
109
+ this.applyFace();
110
+ if (name === 'size')
111
+ this.applySize();
112
+ }
113
+ /** The face, as a `FaceSpec`. Set this instead of the attribute when you have one. */
114
+ set faceSpec(face) {
115
+ this.face = face;
116
+ // Cleared means "back to the engine default", applied to the LIVE avatar
117
+ // too. Clearing only the stored copy left the current face on screen until
118
+ // the next reconnect, at which point it silently changed — the same input
119
+ // producing two different results depending on DOM history.
120
+ if (this.avatar === null)
121
+ return;
122
+ this.avatar.setFace(face ?? PLAIN);
123
+ }
124
+ get faceSpec() {
125
+ return this.face;
126
+ }
127
+ applyFace() {
128
+ const raw = this.getAttribute('face');
129
+ if (raw === null) {
130
+ this.faceSpec = undefined;
131
+ return;
132
+ }
133
+ let value;
134
+ try {
135
+ value = JSON.parse(raw);
136
+ }
137
+ catch {
138
+ // A malformed attribute leaves the current face alone rather than
139
+ // throwing inside a lifecycle callback, where nothing can catch it.
140
+ return;
141
+ }
142
+ const result = parseFaceSpec(value);
143
+ if (result.ok)
144
+ this.faceSpec = result.face;
145
+ }
146
+ applyEmotion() {
147
+ const wanted = this.getAttribute('emotion');
148
+ // Absent, or a word this engine does not have, both mean neutral. Leaving
149
+ // the previous emotion running would make `removeAttribute` do nothing.
150
+ this.avatar?.setEmotion({
151
+ emotion: isEmotion(wanted) ? wanted : 'neutral',
152
+ intensity: isEmotion(wanted) ? 1 : 0,
153
+ });
154
+ }
155
+ applySize() {
156
+ this.avatar?.setSizePercent(sizeFrom(this.getAttribute('size')));
157
+ this.fitToElement();
158
+ }
159
+ fitToElement() {
160
+ const canvas = this.canvas;
161
+ if (canvas === null || this.avatar === null)
162
+ return;
163
+ const rect = this.getBoundingClientRect();
164
+ if (rect.width === 0 || rect.height === 0)
165
+ return;
166
+ const ratio = window.devicePixelRatio || 1;
167
+ /*
168
+ The BACKING STORE, not only the geometry.
169
+
170
+ `resize` tells the avatar how big she is in CSS pixels; nothing in the
171
+ engine touches `canvas.width`, because the engine does not own the canvas.
172
+ Without these two lines the bitmap stays at its 300x150 default forever —
173
+ a 200x200 element at ratio 2 asked for 400x400 and got a 300x150 buffer,
174
+ so she was drawn clipped and then stretched by the browser to fit.
175
+
176
+ Assigned only on change: writing `canvas.width` clears the canvas and
177
+ resets the context state even when the value is identical, which would
178
+ throw away the frame on every single resize callback.
179
+ */
180
+ const width = Math.max(1, Math.round(rect.width * ratio));
181
+ const height = Math.max(1, Math.round(rect.height * ratio));
182
+ if (canvas.width !== width)
183
+ canvas.width = width;
184
+ if (canvas.height !== height)
185
+ canvas.height = height;
186
+ this.avatar.resize(rect.width, rect.height, ratio);
187
+ }
188
+ }
189
+ //# sourceMappingURL=dough-avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dough-avatar.js","sourceRoot":"","sources":["../../src/element/dough-avatar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { DoughAvatarElement } from './dough-avatar.js';
2
+ export declare function defineDoughAvatar(tag?: string): void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/element/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAatD,wBAAgB,iBAAiB,CAAC,GAAG,SAAiB,GAAG,IAAI,CAS5D"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Drop-in registration. `import '@hando/dough/element'` and the tag works.
3
+ *
4
+ * **Browser only** — see the note in `./dough-avatar`. This module is listed in
5
+ * the package's `sideEffects` array precisely because it has one: importing it
6
+ * registers the element, and a blanket `"sideEffects": false` let bundlers drop
7
+ * the documented bare import entirely and emit nothing.
8
+ */
9
+ import { DoughAvatarElement } from './dough-avatar.js';
10
+ export { DoughAvatarElement } from './dough-avatar.js';
11
+ /**
12
+ * Whether the base class has been handed to `customElements` yet.
13
+ *
14
+ * A constructor may be registered exactly ONCE. Registering the same class
15
+ * under a second tag throws `NotSupportedError`, so checking only whether the
16
+ * requested NAME is free is not enough — after the automatic registration
17
+ * below, every explicit `defineDoughAvatar('my-avatar')` hit that error.
18
+ * Additional names therefore get their own trivial subclass.
19
+ */
20
+ let baseRegistered = false;
21
+ export function defineDoughAvatar(tag = 'dough-avatar') {
22
+ if (typeof customElements === 'undefined')
23
+ return;
24
+ if (customElements.get(tag) !== undefined)
25
+ return;
26
+ if (baseRegistered) {
27
+ customElements.define(tag, class extends DoughAvatarElement {
28
+ });
29
+ return;
30
+ }
31
+ customElements.define(tag, DoughAvatarElement);
32
+ baseRegistered = true;
33
+ }
34
+ defineDoughAvatar();
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/element/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD;;;;;;;;GAQG;AACH,IAAI,cAAc,GAAG,KAAK,CAAA;AAE1B,MAAM,UAAU,iBAAiB,CAAC,GAAG,GAAG,cAAc;IACpD,IAAI,OAAO,cAAc,KAAK,WAAW;QAAE,OAAM;IACjD,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS;QAAE,OAAM;IACjD,IAAI,cAAc,EAAE,CAAC;QACnB,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,KAAM,SAAQ,kBAAkB;SAAG,CAAC,CAAA;QAC/D,OAAM;IACR,CAAC;IACD,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAA;IAC9C,cAAc,GAAG,IAAI,CAAA;AACvB,CAAC;AAED,iBAAiB,EAAE,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A soft-body companion character engine.
3
+ *
4
+ * The shape is a superellipse ovoid with independent exponents above and below
5
+ * its widest point; it squashes area-preservingly, leans by a shear pinned at
6
+ * its contact point, breathes on a one-sided curve and settles on a
7
+ * second-order spring. A face rides on it at a configurable grip, so it deforms
8
+ * with the body without being printed on it.
9
+ *
10
+ * Everything in `core` is pure arithmetic returning points and numbers. Nothing
11
+ * there imports a canvas, a document, or a platform. `canvas2d` turns those
12
+ * points into fills on any 2D context — a browser canvas, an OffscreenCanvas in
13
+ * a worker, or `@napi-rs/canvas` under Node. `svg` emits the same silhouette as
14
+ * a vector, so an icon pipeline and the running app cannot disagree about the
15
+ * shape.
16
+ *
17
+ * A face is DATA — see `FaceSpec` — so a design is a JSON file rather than a
18
+ * fork. `PLAIN` is the default; it is intended to be replaced.
19
+ */
20
+ export * from './core/spec.js';
21
+ export * from './core/plain.js';
22
+ export * from './core/vocabulary.js';
23
+ export * from './core/layout.js';
24
+ export * from './core/geometry.js';
25
+ export * from './core/lens.js';
26
+ export * from './core/looks.js';
27
+ export * from './core/idle.js';
28
+ export * from './core/spring.js';
29
+ export * from './core/motion.js';
30
+ export * from './core/envelope.js';
31
+ export * from './core/mouth.js';
32
+ export * from './core/colour.js';
33
+ export * from './canvas2d/avatar.js';
34
+ export * from './canvas2d/face.js';
35
+ export * from './canvas2d/paths.js';
36
+ export * from './svg/silhouette.js';
37
+ export * from './characters/index.js';
38
+ //# sourceMappingURL=index.d.ts.map