libraries-dev-svelte 0.1.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 -0
- package/README.md +48 -0
- package/dist/bot-avatars/BotAvatar.svelte +368 -0
- package/dist/bot-avatars/BotAvatar.svelte.d.ts +4 -0
- package/dist/bot-avatars/color.d.ts +14 -0
- package/dist/bot-avatars/color.js +89 -0
- package/dist/bot-avatars/draw.d.ts +55 -0
- package/dist/bot-avatars/draw.js +495 -0
- package/dist/bot-avatars/engine.d.ts +138 -0
- package/dist/bot-avatars/engine.js +690 -0
- package/dist/bot-avatars/path-cache.d.ts +2 -0
- package/dist/bot-avatars/path-cache.js +10 -0
- package/dist/bot-avatars/plastic.d.ts +80 -0
- package/dist/bot-avatars/plastic.js +839 -0
- package/dist/bot-avatars/presets.d.ts +12 -0
- package/dist/bot-avatars/presets.js +34 -0
- package/dist/bot-avatars/shapes.d.ts +4 -0
- package/dist/bot-avatars/shapes.js +27 -0
- package/dist/bot-avatars/ticker.d.ts +8 -0
- package/dist/bot-avatars/ticker.js +52 -0
- package/dist/bot-avatars/types.d.ts +163 -0
- package/dist/bot-avatars/types.js +1 -0
- package/dist/gooey-radial-menu/GooeyRadialMenu.svelte +747 -0
- package/dist/gooey-radial-menu/GooeyRadialMenu.svelte.d.ts +7 -0
- package/dist/gooey-radial-menu/motion.d.ts +2 -0
- package/dist/gooey-radial-menu/motion.js +34 -0
- package/dist/gooey-radial-menu/shadow.d.ts +10 -0
- package/dist/gooey-radial-menu/shadow.js +54 -0
- package/dist/gooey-radial-menu/types.d.ts +28 -0
- package/dist/gooey-radial-menu/types.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +12 -0
- package/package.json +46 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/* The rig. A pose is a handful of numbers — head yaw / pitch / roll, a
|
|
2
|
+
position, squash, how open the eyes are, where they look — and blend
|
|
3
|
+
weights for the three states. A Sim advances a pose through time: each
|
|
4
|
+
state sets targets and wanders around them, runs its own events (a
|
|
5
|
+
flip, a hop, a nod, a blink), and a state change eases from one set of
|
|
6
|
+
targets to the next on a timed curve, so it starts and ends softly. */
|
|
7
|
+
export const STATES = ['default', 'working', 'sleeping'];
|
|
8
|
+
/* the working state's hop: its period, and the height of the spinning one */
|
|
9
|
+
const HOP_T = 0.68;
|
|
10
|
+
const HOP_SPIN_H = 26;
|
|
11
|
+
/* a flip is that spinning hop on its own: a crouch before it (the squash
|
|
12
|
+
that, in the working state, the previous landing leaves), the hop, and
|
|
13
|
+
the landing's recovery after — measured in hop periods */
|
|
14
|
+
const FLIP_PRE = 0.2;
|
|
15
|
+
const FLIP_POST = 0.66;
|
|
16
|
+
export const JUMP_DEFAULTS = { height: HOP_SPIN_H, time: HOP_T, stretch: 1, squash: 1.15, squashTime: 0.37, squashEase: 'pulse', groundTime: 0.11, groundEase: 'pulse', riseTime: 0.33, riseEase: 'pulse', clickSquashTime: 0.24, spin: 1, lean: 6, every: 8, land: 0 };
|
|
17
|
+
/* a click's jump crouches for its squash time before take-off and lands
|
|
18
|
+
with a squash of the same length; an idle jump's crouch is the working
|
|
19
|
+
hop's short one */
|
|
20
|
+
const flipPre = (j, poked) => (poked ? j.clickSquashTime : FLIP_PRE * j.time);
|
|
21
|
+
const flipDuration = (j, poked) => flipPre(j, poked) +
|
|
22
|
+
j.time +
|
|
23
|
+
SQUASH_PEAK[j.squashEase] * (poked ? j.clickSquashTime : j.squashTime) +
|
|
24
|
+
Math.max(0, j.groundTime) +
|
|
25
|
+
j.riseTime +
|
|
26
|
+
Math.max(0, j.land) +
|
|
27
|
+
0.05;
|
|
28
|
+
/* where in its time each easing reaches the deepest squash */
|
|
29
|
+
const SQUASH_PEAK = { sharp: 0, pulse: 2 / 7, soft: 0.5, bouncy: 0.144 };
|
|
30
|
+
/* the rise: how the body comes back from its deepest squash to its own
|
|
31
|
+
shape, 1 → 0 over the rise time. `sharp` lets go at once and eases in
|
|
32
|
+
to rest, `pulse` leaves quickly with a long settle, `soft` eases out of
|
|
33
|
+
the squash and into rest, `bouncy` passes rest into a slight stretch
|
|
34
|
+
and settles back. */
|
|
35
|
+
const risePulse = (v, ease) => {
|
|
36
|
+
if (v <= 0)
|
|
37
|
+
return 1;
|
|
38
|
+
if (v >= 1)
|
|
39
|
+
return 0;
|
|
40
|
+
switch (ease) {
|
|
41
|
+
case 'sharp':
|
|
42
|
+
return (1 - v) * (1 - v);
|
|
43
|
+
case 'soft':
|
|
44
|
+
return 0.5 + 0.5 * Math.cos(Math.PI * v);
|
|
45
|
+
case 'bouncy':
|
|
46
|
+
return Math.exp(-3.2 * v) * Math.cos(5.4 * v) - v * v * v * 0.026;
|
|
47
|
+
default: {
|
|
48
|
+
const k = 4.2 * v;
|
|
49
|
+
return (1 + k) * Math.exp(-k) - v * v * v * 0.078;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/* the weight settling through the hold, as a gain on the deepest squash:
|
|
54
|
+
the same four curves as the landing squash, pressing up to a quarter
|
|
55
|
+
deeper and back to the held depth, so the hold's ends never step */
|
|
56
|
+
const groundShape = (v, ease) => 1 + 0.25 * squashPulse(v, ease);
|
|
57
|
+
/* the landing squash over its time, 0–1 → its depth, 0–1 at the peak:
|
|
58
|
+
sharp is all there at contact and eases off; pulse presses quickly and
|
|
59
|
+
recovers without a wobble (a critically damped spring under a short
|
|
60
|
+
push); soft eases in and out; bouncy overshoots into a stretch and
|
|
61
|
+
settles. Each is faded out over the last part so it ends at nothing. */
|
|
62
|
+
const squashPulse = (u, ease) => {
|
|
63
|
+
if (u <= 0 || u >= 1)
|
|
64
|
+
return 0;
|
|
65
|
+
let x;
|
|
66
|
+
switch (ease) {
|
|
67
|
+
case 'sharp':
|
|
68
|
+
x = (1 - u) * (1 - u);
|
|
69
|
+
break;
|
|
70
|
+
case 'soft':
|
|
71
|
+
x = Math.sin(Math.PI * u) ** 2;
|
|
72
|
+
break;
|
|
73
|
+
case 'bouncy':
|
|
74
|
+
x = (Math.exp(-3.15 * u) * Math.sin(8.43 * u)) / 0.596;
|
|
75
|
+
break;
|
|
76
|
+
default: {
|
|
77
|
+
const k = 7 * u;
|
|
78
|
+
x = (k * k * Math.exp(2 - k)) / 4;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const tail = u > 0.85 ? 1 - (u - 0.85) / 0.15 : 1;
|
|
82
|
+
return x * tail * tail * (3 - 2 * tail);
|
|
83
|
+
};
|
|
84
|
+
/* the hop's shaping: squash on the ground, stretch at the top */
|
|
85
|
+
const hopSquash = (a) => Math.exp(-Math.pow(Math.min(Math.abs(a), Math.abs(a - 1)) / 0.11, 2));
|
|
86
|
+
const DEG = Math.PI / 180;
|
|
87
|
+
const TAU = Math.PI * 2;
|
|
88
|
+
/* how long a state change takes: dozing off is slow, waking a little
|
|
89
|
+
quicker, the rest brisk */
|
|
90
|
+
/* How long a switch takes. Settling down takes longer than getting to
|
|
91
|
+
work: coming back to idle the body has to let go of the hops, so it
|
|
92
|
+
eases out over more than a second. */
|
|
93
|
+
const SWITCH_TO = { default: 1.2, working: 0.7, sleeping: 1.4 };
|
|
94
|
+
const SWITCH_FROM_SLEEP = 1;
|
|
95
|
+
/* Deterministic per-instance randomness (mulberry32). */
|
|
96
|
+
function rng(seed) {
|
|
97
|
+
let a = (seed * 0x9e3779b1) >>> 0 || 1;
|
|
98
|
+
return () => {
|
|
99
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
100
|
+
let t = a;
|
|
101
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
102
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
103
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Exponential approach: `rate` per second, frame-rate independent. */
|
|
107
|
+
function approach(cur, target, rate, dt) {
|
|
108
|
+
return cur + (target - cur) * (1 - Math.exp(-rate * dt));
|
|
109
|
+
}
|
|
110
|
+
const easeInOut = (p) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);
|
|
111
|
+
/* gentler at both ends than the cubic: for drifting off and waking */
|
|
112
|
+
const easeSine = (p) => 0.5 - 0.5 * Math.cos(Math.PI * p);
|
|
113
|
+
/* A value that drifts: picks a new target inside its range every hold,
|
|
114
|
+
and eases toward it. Ranges change with the state; the value never
|
|
115
|
+
jumps. */
|
|
116
|
+
class Wander {
|
|
117
|
+
rand;
|
|
118
|
+
amp;
|
|
119
|
+
holdMin;
|
|
120
|
+
holdMax;
|
|
121
|
+
rate;
|
|
122
|
+
spring;
|
|
123
|
+
value = 0;
|
|
124
|
+
vel = 0;
|
|
125
|
+
target = 0;
|
|
126
|
+
next = 0;
|
|
127
|
+
/* A channel that picks a new target now and then and moves to it. The
|
|
128
|
+
head channels move as a lightly damped spring — the turn starts and
|
|
129
|
+
ends softly, the way a head does — while the eyes dart with an
|
|
130
|
+
exponential approach, the way eyes do. */
|
|
131
|
+
constructor(rand, amp, holdMin, holdMax, rate, spring = false) {
|
|
132
|
+
this.rand = rand;
|
|
133
|
+
this.amp = amp;
|
|
134
|
+
this.holdMin = holdMin;
|
|
135
|
+
this.holdMax = holdMax;
|
|
136
|
+
this.rate = rate;
|
|
137
|
+
this.spring = spring;
|
|
138
|
+
}
|
|
139
|
+
update(t, dt) {
|
|
140
|
+
if (t >= this.next) {
|
|
141
|
+
this.target = (this.rand() * 2 - 1) * this.amp;
|
|
142
|
+
this.next = t + this.holdMin + this.rand() * (this.holdMax - this.holdMin);
|
|
143
|
+
}
|
|
144
|
+
if (this.spring) {
|
|
145
|
+
const w = this.rate * 1.6, z = 0.9;
|
|
146
|
+
this.vel += (w * w * (this.target - this.value) - 2 * z * w * this.vel) * dt;
|
|
147
|
+
this.value += this.vel * dt;
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
this.value = approach(this.value, this.target, this.rate, dt);
|
|
151
|
+
}
|
|
152
|
+
/** aim at a value and stay there: the rig picks the next one */
|
|
153
|
+
aim(v) {
|
|
154
|
+
this.target = v;
|
|
155
|
+
this.next = Infinity;
|
|
156
|
+
}
|
|
157
|
+
set(amp, holdMin, holdMax, rate) {
|
|
158
|
+
this.amp = amp;
|
|
159
|
+
this.holdMin = holdMin;
|
|
160
|
+
this.holdMax = holdMax;
|
|
161
|
+
this.rate = rate;
|
|
162
|
+
this.next = 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/* A one-shot event: progress 0 → 1 over its duration, then idle at -1. */
|
|
166
|
+
class Event {
|
|
167
|
+
duration;
|
|
168
|
+
p = -1;
|
|
169
|
+
constructor(duration) {
|
|
170
|
+
this.duration = duration;
|
|
171
|
+
}
|
|
172
|
+
fire() {
|
|
173
|
+
this.p = 0;
|
|
174
|
+
}
|
|
175
|
+
get active() {
|
|
176
|
+
return this.p >= 0;
|
|
177
|
+
}
|
|
178
|
+
update(dt) {
|
|
179
|
+
if (this.p < 0)
|
|
180
|
+
return;
|
|
181
|
+
this.p += dt / this.duration;
|
|
182
|
+
if (this.p >= 1)
|
|
183
|
+
this.p = -1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/* The idle gaze: how far the head swings, and how long it stays. The
|
|
187
|
+
sideways reach is the big one — a look to a corner reads as a turn of
|
|
188
|
+
the head, with the rise and a slight tilt going along. */
|
|
189
|
+
const GAZE_YAW = 35 * DEG;
|
|
190
|
+
const GAZE_PITCH = 14 * DEG;
|
|
191
|
+
const GAZE_ROLL = 3.2 * DEG;
|
|
192
|
+
const GAZE_HOLD_MIN = 2.6;
|
|
193
|
+
const GAZE_HOLD_MAX = 4.4;
|
|
194
|
+
const REST = {
|
|
195
|
+
default: { pitch: 0, roll: 0, y: 0, lookX: 0, lookY: 0 },
|
|
196
|
+
working: { pitch: 5 * DEG, roll: 0, y: 0, lookX: 0, lookY: 0 },
|
|
197
|
+
sleeping: { pitch: -16 * DEG, roll: 6 * DEG, y: 3, lookX: 0, lookY: 1 },
|
|
198
|
+
};
|
|
199
|
+
export class Sim {
|
|
200
|
+
pose = { yaw: 0, pitch: 0, roll: 0, x: 0, y: 0, sx: 1, sy: 1, eyeOpen: 1, blinkL: 0, blinkR: 0, lookX: 0, lookY: 0, breath: 0, laugh: 0, whirl: 0, whirlAngle: 0, w: [1, 0, 0] };
|
|
201
|
+
state = 'default';
|
|
202
|
+
rand;
|
|
203
|
+
t = 0;
|
|
204
|
+
/* a state change: the weights it started from and its progress */
|
|
205
|
+
wFrom = [1, 0, 0];
|
|
206
|
+
tr = 1;
|
|
207
|
+
trDuration = 1.2;
|
|
208
|
+
yawW;
|
|
209
|
+
pitchW;
|
|
210
|
+
rollW;
|
|
211
|
+
lookXW;
|
|
212
|
+
lookYW;
|
|
213
|
+
blink = new Event(0.17);
|
|
214
|
+
blinkAt;
|
|
215
|
+
blinkAgain = false;
|
|
216
|
+
/* −1 left eye only, 1 right eye only, 0 both */
|
|
217
|
+
dart = new Event(0.12);
|
|
218
|
+
dartAt;
|
|
219
|
+
dartX = 0;
|
|
220
|
+
dartY = 0;
|
|
221
|
+
flip = new Event(flipDuration(JUMP_DEFAULTS, false));
|
|
222
|
+
flipPoked = false;
|
|
223
|
+
jump = { ...JUMP_DEFAULTS };
|
|
224
|
+
flipAt;
|
|
225
|
+
flipSide = 1;
|
|
226
|
+
nod = new Event(1.7);
|
|
227
|
+
nodAt;
|
|
228
|
+
hopPhase = 0;
|
|
229
|
+
hopCount = 0;
|
|
230
|
+
/** the hops' gain: the working weight while in the state, then held so
|
|
231
|
+
a hop under way finishes whole when the state is left */
|
|
232
|
+
hopGain = 0;
|
|
233
|
+
laughEv = new Event(0.8);
|
|
234
|
+
laughAt;
|
|
235
|
+
/* the jelly: a damped spring driven by how fast the head turns, so a
|
|
236
|
+
sweep stretches the body and it wobbles back */
|
|
237
|
+
prevYaw = 0;
|
|
238
|
+
jelly = 0;
|
|
239
|
+
jellyV = 0;
|
|
240
|
+
/** how far the eyes run ahead of a head turn */
|
|
241
|
+
gazeLead = 0;
|
|
242
|
+
/** the idle gaze: where the head is looking, and when it moves on */
|
|
243
|
+
gazeDir = [0, 0];
|
|
244
|
+
gazeAt = 0;
|
|
245
|
+
/** how far it turns to the side, 1 as the gaze has it */
|
|
246
|
+
turnK = 1;
|
|
247
|
+
/** the breathing cycle's phase, in turns */
|
|
248
|
+
breathPhase = 0;
|
|
249
|
+
/* the pointer, as an offset from the head in head-widths, and how much
|
|
250
|
+
to follow it — both smoothed */
|
|
251
|
+
ptrX = 0;
|
|
252
|
+
ptrY = 0;
|
|
253
|
+
ptrS = 0;
|
|
254
|
+
ptrTargetX = 0;
|
|
255
|
+
ptrTargetY = 0;
|
|
256
|
+
ptrTargetS = 0;
|
|
257
|
+
/* the smoothed yaw the head is turning to on its own */
|
|
258
|
+
baseYaw = 0;
|
|
259
|
+
constructor(seed, state = 'default') {
|
|
260
|
+
this.rand = rng(Math.floor(seed * 1e6) + 1);
|
|
261
|
+
const r = this.rand;
|
|
262
|
+
this.yawW = new Wander(r, 36 * DEG, 1.1, 2.6, 3, true);
|
|
263
|
+
this.pitchW = new Wander(r, 10 * DEG, 1.1, 2.6, 2.6, true);
|
|
264
|
+
this.rollW = new Wander(r, 5 * DEG, 1.6, 3.2, 2, true);
|
|
265
|
+
this.lookXW = new Wander(r, 3.6, 0.5, 2, 14);
|
|
266
|
+
this.lookYW = new Wander(r, 2.4, 0.5, 2, 14);
|
|
267
|
+
/* every instance starts somewhere else in its loops */
|
|
268
|
+
this.t = r() * 10;
|
|
269
|
+
this.hopPhase = r();
|
|
270
|
+
this.breathPhase = r();
|
|
271
|
+
/* event timers count from that start, so nothing fires on the first tick */
|
|
272
|
+
this.blinkAt = this.t + 1 + r() * 3;
|
|
273
|
+
this.flipAt = this.nextFlip(this.t, 1);
|
|
274
|
+
this.nodAt = this.t + 3 + r() * 4;
|
|
275
|
+
this.dartAt = this.t + 1 + r() * 2;
|
|
276
|
+
this.laughAt = this.t + 0.6 + r() * 1.5;
|
|
277
|
+
this.setState(state, true);
|
|
278
|
+
}
|
|
279
|
+
setState(next, immediate = false) {
|
|
280
|
+
if (next === this.state && !immediate)
|
|
281
|
+
return;
|
|
282
|
+
const from = this.state;
|
|
283
|
+
this.state = next;
|
|
284
|
+
const w = this.pose.w;
|
|
285
|
+
if (immediate) {
|
|
286
|
+
for (let i = 0; i < 3; i++)
|
|
287
|
+
w[i] = STATES[i] === next ? 1 : 0;
|
|
288
|
+
this.tr = 1;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
this.wFrom = [w[0], w[1], w[2]];
|
|
292
|
+
this.tr = 0;
|
|
293
|
+
this.trDuration = from === 'sleeping' ? SWITCH_FROM_SLEEP : SWITCH_TO[next];
|
|
294
|
+
}
|
|
295
|
+
switch (next) {
|
|
296
|
+
case 'default':
|
|
297
|
+
/* the head takes its time: it holds a place, then swings to the
|
|
298
|
+
next over about a second — no short quick jabs. The gaze picks
|
|
299
|
+
where, so these carry the motion and their amplitude is unused. */
|
|
300
|
+
this.yawW.set(GAZE_YAW, 2.6, 5.4, 2);
|
|
301
|
+
this.pitchW.set(GAZE_PITCH, 2.8, 5.8, 1.8);
|
|
302
|
+
this.rollW.set(GAZE_ROLL, 3.4, 6.6, 1.5);
|
|
303
|
+
this.gazeAt = 0;
|
|
304
|
+
this.gazeDir = [0, 0];
|
|
305
|
+
/* the eyes keep their quickness: they dart, the head follows */
|
|
306
|
+
this.lookXW.set(3.6, 0.6, 2.2, 13);
|
|
307
|
+
this.lookYW.set(2.4, 0.6, 2.2, 13);
|
|
308
|
+
this.flipAt = this.nextFlip(this.t, 0.6);
|
|
309
|
+
break;
|
|
310
|
+
case 'working':
|
|
311
|
+
this.yawW.set(16 * DEG, 0.9, 1.8, 4);
|
|
312
|
+
this.pitchW.set(3 * DEG, 1.2, 2.4, 3);
|
|
313
|
+
this.rollW.set(0, 1, 2, 3);
|
|
314
|
+
this.lookXW.set(2, 0.5, 1.2, 12);
|
|
315
|
+
this.lookYW.set(1, 0.5, 1.2, 12);
|
|
316
|
+
this.hopPhase = 0;
|
|
317
|
+
this.hopCount = 0;
|
|
318
|
+
this.laughAt = this.t + 0.5 + this.rand() * 1.2;
|
|
319
|
+
break;
|
|
320
|
+
case 'sleeping':
|
|
321
|
+
this.yawW.set(7 * DEG, 3, 6, 0.7);
|
|
322
|
+
this.pitchW.set(3 * DEG, 3, 6, 0.7);
|
|
323
|
+
this.rollW.set(2 * DEG, 3, 6, 0.6);
|
|
324
|
+
this.lookXW.set(0, 2, 4, 2);
|
|
325
|
+
this.lookYW.set(0, 2, 4, 2);
|
|
326
|
+
this.nodAt = this.t + 2.5 + this.rand() * 4;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** Where the pointer is, relative to the head (−1 … 1 across a head
|
|
331
|
+
width), and how strongly to follow it (0 lets go). */
|
|
332
|
+
setPointer(x, y, strength) {
|
|
333
|
+
this.ptrTargetX = Math.max(-1.2, Math.min(1.2, x));
|
|
334
|
+
this.ptrTargetY = Math.max(-1.2, Math.min(1.2, y));
|
|
335
|
+
this.ptrTargetS = Math.max(0, Math.min(1, strength));
|
|
336
|
+
}
|
|
337
|
+
/** A hop and a full turn, right now, whatever the state. */
|
|
338
|
+
poke() {
|
|
339
|
+
if (this.flip.active && this.flip.p < 0.6)
|
|
340
|
+
return;
|
|
341
|
+
this.flipPoked = true;
|
|
342
|
+
this.flip.duration = flipDuration(this.jump, true);
|
|
343
|
+
this.flipSide = this.rand() < 0.5 ? -1 : 1;
|
|
344
|
+
this.flip.fire();
|
|
345
|
+
this.flipAt = this.nextFlip(this.t, 1.1);
|
|
346
|
+
}
|
|
347
|
+
/** How far the head turns to the side while idle: 1 as the gaze has
|
|
348
|
+
it, 0 keeps it facing forward. */
|
|
349
|
+
setTurn(k) {
|
|
350
|
+
const next = Math.max(0, k);
|
|
351
|
+
if (next === this.turnK)
|
|
352
|
+
return;
|
|
353
|
+
this.turnK = next;
|
|
354
|
+
/* take the new reach at once rather than at the next place */
|
|
355
|
+
if (this.state === 'default')
|
|
356
|
+
this.gazeAt = 0;
|
|
357
|
+
}
|
|
358
|
+
/** The jump's numbers; any subset. */
|
|
359
|
+
setJump(j) {
|
|
360
|
+
const every = this.jump.every;
|
|
361
|
+
Object.assign(this.jump, j);
|
|
362
|
+
/* a new interval takes effect at once, so `every: 0` stops the next
|
|
363
|
+
jump that was already due and a first value starts the clock */
|
|
364
|
+
if (j.every !== undefined && j.every !== every)
|
|
365
|
+
this.flipAt = this.nextFlip(this.t, 1);
|
|
366
|
+
}
|
|
367
|
+
/* Where the head looks next. From a corner it mostly swings straight
|
|
368
|
+
across to the opposite one — top right, stay, bottom left — now and
|
|
369
|
+
then only sideways, or back to the middle for a beat. */
|
|
370
|
+
nextGaze() {
|
|
371
|
+
const r = this.rand;
|
|
372
|
+
const [px, py] = this.gazeDir;
|
|
373
|
+
if (px !== 0 || py !== 0) {
|
|
374
|
+
const p = r();
|
|
375
|
+
if (p < 0.66)
|
|
376
|
+
return [-px, -py];
|
|
377
|
+
if (p < 0.85)
|
|
378
|
+
return [-px, py];
|
|
379
|
+
return [0, 0];
|
|
380
|
+
}
|
|
381
|
+
const corners = [
|
|
382
|
+
[1, -1],
|
|
383
|
+
[-1, 1],
|
|
384
|
+
[-1, -1],
|
|
385
|
+
[1, 1],
|
|
386
|
+
];
|
|
387
|
+
return corners[Math.floor(r() * corners.length)];
|
|
388
|
+
}
|
|
389
|
+
/** when the next idle jump is due: `every` seconds, give or take 40 % */
|
|
390
|
+
nextFlip(t, k) {
|
|
391
|
+
const every = this.jump.every;
|
|
392
|
+
return every > 0 ? t + every * k * (0.625 + this.rand() * 0.75) : Infinity;
|
|
393
|
+
}
|
|
394
|
+
/** Advance by `dt` seconds (already scaled by the speed). */
|
|
395
|
+
update(dt) {
|
|
396
|
+
dt = Math.min(dt, 0.05);
|
|
397
|
+
this.t += dt;
|
|
398
|
+
const t = this.t;
|
|
399
|
+
const p = this.pose;
|
|
400
|
+
const w = p.w;
|
|
401
|
+
/* the state change eases from the weights it started with to the new
|
|
402
|
+
state's on an S-curve: soft start, soft finish, no creeping tail */
|
|
403
|
+
if (this.tr < 1) {
|
|
404
|
+
this.tr = Math.min(1, this.tr + dt / this.trDuration);
|
|
405
|
+
/* a sine ease: no kick at either end of a switch */
|
|
406
|
+
const e = easeSine(this.tr);
|
|
407
|
+
for (let i = 0; i < 3; i++) {
|
|
408
|
+
const target = STATES[i] === this.state ? 1 : 0;
|
|
409
|
+
w[i] = this.wFrom[i] + (target - this.wFrom[i]) * e;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const [wd, ww, ws] = w;
|
|
413
|
+
/* rest targets, blended */
|
|
414
|
+
const rest = { pitch: 0, roll: 0, y: 0, lookX: 0, lookY: 0 };
|
|
415
|
+
for (let i = 0; i < 3; i++) {
|
|
416
|
+
const r = REST[STATES[i]];
|
|
417
|
+
rest.pitch += r.pitch * w[i];
|
|
418
|
+
rest.roll += r.roll * w[i];
|
|
419
|
+
rest.y += r.y * w[i];
|
|
420
|
+
rest.lookX += r.lookX * w[i];
|
|
421
|
+
rest.lookY += r.lookY * w[i];
|
|
422
|
+
}
|
|
423
|
+
/* the idle gaze: a place to look, a while to stay, then the swing to
|
|
424
|
+
the next — the wanderers carry it on their own springs */
|
|
425
|
+
if (this.state === 'default' && t >= this.gazeAt) {
|
|
426
|
+
const [gx, gy] = this.nextGaze();
|
|
427
|
+
this.gazeDir = [gx, gy];
|
|
428
|
+
const reach = 0.84 + this.rand() * 0.16;
|
|
429
|
+
this.yawW.aim(gx * GAZE_YAW * reach * this.turnK);
|
|
430
|
+
this.pitchW.aim(gy * GAZE_PITCH * reach);
|
|
431
|
+
/* the tilt goes with the turn, so it follows it */
|
|
432
|
+
this.rollW.aim(gx * GAZE_ROLL * reach * this.turnK);
|
|
433
|
+
this.gazeAt = t + GAZE_HOLD_MIN + this.rand() * (GAZE_HOLD_MAX - GAZE_HOLD_MIN);
|
|
434
|
+
}
|
|
435
|
+
/* wander */
|
|
436
|
+
this.yawW.update(t, dt);
|
|
437
|
+
this.pitchW.update(t, dt);
|
|
438
|
+
this.rollW.update(t, dt);
|
|
439
|
+
this.lookXW.update(t, dt);
|
|
440
|
+
this.lookYW.update(t, dt);
|
|
441
|
+
/* following the pointer: the eyes lead, the head turns after them,
|
|
442
|
+
and the wander quietens while it lasts */
|
|
443
|
+
this.ptrS = approach(this.ptrS, this.ptrTargetS, 8, dt);
|
|
444
|
+
this.ptrX = approach(this.ptrX, this.ptrTargetX, 14, dt);
|
|
445
|
+
this.ptrY = approach(this.ptrY, this.ptrTargetY, 14, dt);
|
|
446
|
+
const ps = this.ptrS;
|
|
447
|
+
const quiet = 1 - 0.75 * ps;
|
|
448
|
+
/* the base pose: the blended rest plus the smoothed wander and the
|
|
449
|
+
pointer's pull — every term is already continuous */
|
|
450
|
+
this.baseYaw = approach(this.baseYaw, this.yawW.value * quiet + 22 * DEG * this.ptrX * ps, 5, dt);
|
|
451
|
+
const basePitch = rest.pitch + this.pitchW.value * quiet - 12 * DEG * this.ptrY * ps;
|
|
452
|
+
const baseRoll = rest.roll + this.rollW.value * quiet;
|
|
453
|
+
const baseY = rest.y;
|
|
454
|
+
const baseLookX = rest.lookX + this.lookXW.value * quiet + 4.5 * this.ptrX * ps;
|
|
455
|
+
const baseLookY = rest.lookY + this.lookYW.value * quiet + 3 * this.ptrY * ps;
|
|
456
|
+
/* ── events ── */
|
|
457
|
+
let spin = 0, hopY = 0, sx = 1, sy = 1, pitchAdd = 0, rollAdd = 0, blinkClose = 0, lookXAdd = 0, lookYAdd = 0, laugh = 0;
|
|
458
|
+
let whirl = 0, whirlAngle = 0;
|
|
459
|
+
/* the whirl: not there until the turn is visibly under way, gone
|
|
460
|
+
before the landing */
|
|
461
|
+
const smooth = (a, b, v) => {
|
|
462
|
+
const x = Math.min(1, Math.max(0, (v - a) / (b - a)));
|
|
463
|
+
return x * x * (3 - 2 * x);
|
|
464
|
+
};
|
|
465
|
+
const envelope = (q) => smooth(0.1, 0.26, q) * (1 - smooth(0.66, 0.9, q));
|
|
466
|
+
/* the ring's own travel: fast and steady from the first frame, with a
|
|
467
|
+
little of the body's own easing on top, so it is moving the moment
|
|
468
|
+
it shows */
|
|
469
|
+
const ringAngle = (q) => TAU * (1.5 * q + 0.9 * easeInOut(q));
|
|
470
|
+
/* blinks: idle and working blink; a double blink now and then */
|
|
471
|
+
if (t >= this.blinkAt && !this.blink.active && wd + ww > 0.5) {
|
|
472
|
+
this.blink.fire();
|
|
473
|
+
this.blinkAgain = !this.blinkAgain && this.rand() < 0.22;
|
|
474
|
+
this.blinkAt = t + (this.blinkAgain ? 0.28 : 2.2 + this.rand() * 2.6);
|
|
475
|
+
}
|
|
476
|
+
this.blink.update(dt);
|
|
477
|
+
if (this.blink.active)
|
|
478
|
+
blinkClose = Math.sin(Math.PI * this.blink.p);
|
|
479
|
+
/* eye darts: a quick glance to the side and back, between the slower
|
|
480
|
+
looks — the eyes have a life of their own */
|
|
481
|
+
if (t >= this.dartAt && !this.dart.active && wd + ww > 0.5) {
|
|
482
|
+
this.dart.fire();
|
|
483
|
+
this.dartX = (this.rand() * 2 - 1) * 4;
|
|
484
|
+
this.dartY = (this.rand() * 2 - 1) * 2;
|
|
485
|
+
this.dart.duration = 0.25 + this.rand() * 0.45;
|
|
486
|
+
this.dartAt = t + 1.2 + this.rand() * 2.6;
|
|
487
|
+
}
|
|
488
|
+
this.dart.update(dt);
|
|
489
|
+
if (this.dart.active) {
|
|
490
|
+
const q = this.dart.p;
|
|
491
|
+
/* snap out, hold, snap back */
|
|
492
|
+
const hold = q < 0.15 ? q / 0.15 : q > 0.8 ? (1 - q) / 0.2 : 1;
|
|
493
|
+
lookXAdd += this.dartX * hold * (wd + ww);
|
|
494
|
+
lookYAdd += this.dartY * hold * (wd + ww);
|
|
495
|
+
}
|
|
496
|
+
/* idle: a full turn now and then, with a jump */
|
|
497
|
+
if (this.state === 'default' && t >= this.flipAt && !this.flip.active) {
|
|
498
|
+
this.flipPoked = false;
|
|
499
|
+
this.flip.duration = flipDuration(this.jump, false);
|
|
500
|
+
this.flipSide = this.rand() < 0.5 ? -1 : 1;
|
|
501
|
+
this.flip.fire();
|
|
502
|
+
this.flipAt = this.nextFlip(t, 1);
|
|
503
|
+
}
|
|
504
|
+
this.flip.update(dt);
|
|
505
|
+
if (this.flip.active) {
|
|
506
|
+
const J = this.jump;
|
|
507
|
+
/* the hop's own phase: below 0 the crouch, above 1 the recovery */
|
|
508
|
+
const preS = flipPre(J, this.flipPoked);
|
|
509
|
+
const a = (this.flip.p * this.flip.duration - preS) / J.time;
|
|
510
|
+
const q = Math.min(1, Math.max(0, a));
|
|
511
|
+
const arc = Math.sin(Math.PI * q);
|
|
512
|
+
/* whole turns, eased in and out, no overshoot to snap back from */
|
|
513
|
+
spin += TAU * J.spin * easeInOut(q);
|
|
514
|
+
hopY -= J.height * arc;
|
|
515
|
+
/* the crouch before take-off; then the landing squash from `land`
|
|
516
|
+
seconds round the moment of contact — before it, the body braces
|
|
517
|
+
for the ground; after it, it gives late */
|
|
518
|
+
const tl = (a - 1) * J.time - J.land;
|
|
519
|
+
const squashTime = this.flipPoked ? J.clickSquashTime : J.squashTime;
|
|
520
|
+
/* the crouch: a click's builds over its squash time, an idle jump's
|
|
521
|
+
is the hop's short bump; then the hop, then the landing squash */
|
|
522
|
+
const crouch = (u) => (this.flipPoked ? u * u * (3 - 2 * u) : hopSquash(u - 1));
|
|
523
|
+
/* the landing in three parts: the press down to the deepest squash
|
|
524
|
+
in the squash easing's own shape, the hold on the ground, then
|
|
525
|
+
the rise back to the body's shape — each with its own time and
|
|
526
|
+
easing, and all three meeting at the same depth, so nothing steps */
|
|
527
|
+
const hold = Math.max(0, J.groundTime);
|
|
528
|
+
const peakT = SQUASH_PEAK[J.squashEase] * squashTime;
|
|
529
|
+
const rise = tl - peakT - hold;
|
|
530
|
+
const depth = tl <= peakT
|
|
531
|
+
? squashPulse(tl / squashTime, J.squashEase)
|
|
532
|
+
: rise <= 0
|
|
533
|
+
? groundShape((tl - peakT) / hold, J.groundEase)
|
|
534
|
+
: risePulse(rise / J.riseTime, J.riseEase);
|
|
535
|
+
const land = (a < 0 ? crouch(Math.max(0, 1 + (a * J.time) / preS)) : tl > 0 ? depth : a < 0.2 ? hopSquash(a) : 0) * J.squash;
|
|
536
|
+
sx += 0.16 * land - 0.06 * arc * J.stretch;
|
|
537
|
+
sy += -0.18 * land + 0.09 * arc * J.stretch;
|
|
538
|
+
/* lean into it; eyes shut for a turn */
|
|
539
|
+
rollAdd += this.flipSide * J.lean * DEG * arc;
|
|
540
|
+
if (J.spin > 0) {
|
|
541
|
+
laugh = Math.max(laugh, arc);
|
|
542
|
+
whirl = Math.max(whirl, envelope(q));
|
|
543
|
+
whirlAngle = ringAngle(q);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/* working: hops all the time; every third one spins. Leaving the
|
|
547
|
+
state, the hop under way keeps the strength it had — it flies its
|
|
548
|
+
whole arc, finishes its turn and lands — and that last landing is
|
|
549
|
+
not the rhythm's quick bump but a jump's proper one: the hold on
|
|
550
|
+
the ground and the rise back, from the depth the hop reached. */
|
|
551
|
+
const J = this.jump;
|
|
552
|
+
const exitHold = Math.max(0, J.groundTime);
|
|
553
|
+
const exitFor = exitHold + J.riseTime;
|
|
554
|
+
if (this.state === 'working')
|
|
555
|
+
this.hopGain = ww;
|
|
556
|
+
if (this.hopGain > 0.02 && (this.state === 'working' || this.hopPhase > 0)) {
|
|
557
|
+
this.hopPhase += dt / HOP_T;
|
|
558
|
+
if (this.hopPhase >= 1) {
|
|
559
|
+
if (this.state === 'working') {
|
|
560
|
+
this.hopPhase -= 1;
|
|
561
|
+
this.hopCount += 1;
|
|
562
|
+
}
|
|
563
|
+
else if ((this.hopPhase - 1) * HOP_T >= exitFor) {
|
|
564
|
+
/* the last hop has landed and settled */
|
|
565
|
+
this.hopPhase = 0;
|
|
566
|
+
this.hopGain = 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const g = this.hopGain;
|
|
570
|
+
/* past 1 the body is down and only the landing is still playing */
|
|
571
|
+
const q = Math.min(1, this.hopPhase);
|
|
572
|
+
const arc = Math.sin(Math.PI * q);
|
|
573
|
+
const spinning = this.hopCount % 3 === 2;
|
|
574
|
+
const h = spinning ? HOP_SPIN_H : 18;
|
|
575
|
+
hopY -= h * arc * g;
|
|
576
|
+
/* squash on landing, stretch at the top */
|
|
577
|
+
const exitT = this.state !== 'working' && this.hopPhase > 1 ? (this.hopPhase - 1) * HOP_T : -1;
|
|
578
|
+
const land = exitT < 0
|
|
579
|
+
? hopSquash(this.hopPhase)
|
|
580
|
+
: exitT < exitHold
|
|
581
|
+
? groundShape(exitT / exitHold, J.groundEase)
|
|
582
|
+
: risePulse((exitT - exitHold) / J.riseTime, J.riseEase);
|
|
583
|
+
sx += (0.16 * land - 0.06 * arc) * g;
|
|
584
|
+
sy += (-0.18 * land + 0.09 * arc) * g;
|
|
585
|
+
if (spinning) {
|
|
586
|
+
spin += TAU * easeInOut(q) * g;
|
|
587
|
+
/* eyes shut for the spin */
|
|
588
|
+
laugh = Math.max(laugh, arc * g);
|
|
589
|
+
if (envelope(q) * g > whirl) {
|
|
590
|
+
whirl = envelope(q) * g;
|
|
591
|
+
whirlAngle = ringAngle(q);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/* lean into each hop, alternating sides */
|
|
595
|
+
rollAdd += (this.hopCount % 2 === 0 ? 1 : -1) * 6 * DEG * arc * g;
|
|
596
|
+
}
|
|
597
|
+
/* working: now and then a laugh shuts the eyes into arcs, then they
|
|
598
|
+
open again */
|
|
599
|
+
if (this.state === 'working' && t >= this.laughAt && !this.laughEv.active) {
|
|
600
|
+
this.laughEv.fire();
|
|
601
|
+
this.laughEv.duration = 0.6 + this.rand() * 0.5;
|
|
602
|
+
this.laughAt = t + 1.6 + this.rand() * 2.2;
|
|
603
|
+
}
|
|
604
|
+
this.laughEv.update(dt);
|
|
605
|
+
if (this.laughEv.active) {
|
|
606
|
+
const q = this.laughEv.p;
|
|
607
|
+
/* quick shut, hold, quick open */
|
|
608
|
+
laugh = Math.max(laugh, q < 0.18 ? q / 0.18 : q > 0.78 ? (1 - q) / 0.22 : 1);
|
|
609
|
+
}
|
|
610
|
+
/* sleeping: the head drops, then jerks back up */
|
|
611
|
+
if (this.state === 'sleeping' && t >= this.nodAt && !this.nod.active) {
|
|
612
|
+
this.nod.fire();
|
|
613
|
+
this.nodAt = t + 4 + this.rand() * 4;
|
|
614
|
+
}
|
|
615
|
+
this.nod.update(dt);
|
|
616
|
+
if (this.nod.active) {
|
|
617
|
+
const q = this.nod.p;
|
|
618
|
+
/* slow slide down, quick recovery */
|
|
619
|
+
const dip = q < 0.72 ? easeInOut(q / 0.72) : 1 - easeInOut((q - 0.72) / 0.28);
|
|
620
|
+
pitchAdd -= 13 * DEG * dip * ws;
|
|
621
|
+
}
|
|
622
|
+
/* breathing, always, deeper and slower asleep. The phase is integrated
|
|
623
|
+
so the period can change with the state without the cycle jumping */
|
|
624
|
+
this.breathPhase += dt / (3.6 + 1.2 * ws);
|
|
625
|
+
const breath = Math.sin(this.breathPhase * TAU);
|
|
626
|
+
p.breath = breath;
|
|
627
|
+
sx += breath * (0.008 + 0.014 * ws);
|
|
628
|
+
sy += breath * (0.012 + 0.02 * ws);
|
|
629
|
+
const bob = Math.sin(t * TAU / 3.4) * 2 * (1 - ws);
|
|
630
|
+
/* ── compose ── */
|
|
631
|
+
p.yaw = this.baseYaw + spin;
|
|
632
|
+
/* the jelly: the faster the head turns, the more the body stretches
|
|
633
|
+
along the turn, on a spring that overshoots and settles. A flip's
|
|
634
|
+
spin is left out: that is a jump, and the landing spring handles it. */
|
|
635
|
+
let dyaw = this.baseYaw - this.prevYaw;
|
|
636
|
+
dyaw = ((dyaw + Math.PI) % TAU + TAU) % TAU - Math.PI;
|
|
637
|
+
this.prevYaw = this.baseYaw;
|
|
638
|
+
const rate = dt > 0 ? Math.abs(dyaw) / dt : 0;
|
|
639
|
+
/* the eyes lead a head turn, the way a gaze shift goes: they run
|
|
640
|
+
ahead of it and come back as the head settles */
|
|
641
|
+
const leadTarget = dt > 0 ? Math.max(-2.2, Math.min(2.2, (dyaw / dt) * 2.4)) : 0;
|
|
642
|
+
this.gazeLead = approach(this.gazeLead, leadTarget, 9, dt);
|
|
643
|
+
const jellyTarget = Math.min(0.22, 0.055 * rate);
|
|
644
|
+
const omega = 16, zeta = 0.45;
|
|
645
|
+
this.jellyV += (omega * omega * (jellyTarget - this.jelly) - 2 * zeta * omega * this.jellyV) * dt;
|
|
646
|
+
this.jelly += this.jellyV * dt;
|
|
647
|
+
const jelly = Math.max(-0.08, Math.min(0.28, this.jelly)) * 0.6;
|
|
648
|
+
sx *= 1 + jelly;
|
|
649
|
+
sy *= 1 - 0.55 * jelly;
|
|
650
|
+
p.pitch = basePitch + pitchAdd;
|
|
651
|
+
p.roll = baseRoll + rollAdd;
|
|
652
|
+
p.x = 0;
|
|
653
|
+
p.y = baseY + hopY + bob;
|
|
654
|
+
p.sx = sx;
|
|
655
|
+
p.sy = sy;
|
|
656
|
+
/* the lids: the sleeping state closes them through its own weight in
|
|
657
|
+
the renderer, so here the eyes stay open apart from blinks */
|
|
658
|
+
p.eyeOpen = 1;
|
|
659
|
+
p.laugh = approach(p.laugh, laugh, 30, dt);
|
|
660
|
+
p.blinkL = blinkClose;
|
|
661
|
+
p.blinkR = blinkClose;
|
|
662
|
+
p.lookX = baseLookX + lookXAdd + this.gazeLead;
|
|
663
|
+
p.lookY = baseLookY + lookYAdd;
|
|
664
|
+
p.whirl = whirl;
|
|
665
|
+
p.whirlAngle = whirlAngle;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** The still pose of a state, for reduced motion and the first paint. */
|
|
669
|
+
export function restPose(state) {
|
|
670
|
+
const r = REST[state];
|
|
671
|
+
return {
|
|
672
|
+
yaw: 0,
|
|
673
|
+
pitch: r.pitch,
|
|
674
|
+
roll: r.roll,
|
|
675
|
+
x: 0,
|
|
676
|
+
y: r.y,
|
|
677
|
+
sx: 1,
|
|
678
|
+
sy: 1,
|
|
679
|
+
eyeOpen: 1,
|
|
680
|
+
blinkL: 0,
|
|
681
|
+
blinkR: 0,
|
|
682
|
+
lookX: r.lookX,
|
|
683
|
+
lookY: r.lookY,
|
|
684
|
+
breath: 0,
|
|
685
|
+
laugh: 0,
|
|
686
|
+
whirl: 0,
|
|
687
|
+
whirlAngle: 0,
|
|
688
|
+
w: STATES.map((s) => (s === state ? 1 : 0)),
|
|
689
|
+
};
|
|
690
|
+
}
|