@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.
- package/animations/axel.ts +459 -0
- package/animations/flo.ts +458 -0
- package/animations/index.ts +12 -0
- package/animations/rex.ts +377 -0
- package/animations/scout.ts +437 -0
- package/animations/vera.ts +417 -0
- package/components/AequaraAvatar.tsx +130 -0
- package/components/AvatarShell.tsx +84 -0
- package/components/AxelAvatar.tsx +330 -0
- package/components/FloAvatar.tsx +312 -0
- package/components/GoldAvatar.tsx +304 -0
- package/components/RexAvatar.tsx +330 -0
- package/components/ScoutAvatar.tsx +308 -0
- package/components/SilverAvatar.tsx +70 -0
- package/components/VeraAvatar.tsx +342 -0
- package/css/mascots.css +310 -0
- package/hooks/useAvatarState.ts +84 -0
- package/hooks/useGazeTracking.ts +227 -0
- package/hooks/useIdleVariety.ts +80 -0
- package/hooks/useReducedMotion.ts +76 -0
- package/hooks/useSpringPhysics.ts +119 -0
- package/hooks/useVoiceSync.ts +173 -0
- package/index.ts +85 -0
- package/package.json +47 -0
- package/personas.ts +495 -0
- package/state-machine.ts +180 -0
- package/svg/AxelSVG.tsx +320 -0
- package/svg/FloSVG.tsx +302 -0
- package/svg/RexSVG.tsx +370 -0
- package/svg/ScoutSVG.tsx +330 -0
- package/svg/VeraSVG.tsx +322 -0
- package/tsconfig.json +20 -0
- package/types.ts +168 -0
|
@@ -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
|
+
}
|