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