@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.
- package/README.md +65 -154
- package/dist/index.d.ts +37 -26
- package/dist/index.js +48 -25
- package/dist/pin.d.ts +2 -0
- package/dist/pin.js +3 -0
- package/dist/react.d.ts +21 -6
- package/dist/react.js +24 -6
- package/package.json +8 -67
- package/LICENSE-APACHE-2.0 +0 -201
- package/LICENSE-CC-BY-4.0 +0 -396
- package/assets/README.md +0 -60
- package/assets/tanvi.glb +0 -0
- package/assets/tanya.glb +0 -0
- package/assets/tara.glb +0 -0
- package/assets/tess.glb +0 -0
- package/assets/tushar.glb +0 -0
- package/client/Avatar.tsx +0 -57
- package/client/AvatarClient.ts +0 -818
- package/client/createAvatar.ts +0 -177
- package/client/index.ts +0 -44
- package/client/internal.ts +0 -132
- package/client/playout.ts +0 -95
- package/client/react.ts +0 -13
- package/client/supports.ts +0 -27
- package/client/three/assets.ts +0 -26
- package/client/three/budgets.ts +0 -27
- package/client/three/character-rig.ts +0 -1713
- package/client/three/createCharacter.ts +0 -109
- package/client/three/holds.ts +0 -69
- package/client/three/internal.ts +0 -55
- package/client/three/motion-limits.json +0 -124
- package/client/three/sequences.ts +0 -281
- package/client/three/tanvi-asset.ts +0 -6
- package/client/three/tanvi.ts +0 -30
- package/client/three/tanya-asset.ts +0 -6
- package/client/three/tanya.ts +0 -29
- package/client/three/tara-asset.ts +0 -25
- package/client/three/tara.ts +0 -36
- package/client/three/tess-asset.ts +0 -6
- package/client/three/tess.ts +0 -30
- package/client/three/tushar-asset.ts +0 -6
- package/client/three/tushar.ts +0 -28
- package/client/types.ts +0 -235
- package/client/useAvatar.ts +0 -74
- package/dist/Avatar.d.ts +0 -44
- package/dist/Avatar.d.ts.map +0 -1
- package/dist/Avatar.js +0 -7
- package/dist/Avatar.js.map +0 -1
- package/dist/AvatarClient.d.ts +0 -274
- package/dist/AvatarClient.d.ts.map +0 -1
- package/dist/AvatarClient.js +0 -712
- package/dist/AvatarClient.js.map +0 -1
- package/dist/createAvatar.d.ts +0 -135
- package/dist/createAvatar.d.ts.map +0 -1
- package/dist/createAvatar.js +0 -75
- package/dist/createAvatar.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts +0 -34
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js +0 -56
- package/dist/internal.js.map +0 -1
- package/dist/playout.d.ts +0 -42
- package/dist/playout.d.ts.map +0 -1
- package/dist/playout.js +0 -85
- package/dist/playout.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/supports.d.ts +0 -19
- package/dist/supports.d.ts.map +0 -1
- package/dist/supports.js +0 -25
- package/dist/supports.js.map +0 -1
- package/dist/three/assets.d.ts +0 -10
- package/dist/three/assets.d.ts.map +0 -1
- package/dist/three/assets.js +0 -23
- package/dist/three/assets.js.map +0 -1
- package/dist/three/budgets.d.ts +0 -17
- package/dist/three/budgets.d.ts.map +0 -1
- package/dist/three/budgets.js +0 -21
- package/dist/three/budgets.js.map +0 -1
- package/dist/three/character-rig.d.ts +0 -366
- package/dist/three/character-rig.d.ts.map +0 -1
- package/dist/three/character-rig.js +0 -1611
- package/dist/three/character-rig.js.map +0 -1
- package/dist/three/createCharacter.d.ts +0 -60
- package/dist/three/createCharacter.d.ts.map +0 -1
- package/dist/three/createCharacter.js +0 -84
- package/dist/three/createCharacter.js.map +0 -1
- package/dist/three/holds.d.ts +0 -38
- package/dist/three/holds.d.ts.map +0 -1
- package/dist/three/holds.js +0 -61
- package/dist/three/holds.js.map +0 -1
- package/dist/three/internal.d.ts +0 -52
- package/dist/three/internal.d.ts.map +0 -1
- package/dist/three/internal.js +0 -49
- package/dist/three/internal.js.map +0 -1
- package/dist/three/motion-limits.json +0 -124
- package/dist/three/sequences.d.ts +0 -71
- package/dist/three/sequences.d.ts.map +0 -1
- package/dist/three/sequences.js +0 -262
- package/dist/three/sequences.js.map +0 -1
- package/dist/three/tanvi-asset.d.ts +0 -7
- package/dist/three/tanvi-asset.d.ts.map +0 -1
- package/dist/three/tanvi-asset.js +0 -7
- package/dist/three/tanvi-asset.js.map +0 -1
- package/dist/three/tanvi.d.ts +0 -24
- package/dist/three/tanvi.d.ts.map +0 -1
- package/dist/three/tanvi.js +0 -24
- package/dist/three/tanvi.js.map +0 -1
- package/dist/three/tanya-asset.d.ts +0 -7
- package/dist/three/tanya-asset.d.ts.map +0 -1
- package/dist/three/tanya-asset.js +0 -7
- package/dist/three/tanya-asset.js.map +0 -1
- package/dist/three/tanya.d.ts +0 -23
- package/dist/three/tanya.d.ts.map +0 -1
- package/dist/three/tanya.js +0 -23
- package/dist/three/tanya.js.map +0 -1
- package/dist/three/tara-asset.d.ts +0 -26
- package/dist/three/tara-asset.d.ts.map +0 -1
- package/dist/three/tara-asset.js +0 -26
- package/dist/three/tara-asset.js.map +0 -1
- package/dist/three/tara.d.ts +0 -30
- package/dist/three/tara.d.ts.map +0 -1
- package/dist/three/tara.js +0 -30
- package/dist/three/tara.js.map +0 -1
- package/dist/three/tess-asset.d.ts +0 -7
- package/dist/three/tess-asset.d.ts.map +0 -1
- package/dist/three/tess-asset.js +0 -7
- package/dist/three/tess-asset.js.map +0 -1
- package/dist/three/tess.d.ts +0 -24
- package/dist/three/tess.d.ts.map +0 -1
- package/dist/three/tess.js +0 -24
- package/dist/three/tess.js.map +0 -1
- package/dist/three/tushar-asset.d.ts +0 -7
- package/dist/three/tushar-asset.d.ts.map +0 -1
- package/dist/three/tushar-asset.js +0 -7
- package/dist/three/tushar-asset.js.map +0 -1
- package/dist/three/tushar.d.ts +0 -22
- package/dist/three/tushar.d.ts.map +0 -1
- package/dist/three/tushar.js +0 -22
- package/dist/three/tushar.js.map +0 -1
- package/dist/types.d.ts +0 -149
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -140
- package/dist/types.js.map +0 -1
- package/dist/useAvatar.d.ts +0 -39
- package/dist/useAvatar.d.ts.map +0 -1
- package/dist/useAvatar.js +0 -35
- package/dist/useAvatar.js.map +0 -1
- package/src/avatar.d.ts +0 -436
- package/src/avatar.js +0 -1370
- package/src/behavior.d.ts +0 -42
- package/src/behavior.js +0 -114
- package/src/camera.js +0 -29
- package/src/clips.js +0 -155
- package/src/conformance.js +0 -119
- package/src/emotions.js +0 -58
- package/src/face-core.js +0 -180
- package/src/face-myna.d.ts +0 -7
- package/src/face-myna.js +0 -786
- package/src/face-peep-control-plane.js +0 -167
- package/src/face-peep.d.ts +0 -7
- package/src/face-peep.js +0 -925
- package/src/face-wren.d.ts +0 -7
- package/src/face-wren.js +0 -468
- package/src/faces.d.ts +0 -12
- package/src/faces.js +0 -53
- package/src/gaze.js +0 -504
- package/src/hand.js +0 -709
- package/src/head.js +0 -134
- package/src/idle.js +0 -668
- package/src/interjections.js +0 -747
- package/src/line-art.js +0 -137
- package/src/params.js +0 -180
- package/src/perform.js +0 -107
- package/src/prosody.js +0 -741
- package/src/rig.d.ts +0 -31
- package/src/rig.js +0 -32
- package/src/speech-timing.js +0 -32
- 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
|
-
}
|