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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ /** Parse and reuse the package's SVG outline paths. */
2
+ export declare function bodyPath(data: string): Path2D;