@voqalize/avatar 0.4.3 → 0.5.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 (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 -1713
  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 -1611
  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 -1370
  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 -32
  180. package/src/visemes.js +0 -410
package/src/gaze.js DELETED
@@ -1,504 +0,0 @@
1
- /**
2
- * Gaze.
3
- *
4
- * The server sends a semantic direction; the client does the oculomotor work.
5
- * Three details do almost all the perceptual heavy lifting, and skipping any
6
- * one of them makes the face read as a puppet:
7
- *
8
- * 1. The eyes get there first. Saccades are near-instant; the head ambles
9
- * after — accelerating, cruising, braking to a stop (see HEAD_ACCEL).
10
- * 2. The head only goes part of the way. Real people under-rotate the head
11
- * and let the eyes carry the remainder.
12
- * 3. A blink rides a large shift. Gaze-evoked blinks are involuntary, more
13
- * likely the bigger the shift (Evinger et al. 1994), and their absence
14
- * is uncanny even though nobody can name what's wrong.
15
- *
16
- * A rig that states what a pose unit is in degrees (`angles`) gets the rest of
17
- * the eye-head system too: its own target table, where the head carries most
18
- * of a look and the eyes land about a third of the way off centre (Freedman &
19
- * Sparks; Pejsa & Andrist's gaze model), and a vestibulo-ocular reflex in the
20
- * mixer that holds the eyes on their target while the head moves under them.
21
- * Without the reflex every nod and every speech pose change is also a look
22
- * somewhere else.
23
- */
24
-
25
- export const GAZE_TARGETS = {
26
- // Straight down the barrel of the webcam — the default during conversation.
27
- USER: { px: 0.00, py: 0.06, hx: 0.00, hy: 0.00 },
28
- // Still on the user, but the head is cheated aside so an ear favors the
29
- // speaker — the "I'm trying to hear you" attitude. The trick is that the
30
- // head-follow and the pupils point OPPOSITE ways: the head turns off-axis
31
- // and the eyes counter back to the camera, which is what keeps it reading
32
- // as contact rather than as looking away. CANT_HEAR sits on this target.
33
- USER_EAR: { px: -0.42, py: 0.05, hx: 0.55, hy: 0.02, roll: 0.55 },
34
- SCREEN_CENTER: { px: 0.00, py: -0.16, hx: 0.00, hy: -0.10 },
35
- SCREEN_LEFT: { px: -0.78, py: -0.10, hx: -0.36, hy: -0.05 },
36
- SCREEN_RIGHT: { px: 0.78, py: -0.10, hx: 0.36, hy: -0.05 },
37
- SCREEN_TOP: { px: 0.00, py: -0.72, hx: 0.00, hy: -0.30 },
38
- SCREEN_BOTTOM: { px: 0.00, py: 0.62, hx: 0.00, hy: 0.24 },
39
- SCREEN_WORK: { px: -0.62, py: 0.18, hx: -0.26, hy: 0.12 },
40
- NOTES: { px: 0.18, py: 0.72, hx: 0.04, hy: 0.28 },
41
- // The agent's own display, just under the camera: where anyone on a video
42
- // call looks while they do something for you. Eyes a little down and the
43
- // head barely joins, because reading your own screen is an eye movement.
44
- // SCREEN_WORK turned the head a quarter of the way to the left and held it
45
- // there for the whole task, which read as turning away from the user.
46
- OWN_SCREEN: { px: 0.04, py: 0.30, hx: 0.02, hy: 0.07 },
47
- // The classic "recalling something" break of eye contact. Keep it for the
48
- // stylized "let me think" beat — audiences read up-and-away regardless of
49
- // whether real thinkers do it.
50
- AWAY_THINKING: { px: -0.58, py: -0.58, hx: -0.20, hy: -0.18, roll: 0.08 },
51
- AWAY_RIGHT: { px: 0.58, py: -0.52, hx: 0.20, hy: -0.16, roll: -0.06 },
52
- // Where measured cognitive aversion actually goes: DOWN (39%, more than up
53
- // or side — docs/research-biomechanics.md §4.2). Down-left, mild enough
54
- // that the lid follow shades the eyes without sealing them; the head
55
- // carries a share so the pupils stay inside the aperture at tile size.
56
- AWAY_DOWN: { px: -0.45, py: 0.42, hx: -0.18, hy: 0.20, roll: 0.04 },
57
- // Level and to the side: the other third of measured cognitive aversions
58
- // (31.3%, §4.2), neither up nor down. Right, so a thinker who alternates it
59
- // with AWAY_DOWN's left is not looking at the same patch of floor twice.
60
- AWAY_SIDE: { px: 0.55, py: 0.08, hx: 0.20, hy: 0.03, roll: -0.03 },
61
- };
62
-
63
- export const GAZE_NAMES = Object.keys(GAZE_TARGETS);
64
-
65
- const HEAD_FOLLOW_TAU = 0.34; // roll only — the head lags the eyes badly, on purpose
66
- // Gaze-evoked blink odds, ramping with the size of the shift: none below the
67
- // first number, certain above the second. Degrees on a rig that states them —
68
- // stylised, well under Evinger's 10-40°, because every look here is scaled
69
- // down to what a head-and-shoulders crop shows: a thinking look (~11°) always
70
- // blinks, a speaker's glance aside (~5°) about a third of the time, a reading
71
- // step never. Pose units on one that does not, where 0.45 was once a hard
72
- // threshold and now blinks a quarter of the time.
73
- const BLINK_RAMP_DEG = [3, 9];
74
- const BLINK_RAMP_UNITS = [0.3, 0.9];
75
- // The upper lid rides the eye, as a share of the pupil's travel from USER. A
76
- // line face's lid follows down only: its lid travels further than its iris,
77
- // and one that opened past rest on an upward look bared a band of sclera and
78
- // read as alarm. A rig whose lid and iris travel alike follows both ways.
79
- const LID_FOLLOW = { down: 0.34, up: 0 };
80
- // A turning head dips. Heads travel in arcs, not on a flat plane — a turn that
81
- // holds its pitch reads as a turntable (Williams, *The Animator's Survival
82
- // Kit*, on head turns). A share of the turn's length, read off the yaw speed
83
- // as v²/a — which at the middle of a braked turn *is* its length — so the dip
84
- // peaks mid-turn, is gone when the head lands, and sizes itself on a rig whose
85
- // head moves four times faster. A 0.75-unit look on tara sinks ~0.8°.
86
- const HEAD_DIP = 0.06;
87
- // How much of a drift the head joins. Little: a drift is a reader's scan or a
88
- // thinker's gaze moving where it rests, and both are eye movements. At 0.35
89
- // the head nodded along with every step of a reading scan, which on a
90
- // photographic face reads as a bobbing head rather than a moving eye.
91
- const DRIFT_HEAD = 0.12;
92
-
93
- // Where the eyes land inside a fixation on the user: the user's two eyes and,
94
- // less often, the mouth — the triangle a listener's gaze scans a face with.
95
- // A fixation that sits on one point is the stare; one that visits these
96
- // reads as someone looking at a person. Pose units at scanGain 1.
97
- const FACE_POINTS = [[-0.05, 0], [0.05, 0], [-0.05, 0], [0.05, 0], [0, 0.07], [0, 0]];
98
- // Default gap between those fixational saccades, seconds. Eyes Alive
99
- // (docs/research-biomechanics.md §4.5) is that this interval differs by mode,
100
- // so states set their own through the mixer.
101
- const SCAN_EVERY = [0.5, 1.6];
102
- // Fixational jumps inside a look away, as a share of the usual size. The look
103
- // is one saccade out and one back; big jumps inside it read as searching, and
104
- // on a rig with a large scanGain one could land the eyes back on the face
105
- // mid-look, so the look happened and did not read.
106
- const AVERT_SCAN = 0.35;
107
- // The least a new fixation moves from the last, pose units at scanGain 1.
108
- // Drawn independently, one in a few landed on the one before, and two jumps
109
- // that go nowhere are one long stare: 6 s on a single point, measured, in the
110
- // state that is waiting for the user to come back.
111
- const MIN_STEP = 0.025;
112
-
113
- // Head follow is ballistic, not exponential. An exponential chase has its peak
114
- // velocity at t=0 and then creeps forever — motion that starts instantly and
115
- // never quite arrives is what "drifting" looks like. A real orienting head
116
- // accelerates, cruises, and *brakes to a stop*; the stop is the cue that
117
- // attention has landed (docs/research-biomechanics.md §1.2 — the head's travel
118
- // is a discrete arriving move, eyes first, head after). The constants keep the
119
- // old τ=0.34 amble: a typical 0.3-unit shift completes in ~0.55s, USER_EAR's
120
- // 0.55-unit swing in ~0.85s. The mixer still low-passes headYaw/headPitch at
121
- // τ=0.16 downstream, which rounds the hard stop into a short settle — that
122
- // cascade is deliberate, so do not "help" by softening the brake here too.
123
- // A rig whose head unit is a few degrees sets its own (`headAccel`,
124
- // `headSpeed`): an amble sized in line-face units took a 7° look 1.2 s.
125
- const HEAD_ACCEL = 4.0; // units/s² — sets both launch and braking firmness
126
- const HEAD_SPEED = 0.9; // units/s — cruise cap; only long swings ever reach it
127
- // The head takes only a fraction of what an aversion takes — a brief look-away
128
- // is an eye movement that the head barely joins, and a head that follows it
129
- // fully reads as turning away. Sized for a line face, whose pupils travel far
130
- // inside a big eye; a rig sets its own split (`avertSplit`).
131
- const HEAD_SHARE = 0.22;
132
-
133
- /**
134
- * Ballistic follow: steer velocity toward "full speed at the goal, but never
135
- * faster than can still brake to a stop within the distance left". The
136
- * braking bound is v² = 2·a·d solved for v, with a half-step correction for
137
- * discrete time (the −maxA term) so a frame never lands past the goal. As d
138
- * shrinks the bound falls to zero, which *is* the deceleration — no separate
139
- * easing curve.
140
- */
141
- function chase(pos, vel, gx, gy, dt, accel, speed) {
142
- const dx = gx - pos.x;
143
- const dy = gy - pos.y;
144
- const d = Math.hypot(dx, dy);
145
- const maxA = accel * dt;
146
- const brake = 0.5 * (Math.sqrt(maxA * maxA + 8 * accel * d) - maxA);
147
- const goal = Math.min(speed, brake);
148
- let ax = (d ? (dx / d) * goal : 0) - vel.x;
149
- let ay = (d ? (dy / d) * goal : 0) - vel.y;
150
- const a = Math.hypot(ax, ay);
151
- if (a > maxA) { ax *= maxA / a; ay *= maxA / a; }
152
- vel.x += ax;
153
- vel.y += ay;
154
- // A dropped-frame dt could step past the goal; land on it instead. Judged
155
- // on the toward-goal component so a mid-retarget frame with sideways
156
- // velocity keeps flying rather than teleporting.
157
- const along = d ? ((vel.x * dx + vel.y * dy) / d) * dt : 0;
158
- if (along >= d && d >= 0) {
159
- pos.x = gx;
160
- pos.y = gy;
161
- vel.x = 0;
162
- vel.y = 0;
163
- } else {
164
- pos.x += vel.x * dt;
165
- pos.y += vel.y * dt;
166
- }
167
- }
168
-
169
- /**
170
- * Aversion profiles: a brief, deliberate break of eye contact that returns to
171
- * whatever the gaze target already was.
172
- *
173
- * This exists because *holding* the user's eyes is not the attentive pose it
174
- * looks like. Wang & Gratch (CHI 2010, n=133) ran the condition directly: a
175
- * virtual listener that simply stares rated no better than one that visibly
176
- * ignored the user (rapport 3.49 vs 3.34, n.s.), was rated the most *tense* of
177
- * the three conditions, and raised the speaker's own disfluency rate to
178
- * 36.75/min against 22.44 for a responsive listener. All three were rated
179
- * equally natural, so it is not an animation-quality artefact. Rossano supplies
180
- * the mechanism — sustained mutual gaze is a *demand for more talk*, not a
181
- * signal of attention (95% of sequences expanded when both parties kept looking;
182
- * 84% closed when both withdrew) — and Binetti (N=498) puts preferred mutual
183
- * gaze at 3295 ± 706 ms, which is well short of forever.
184
- *
185
- * The other wall is just as hard: sustained *aversion* is an ostracism cue, and
186
- * measured on an animated character (Chotpitayasunondh & Douglas, N=128) it
187
- * costs η²ₚ = .52–.56 with post-hoc d of 1.09–2.69, with partial inattention
188
- * costing most of what total inattention costs. So this is not "look away
189
- * sometimes" — it is a narrow band, and the numbers below are Andrist's measured
190
- * ones rather than a taste call (docs/research-biomechanics.md §4.2).
191
- *
192
- * `every`/`dur` are seconds. `dirs` are unit-ish directions weighted by how
193
- * often each is taken; magnitude is scaled per-fire.
194
- */
195
- export const AVERSION = {
196
- // Idle, and before a call: present and relaxed rather than attending. The
197
- // eyes rest on the user a comfortable bout (preferred mutual gaze 3.3 s,
198
- // §4.1) and leave for longer, softer looks than a listener's — level or a
199
- // little down, never up, because up is the "recalling" look and there is
200
- // nothing to recall. This is the first face anyone sees, and it used to
201
- // open looking up and to the left.
202
- IDLE: {
203
- every: [3.2, 6.0],
204
- dur: [1.4, 2.8],
205
- mag: [0.30, 0.46],
206
- dirs: [[-1, 0.10], [1, 0.10], [-0.8, 0.45], [0.8, 0.40], [-1, 0.25]],
207
- },
208
- // While listening: 1.14 s (SD 0.27) every 7.21 s (SD 1.88), 57.5% sideways.
209
- // Sideways dominates because down reads as submission and up reads as
210
- // exasperation on a face this schematic.
211
- LISTEN: {
212
- every: [5.3, 9.1],
213
- dur: [0.85, 1.45],
214
- mag: [0.30, 0.44],
215
- dirs: [[-1, 0.06], [-1, 0.06], [1, 0.02], [1, 0.02], [-0.7, -0.5], [0.5, 0.35]],
216
- },
217
- };
218
- // **There is deliberately no SPEAK profile, and that is the owner's call
219
- // (2026-09-21): while the avatar talks, its eyes are on the user.** There was
220
- // one — Andrist's measured speaker, a planning look away at the start of most
221
- // turns and a short one every few seconds at a phrase boundary — and in a live
222
- // call it read as the eyes darting off and coming back. The research it came
223
- // from is not disputed and the reason it does not transfer is structural: a
224
- // human speaker's look away is a *head and body* movement that the eyes only
225
- // lead, and this rig has no such movement to give it. Eye travel on its own is
226
- // not a smaller version of that, it is a different thing, and on tara the
227
- // amplitude it needed to be visible at all (`aversionGain` 1.8) is exactly what
228
- // made it read as a dart. What the speaking face moves instead is the head it
229
- // holds per phrase and the trunk under it (prosody.js), which is communicative
230
- // rather than evasive. If the face and body half is ever built, this is where
231
- // the eye half comes back.
232
- //
233
- // There is deliberately no THINK profile either. The *cognitive* aversion —
234
- // 3.54 s (SD 1.26), splitting 39.3% down / 29.4% up / 31.3% side (§4.2) — is longer
235
- // and deeper than the listening kind, and THINKING renders it with its own
236
- // dwell cycle (a `glance` in STATES), which moves the whole gaze target rather
237
- // than nudging off it. Two mechanisms producing the same look would fight.
238
-
239
- export class GazeLayer {
240
- constructor() {
241
- this.target = GAZE_TARGETS.USER;
242
- this.name = 'USER';
243
- this.head = { x: 0, y: 0, roll: 0 };
244
- this.vel = { x: 0, y: 0 };
245
- this.onLargeShift = null;
246
- this.jitter = { x: 0, y: 0 };
247
- this._nextMicro = 0;
248
- /** How far a fixational saccade travels. A pupil unit is a different
249
- * angle on every rig, and one sized for a line face is invisible on a
250
- * photographic one — the mixer passes the rig's `saccadeGain`. */
251
- this.scanGain = 1;
252
- /** The same for conversational aversions (AVERSION), which are sized for
253
- * a line face too. On tara a speaking look-away at 1 is 2-3 px of iris
254
- * travel, and a recorded call read the whole turn as a locked stare. */
255
- this.avertGain = 1;
256
- /** Where each named look goes, on this rig. The shared table is sized for
257
- * a line face; a rig whose eyes are a fraction of its head's reach
258
- * brings its own, where the head carries the look. */
259
- this.targets = GAZE_TARGETS;
260
- /** How an aversion divides: `eye` of it stays in the eyes, and `head` of
261
- * it (in head units per pupil unit) goes to the neck. */
262
- this.avertSplit = { eye: 1, head: HEAD_SHARE };
263
- /** Degrees per pose unit, `{ eye: {x, y}, head: {x, y} }`, or null on a
264
- * rig that has not said — then shifts are measured in pupil units. */
265
- this.angles = null;
266
- this.lidFollow = LID_FOLLOW;
267
- this.headAccel = HEAD_ACCEL;
268
- this.headSpeed = HEAD_SPEED;
269
- /** [min, max] seconds between fixational saccades, or null for the
270
- * default. Set per state by the mixer. */
271
- this.scanEvery = null;
272
- this._t = 0;
273
- // --- aversion scheduler
274
- this.aversion = null; // one of AVERSION, or null for none
275
- /** Set true when eye contact must be held: the floor is about to change
276
- * hands. Andrist prohibits intimacy-regulating aversions near utterance
277
- * end for exactly this reason — the floor is passed with mutual gaze, and
278
- * an avatar that looks away as the user finishes has just declined it. */
279
- this.hold = false;
280
- this._avNext = 0;
281
- this._avUntil = 0;
282
- this._avVec = { x: 0, y: 0 };
283
- this._avAmt = 0; // glided 0..1 so the return is a movement, not a cut
284
- this._avWant = 0; // where _avAmt is going; the head chases this
285
- this._avHead = { x: 0, y: 0 };
286
- this._avVel = { x: 0, y: 0 };
287
- this._avProfileRef = undefined;
288
- // --- drift: a small held offset about the target, set by the mixer
289
- this.drift = { x: 0, y: 0 };
290
- this._drift = { x: 0, y: 0 };
291
- }
292
-
293
- /** Adopt an aversion profile (or null). Cheap to call every frame. */
294
- setAversion(p) {
295
- if (p === this._avProfileRef) return;
296
- this._avProfileRef = p;
297
- this.aversion = p || null;
298
- // Re-arm rather than inherit: a state that averts must not fire the instant
299
- // it is entered off a stale timestamp from one that didn't.
300
- this._avNext = this._t + (p ? p.every[0] + Math.random() * (p.every[1] - p.every[0]) : 0);
301
- this._avUntil = 0;
302
- }
303
-
304
- _avert(t, dt) {
305
- const p = this.aversion;
306
- if (!p) {
307
- // Glide home even after the profile is gone, so a state change mid-look
308
- // returns the eyes instead of snapping them.
309
- this._avAmt = Math.max(0, this._avAmt - dt / 0.18);
310
- this._avWant = 0;
311
- return;
312
- }
313
- if (this._avUntil && this.hold) {
314
- // `hold` ends a look already running, and the schedule starts over
315
- // behind it. Suspending the look instead sent the eyes back out the
316
- // moment the hold lifted — out, back and out again across one turn
317
- // edge, which is three saccades where the floor wanted none.
318
- this._avUntil = 0;
319
- this._avNext = t + p.every[0] + Math.random() * (p.every[1] - p.every[0]);
320
- } else if (this._avUntil && t >= this._avUntil) {
321
- this._avUntil = 0;
322
- this._nextMicro = 0;
323
- this._avNext = t + p.every[0] + Math.random() * (p.every[1] - p.every[0]);
324
- } else if (!this._avUntil && !this.hold && t >= this._avNext) {
325
- this._avUntil = t + p.dur[0] + Math.random() * (p.dur[1] - p.dur[0]);
326
- this._nextMicro = 0;
327
- const d = p.dirs[(Math.random() * p.dirs.length) | 0];
328
- const m = (p.mag[0] + Math.random() * (p.mag[1] - p.mag[0])) * this.avertGain;
329
- this._avVec.x = d[0] * m;
330
- this._avVec.y = d[1] * m;
331
- // A look away is a gaze shift like any other, and blinks by its size.
332
- const s = this.avertSplit;
333
- const out = { px: d[0] * m * s.eye, py: d[1] * m * s.eye, hx: d[0] * m * s.head, hy: d[1] * m * s.head };
334
- this._shiftBlink({ px: 0, py: 0, hx: 0, hy: 0 }, out);
335
- }
336
- // `hold` cancels an aversion already running, it does not merely postpone
337
- // the next: the turn can end mid-look, and the eyes have to be back.
338
- const want = this._avUntil && !this.hold ? 1 : 0;
339
- this._avWant = want;
340
- // Out fast, back slightly slower. A saccade away is ballistic; the return
341
- // to a face is a fraction more deliberate, and symmetric timing here is one
342
- // of the things that makes a rig read as a metronome.
343
- const rate = want ? dt / 0.055 : dt / 0.11;
344
- this._avAmt = want
345
- ? Math.min(1, this._avAmt + rate)
346
- : Math.max(0, this._avAmt - rate);
347
- }
348
-
349
- /** How far off-target the eyes currently are, 0..1. The mixer reads this to
350
- * keep the trunk out of it — an aversion is eyes and a little head, never a
351
- * body turn. */
352
- get averted() { return this._avAmt; }
353
-
354
- /**
355
- * @param {string} name one of GAZE_NAMES
356
- * @param {{x:number,y:number}} [override] normalized -1..1 escape hatch for
357
- * when the server knows exact screen coordinates
358
- * @param {boolean} [blink] true to ask for the evoked blink whatever the
359
- * size, false to withhold it, omitted to leave it to the odds
360
- */
361
- set(name, override, blink) {
362
- const next = override
363
- ? { px: override.x, py: override.y, hx: override.x * 0.42, hy: override.y * 0.36 }
364
- : this.targets[name] || this.targets.USER;
365
- const prev = this.target;
366
- this.target = next;
367
- this.name = override ? 'CUSTOM' : name;
368
- if (blink !== false) this._shiftBlink(prev, next, blink);
369
- }
370
-
371
- /** Roll the gaze-evoked blink for a shift from one target to another. */
372
- _shiftBlink(a, b, force) {
373
- if (!this.onLargeShift) return;
374
- const g = this.angles;
375
- const [lo, hi] = g ? BLINK_RAMP_DEG : BLINK_RAMP_UNITS;
376
- const size = g
377
- ? Math.hypot((b.px - a.px) * g.eye.x + (b.hx - a.hx) * g.head.x,
378
- (b.py - a.py) * g.eye.y + ((b.hy || 0) - (a.hy || 0)) * g.head.y)
379
- : Math.hypot(b.px - a.px, b.py - a.py);
380
- // The listener is told which of the two it is: a beat the state named
381
- // outranks the blink budget, a roll of the odds does not.
382
- //
383
- // A shift past `hi` is not a roll. The ramp prices those at probability 1,
384
- // so the odds have already decided, and the budget was overruling the
385
- // ramp's own certainty: on the call reel the three biggest hops a screen
386
- // hunt makes -- 10.9°, 9.5°, 9.4° -- reached the eyelids as a coin toss
387
- // that the budget then refused. A saccade that size wants every channel a
388
- // head turn has (research-biomechanics §1.2: yaw, a pitch dip, a blink).
389
- //
390
- // Affordable because it is only the tail. Forcing the *whole* evoked
391
- // population is what cost 2.12x the authored rate and is why
392
- // EVOKED_EARLIEST exists; this is three shifts in a minute-long run, and
393
- // over four seeds 78 blinks fired where the budget alone fired 79.
394
- //
395
- // The refractory is untouched and refuses half of these, because a hunt
396
- // retargets every 0.8-2.0 s and that is faster than 1.5 s. That is the
397
- // refractory working as written -- a glance out and back carries one
398
- // blink, not two. So this masks the isolated large look and not the
399
- // rapid ones inside a hunt, which stay open-eyed.
400
- if (force || size >= hi) this.onLargeShift(true);
401
- else if (Math.random() < (size - lo) / (hi - lo)) this.onLargeShift(false);
402
- }
403
-
404
- /** The lid's share of an eye at `pupilY`, added to the lid channels. */
405
- lidBias(pupilY) {
406
- const rest = GAZE_TARGETS.USER.py;
407
- const f = this.lidFollow;
408
- return rest * f.down + (pupilY - rest) * (pupilY > rest ? f.down : f.up);
409
- }
410
-
411
- /**
412
- * Fixational saccades. Eyes are never still, and they do not wobble about a
413
- * point either: they jump and *hold*, a new fixation every half second or
414
- * so. On the user they visit the user's eyes and mouth; anywhere else they
415
- * land a little way about the target. These used to decay back to the exact
416
- * centre within a second, so between jumps every state stared at one point,
417
- * and at a photographic face's scale the jumps were under a pixel —
418
- * §4.6's "sub-threshold jitter signals nothing", measured.
419
- */
420
- _micro(t) {
421
- // A state's `scan` is [min gap, max gap, amplitude]: a reader's own
422
- // line steps carry the movement, so a small third value keeps the
423
- // fixational jumps from blurring them.
424
- const [a, b, k = 1] = this.scanEvery || SCAN_EVERY;
425
- // And a state that scans faster gets it from the frame it starts, not once
426
- // the state before it has finished waiting. The deadline is drawn when a
427
- // fixation fires, under whatever cadence was in force *then*, so entering a
428
- // hunt out of a slower state finds a gap longer than the hunt's own maximum
429
- // already on the clock and spends its first second at the old rate. On a
430
- // two-second state that is most of the state: eyes that hold dead still and
431
- // then jump once, which is read as mechanical rather than as searching.
432
- // Clamped, not redrawn: the wait already served still counts, and a state
433
- // change must not cost a draw from the shared stream — every blink and
434
- // wander in the reel comes off it, and spending one here would move them.
435
- if (this._nextMicro > t + b) this._nextMicro = t + b;
436
- if (t < this._nextMicro) return;
437
- this._nextMicro = t + a + Math.random() * (b - a);
438
- const g = this.scanGain * k * (this._avUntil ? AVERT_SCAN : 1);
439
- // The face scan is for looking *at* the user. A look away is not one,
440
- // and a face-point offset riding on it pulled short looks back onto the
441
- // face — the look happened and did not read.
442
- const face = (this.name === 'USER' || this.name === 'USER_EAR') && !this._avUntil;
443
- let x = 0, y = 0;
444
- for (let i = 0; i < 4; i++) {
445
- if (face) {
446
- const [fx, fy] = FACE_POINTS[(Math.random() * FACE_POINTS.length) | 0];
447
- x = (fx + (Math.random() - 0.5) * 0.02) * g;
448
- y = (fy + (Math.random() - 0.5) * 0.02) * g;
449
- } else {
450
- x = (Math.random() - 0.5) * 0.12 * g;
451
- y = (Math.random() - 0.5) * 0.08 * g;
452
- }
453
- if (Math.hypot(x - this.jitter.x, y - this.jitter.y) >= MIN_STEP * g) break;
454
- }
455
- this.jitter.x = x;
456
- this.jitter.y = y;
457
- }
458
-
459
- update(t, dt) {
460
- this._t = t;
461
- this._avert(t, dt);
462
- this._micro(t);
463
- // Drift lands like a saccade — quick, then held — not as a slide.
464
- const kd = 1 - Math.exp(-dt / 0.04);
465
- this._drift.x += (this.drift.x - this._drift.x) * kd;
466
- this._drift.y += (this.drift.y - this._drift.y) * kd;
467
- const s = this.avertSplit;
468
- const avx = this._avVec.x * this._avAmt * s.eye + this._drift.x;
469
- const avy = this._avVec.y * this._avAmt * s.eye + this._drift.y;
470
- // The head's part of an aversion is a head movement like any other, so it
471
- // gets the same ballistic launch and brake as the gaze follow — on its own
472
- // follower, so the trunk (which tracks `this.head`) sits a look out.
473
- const ahx = this._avVec.x * this._avWant * s.head;
474
- const ahy = this._avVec.y * this._avWant * s.head;
475
- chase(this._avHead, this._avVel, ahx, ahy, dt, this.headAccel, this.headSpeed);
476
- chase(this.head, this.vel, this.target.hx, this.target.hy, dt, this.headAccel, this.headSpeed);
477
-
478
- // Roll stays on the exponential: its travels are tiny (≤0.55 and usually
479
- // ~0.08), far below where the drift tail is visible.
480
- const k = 1 - Math.exp(-dt / HEAD_FOLLOW_TAU);
481
- this.head.roll += ((this.target.roll || 0) - this.head.roll) * k;
482
-
483
- const vx = this.vel.x + this._avVel.x;
484
- const dip = HEAD_DIP * vx * vx / this.headAccel;
485
- return {
486
- pupilX: this.target.px + this.jitter.x + avx,
487
- pupilY: this.target.py + this.jitter.y + avy,
488
- headYaw: this.head.x + this._avHead.x + this._drift.x * DRIFT_HEAD,
489
- headPitch: this.head.y + this._avHead.y + this._drift.y * DRIFT_HEAD + dip,
490
- headRoll: this.head.roll,
491
- // Where the head is *going*, without the looks that ride on it. The
492
- // mixer turns the trunk toward this and not toward headYaw: an aversion
493
- // is eyes and a little head, never a body turn.
494
- trunkYaw: this.head.x,
495
- // Where the head will be once it lands — the pose the eyes' target was
496
- // authored against. The mixer's reflex holds the eyes on that target
497
- // against every difference between this and the head actually drawn:
498
- // the follow still under way, the dip, and everything above the gaze
499
- // layer that moves the head.
500
- aimYaw: this.target.hx + ahx + this._drift.x * DRIFT_HEAD,
501
- aimPitch: this.target.hy + ahy + this._drift.y * DRIFT_HEAD,
502
- };
503
- }
504
- }