@voqalize/avatar 0.4.4 → 0.5.1

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 (180) hide show
  1. package/README.md +65 -154
  2. package/dist/index.d.ts +37 -26
  3. package/dist/index.js +48 -25
  4. package/dist/pin.d.ts +2 -0
  5. package/dist/pin.js +3 -0
  6. package/dist/react.d.ts +21 -6
  7. package/dist/react.js +24 -6
  8. package/package.json +8 -67
  9. package/LICENSE-APACHE-2.0 +0 -201
  10. package/LICENSE-CC-BY-4.0 +0 -396
  11. package/assets/README.md +0 -60
  12. package/assets/tanvi.glb +0 -0
  13. package/assets/tanya.glb +0 -0
  14. package/assets/tara.glb +0 -0
  15. package/assets/tess.glb +0 -0
  16. package/assets/tushar.glb +0 -0
  17. package/client/Avatar.tsx +0 -57
  18. package/client/AvatarClient.ts +0 -818
  19. package/client/createAvatar.ts +0 -177
  20. package/client/index.ts +0 -44
  21. package/client/internal.ts +0 -132
  22. package/client/playout.ts +0 -95
  23. package/client/react.ts +0 -13
  24. package/client/supports.ts +0 -27
  25. package/client/three/assets.ts +0 -26
  26. package/client/three/budgets.ts +0 -27
  27. package/client/three/character-rig.ts +0 -1746
  28. package/client/three/createCharacter.ts +0 -109
  29. package/client/three/holds.ts +0 -69
  30. package/client/three/internal.ts +0 -55
  31. package/client/three/motion-limits.json +0 -124
  32. package/client/three/sequences.ts +0 -281
  33. package/client/three/tanvi-asset.ts +0 -6
  34. package/client/three/tanvi.ts +0 -30
  35. package/client/three/tanya-asset.ts +0 -6
  36. package/client/three/tanya.ts +0 -29
  37. package/client/three/tara-asset.ts +0 -25
  38. package/client/three/tara.ts +0 -36
  39. package/client/three/tess-asset.ts +0 -6
  40. package/client/three/tess.ts +0 -30
  41. package/client/three/tushar-asset.ts +0 -6
  42. package/client/three/tushar.ts +0 -28
  43. package/client/types.ts +0 -235
  44. package/client/useAvatar.ts +0 -74
  45. package/dist/Avatar.d.ts +0 -44
  46. package/dist/Avatar.d.ts.map +0 -1
  47. package/dist/Avatar.js +0 -7
  48. package/dist/Avatar.js.map +0 -1
  49. package/dist/AvatarClient.d.ts +0 -274
  50. package/dist/AvatarClient.d.ts.map +0 -1
  51. package/dist/AvatarClient.js +0 -712
  52. package/dist/AvatarClient.js.map +0 -1
  53. package/dist/createAvatar.d.ts +0 -135
  54. package/dist/createAvatar.d.ts.map +0 -1
  55. package/dist/createAvatar.js +0 -75
  56. package/dist/createAvatar.js.map +0 -1
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.js.map +0 -1
  59. package/dist/internal.d.ts +0 -34
  60. package/dist/internal.d.ts.map +0 -1
  61. package/dist/internal.js +0 -56
  62. package/dist/internal.js.map +0 -1
  63. package/dist/playout.d.ts +0 -42
  64. package/dist/playout.d.ts.map +0 -1
  65. package/dist/playout.js +0 -85
  66. package/dist/playout.js.map +0 -1
  67. package/dist/react.d.ts.map +0 -1
  68. package/dist/react.js.map +0 -1
  69. package/dist/supports.d.ts +0 -19
  70. package/dist/supports.d.ts.map +0 -1
  71. package/dist/supports.js +0 -25
  72. package/dist/supports.js.map +0 -1
  73. package/dist/three/assets.d.ts +0 -10
  74. package/dist/three/assets.d.ts.map +0 -1
  75. package/dist/three/assets.js +0 -23
  76. package/dist/three/assets.js.map +0 -1
  77. package/dist/three/budgets.d.ts +0 -17
  78. package/dist/three/budgets.d.ts.map +0 -1
  79. package/dist/three/budgets.js +0 -21
  80. package/dist/three/budgets.js.map +0 -1
  81. package/dist/three/character-rig.d.ts +0 -366
  82. package/dist/three/character-rig.d.ts.map +0 -1
  83. package/dist/three/character-rig.js +0 -1644
  84. package/dist/three/character-rig.js.map +0 -1
  85. package/dist/three/createCharacter.d.ts +0 -60
  86. package/dist/three/createCharacter.d.ts.map +0 -1
  87. package/dist/three/createCharacter.js +0 -84
  88. package/dist/three/createCharacter.js.map +0 -1
  89. package/dist/three/holds.d.ts +0 -38
  90. package/dist/three/holds.d.ts.map +0 -1
  91. package/dist/three/holds.js +0 -61
  92. package/dist/three/holds.js.map +0 -1
  93. package/dist/three/internal.d.ts +0 -52
  94. package/dist/three/internal.d.ts.map +0 -1
  95. package/dist/three/internal.js +0 -49
  96. package/dist/three/internal.js.map +0 -1
  97. package/dist/three/motion-limits.json +0 -124
  98. package/dist/three/sequences.d.ts +0 -71
  99. package/dist/three/sequences.d.ts.map +0 -1
  100. package/dist/three/sequences.js +0 -262
  101. package/dist/three/sequences.js.map +0 -1
  102. package/dist/three/tanvi-asset.d.ts +0 -7
  103. package/dist/three/tanvi-asset.d.ts.map +0 -1
  104. package/dist/three/tanvi-asset.js +0 -7
  105. package/dist/three/tanvi-asset.js.map +0 -1
  106. package/dist/three/tanvi.d.ts +0 -24
  107. package/dist/three/tanvi.d.ts.map +0 -1
  108. package/dist/three/tanvi.js +0 -24
  109. package/dist/three/tanvi.js.map +0 -1
  110. package/dist/three/tanya-asset.d.ts +0 -7
  111. package/dist/three/tanya-asset.d.ts.map +0 -1
  112. package/dist/three/tanya-asset.js +0 -7
  113. package/dist/three/tanya-asset.js.map +0 -1
  114. package/dist/three/tanya.d.ts +0 -23
  115. package/dist/three/tanya.d.ts.map +0 -1
  116. package/dist/three/tanya.js +0 -23
  117. package/dist/three/tanya.js.map +0 -1
  118. package/dist/three/tara-asset.d.ts +0 -26
  119. package/dist/three/tara-asset.d.ts.map +0 -1
  120. package/dist/three/tara-asset.js +0 -26
  121. package/dist/three/tara-asset.js.map +0 -1
  122. package/dist/three/tara.d.ts +0 -30
  123. package/dist/three/tara.d.ts.map +0 -1
  124. package/dist/three/tara.js +0 -30
  125. package/dist/three/tara.js.map +0 -1
  126. package/dist/three/tess-asset.d.ts +0 -7
  127. package/dist/three/tess-asset.d.ts.map +0 -1
  128. package/dist/three/tess-asset.js +0 -7
  129. package/dist/three/tess-asset.js.map +0 -1
  130. package/dist/three/tess.d.ts +0 -24
  131. package/dist/three/tess.d.ts.map +0 -1
  132. package/dist/three/tess.js +0 -24
  133. package/dist/three/tess.js.map +0 -1
  134. package/dist/three/tushar-asset.d.ts +0 -7
  135. package/dist/three/tushar-asset.d.ts.map +0 -1
  136. package/dist/three/tushar-asset.js +0 -7
  137. package/dist/three/tushar-asset.js.map +0 -1
  138. package/dist/three/tushar.d.ts +0 -22
  139. package/dist/three/tushar.d.ts.map +0 -1
  140. package/dist/three/tushar.js +0 -22
  141. package/dist/three/tushar.js.map +0 -1
  142. package/dist/types.d.ts +0 -149
  143. package/dist/types.d.ts.map +0 -1
  144. package/dist/types.js +0 -140
  145. package/dist/types.js.map +0 -1
  146. package/dist/useAvatar.d.ts +0 -39
  147. package/dist/useAvatar.d.ts.map +0 -1
  148. package/dist/useAvatar.js +0 -35
  149. package/dist/useAvatar.js.map +0 -1
  150. package/src/avatar.d.ts +0 -436
  151. package/src/avatar.js +0 -1378
  152. package/src/behavior.d.ts +0 -42
  153. package/src/behavior.js +0 -114
  154. package/src/camera.js +0 -29
  155. package/src/clips.js +0 -155
  156. package/src/conformance.js +0 -119
  157. package/src/emotions.js +0 -58
  158. package/src/face-core.js +0 -180
  159. package/src/face-myna.d.ts +0 -7
  160. package/src/face-myna.js +0 -786
  161. package/src/face-peep-control-plane.js +0 -167
  162. package/src/face-peep.d.ts +0 -7
  163. package/src/face-peep.js +0 -925
  164. package/src/face-wren.d.ts +0 -7
  165. package/src/face-wren.js +0 -468
  166. package/src/faces.d.ts +0 -12
  167. package/src/faces.js +0 -53
  168. package/src/gaze.js +0 -504
  169. package/src/hand.js +0 -709
  170. package/src/head.js +0 -134
  171. package/src/idle.js +0 -668
  172. package/src/interjections.js +0 -747
  173. package/src/line-art.js +0 -137
  174. package/src/params.js +0 -180
  175. package/src/perform.js +0 -107
  176. package/src/prosody.js +0 -741
  177. package/src/rig.d.ts +0 -31
  178. package/src/rig.js +0 -32
  179. package/src/speech-timing.js +0 -51
  180. package/src/visemes.js +0 -410
package/src/face-myna.js DELETED
@@ -1,786 +0,0 @@
1
- /**
2
- * Avatar: "myna".
3
- *
4
- * The third line-art character, authored against a user-supplied reference
5
- * asset (a raster illustration): a young contemporary Indian woman — wavy
6
- * shoulder-length dark hair with paper highlight slashes, orange hoop
7
- * earrings, a white crew tee under an open overshirt. Hand-authored geometry
8
- * with the reference on screen; nothing is traced. Same idiom as peep and
9
- * wren — no strokes anywhere, every mark a filled taper — and the same
10
- * 30-channel contract underneath.
11
- *
12
- * What is deliberately different from the siblings, and why:
13
- * - THE HAIR IS A HORSESHOE, NOT A CLOUD. One closed loop: wavy outer
14
- * silhouette down to pointed lock tips IN FRONT of both shoulders, then the
15
- * inner edge back up the face, joined across the forehead by the fringe
16
- * sweep. The enclosed "hole" (face, neck, chest V) opens downward between
17
- * the two lock groups, so the region needs no fill-rule tricks.
18
- * - THE EARS SIT ON THE HAIR. In the reference the mass runs behind the ears,
19
- * so ears and hoops are drawn in the hair layer, after the mass. The
20
- * head-layer underlay (same loop at head parallax) backfills the ±3-unit
21
- * slide between the two layers under yaw — same insurance as peep's, doing
22
- * one extra job here.
23
- * - THE OVERSHIRT IS TWO PAPER PANELS over a paper tee, distinguished only by
24
- * ink outline — no fill colour on the garment. The tee still runs
25
- * edge-to-edge under both panels and everything overshoots the frame.
26
- * - THE MOUTH HAS LIPS. The proven aperture-contour model, but the ring's
27
- * width profile carries a cupid's-bow dip at the top centre and a fuller
28
- * lower lip. (An under-lip shadow mark existed through v7; it was deleted —
29
- * a mark inside the deformation zone either rides every open perfectly or
30
- * it is a defect, and its ink went to the lip band instead.)
31
- *
32
- * v8 calibration note: the expressive gains here (brow travel, corner lift,
33
- * squint, lip weights, the lid mapping) are authored against a 130 px
34
- * DOWNSAMPLE of the rest/emotion renders, not the close-up — this face ships
35
- * in a call tile. Slightly theatrical at full size is correct.
36
- *
37
- * Like peep, there is no drawn jaw line to drop, so `jaw` has nothing to
38
- * move — same documented character decision.
39
- */
40
-
41
- import { clamp, lerp } from './params.js';
42
- import {
43
- f, createFaceShell, faceApi, poseTransforms, pairedTeeth,
44
- } from './face-core.js';
45
- import { taper, taperRing, region } from './line-art.js';
46
- import { viewBoxForHead } from './camera.js';
47
-
48
- export const THEME = {
49
- ink: '#191919',
50
- paper: '#ffffff',
51
- // The hoop earrings' orange. ~9% duskier than the asset's sample: at tile
52
- // size the sampled value out-shouted the face.
53
- accent: '#dc8432',
54
- mouthIn: '#191919',
55
- teeth: '#ffffff',
56
- tongue: '#8d7f79',
57
- };
58
-
59
- // --- reshape ------------------------------------------------------------
60
- // Same move as peep, for the same reason: built next to the Canvas2D
61
- // professional identities, this head read tall and narrow — 285 units wide in
62
- // a 907-unit 4:3 frame — where the canvas avatars were calibrated at call-tile
63
- // size from the start. One static, non-animated transform on the head cluster
64
- // only (skull, hair, features — never neck or body), pivoted at CHIN_Y so the
65
- // chin's own silhouette point does not move and the jaw-to-collar join needs
66
- // no rework. Every landmark constant below still describes the original art
67
- // space. See face-peep.js for the full derivation, including why "broader"
68
- // here means a tighter 4:3 crop rather than a wider aspect.
69
- const CX = 380;
70
- const CHIN_Y = 572;
71
- const RESHAPE_SX = 1.10;
72
- const RESHAPE_SY = 0.88;
73
- const RESHAPE = `translate(${CX} ${CHIN_Y}) scale(${RESHAPE_SX} ${RESHAPE_SY}) translate(${-CX} ${-CHIN_Y})`;
74
-
75
- // The outer wave of hair, rather than HEAD_TOP under it, is the crown a viewer
76
- // measures. The chin is the lowest point of the resting jaw silhouette.
77
- // crownY is the reshaped crown: CHIN_Y + (96 − CHIN_Y) × RESHAPE_SY, worked out
78
- // by hand because the camera reads landmarks, not the transform above it.
79
- const FRAME = { centerX: CX, crownY: 153, chinY: CHIN_Y };
80
- const VB = viewBoxForHead(FRAME);
81
-
82
- export const META = {
83
- viewBox: { x: VB.x, y: VB.y, w: VB.w, h: VB.h },
84
- // The original crop (298, 436, 164, 100) with its corners carried through
85
- // RESHAPE — a camera change does not move a path, but the reshape is
86
- // geometry rather than camera, so the inspection crop has to follow it.
87
- mouthCrop: { x: 290, y: 452, w: 180, h: 88 },
88
- };
89
-
90
- // --- landmarks --------------------------------------------------------------
91
- // These are the NATIVE art space and RESHAPE never enters them: it is a
92
- // wrapper on the rendered group, so `apply()` and every number below still
93
- // mean what they meant before it existed.
94
- const HEAD_TOP = 150;
95
-
96
- // rx widened from 15 — the bean was very nearly round, and cutting an almond
97
- // aperture from it without an absurd lid line needed the same headroom peep's
98
- // eye needed. See the eyes section.
99
- const EYE = { y: 386, dx: 56, rx: 17, ry: 16 };
100
- const MOUTH = { cx: CX, cy: 492 };
101
- const MOUTH_APERTURE = 36;
102
-
103
- // Brows: bold, long and clearly arched, with real air between them — the
104
- // reference's brows are its strongest single mark. The rest shape is a
105
- // gentle taper-arc: v7's outer third dropped 17 units in 15 and over a
106
- // resting smile the kink composited into a smirk that curdled across a long
107
- // session. The kink is still available — as a browAngle POSE.
108
- const BROW_L = [[CX - 22, 340], [CX - 40, 327], [CX - 60, 320], [CX - 76, 319],
109
- [CX - 88, 322], [CX - 97, 327], [CX - 103, 332]];
110
- // Near-mirrored: the right arch rides 1 unit higher (personality, small
111
- // enough to survive the mirror-flip levelness check, which is judged on the
112
- // eye lines). Anything bigger read as a baked-in head tilt — the mixer's
113
- // headRoll adds on top of the art and nothing can subtract a drawn lean.
114
- const BROW_R = [[CX + 22, 340], [CX + 40, 326], [CX + 60, 319], [CX + 76, 318],
115
- [CX + 88, 321], [CX + 97, 327], [CX + 103, 332]];
116
-
117
- // ---------------------------------------------------------------------------
118
- // Static art: head.
119
- //
120
- // Same three-segment jaw rule as every head here, but the runs are the softest
121
- // of the three rigs: rounded gonion, full cheek, small chin pad slightly off
122
- // the midline. Chin at 568 against wren's 572 and peep's 597 — the shorter
123
- // lower face is most of the "mid twenties" read.
124
- // ---------------------------------------------------------------------------
125
- const HEAD = [
126
- [CX, HEAD_TOP],
127
- [452, 149], [518, 206], [522, 288],
128
- [525, 338], [522, 386], [518, 424],
129
- [516, 456], [510, 482], [498, 508],
130
- [484, 530], [452, 556], [422, 566],
131
- [404, 572], [350, 571], [332, 563],
132
- [306, 551], [282, 529], [266, 505],
133
- [252, 479], [246, 453], [250, 423],
134
- [244, 385], [240, 337], [243, 287],
135
- [247, 205], [308, 148], [CX, HEAD_TOP],
136
- ];
137
- // Lighter than wren's ring by ~20% — the reference line is confident but not
138
- // woodcut. Heavy low on the jaw, nothing at the crown (under hair).
139
- //
140
- // Scaled to ~0.65 of the original profile ([3, 6, 9.5, 12, 13, 11.5, 9, 6, 4,
141
- // 3]) when RESHAPE landed: the same taper shape, but a line rather than a rope
142
- // once the head is drawn broader and the frame tighter around it.
143
- const HEAD_W = [2, 4, 6, 8, 8.5, 7.5, 6, 4, 2.5, 2];
144
-
145
- // Neck: the shared truncated-cone construction, slim; fused into the head
146
- // layer and overshot past every neckline for the standard parallax reasons.
147
- const NECK_FILL =
148
- 'M344 512C336 570 330 620 328 680L328 790L434 790L434 680C432 620 426 570 418 512Z';
149
- const NECK_L = [[344, 530], [338, 580], [333, 634], [331, 720]];
150
- const NECK_R = [[418, 528], [424, 578], [429, 632], [431, 720]];
151
- const JAW_UNDER = [[340, 558], [356, 582], [404, 584], [422, 556]];
152
-
153
- // ---------------------------------------------------------------------------
154
- // Static art: hair — the wavy lob.
155
- //
156
- // One closed horseshoe (see header). Waves are S-curves on the outer edge and
157
- // pointed tips at the bottom; the fringe sweeps from a part left of centre
158
- // down across the forehead, bottoming out above the right brow. Interior
159
- // highlight slashes are separate paper marks on top of the mass.
160
- // ---------------------------------------------------------------------------
161
- const HAIR = [
162
- // left lock tip (in front of the shoulder), then one curl and a smooth
163
- // outer edge going up — deep or repeated scallops here read as braids,
164
- // which is what sank the first two drafts
165
- [252, 712],
166
- [224, 706], [204, 684], [198, 654],
167
- [186, 622], [178, 588], [182, 554],
168
- [172, 520], [168, 480], [176, 444],
169
- [170, 404], [174, 360], [186, 320],
170
- [188, 270], [210, 226], [240, 196],
171
- [258, 158], [296, 130], [336, 114],
172
- [366, 100], [420, 96], [458, 112],
173
- [498, 116], [534, 142], [548, 178],
174
- // outer right, down with the same gentle waves
175
- [574, 202], [586, 248], [580, 288],
176
- [594, 326], [594, 382], [582, 418],
177
- [586, 452], [578, 498], [566, 528],
178
- [574, 560], [566, 600], [550, 626],
179
- [556, 648], [540, 674], [516, 686],
180
- [508, 700], [484, 704], [470, 690],
181
- // right front lock: inner boundary climbing over the chest to the jaw
182
- [458, 670], [454, 642], [462, 612],
183
- [478, 588], [500, 564], [518, 540],
184
- // right inner edge up the face side to the temple
185
- [530, 502], [530, 454], [532, 410],
186
- [532, 382], [524, 358], [510, 346],
187
- // the fringe hairline, right to left: a HIGH arc — the reference forehead
188
- // is largely open (brow-to-hairline reads close to brow-to-mouth). The
189
- // right descent is short, a strand tip dipping to just above the right
190
- // frame corner; v6 let the arc sag to the brows and the whole upper face
191
- // went dark
192
- [500, 344], [494, 322], [488, 298],
193
- [478, 268], [462, 250], [448, 240],
194
- [436, 236], [427, 242], [416, 238],
195
- // …the cleft dips at the part (right of centre, as drawn), then the left
196
- // branch is one long high swoop to the temple — it must clear the left
197
- // brow tail entirely, so the descent happens outboard of x≈276
198
- [398, 236], [376, 242], [352, 252],
199
- [328, 264], [308, 278], [294, 292],
200
- [284, 306], [276, 320], [270, 332],
201
- [264, 344], [258, 356], [256, 366],
202
- // left inner edge: down the face side, then slanting AWAY from the neck —
203
- // an edge that hugs the neck makes the side mass a rope
204
- [250, 392], [244, 432], [248, 478],
205
- [252, 516], [242, 554], [230, 592],
206
- [224, 622], [220, 654], [228, 684],
207
- [236, 700], [244, 708], [252, 712],
208
- ];
209
- const HAIR_D = region(HAIR);
210
-
211
- // Paper highlight slashes: crescent cuts following the flow. These, plus the
212
- // wavy silhouette, are the contemporary read; without them the mass is a
213
- // helmet whatever its outline does.
214
- // v8: fewer, bolder. Eight slivers flickered during nods — the most-played
215
- // motion in the product — so each side-fall keeps two large crescents instead
216
- // of three ticks, and every survivor gained width. The silhouette, not the
217
- // interior, is the identity feature.
218
- const SHINE = [
219
- { p: [[330, 150], [300, 168], [280, 196], [272, 228]], w: [2, 7, 2] },
220
- // the long echo of the fringe sweep — the reference's most prominent slash;
221
- // it lives in the band ABOVE the hairline, tight to the sweep's direction
222
- { p: [[468, 220], [446, 196], [416, 176], [382, 166]], w: [1.5, 5.5, 1.5] },
223
- { p: [[196, 336], [187, 380], [187, 424], [196, 462]], w: [2, 8, 2] },
224
- { p: [[204, 522], [196, 574], [206, 648], [242, 700]], w: [1.5, 7, 1.5] },
225
- { p: [[574, 330], [583, 372], [583, 414], [574, 448]], w: [2, 8, 2] },
226
- { p: [[550, 538], [552, 592], [530, 652], [482, 684]], w: [1.5, 7, 1.5] },
227
- ];
228
-
229
- // Ears and hoops: painted ON the hair mass (see header). Small C-shapes; the
230
- // hoop is a thin accent ring hanging from the lobe — the reference's one
231
- // piece of jewellery.
232
- const EAR_L = [[246, 404], [224, 396], [212, 418], [216, 446], [222, 468], [240, 474], [250, 464]];
233
- const EAR_R = [[514, 404], [536, 396], [548, 418], [544, 446], [538, 468], [520, 474], [510, 464]];
234
- const EAR_L_IN = [[230, 420], [220, 430], [221, 446], [228, 455]];
235
- const EAR_R_IN = [[530, 420], [540, 430], [539, 446], [532, 455]];
236
-
237
- const K = 0.5523;
238
- function circlePts(cx, cy, r) {
239
- const k = r * K;
240
- return [
241
- [cx + r, cy], [cx + r, cy + k], [cx + k, cy + r], [cx, cy + r],
242
- [cx - k, cy + r], [cx - r, cy + k], [cx - r, cy], [cx - r, cy - k],
243
- [cx - k, cy - r], [cx, cy - r], [cx + k, cy - r], [cx + r, cy - k],
244
- [cx + r, cy],
245
- ];
246
- }
247
- const HOOP_L = taperRing(circlePts(236, 488, 12), [4.5, 5, 4.5]);
248
- const HOOP_R = taperRing(circlePts(524, 488, 12), [4.5, 5, 4.5]);
249
-
250
- /** The nose: a tiny soft hook, tip only. It sits lower than v6 (the
251
- * reference's nose mark tops out at ~55% of eye→mouth, not 39%). */
252
- const NOSE_D = taper(
253
- [[CX + 3, 439], [CX - 2, 448], [CX - 6, 456], [CX - 4, 462],
254
- [CX + 1, 465], [CX + 7, 463], [CX + 10, 458]],
255
- [1.5, 5.5, 2]
256
- );
257
-
258
- // ---------------------------------------------------------------------------
259
- // Static art: torso — a white crew tee under an open overshirt, both paper,
260
- // distinguished only by ink outline.
261
- //
262
- // The tee is edge-to-edge under everything; the two overshirt panels sit on
263
- // top, their inner edges making the open V. Both panels keep the house
264
- // two-run shoulder: outer arm edge, hard acromion turn, near-horizontal
265
- // trapezius shelf. Everything overshoots the frame.
266
- // ---------------------------------------------------------------------------
267
- const TEE = [
268
- [288, 960],
269
- [290, 860], [292, 760], [296, 688],
270
- [300, 656], [318, 636], [352, 626],
271
- [364, 623], [396, 623], [408, 625],
272
- [442, 634], [460, 658], [464, 688],
273
- [468, 760], [470, 860], [472, 960],
274
- [410, 960], [348, 960], [288, 960],
275
- ];
276
- const TEE_NECK = [[318, 640], [338, 652], [358, 658], [380, 660],
277
- [402, 657], [422, 650], [440, 638]];
278
-
279
- // The shoulder line sits high — chin to trapezius is ~75 units. v1 had it 60
280
- // lower and the neck read as a column; the reference's neck is short.
281
- const PANEL_L = [
282
- [302, 960],
283
- [306, 890], [308, 810], [314, 742],
284
- [318, 710], [322, 676], [318, 638],
285
- [298, 634], [264, 644], [232, 656],
286
- [186, 672], [132, 702], [106, 748],
287
- [72, 814], [60, 882], [58, 960],
288
- [180, 960], [240, 960], [302, 960],
289
- ];
290
- const PANEL_R = [
291
- [454, 960],
292
- [450, 890], [448, 810], [442, 742],
293
- [438, 710], [434, 676], [438, 636],
294
- [458, 632], [492, 642], [524, 654],
295
- [570, 670], [624, 700], [650, 746],
296
- [684, 812], [696, 882], [698, 960],
297
- [620, 960], [540, 960], [454, 960],
298
- ];
299
-
300
- // Ink runs for the panels: outer silhouette from under the collar over the
301
- // shoulder and down the arm; inner run down the open front edge.
302
- const PANEL_L_OUT = [
303
- [318, 638],
304
- [298, 634], [264, 644], [232, 656],
305
- [186, 672], [132, 702], [106, 748],
306
- [72, 814], [60, 882], [58, 950],
307
- ];
308
- const PANEL_L_IN = [[318, 638], [322, 676], [318, 710], [314, 742],
309
- [308, 810], [306, 890], [304, 950]];
310
- const PANEL_R_OUT = [
311
- [438, 636],
312
- [458, 632], [492, 642], [524, 654],
313
- [570, 670], [624, 700], [650, 746],
314
- [684, 812], [696, 882], [698, 950],
315
- ];
316
- const PANEL_R_IN = [[438, 636], [434, 676], [438, 710], [442, 742],
317
- [448, 810], [450, 890], [452, 950]];
318
- const PANEL_W = [7, 8, 7, 8, 7, 6];
319
-
320
- // The collar: a down-pointing folded flap hugging each side of the neck —
321
- // v1's up-pointing lapel triangles read as horns. The lower vertex is a
322
- // single sharp point so the notch against the shoulder reads as tailoring,
323
- // not a blob. Not mirrored; the reference's collar is not symmetric and
324
- // neither is any worn one.
325
- const COLLAR_L = [[338, 600], [316, 604], [298, 624], [288, 648],
326
- [296, 672], [310, 686], [324, 662],
327
- [330, 640], [335, 618], [338, 600]];
328
- const COLLAR_R = [[424, 598], [446, 602], [464, 622], [474, 646],
329
- [466, 670], [452, 684], [438, 660],
330
- [432, 638], [427, 616], [424, 598]];
331
-
332
- // Folds where the cloth is actually pulled, tapered to nothing at both ends:
333
- // out of the collar roll toward each armpit, and off the shoulder seam down
334
- // the sleeve. Few and curved — v6 scattered short parallel ticks and they
335
- // read as scratches on the garment, not drape.
336
- const CREASES = [
337
- { p: [[300, 692], [284, 722], [272, 756], [268, 788]], w: [1, 4.5, 1] },
338
- { p: [[150, 700], [168, 742], [178, 788], [180, 830]], w: [1.5, 5, 1.5] },
339
- { p: [[118, 742], [134, 776], [143, 810], [144, 838]], w: [1, 3.5, 1] },
340
- { p: [[464, 690], [478, 722], [488, 758], [491, 792]], w: [1, 4.5, 1] },
341
- { p: [[610, 698], [594, 740], [585, 786], [586, 828]], w: [1.5, 5, 1.5] },
342
- { p: [[644, 752], [630, 786], [621, 816], [619, 842]], w: [1, 3.5, 1] },
343
- ];
344
-
345
- // ---------------------------------------------------------------------------
346
- // Layers: the house four, same order, same roles.
347
- // ---------------------------------------------------------------------------
348
- const PARALLAX = { head: 1.0, body: 0.1, features: 1.22, hair: 1.12 };
349
- const LAYERS = ['head', 'body', 'features', 'hair'];
350
- const PIVOT = { x: CX, y: 700 };
351
-
352
- const POSE = {
353
- leanTravel: 23, leanPivot: { x: PIVOT.x, y: 560 },
354
- shrugLift: 30, shrugTiltDeg: 1.7, shrugPivot: { x: PIVOT.x, y: 800 },
355
- // Under peep's 28 for the same reason as wren: a big hair mass at full
356
- // parallax slides like a wig.
357
- yawPx: 26, pitchPx: 16,
358
- pivot: PIVOT,
359
- breathSwell: 0.008, swellPivot: { x: CX, y: 950 },
360
- turnPx: 16,
361
- layers: LAYERS, parallax: PARALLAX,
362
- torsoLayers: ['body'],
363
- units: 1,
364
- };
365
-
366
- // ---------------------------------------------------------------------------
367
- // Generators: mouth — the proven aperture-contour model with myna's lips.
368
- //
369
- // See face-peep.js for the full derivation (aperture-not-centreline, the 0.18
370
- // ramp, 3:1 downward opening, the 0.75 cubic solve). Myna's additions: a
371
- // 9-stop ring profile whose top centre dips (the cupid's bow) and whose lower
372
- // centre swells (the fuller lip), and an under-lip shadow drawn off the
373
- // contour bottom in apply().
374
- // ---------------------------------------------------------------------------
375
- // v8: both bands up ~15% from v7 — at a 130 px tile the closed smile was
376
- // merely *present*; the floor raise is what makes it read as smiling there.
377
- const LIP_UP = 10.5; // upper lip band thickness at its fullest
378
- const LIP_LO = 15; // lower lip band at centre
379
-
380
- function mouthGeometry(p) {
381
- const cx = MOUTH.cx;
382
- const cy = MOUTH.cy;
383
- const open = clamp(p.mouthOpen);
384
- const round = clamp(p.mouthRound);
385
- const tuck = clamp(p.mouthTuck);
386
- const press = clamp(p.mouthPress);
387
-
388
- const w = (28 + clamp(p.mouthWidth) * 30) * (1 - 0.36 * round);
389
-
390
- // Press changes SHAPE, not just weight. A bilabial closure (viseme A,
391
- // press .55) must render as a pressed band — corners neutralized, bow
392
- // scallop ironed flat, thickness nearly kept — or A is indistinguishable
393
- // from idle X and lipsync reads mushy however good the timing is. The
394
- // pressed band is the anchor viewers use to verify sync.
395
- const t = 1 - 0.18 * press;
396
- // Nine stops around the ring: s=0 left corner, 0.25 top centre, 0.5 right
397
- // corner, 0.75 bottom centre. The dip at 0.25 is the bow notch (a close-up
398
- // detail, deliberately light); press irons it up to band thickness.
399
- const bow = lerp(5, LIP_UP * 0.92, clamp(press * 1.6));
400
- const lo = LIP_LO * t * (1 + 0.35 * tuck);
401
- const profile = [
402
- 3 * t, LIP_UP * t, bow * t, LIP_UP * t, 3.5 * t,
403
- 11.5 * t, lo, 11.5 * t, 3 * t,
404
- ];
405
- const halfUp = (LIP_UP * t) / 2;
406
- const halfLo = lo / 2;
407
-
408
- const h = open * MOUTH_APERTURE;
409
- const k = clamp(open / 0.18);
410
-
411
- // Corner travel 28 (was 23): the tile swallows less. Press straightens the
412
- // band by pulling the corner lift toward zero.
413
- const cLift = 1 - 0.85 * press;
414
- const yL = cy - (1.5 + p.mouthCornerL * 28) * cLift;
415
- const yR = cy - (1.5 + p.mouthCornerR * 28) * cLift;
416
-
417
- const apTop = cy - h * 0.25;
418
- let apBot = cy + h * 0.75;
419
- if (tuck > 0) apBot = Math.max(apTop + 6, apBot - tuck * (h * 0.6 + 4));
420
-
421
- const cornerMid = (yL + yR) / 8;
422
- const topY = (apTop - k * halfUp - cornerMid) / 0.75;
423
- const botY = (apBot + k * halfLo - cornerMid) / 0.75;
424
-
425
- const contour = [
426
- [cx - w, yL],
427
- [cx - w * 0.55, topY], [cx + w * 0.55, topY], [cx + w, yR],
428
- [cx + w * 0.55, botY], [cx - w * 0.55, botY], [cx - w, yL],
429
- ];
430
-
431
- const innerTop = cornerMid + 0.75 * topY + halfUp;
432
- const innerBot = cornerMid + 0.75 * botY - halfLo;
433
-
434
- return { contour, profile, cx, cy, w, h, topY, botY, innerTop, innerBot, open, tuck };
435
- }
436
-
437
- /** peep's dental-arch teeth, myna's width factors. */
438
- function teethPath(m, amt, lower) {
439
- if (amt < 0.01) return '';
440
- const gap = m.innerBot - m.innerTop;
441
- if (gap < 2) return '';
442
- const tw = m.w * (lower ? 0.6 : 0.76);
443
- const cap = lower ? 0.5 : 0.5 + 0.35 * m.tuck;
444
- const th = Math.min(amt * (lower ? 13 : 20), gap * cap);
445
-
446
- if (lower) {
447
- const base = m.innerBot + 8;
448
- const edge = m.innerBot - th;
449
- const end = m.innerBot - th * 0.35;
450
- return (
451
- `M${f(m.cx - tw)} ${f(base)}L${f(m.cx + tw)} ${f(base)}` +
452
- `L${f(m.cx + tw * 0.92)} ${f(end)}` +
453
- `Q${f(m.cx)} ${f(2 * edge - end)} ${f(m.cx - tw * 0.92)} ${f(end)}Z`
454
- );
455
- }
456
- const top = m.innerTop - 8;
457
- const edge = m.innerTop + th;
458
- const end = m.innerTop + th * 0.35;
459
- return (
460
- `M${f(m.cx - tw)} ${f(top)}L${f(m.cx + tw)} ${f(top)}` +
461
- `L${f(m.cx + tw * 0.92)} ${f(end)}` +
462
- `Q${f(m.cx)} ${f(2 * edge - end)} ${f(m.cx - tw * 0.92)} ${f(end)}Z`
463
- );
464
- }
465
-
466
- // Eyes: the lid-line-plus-iris model, ported from face-peep.js. It reads
467
- // better against the "big and dark pupils" brief than the old whole-bean
468
- // translation did, not worse — the reference's pupils get a real iris that
469
- // carries the gaze while the lash and lid line hold still. See face-peep.js
470
- // for the two wrong passes (paper-on-ink, then a too-small pupil that read as
471
- // a startled target) that are not recoverable from the numbers alone.
472
-
473
- /** Top-lid height for a given lid value — shared by bean and lash so they can
474
- * never disagree. The mapping is a 0.6-power, not linear: the mixer's rest
475
- * lid is 0.12 (params.js: real neutral eyes graze the iris) and on beans
476
- * this big a linear map spent only 7% of the travel by then — rest rendered
477
- * as a wide-eyed stare, a long-session comfort failure. The power curve
478
- * spends ~28% by 0.12 (a visible flat on the bean top: attentive-relaxed,
479
- * not sleepy) while a true wide (curious sends lid −0.10 → clamps to ~0)
480
- * still rounds fully open, and closed is unchanged. */
481
- function lidTopY(cy, lid) {
482
- return lerp(cy - EYE.ry * 1.05, cy - EYE.ry * 0.42, Math.pow(clamp(lid), 0.6));
483
- }
484
-
485
- // A cubic whose two controls share a y reaches only 3/4 of the way to it.
486
- // Everything below works in DRAWN extents and divides that back out, so an
487
- // inset of n units is n units on screen — see face-peep.js for why the inset
488
- // below cannot be proportional instead.
489
- const BULGE = 0.75;
490
-
491
- /** Two cubics between (cx±rx, cyMid), bulging to yTop and yBot; tilt about cy. */
492
- function lensPath(cx, cy, g, tiltDeg) {
493
- if (!g) return '';
494
- const a = (tiltDeg * Math.PI) / 180;
495
- const ca = Math.cos(a), sa = Math.sin(a);
496
- const R = (x, y) => {
497
- const dx = x - cx, dy = y - cy;
498
- return `${f(cx + dx * ca - dy * sa)} ${f(cy + dx * sa + dy * ca)}`;
499
- };
500
- const ctlTop = g.cyMid + (g.yTop - g.cyMid) / BULGE;
501
- const ctlBot = g.cyMid + (g.yBot - g.cyMid) / BULGE;
502
- return (
503
- `M${R(cx - g.rx, g.cyMid)}` +
504
- `C${R(cx - g.rx * 0.5, ctlTop)} ${R(cx + g.rx * 0.5, ctlTop)} ${R(cx + g.rx, g.cyMid)}` +
505
- `C${R(cx + g.rx * 0.5, ctlBot)} ${R(cx - g.rx * 0.5, ctlBot)} ${R(cx - g.rx, g.cyMid)}Z`
506
- );
507
- }
508
-
509
- /** The lid silhouette, in drawn extents. Same power-curve lid mapping and
510
- * squint gain (.95: the smile-squint is most of what separates warm from
511
- * neutral at tile size) as before the eye was opened out. */
512
- function eyeGeom(cy, lid, squint) {
513
- const L = clamp(lid);
514
- const ctlTop = lidTopY(cy, lid);
515
- const ctlBot = lerp(cy + EYE.ry * 1.05, cy - EYE.ry * 0.05, L) - clamp(squint) * EYE.ry * 0.95;
516
- return {
517
- cyMid: cy,
518
- rx: EYE.rx,
519
- yTop: cy + BULGE * (ctlTop - cy),
520
- yBot: cy + BULGE * (ctlBot - cy),
521
- };
522
- }
523
-
524
- // The lid line, as constant distances — a proportional inset inverts the
525
- // moment a closing lid carries the bean's lower edge above the eye centre and
526
- // leaks paper out through a shut eye.
527
- const LASH_X = 3.0;
528
- const LASH_TOP = 5.5;
529
- const LASH_BOT = 4.5;
530
-
531
- /** The paper almond inside the lid line, or null once the lid has shut on it. */
532
- function apertureGeom(g) {
533
- const yTop = g.yTop + LASH_TOP;
534
- const yBot = g.yBot - LASH_BOT;
535
- const rx = g.rx - LASH_X;
536
- if (yBot - yTop < 0.5 || rx < 0.5) return null;
537
- return { cyMid: (yTop + yBot) / 2, rx, yTop, yBot };
538
- }
539
-
540
- // Sized to fill the resting aperture's height rather than float inside it —
541
- // this is what "big and dark pupils" means now that the eye has a real iris
542
- // instead of a translating bean.
543
- const IRIS_R = 8.0;
544
-
545
- // Travel, native units: one corner's worth of paper. At either extreme one
546
- // sliver closes and the other doubles, which is the whole gaze signal.
547
- const IRIS_TRAVEL = { x: 4.5, y: 2.2 };
548
-
549
- /**
550
- * The upper lash line: lies ON the bean's top edge and ends in a small
551
- * outward flick at the outer corner. The body samples the bean's own top
552
- * cubic (controls at ±rx/2, topY, so y(u) = 3u(1−u)·(topY−cy)) rather than
553
- * approximating it — v6 floated a straight slash near the lens top and it
554
- * read as a second, angrier brow inside the glasses. Same lid value as the
555
- * bean, so a blink carries the lash down with the closing lid; the flick is
556
- * anchored at the corner, which the lid never moves.
557
- * `dir` is −1 for the left eye (flick outward-left), +1 for the right.
558
- */
559
- function lashPath(cx, cy, lid, dir) {
560
- const topY = lidTopY(cy, lid);
561
- const rx = EYE.rx;
562
- const at = (u) => {
563
- const v = 1 - u;
564
- const x = v * v * v * -rx + 3 * v * v * u * -rx * 0.5
565
- + 3 * v * u * u * rx * 0.5 + u * u * u * rx;
566
- // Proud of the edge by 1.5 so the liner shows against the dark bean.
567
- return [cx + dir * x, cy + 3 * v * u * (topY - cy) - 1.5];
568
- };
569
- const pts = [
570
- at(0.3), at(0.5), at(0.7), at(0.88), at(1),
571
- [cx + dir * (rx + 5), cy - 4.5],
572
- [cx + dir * (rx + 9), cy - 9],
573
- ];
574
- return taper(pts, [2.5, 4, 3, 1.5], 6);
575
- }
576
-
577
- // Brows: the point-list deformation. Travel is ASYMMETRIC by design:
578
- // - Raises land at 20 (v7: 16) — the minimal-face lesson says small deltas
579
- // vanish, and at a 130 px tile the emotion poses collapsed below this.
580
- // - Descent is capped at 8.5 and weighted toward the inner half (corrugator
581
- // pull), so brows-down reads as concentration rather than a merged smear.
582
- // What travel can't carry, thickness does: a knitted brow bulks up 30% at
583
- // full descent.
584
- function browPath(pts, raise, angle, inner) {
585
- const n = pts.length - 1;
586
- const up = Math.max(0, raise);
587
- const dn = Math.max(0, -raise);
588
- const out = pts.map(([x, y], i) => {
589
- const u = i / n;
590
- return [x, y - up * 20 + dn * 8.5 * (1 - 0.45 * u)
591
- - inner * 15 * (1 - u) - angle * 16 * u];
592
- });
593
- const wk = 1 + 0.3 * dn;
594
- return taper(out, [3 * wk, 12 * wk, 4 * wk], 6);
595
- }
596
-
597
- // ---------------------------------------------------------------------------
598
- // Static markup
599
- // ---------------------------------------------------------------------------
600
- const HEAD_FILL = region(HEAD);
601
- const HEAD_RING = taperRing(HEAD, HEAD_W, 10);
602
-
603
- function markup(id, t) {
604
- const ink = (d) => `<path d="${d}" fill="${t.ink}"/>`;
605
- const paper = (d) => `<path d="${d}" fill="${t.paper}"/>`;
606
- return `
607
- <svg id="${id}" viewBox="${VB.x} ${VB.y} ${VB.w} ${VB.h}" xmlns="http://www.w3.org/2000/svg"
608
- preserveAspectRatio="xMidYMid meet" style="display:block;width:100%;height:100%">
609
- <defs>
610
- <clipPath id="${id}-clipMouth"><path id="${id}-clipMouthP" d=""/></clipPath>
611
- <clipPath id="${id}-clipEyeL"><path id="${id}-clipEyeLP" d=""/></clipPath>
612
- <clipPath id="${id}-clipEyeR"><path id="${id}-clipEyeRP" d=""/></clipPath>
613
- </defs>
614
-
615
- <!-- head and neck; hair underlay at head parallax is the anti-sliver
616
- insurance AND the backfill behind the hair-layer ears (see header).
617
-
618
- RESHAPE wraps the art INSIDE the parallax group, never the group itself:
619
- poseTransforms writes the group's own transform every frame and would
620
- overwrite it. Same for features and hair below. The neck stays outside
621
- it — the reshape is pivoted on the chin so that join still lands. -->
622
- <g id="${id}-head">
623
- <path d="${NECK_FILL}" fill="${t.paper}"/>
624
- ${ink(taper(NECK_L, [3, 7, 5]))}
625
- ${ink(taper(NECK_R, [3, 7, 5]))}
626
- <g transform="${RESHAPE}">
627
- <path d="${HEAD_FILL}" fill="${t.paper}"/>
628
- ${ink(HEAD_RING)}
629
- ${ink(taper(JAW_UNDER, [2, 5, 2]))}
630
- <path d="${HAIR_D}" fill="${t.ink}"/>
631
- </g>
632
- </g>
633
-
634
- <!-- tee, then the two overshirt panels and their tailoring -->
635
- <g id="${id}-body">
636
- ${paper(region(TEE))}
637
- ${ink(taper(TEE_NECK, [3, 5.5, 3]))}
638
- <path d="${region(PANEL_L)}" fill="${t.paper}"/>
639
- <path d="${region(PANEL_R)}" fill="${t.paper}"/>
640
- ${ink(taper(PANEL_L_OUT, PANEL_W))}
641
- ${ink(taper(PANEL_R_OUT, PANEL_W))}
642
- ${ink(taper(PANEL_L_IN, [6, 7, 5]))}
643
- ${ink(taper(PANEL_R_IN, [6, 7, 5]))}
644
- <path d="${region(COLLAR_L)}" fill="${t.paper}"/>
645
- <path d="${region(COLLAR_R)}" fill="${t.paper}"/>
646
- ${ink(taperRing(COLLAR_L, [3, 4.5, 3], 6))}
647
- ${ink(taperRing(COLLAR_R, [3, 4.5, 3], 6))}
648
- ${CREASES.map((c) => ink(taper(c.p, c.w))).join('\n ')}
649
- </g>
650
-
651
- <!-- features: brows, eyes+lashes, nose, mouth -->
652
- <g id="${id}-features">
653
- <g transform="${RESHAPE}">
654
- <path id="${id}-browL" fill="${t.ink}"/>
655
- <path id="${id}-browR" fill="${t.ink}"/>
656
- <g id="${id}-eyes">
657
- <path id="${id}-eyeL" fill="${t.ink}"/>
658
- <path id="${id}-eyeR" fill="${t.ink}"/>
659
- <path id="${id}-apertureL" fill="${t.paper}"/>
660
- <path id="${id}-apertureR" fill="${t.paper}"/>
661
- <g clip-path="url(#${id}-clipEyeL)"><circle id="${id}-irisL" fill="${t.ink}"/></g>
662
- <g clip-path="url(#${id}-clipEyeR)"><circle id="${id}-irisR" fill="${t.ink}"/></g>
663
- <path id="${id}-lashL" fill="${t.ink}"/>
664
- <path id="${id}-lashR" fill="${t.ink}"/>
665
- </g>
666
- <path d="${NOSE_D}" fill="${t.ink}"/>
667
- <g id="${id}-mouth">
668
- <path id="${id}-mouthIn" fill="${t.mouthIn}"/>
669
- <g clip-path="url(#${id}-clipMouth)">
670
- <ellipse id="${id}-tongue" fill="${t.tongue}"/>
671
- <path id="${id}-teeth" fill="${t.teeth}"/>
672
- <path id="${id}-teethLo" fill="${t.teeth}" opacity=".85"/>
673
- </g>
674
- <path id="${id}-lips" fill="${t.ink}"/>
675
- </g>
676
- </g>
677
- </g>
678
-
679
- <!-- hair: the mass, its highlights, then ears and hoops on top -->
680
- <g id="${id}-hair">
681
- <g transform="${RESHAPE}">
682
- <path d="${HAIR_D}" fill="${t.ink}"/>
683
- ${SHINE.map((s) => paper(taper(s.p, s.w, 6))).join('\n ')}
684
- <path d="${region(EAR_L)}" fill="${t.paper}"/>
685
- <path d="${region(EAR_R)}" fill="${t.paper}"/>
686
- ${ink(taper(EAR_L, [3, 6, 3]))}
687
- ${ink(taper(EAR_R, [3, 6, 3]))}
688
- ${ink(taper(EAR_L_IN, [2, 4, 2]))}
689
- ${ink(taper(EAR_R_IN, [2, 4, 2]))}
690
- <path d="${HOOP_L}" fill="${t.accent}"/>
691
- <path d="${HOOP_R}" fill="${t.accent}"/>
692
- </g>
693
- </g>
694
- </svg>`;
695
- }
696
-
697
- // ---------------------------------------------------------------------------
698
- // Renderer — the invariant apply() shape; only the travels and the extra lip
699
- // marks are myna's own.
700
- // ---------------------------------------------------------------------------
701
- let uid = 0;
702
-
703
- export function createFace(mount, theme = {}) {
704
- const t = Object.assign({}, THEME, theme);
705
- const id = `myna${++uid}`;
706
- const { svg, $, set } = createFaceShell(mount, id, markup(id, t));
707
-
708
- const el = {
709
- head: $('head'), body: $('body'), features: $('features'), hair: $('hair'),
710
- browL: $('browL'), browR: $('browR'),
711
- eyeL: $('eyeL'), eyeR: $('eyeR'),
712
- clipEyeL: $('clipEyeLP'), clipEyeR: $('clipEyeRP'),
713
- apertureL: $('apertureL'), apertureR: $('apertureR'),
714
- irisL: $('irisL'), irisR: $('irisR'),
715
- lashL: $('lashL'), lashR: $('lashR'),
716
- mouthIn: $('mouthIn'), lips: $('lips'),
717
- clipMouth: $('clipMouthP'),
718
- teeth: $('teeth'), teethLo: $('teethLo'), tongue: $('tongue'),
719
- };
720
-
721
- function apply(p) {
722
- poseTransforms(p, set, el, POSE);
723
-
724
- // The lid line holds still and the iris carries the gaze — see the eyes
725
- // section. The lid follows vertical gaze downward only: looking up
726
- // genuinely does widen the aperture, so there is nothing to add there.
727
- const lidFollow = Math.max(0, p.pupilY) * 0.22;
728
- const lidL = p.lidL + lidFollow;
729
- const lidR = p.lidR + lidFollow;
730
- // Both beans exactly on EYE.y: a ±1 stagger here read as head tilt at
731
- // rest, and rest must be level (see BROW_R).
732
- const eye = (cx, cy, lid, squint, tilt, aperture, iris, clip) => {
733
- const g = eyeGeom(cy, lid, squint);
734
- const ap = apertureGeom(g);
735
- const apD = lensPath(cx, cy, ap, tilt);
736
- set(el[aperture], 'd', apD);
737
- // The iris is clipped to the APERTURE, not to the lid line, so it is
738
- // cropped by the same edge the paper is — a shut lid, which has no
739
- // aperture at all, takes the iris with it and needs no opacity logic.
740
- set(el[clip], 'd', apD);
741
- set(el[iris], 'cx', f(cx + p.pupilX * IRIS_TRAVEL.x));
742
- // Anchored to the aperture's midline rather than the eye's, so a
743
- // half-closed eye keeps the iris centred in the slit instead of showing
744
- // a band of paper above it.
745
- set(el[iris], 'cy', f((ap ? ap.cyMid : cy) + p.pupilY * IRIS_TRAVEL.y));
746
- set(el[iris], 'r', f(IRIS_R));
747
- return lensPath(cx, cy, g, tilt);
748
- };
749
- set(el.eyeL, 'd', eye(CX - EYE.dx, EYE.y, lidL, p.squintL, -3,
750
- 'apertureL', 'irisL', 'clipEyeL'));
751
- set(el.eyeR, 'd', eye(CX + EYE.dx, EYE.y, lidR, p.squintR, 3,
752
- 'apertureR', 'irisR', 'clipEyeR'));
753
- set(el.lashL, 'd', lashPath(CX - EYE.dx, EYE.y, lidL, -1));
754
- set(el.lashR, 'd', lashPath(CX + EYE.dx, EYE.y, lidR, 1));
755
-
756
- set(el.browL, 'd', browPath(BROW_L, p.browRaiseL, p.browAngleL, p.browInnerL));
757
- set(el.browR, 'd', browPath(BROW_R, p.browRaiseR, p.browAngleR, p.browInnerR));
758
-
759
- const m = mouthGeometry(p);
760
- const contour = region(m.contour);
761
- set(el.mouthIn, 'd', contour);
762
- set(el.clipMouth, 'd', contour);
763
- set(el.lips, 'd', taperRing(m.contour, m.profile, 12));
764
- // Keyed off the drawn gap, not mouthOpen — grey is a rendering fault on a
765
- // two-value face (peep's F bug).
766
- set(el.mouthIn, 'opacity', f(clamp((m.innerBot - m.innerTop) / 3)));
767
-
768
- pairedTeeth(p, set, el, teethPath, m);
769
-
770
- const tg = clamp(p.tongue);
771
- set(el.tongue, 'cx', f(m.cx));
772
- set(el.tongue, 'cy', f(m.innerBot + 6 - tg * ((m.innerBot - m.innerTop) * 0.8 + 6)));
773
- set(el.tongue, 'rx', f(m.w * 0.58));
774
- set(el.tongue, 'ry', '8');
775
- set(el.tongue, 'opacity', tg > 0.02 ? '1' : '0');
776
- }
777
-
778
- return faceApi(mount, svg, apply, t);
779
- }
780
-
781
- /**
782
- * This face as a **Face** record — `{ create, meta }`, the shape
783
- * `createAvatar({ face })` takes. Importing it costs this drawing and nothing
784
- * else; `src/faces.js` is the all-three table, for tooling.
785
- */
786
- export const myna = { create: createFace, meta: META };