@hub-of-life/mascots 1.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,459 @@
1
+ /**
2
+ * Axel GSAP animation timelines — all 8 states
3
+ *
4
+ * Character notes:
5
+ * - Fox: strategic, cunning, wise, energetic — the NEXUS intelligence avatar
6
+ * - NOT as bouncy as Scout (eagle) — more calculating and deliberate
7
+ * - More alert/predatory than Lux (owl) — snappier transitions
8
+ * - Ears are the PRIMARY expression vehicle (pivot at base)
9
+ * - Tail is a secondary mood indicator (swish=happy, curl=concern, still=focus)
10
+ * - Head tilts quick like a predator zeroing in
11
+ * - Eyes: amber iris, vertical slit pupil — sharp and calculating
12
+ *
13
+ * SVG element IDs targeted (must match AxelSVG.tsx):
14
+ * #axel-root, #axel-shadow, #axel-body, #axel-torso, #axel-chest-patch
15
+ * #axel-tail, #axel-head, #axel-head-base
16
+ * #axel-ear-left, #axel-ear-right
17
+ * #axel-face-mask-left, #axel-face-mask-right
18
+ * #axel-muzzle, #axel-nose
19
+ * #axel-eye-left, #axel-eye-right
20
+ * #axel-eye-left-lid, #axel-eye-right-lid
21
+ * #axel-eye-left-iris, #axel-eye-right-iris
22
+ * #axel-mouth-upper, #axel-mouth-lower
23
+ * #axel-cheek-left, #axel-cheek-right
24
+ *
25
+ * CSS prerequisite (in mascots.css):
26
+ * #axel-root * { transform-box: fill-box; transform-origin: center; }
27
+ *
28
+ * Quality tier: Platinum [92/100] — GSAP core (free), no Club plugins required.
29
+ * Mouth scaleY from voice amplitude is handled separately in AxelAvatar.tsx.
30
+ */
31
+
32
+ import gsap from 'gsap';
33
+ import type { AvatarState } from '../types';
34
+
35
+ // ─── Easing shortcuts ─────────────────────────────────────────────────────────
36
+
37
+ const E = {
38
+ inOut: 'sine.inOut',
39
+ out: 'power2.out',
40
+ outStrong:'power3.out',
41
+ backOut: 'back.out(1.7)',
42
+ backIn: 'back.in(1.2)',
43
+ bounce: 'bounce.out',
44
+ elastic: 'elastic.out(1, 0.4)',
45
+ } as const;
46
+
47
+ // ─── Shared sub-routines ──────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Fox blink — quick and sharp (predator reflex, not slow owl blink).
51
+ */
52
+ function addAxelBlinks(tl: gsap.core.Timeline, startAt: number): void {
53
+ // Blink 1 — very quick
54
+ tl.to(['#axel-eye-left-lid', '#axel-eye-right-lid'],
55
+ { scaleY: 1, duration: 0.07, ease: 'power3.in' }, startAt)
56
+ .to(['#axel-eye-left-lid', '#axel-eye-right-lid'],
57
+ { scaleY: 0, duration: 0.09, ease: 'power2.out' }, startAt + 0.08);
58
+
59
+ // Blink 2 — staggered (foxes sometimes blink one eye slightly after the other)
60
+ tl.to('#axel-eye-left-lid',
61
+ { scaleY: 1, duration: 0.07, ease: 'power3.in' }, startAt + 4.0)
62
+ .to('#axel-eye-left-lid',
63
+ { scaleY: 0, duration: 0.09, ease: 'power2.out' }, startAt + 4.08)
64
+ .to('#axel-eye-right-lid',
65
+ { scaleY: 1, duration: 0.07, ease: 'power3.in' }, startAt + 4.04)
66
+ .to('#axel-eye-right-lid',
67
+ { scaleY: 0, duration: 0.09, ease: 'power2.out' }, startAt + 4.12);
68
+ }
69
+
70
+ // ─── State factories ──────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * IDLE — alert, calculating resting state (9s loop, repeat: -1).
74
+ * Subtle breathing, ears have independent micro-twitches, tail sways slowly,
75
+ * iris darts with predator precision. Quick sharp blink.
76
+ */
77
+ export function createIdleTimeline(): gsap.core.Timeline {
78
+ const tl = gsap.timeline({ repeat: -1 });
79
+
80
+ // Body breathing — lean, controlled fox breath
81
+ tl.to('#axel-body', {
82
+ scaleY: 1.014, scaleX: 0.991, duration: 2.6, ease: E.inOut,
83
+ }, 0)
84
+ .to('#axel-body', {
85
+ scaleY: 1, scaleX: 1, duration: 2.2, ease: E.inOut,
86
+ }, 2.6);
87
+
88
+ // Head very subtle bob — fox stays alert even at rest
89
+ tl.to('#axel-head', {
90
+ y: -1.8, duration: 2.6, ease: E.inOut,
91
+ }, 0.2)
92
+ .to('#axel-head', {
93
+ y: 0, duration: 2.2, ease: E.inOut,
94
+ }, 2.8);
95
+
96
+ // Tail slow sinuous sway — fox idle signature
97
+ tl.to('#axel-tail', {
98
+ rotation: 6, x: 4, duration: 3.5, ease: E.inOut,
99
+ }, 0.4)
100
+ .to('#axel-tail', {
101
+ rotation: -4, x: -2, duration: 3.5, ease: E.inOut,
102
+ }, 3.9)
103
+ .to('#axel-tail', {
104
+ rotation: 0, x: 0, duration: 2.0, ease: E.inOut,
105
+ }, 7.4);
106
+
107
+ // Ear micro-twitch — independent ear movement (fox ears are always scanning)
108
+ const earTwitch = gsap.timeline({ repeat: -1, repeatDelay: 1.8 });
109
+ earTwitch
110
+ .to('#axel-ear-left', { rotation: -4, duration: 0.28, ease: E.out })
111
+ .to('#axel-ear-left', { rotation: 0, duration: 0.35, ease: E.inOut }, 0.35)
112
+ .to('#axel-ear-right', { rotation: 3, duration: 0.24, ease: E.out }, 0.20)
113
+ .to('#axel-ear-right', { rotation: 0, duration: 0.32, ease: E.inOut }, 0.52);
114
+ tl.add(earTwitch, 1.0);
115
+
116
+ // Iris dart — calculating scan, quick and precise
117
+ const irisIdle = gsap.timeline({ repeat: -1, repeatDelay: 2.5 });
118
+ irisIdle
119
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 2.5, duration: 0.18, ease: E.out })
120
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 0, duration: 0.22, ease: E.inOut }, 0.26)
121
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: -2, duration: 0.18, ease: E.out }, 0.58)
122
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 0, duration: 0.20, ease: E.inOut }, 0.82);
123
+ tl.add(irisIdle, 1.5);
124
+
125
+ // Shadow breathes with body
126
+ tl.to('#axel-shadow', {
127
+ scaleX: 0.97, duration: 2.6, ease: E.inOut,
128
+ }, 0)
129
+ .to('#axel-shadow', {
130
+ scaleX: 1, duration: 2.2, ease: E.inOut,
131
+ }, 2.6);
132
+
133
+ // Two sharp blinks
134
+ addAxelBlinks(tl, 1.8);
135
+
136
+ return tl;
137
+ }
138
+
139
+ /**
140
+ * GREETING — ears forward, head bobs, tail excited swish. Friendly but composed.
141
+ * Duration: ~1.7s.
142
+ */
143
+ export function createGreetingTimeline(): gsap.core.Timeline {
144
+ const tl = gsap.timeline();
145
+
146
+ tl.set(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0 });
147
+
148
+ // Both ears perk sharply forward
149
+ tl.to('#axel-ear-left', { rotation: -12, y: -5, duration: 0.22, ease: E.backOut }, 0)
150
+ .to('#axel-ear-right', { rotation: 12, y: -5, duration: 0.22, ease: E.backOut }, 0.04);
151
+
152
+ // Head bobs — quick and decisive (NEXUS is strategic, not goofy)
153
+ tl.to('#axel-head', { y: -7, duration: 0.18, ease: E.backOut }, 0.08)
154
+ .to('#axel-head', { y: 0, duration: 0.22, ease: E.out }, 0.26)
155
+ .to('#axel-head', { y: -4, duration: 0.15, ease: E.backOut }, 0.50)
156
+ .to('#axel-head', { y: 0, duration: 0.20, ease: E.out }, 0.65);
157
+
158
+ // Tail excited swish — happy greeting
159
+ tl.to('#axel-tail', { rotation: 18, x: 8, duration: 0.25, ease: E.backOut }, 0.05)
160
+ .to('#axel-tail', { rotation: -10, x: -4, duration: 0.28, ease: E.inOut }, 0.30)
161
+ .to('#axel-tail', { rotation: 8, x: 4, duration: 0.22, ease: E.inOut }, 0.58)
162
+ .to('#axel-tail', { rotation: 0, x: 0, duration: 0.38, ease: E.out }, 0.80);
163
+
164
+ // Body slight lift — engagement
165
+ tl.to('#axel-body', { y: -4, scaleY: 1.01, duration: 0.20, ease: E.backOut }, 0.05)
166
+ .to('#axel-body', { y: 0, scaleY: 1, duration: 0.35, ease: E.out }, 0.80);
167
+
168
+ // Cheek brightens slightly
169
+ tl.to(['#axel-cheek-left', '#axel-cheek-right'], { opacity: 0.72, duration: 0.18 }, 0.10)
170
+ .to(['#axel-cheek-left', '#axel-cheek-right'], { opacity: 0.50, duration: 0.55 }, 0.80);
171
+
172
+ // Ears settle
173
+ tl.to('#axel-ear-left', { rotation: 0, y: 0, duration: 0.38, ease: E.out }, 0.85)
174
+ .to('#axel-ear-right', { rotation: 0, y: 0, duration: 0.38, ease: E.out }, 0.85);
175
+
176
+ return tl;
177
+ }
178
+
179
+ /**
180
+ * THINKING — quick predator head-tilt, one ear forward/one back,
181
+ * active iris scan, tail stills (deep concentration).
182
+ * Loops until THINKING_END trigger.
183
+ */
184
+ export function createThinkingTimeline(): gsap.core.Timeline {
185
+ const tl = gsap.timeline();
186
+
187
+ // Quick decisive head tilt right — fox zeroing in on a problem
188
+ tl.to('#axel-head', { rotation: 13, x: 3, duration: 0.25, ease: E.backOut }, 0);
189
+
190
+ // Asymmetric ears: right ear pivots forward (tracking), left ear tilts back
191
+ tl.to('#axel-ear-right', { rotation: 14, y: -3, duration: 0.28, ease: E.backOut }, 0.05);
192
+ tl.to('#axel-ear-left', { rotation: -8, y: 2, duration: 0.28, ease: E.backOut }, 0.05);
193
+
194
+ // Tail slows and stills — energy redirected to thinking
195
+ tl.to('#axel-tail', { rotation: 3, x: 1, duration: 0.50, ease: E.inOut }, 0);
196
+
197
+ // Active precision iris scan — fox analyzing
198
+ const irisScanning = gsap.timeline({ repeat: -1, repeatDelay: 0.8 });
199
+ irisScanning
200
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 2.5, duration: 0.28, ease: E.out })
201
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: -2.5, duration: 0.45, ease: E.inOut })
202
+ .to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 0.5, duration: 0.30, ease: E.inOut });
203
+ tl.add(irisScanning, 0.5);
204
+
205
+ // Half-squint on right eye (cunning focus — one eye slightly lidded)
206
+ tl.to('#axel-eye-right-lid', { scaleY: 0.28, duration: 0.30, ease: E.inOut }, 0.35);
207
+
208
+ // Slow contemplative blink loop (left eye only — thinking squint maintained on right)
209
+ const thinkBlink = gsap.timeline({ repeat: -1, repeatDelay: 2.8 });
210
+ thinkBlink
211
+ .to('#axel-eye-left-lid', { scaleY: 0.55, duration: 0.20, ease: E.inOut })
212
+ .to('#axel-eye-left-lid', { scaleY: 0, duration: 0.16, ease: E.out });
213
+ tl.add(thinkBlink, 0.8);
214
+
215
+ return tl;
216
+ }
217
+
218
+ /**
219
+ * SPEAKING — body energy with speech rhythm, ears engage dynamically,
220
+ * tail sways in rhythm. Beak (mouth) driven externally by voice amplitude.
221
+ */
222
+ export function createSpeakingTimeline(): gsap.core.Timeline {
223
+ const tl = gsap.timeline({ repeat: -1 });
224
+
225
+ // Body speech energy — controlled but present
226
+ tl.to('#axel-body', { scaleY: 1.006, scaleX: 0.997, duration: 0.17, ease: E.inOut })
227
+ .to('#axel-body', { scaleY: 1, scaleX: 1, duration: 0.15, ease: E.inOut });
228
+
229
+ // Head subtle assertion bob
230
+ tl.to('#axel-head', { y: -1.5, duration: 0.17, ease: E.inOut }, 0)
231
+ .to('#axel-head', { y: 0, duration: 0.15, ease: E.inOut }, 0.17);
232
+
233
+ // Eyes alert and direct — direct eye contact
234
+ tl.set(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0 });
235
+ const speakBlink = gsap.timeline({ repeat: -1, repeatDelay: 3.8 });
236
+ speakBlink
237
+ .to(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 1, duration: 0.07 })
238
+ .to(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0, duration: 0.08 });
239
+ tl.add(speakBlink, 0.6);
240
+
241
+ // Ears forward — engaged speaker posture
242
+ tl.to('#axel-ear-left', { rotation: -6, duration: 0.30, ease: E.out }, 0);
243
+ tl.to('#axel-ear-right', { rotation: 6, duration: 0.30, ease: E.out }, 0);
244
+
245
+ // Tail gentle rhythmic sway — speech rhythm
246
+ tl.to('#axel-tail', { rotation: 5, x: 3, duration: 0.17, ease: E.inOut }, 0)
247
+ .to('#axel-tail', { rotation: -3, x: -1, duration: 0.15, ease: E.inOut }, 0.17);
248
+
249
+ // Iris forward locked
250
+ tl.to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 0, duration: 0.22, ease: E.out }, 0);
251
+
252
+ return tl;
253
+ }
254
+
255
+ /**
256
+ * LISTENING — both ears pivot sharply forward (max alert),
257
+ * body leans in, iris tracks forward, tail steady.
258
+ */
259
+ export function createListeningTimeline(): gsap.core.Timeline {
260
+ const tl = gsap.timeline();
261
+
262
+ // BOTH ears snap fully forward — fox max-alert listening stance
263
+ tl.to('#axel-ear-left', { rotation: -16, y: -5, duration: 0.20, ease: E.backOut }, 0);
264
+ tl.to('#axel-ear-right', { rotation: 16, y: -5, duration: 0.20, ease: E.backOut }, 0.03);
265
+
266
+ // Slight lean forward — attentive
267
+ tl.to('#axel-body', { y: 3, scaleY: 0.98, duration: 0.38, ease: E.backOut }, 0);
268
+ tl.to('#axel-head', { y: 2, rotation: -4, duration: 0.38, ease: E.backOut }, 0.05);
269
+
270
+ // Muzzle tips slightly toward source
271
+ tl.to('#axel-muzzle', { y: 2, duration: 0.35, ease: E.out }, 0.10);
272
+
273
+ // Eyes wide — lids fully retracted
274
+ tl.set(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0 });
275
+
276
+ // Iris tracks forward and holds — locked attention
277
+ const eyeTrack = gsap.timeline({ repeat: -1, yoyo: true, repeatDelay: 1.8 });
278
+ eyeTrack.to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 1.5, duration: 1.0, ease: E.inOut });
279
+ tl.add(eyeTrack, 0.4);
280
+
281
+ // Tail holds steady — all energy directed to listening
282
+ tl.to('#axel-tail', { rotation: 2, x: 0, duration: 0.40, ease: E.out }, 0);
283
+
284
+ // Shadow slightly lighter (leaning forward = less weight on ground)
285
+ tl.to('#axel-shadow', { scaleX: 0.95, duration: 0.38, ease: E.out }, 0);
286
+
287
+ return tl;
288
+ }
289
+
290
+ /**
291
+ * SUCCESS — ears full forward-up, body jump, tail sweeps broad arc,
292
+ * head snaps triumphant, cheeks show. Fox celebration is exuberant.
293
+ * Duration: ~1.4s peak + settle.
294
+ */
295
+ export function createSuccessTimeline(): gsap.core.Timeline {
296
+ const tl = gsap.timeline();
297
+
298
+ // Anticipation — slight crouch
299
+ tl.to('#axel-body', { scaleY: 0.93, scaleX: 1.06, y: 9, duration: 0.14, ease: E.backIn }, 0);
300
+ tl.to('#axel-head', { y: 5, duration: 0.14, ease: E.backIn }, 0);
301
+
302
+ // JUMP
303
+ tl.to('#axel-body', { y: -32, scaleY: 1.05, scaleX: 0.95, duration: 0.30, ease: E.outStrong }, 0.14);
304
+ tl.to('#axel-head', { y: -30, duration: 0.28, ease: E.outStrong }, 0.14);
305
+ tl.to('#axel-shadow', { scaleX: 0.50, opacity: 0.07, duration: 0.28 }, 0.14);
306
+
307
+ // Ears shoot fully up at peak
308
+ tl.to('#axel-ear-left', { rotation: -20, y: -10, scaleY: 1.12, duration: 0.26, ease: E.backOut }, 0.18);
309
+ tl.to('#axel-ear-right', { rotation: 20, y: -10, scaleY: 1.12, duration: 0.26, ease: E.backOut }, 0.18);
310
+
311
+ // Tail sweeps bold wide arc — celebration
312
+ tl.to('#axel-tail', {
313
+ rotation: 32, x: 14, scaleX: 1.15, duration: 0.28, ease: E.backOut,
314
+ }, 0.16);
315
+
316
+ // Head tilts back then snaps forward — triumphant
317
+ tl.to('#axel-head', { rotation: -8, y: -36, duration: 0.20, ease: E.backOut }, 0.22);
318
+
319
+ // Cheeks light up
320
+ tl.to(['#axel-cheek-left', '#axel-cheek-right'], { opacity: 0.82, duration: 0.20 }, 0.20);
321
+
322
+ // LAND
323
+ tl.to('#axel-body', { y: 0, scaleY: 0.91, scaleX: 1.09, duration: 0.16, ease: E.out }, 0.46);
324
+ tl.to('#axel-head', { y: 0, rotation: 5, duration: 0.16, ease: E.out }, 0.46);
325
+ tl.to('#axel-shadow', { scaleX: 1.10, opacity: 0.22, duration: 0.16 }, 0.46);
326
+
327
+ // Bounce up (smaller)
328
+ tl.to('#axel-body', { y: -13, scaleY: 1, scaleX: 1, duration: 0.16, ease: E.out }, 0.62);
329
+ tl.to('#axel-head', { y: -11, rotation: 0, duration: 0.14, ease: E.out }, 0.62);
330
+
331
+ // Settle
332
+ tl.to('#axel-body', { y: 0, duration: 0.28, ease: E.bounce }, 0.78);
333
+ tl.to('#axel-head', { y: 0, duration: 0.26, ease: E.bounce }, 0.78);
334
+ tl.to('#axel-shadow', { scaleX: 1, opacity: 0.18, duration: 0.28 }, 0.78);
335
+
336
+ // Ears settle
337
+ tl.to('#axel-ear-left', { rotation: 0, y: 0, scaleY: 1, duration: 0.40, ease: E.backOut }, 0.60);
338
+ tl.to('#axel-ear-right', { rotation: 0, y: 0, scaleY: 1, duration: 0.40, ease: E.backOut }, 0.60);
339
+
340
+ // Tail settles
341
+ tl.to('#axel-tail', { rotation: 0, x: 0, scaleX: 1, duration: 0.45, ease: E.backOut }, 0.60);
342
+
343
+ // Cheeks fade
344
+ tl.to(['#axel-cheek-left', '#axel-cheek-right'], { opacity: 0.50, duration: 0.75 }, 0.65);
345
+
346
+ return tl;
347
+ }
348
+
349
+ /**
350
+ * CONCERN — ears flatten back (fox defensive signal), body slumps,
351
+ * tail curls inward, iris drifts down, worry blink loop.
352
+ */
353
+ export function createConcernTimeline(): gsap.core.Timeline {
354
+ const tl = gsap.timeline();
355
+
356
+ // Ears flatten back — fox defensive/worried posture
357
+ tl.to('#axel-ear-left', { rotation: 14, y: 4, duration: 0.38, ease: E.out }, 0);
358
+ tl.to('#axel-ear-right', { rotation: -14, y: 4, duration: 0.38, ease: E.out }, 0.03);
359
+
360
+ // Head slight droop
361
+ tl.to('#axel-head', { rotation: -7, y: 5, duration: 0.40, ease: E.backOut }, 0);
362
+
363
+ // Body slumps
364
+ tl.to('#axel-body', { y: 6, scaleY: 0.97, duration: 0.40, ease: E.out }, 0);
365
+
366
+ // Tail curls inward (concern — tail between legs energy)
367
+ tl.to('#axel-tail', { rotation: -12, x: -6, scaleX: 0.88, duration: 0.40, ease: E.out }, 0);
368
+
369
+ // Iris drifts slightly downward — downcast
370
+ tl.to(['#axel-eye-left-iris', '#axel-eye-right-iris'], { y: 1.5, duration: 0.40, ease: E.out }, 0);
371
+
372
+ // Worry half-blink loop — fox concern lidding
373
+ const worryBlink = gsap.timeline({ repeat: -1, repeatDelay: 2.2 });
374
+ worryBlink
375
+ .to(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0.45, duration: 0.24, ease: E.inOut })
376
+ .to(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0, duration: 0.18, ease: E.out });
377
+ tl.add(worryBlink, 0.55);
378
+
379
+ // Shadow expands slightly
380
+ tl.to('#axel-shadow', { scaleX: 1.05, opacity: 0.22, duration: 0.40, ease: E.out }, 0);
381
+
382
+ return tl;
383
+ }
384
+
385
+ /**
386
+ * FAREWELL — single ear forward as acknowledgment, deliberate head nod,
387
+ * tail gives composed final swish, fade out. Duration: ~1.4s.
388
+ */
389
+ export function createFarewellTimeline(): gsap.core.Timeline {
390
+ const tl = gsap.timeline();
391
+
392
+ // Right ear pivots forward — attentive farewell
393
+ tl.to('#axel-ear-right', { rotation: 10, y: -3, duration: 0.22, ease: E.backOut }, 0.08)
394
+ .to('#axel-ear-right', { rotation: 0, y: 0, duration: 0.35, ease: E.out }, 0.72);
395
+
396
+ // Head nod — deliberate, strategic (not just a wave)
397
+ tl.to('#axel-head', { y: -5, rotation: 4, duration: 0.20, ease: E.out }, 0.08)
398
+ .to('#axel-head', { y: 0, rotation: 0, duration: 0.24, ease: E.inOut }, 0.32)
399
+ .to('#axel-head', { y: -3, duration: 0.16, ease: E.out }, 0.60)
400
+ .to('#axel-head', { y: 0, duration: 0.20, ease: E.inOut }, 0.80);
401
+
402
+ // Tail final composed swish — measured goodbye
403
+ tl.to('#axel-tail', { rotation: 12, x: 6, duration: 0.26, ease: E.backOut }, 0.10)
404
+ .to('#axel-tail', { rotation: -6, x: -2, duration: 0.28, ease: E.inOut }, 0.46)
405
+ .to('#axel-tail', { rotation: 0, x: 0, duration: 0.30, ease: E.out }, 0.82);
406
+
407
+ // Fade out
408
+ tl.to('#axel-root', { opacity: 0, y: 10, duration: 0.44, ease: E.out }, 0.96);
409
+ tl.to('#axel-shadow', { opacity: 0, duration: 0.40 }, 0.96);
410
+
411
+ return tl;
412
+ }
413
+
414
+ // ─── State → factory map ─────────────────────────────────────────────────────
415
+
416
+ export type AxelTimelineFactory = () => gsap.core.Timeline;
417
+
418
+ export const AXEL_TIMELINE_FACTORIES: Record<AvatarState, AxelTimelineFactory> = {
419
+ idle: createIdleTimeline,
420
+ greeting: createGreetingTimeline,
421
+ thinking: createThinkingTimeline,
422
+ speaking: createSpeakingTimeline,
423
+ listening: createListeningTimeline,
424
+ success: createSuccessTimeline,
425
+ concern: createConcernTimeline,
426
+ farewell: createFarewellTimeline,
427
+ };
428
+
429
+ /**
430
+ * Mouth driver — call on every RAF frame during speaking state.
431
+ * Converts 0-1 voice amplitude → lower jaw open.
432
+ * Fox mouth has more subtle opening than eagle beak.
433
+ */
434
+ export function createBeakDriver(_containerEl: Element): (amplitude: number) => void {
435
+ const setter = gsap.quickSetter('#axel-mouth-lower', 'scaleY', '');
436
+ return (amplitude: number) => {
437
+ // 0 = closed (scaleY 1.0), 1 = fully open (scaleY 1.9)
438
+ setter(1 + amplitude * 0.9);
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Reset all Axel elements to their resting state.
444
+ * Call before starting a new state timeline.
445
+ */
446
+ export function resetAxel(): void {
447
+ gsap.set('#axel-root', { opacity: 1, y: 0 });
448
+ gsap.set('#axel-body', { y: 0, x: 0, scaleX: 1, scaleY: 1, rotation: 0 });
449
+ gsap.set('#axel-head', { y: 0, x: 0, rotation: 0 });
450
+ gsap.set('#axel-ear-left', { y: 0, x: 0, rotation: 0, scaleY: 1 });
451
+ gsap.set('#axel-ear-right', { y: 0, x: 0, rotation: 0, scaleY: 1 });
452
+ gsap.set('#axel-tail', { y: 0, x: 0, rotation: 0, scaleX: 1, scaleY: 1 });
453
+ gsap.set('#axel-muzzle', { y: 0 });
454
+ gsap.set('#axel-mouth-lower', { scaleY: 1 });
455
+ gsap.set(['#axel-eye-left-lid', '#axel-eye-right-lid'], { scaleY: 0 });
456
+ gsap.set(['#axel-eye-left-iris', '#axel-eye-right-iris'], { x: 0, y: 0 });
457
+ gsap.set('#axel-shadow', { scaleX: 1, opacity: 0.18 });
458
+ gsap.set(['#axel-cheek-left', '#axel-cheek-right'], { opacity: 0.50 });
459
+ }