agent-robot-avatar 0.2.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,1056 @@
1
+ const AgentRobotAvatar = (() => {
2
+ const HTMLElementBase = typeof HTMLElement === 'undefined' ? class {} : HTMLElement;
3
+ const EASE = {
4
+ outQuint: t => 1 - Math.pow(1 - t, 5),
5
+ inOutCubic: t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3)/2,
6
+ };
7
+
8
+ const POSES = {
9
+ idle: {
10
+ w: 54, h: 58, rx: 27,
11
+ topY: -36, bottomY: 36,
12
+ topAL: 0, topAR: 0, bottomAL: 0, bottomAR: 0,
13
+ scale: 1,
14
+ },
15
+ happy: {
16
+ w: 58, h: 58, rx: 29,
17
+ topY: -36, bottomY: 0,
18
+ topAL: 0, topAR: 0, bottomAL: 0, bottomAR: 0,
19
+ scale: 1,
20
+ },
21
+ sad: {
22
+ w: 58, h: 58, rx: 29,
23
+ topY: -5, bottomY: 36,
24
+ topAL: -16, topAR: 16, bottomAL: 0, bottomAR: 0,
25
+ scale: 1,
26
+ },
27
+ angry: {
28
+ w: 58, h: 58, rx: 29,
29
+ topY: -5, bottomY: 36,
30
+ topAL: 16, topAR: -16, bottomAL: 0, bottomAR: 0,
31
+ scale: 1,
32
+ },
33
+ sleepy: {
34
+ w: 58, h: 56, rx: 28,
35
+ topY: 0, bottomY: 36,
36
+ topAL: 0, topAR: 0, bottomAL: 0, bottomAR: 0,
37
+ scale: 1,
38
+ },
39
+ input: {
40
+ w: 12, h: 78, rx: 2,
41
+ topY: -44, bottomY: 44,
42
+ topAL: 0, topAR: 0, bottomAL: 0, bottomAR: 0,
43
+ scale: 1,
44
+ },
45
+ sleep: {
46
+ w: 62, h: 10, rx: 2,
47
+ topY: -20, bottomY: 20,
48
+ topAL: 0, topAR: 0, bottomAL: 0, bottomAR: 0,
49
+ scale: 1,
50
+ },
51
+ surprise: {
52
+ w: 68, h: 68, rx: 34,
53
+ topY: -40, bottomY: 40,
54
+ topAL: 0, topAR: 0, bottomAL: 0, bottomAR: 0,
55
+ scale: 1,
56
+ },
57
+ };
58
+
59
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
60
+ const lerp = (a, b, t) => a + (b - a) * t;
61
+ const clonePose = p => ({...p});
62
+ const connectedFaces = new Set();
63
+
64
+ const forEachConnectedFace = callback => {
65
+ for (const face of connectedFaces) {
66
+ if (face.isConnected) callback(face);
67
+ }
68
+ };
69
+
70
+ const handleGlobalPointerMove = event => {
71
+ forEachConnectedFace(face => {
72
+ face._onPointerMove(event);
73
+ if (face._dragJelly.active) face._onDragMove(event);
74
+ });
75
+ };
76
+ const handleGlobalPointerDown = () => forEachConnectedFace(face => face.noteActivity());
77
+ const handleGlobalKeyDown = () => forEachConnectedFace(face => face.noteActivity());
78
+ const handleGlobalPointerEnd = event => {
79
+ forEachConnectedFace(face => {
80
+ if (face._dragJelly.active) face._onDragEnd(event);
81
+ });
82
+ };
83
+ const handleGlobalVisibility = () => forEachConnectedFace(face => face._onVisibility());
84
+
85
+ function registerConnectedFace(face) {
86
+ if (connectedFaces.has(face)) return;
87
+ const first = connectedFaces.size === 0;
88
+ connectedFaces.add(face);
89
+ if (!first) return;
90
+
91
+ window.addEventListener('pointermove', handleGlobalPointerMove, {passive:true});
92
+ window.addEventListener('pointerdown', handleGlobalPointerDown, {passive:true});
93
+ window.addEventListener('keydown', handleGlobalKeyDown, {passive:true});
94
+ window.addEventListener('pointerup', handleGlobalPointerEnd, {passive:true});
95
+ window.addEventListener('pointercancel', handleGlobalPointerEnd, {passive:true});
96
+ document.addEventListener('visibilitychange', handleGlobalVisibility);
97
+ }
98
+
99
+ function unregisterConnectedFace(face) {
100
+ connectedFaces.delete(face);
101
+ if (connectedFaces.size > 0) return;
102
+
103
+ window.removeEventListener('pointermove', handleGlobalPointerMove);
104
+ window.removeEventListener('pointerdown', handleGlobalPointerDown);
105
+ window.removeEventListener('keydown', handleGlobalKeyDown);
106
+ window.removeEventListener('pointerup', handleGlobalPointerEnd);
107
+ window.removeEventListener('pointercancel', handleGlobalPointerEnd);
108
+ document.removeEventListener('visibilitychange', handleGlobalVisibility);
109
+ }
110
+
111
+ class AgentFace extends HTMLElementBase {
112
+ constructor() {
113
+ super();
114
+ this.attachShadow({mode: 'open'});
115
+ this._state = 'idle';
116
+ this._pose = clonePose(POSES.idle);
117
+ this._fromPose = clonePose(this._pose);
118
+ this._toPose = clonePose(this._pose);
119
+ this._morphStart = performance.now();
120
+ this._morphDuration = 0;
121
+ this._morphEase = EASE.outQuint;
122
+
123
+ this._wander = {x:0, y:0};
124
+ this._wanderTarget = {x:0, y:0};
125
+ this._nextWanderAt = performance.now() + 500;
126
+ this._pointer = {x:0, y:0, active:false, lastMove:0};
127
+ this._look = {x:0, y:0};
128
+ this._headFollowPose = {x:0,y:0,rot:0};
129
+ this._headFollowVel = {x:0,y:0,rot:0};
130
+ this._headCenteringUntil = 0;
131
+ this._eyeMicro = {lX:0,lY:0,rX:0,rY:0};
132
+ this._blink = 1;
133
+ this._blinkAnim = null;
134
+ this._nextBlinkAt = performance.now() + 1800 + Math.random()*1800;
135
+ this._inputPhase = 0;
136
+ this._eyeNod = null;
137
+ this._eyeBob = null;
138
+ this._angryEyeDrop = null;
139
+ this._surpriseShake = null;
140
+ this._expressionLock = false;
141
+ this._transitionToken = 0;
142
+ this._inputWanted = false;
143
+ this._sleeping = false;
144
+ this._idleTimer = null;
145
+ this._boredRoutine = null;
146
+ this._boredLookSpeed = null;
147
+ this._autoSleepMs = Number(this.getAttribute('auto-sleep')) || 0;
148
+ this._lastActivity = performance.now();
149
+ this._running = true;
150
+ this._framePaused = false;
151
+ this._lastFrame = 0;
152
+ this._raf = 0;
153
+ this._dragJelly = {
154
+ active:false, returning:false, snapped:false, moved:false, pointerId:null,
155
+ startX:0, startY:0, hotX:120, hotY:120, targetX:0, targetY:0, x:0, y:0, vx:0, vy:0,
156
+ targetStretch:0, stretch:0, stretchVel:0, angle:0, threshold:52,
157
+ ux:0, uy:0, anchorX:50, anchorY:50, shear:0, targetShear:0, shearVel:0,
158
+ targetPullX:0, targetPullY:0, pullX:0, pullY:0, pullVX:0, pullVY:0,
159
+ maxDist:0, angryThreshold:32, pendingReaction:null, intent:'angry'
160
+ };
161
+ this._suppressClick = false;
162
+ this._boundDragStart = e => this._onDragStart(e);
163
+ this._boundClickCapture = e => this._onClickCapture(e);
164
+ this._renderShell();
165
+ }
166
+
167
+ connectedCallback() {
168
+ registerConnectedFace(this);
169
+ this.addEventListener('pointerdown', this._boundDragStart);
170
+ this.addEventListener('click', this._boundClickCapture, true);
171
+ this._running = !document.hidden;
172
+ this._resumeFrames();
173
+ }
174
+
175
+ disconnectedCallback() {
176
+ unregisterConnectedFace(this);
177
+ this.removeEventListener('pointerdown', this._boundDragStart);
178
+ this.removeEventListener('click', this._boundClickCapture, true);
179
+ this._running = false;
180
+ cancelAnimationFrame(this._raf);
181
+ this._raf = 0;
182
+ }
183
+
184
+ static get observedAttributes() { return ['color','size','auto-sleep']; }
185
+ attributeChangedCallback(name, oldV, newV) {
186
+ if (oldV === newV) return;
187
+ if (name === 'color' && this._head) this._head.setAttribute('fill', newV || '#08090b');
188
+ if (name === 'size') this.style.setProperty('--face-size', `${Number(newV)||112}px`);
189
+ if (name === 'auto-sleep') this._autoSleepMs = Number(newV) || 0;
190
+ }
191
+
192
+ _renderShell() {
193
+ this.shadowRoot.innerHTML = `
194
+ <style>
195
+ :host{display:inline-block;width:var(--face-size,112px);height:var(--face-size,112px);contain:layout style;overflow:visible;cursor:pointer;user-select:none;-webkit-user-select:none;touch-action:manipulation}
196
+ svg{display:block;width:100%;height:100%;overflow:visible}
197
+ #dragMotion{transform-box:view-box;transform-origin:120px 120px;will-change:transform}
198
+ #headFollow{will-change:transform}
199
+ #headMotion{transform-origin:120px 120px;will-change:transform}
200
+ #inputL,#inputR{opacity:1}
201
+ </style>
202
+ <svg viewBox="0 0 240 240" role="img" aria-label="Agent robot avatar">
203
+ <defs>
204
+ <path id="headShape" d="M 120.00 20.00 L 132.80 20.05 L 139.87 20.20 L 145.68 20.44 L 150.79 20.78 L 155.43 21.22 L 159.71 21.76 L 163.69 22.40 L 167.44 23.14 L 170.99 23.97 L 174.34 24.90 L 177.53 25.93 L 180.57 27.07 L 183.46 28.30 L 186.22 29.63 L 188.85 31.06 L 191.36 32.59 L 193.75 34.22 L 196.03 35.96 L 198.19 37.80 L 200.25 39.75 L 202.20 41.81 L 204.04 43.97 L 205.78 46.25 L 207.41 48.64 L 208.94 51.15 L 210.37 53.78 L 211.70 56.54 L 212.93 59.43 L 214.07 62.47 L 215.10 65.66 L 216.03 69.01 L 216.86 72.56 L 217.60 76.31 L 218.24 80.29 L 218.78 84.57 L 219.22 89.21 L 219.56 94.32 L 219.80 100.13 L 219.95 107.20 L 220.00 120.00 L 219.95 132.80 L 219.80 139.87 L 219.56 145.68 L 219.22 150.79 L 218.78 155.43 L 218.24 159.71 L 217.60 163.69 L 216.86 167.44 L 216.03 170.99 L 215.10 174.34 L 214.07 177.53 L 212.93 180.57 L 211.70 183.46 L 210.37 186.22 L 208.94 188.85 L 207.41 191.36 L 205.78 193.75 L 204.04 196.03 L 202.20 198.19 L 200.25 200.25 L 198.19 202.20 L 196.03 204.04 L 193.75 205.78 L 191.36 207.41 L 188.85 208.94 L 186.22 210.37 L 183.46 211.70 L 180.57 212.93 L 177.53 214.07 L 174.34 215.10 L 170.99 216.03 L 167.44 216.86 L 163.69 217.60 L 159.71 218.24 L 155.43 218.78 L 150.79 219.22 L 145.68 219.56 L 139.87 219.80 L 132.80 219.95 L 120.00 220.00 L 107.20 219.95 L 100.13 219.80 L 94.32 219.56 L 89.21 219.22 L 84.57 218.78 L 80.29 218.24 L 76.31 217.60 L 72.56 216.86 L 69.01 216.03 L 65.66 215.10 L 62.47 214.07 L 59.43 212.93 L 56.54 211.70 L 53.78 210.37 L 51.15 208.94 L 48.64 207.41 L 46.25 205.78 L 43.97 204.04 L 41.81 202.20 L 39.75 200.25 L 37.80 198.19 L 35.96 196.03 L 34.22 193.75 L 32.59 191.36 L 31.06 188.85 L 29.63 186.22 L 28.30 183.46 L 27.07 180.57 L 25.93 177.53 L 24.90 174.34 L 23.97 170.99 L 23.14 167.44 L 22.40 163.69 L 21.76 159.71 L 21.22 155.43 L 20.78 150.79 L 20.44 145.68 L 20.20 139.87 L 20.05 132.80 L 20.00 120.00 L 20.05 107.20 L 20.20 100.13 L 20.44 94.32 L 20.78 89.21 L 21.22 84.57 L 21.76 80.29 L 22.40 76.31 L 23.14 72.56 L 23.97 69.01 L 24.90 65.66 L 25.93 62.47 L 27.07 59.43 L 28.30 56.54 L 29.63 53.78 L 31.06 51.15 L 32.59 48.64 L 34.22 46.25 L 35.96 43.97 L 37.80 41.81 L 39.75 39.75 L 41.81 37.80 L 43.97 35.96 L 46.25 34.22 L 48.64 32.59 L 51.15 31.06 L 53.78 29.63 L 56.54 28.30 L 59.43 27.07 L 62.47 25.93 L 65.66 24.90 L 69.01 23.97 L 72.56 23.14 L 76.31 22.40 L 80.29 21.76 L 84.57 21.22 L 89.21 20.78 L 94.32 20.44 L 100.13 20.20 L 107.20 20.05 Z"/>
205
+ <clipPath id="headClip"><use href="#headShape" transform="translate(7.2 7.2) scale(.94)"/></clipPath>
206
+ <clipPath id="eyeMaskClip" clipPathUnits="userSpaceOnUse"><rect x="-40" y="-54" width="80" height="108"/></clipPath>
207
+ </defs>
208
+ <g id="dragMotion">
209
+ <g id="headFollow">
210
+ <g id="headMotion">
211
+ <use id="head" href="#headShape" fill="#08090b" transform="translate(7.2 7.2) scale(.94)"/>
212
+ <g clip-path="url(#headClip)">
213
+ <g id="leftEye" transform="translate(86 126)">
214
+ <ellipse id="leftBase" cx="0" cy="0" rx="27" ry="29" fill="#fff"/>
215
+ <rect id="leftInputBase" x="-6" y="-39" width="12" height="78" rx="3" fill="#fff" opacity="0"/>
216
+ <g clip-path="url(#eyeMaskClip)">
217
+ <rect id="leftTop" x="-70" y="-96" width="140" height="60" fill="#08090b"/>
218
+ <rect id="leftBottom" x="-70" y="36" width="140" height="60" fill="#08090b"/>
219
+ </g>
220
+ </g>
221
+ <g id="rightEye" transform="translate(154 126)">
222
+ <ellipse id="rightBase" cx="0" cy="0" rx="27" ry="29" fill="#fff"/>
223
+ <rect id="rightInputBase" x="-6" y="-39" width="12" height="78" rx="3" fill="#fff" opacity="0"/>
224
+ <g clip-path="url(#eyeMaskClip)">
225
+ <rect id="rightTop" x="-70" y="-96" width="140" height="60" fill="#08090b"/>
226
+ <rect id="rightBottom" x="-70" y="36" width="140" height="60" fill="#08090b"/>
227
+ </g>
228
+ </g>
229
+ </g>
230
+ </g>
231
+ </g>
232
+ </g>
233
+ </svg>`;
234
+ this._headShape = this.shadowRoot.getElementById('headShape');
235
+ this._dragMotion = this.shadowRoot.getElementById('dragMotion');
236
+ this._headFollow = this.shadowRoot.getElementById('headFollow');
237
+ this._headMotion = this.shadowRoot.getElementById('headMotion');
238
+ this._head = this.shadowRoot.getElementById('head');
239
+ this._leftEye = this.shadowRoot.getElementById('leftEye');
240
+ this._rightEye = this.shadowRoot.getElementById('rightEye');
241
+ this._leftBase = this.shadowRoot.getElementById('leftBase');
242
+ this._rightBase = this.shadowRoot.getElementById('rightBase');
243
+ this._leftInputBase = this.shadowRoot.getElementById('leftInputBase');
244
+ this._rightInputBase = this.shadowRoot.getElementById('rightInputBase');
245
+ this._leftTop = this.shadowRoot.getElementById('leftTop');
246
+ this._rightTop = this.shadowRoot.getElementById('rightTop');
247
+ this._leftBottom = this.shadowRoot.getElementById('leftBottom');
248
+ this._rightBottom = this.shadowRoot.getElementById('rightBottom');
249
+ const c = this.getAttribute('color') || '#08090b';
250
+ this._head.setAttribute('fill', c);
251
+ [this._leftTop,this._rightTop,this._leftBottom,this._rightBottom].forEach(el=>el.setAttribute('fill',c));
252
+ this.style.setProperty('--face-size', `${Number(this.getAttribute('size'))||112}px`);
253
+ this._baseHeadPathD = this._headShape.getAttribute('d');
254
+ this._baseHeadPoints = this._parseHeadPoints(this._baseHeadPathD);
255
+ }
256
+
257
+ _parseHeadPoints(d) {
258
+ const nums = (d.match(/-?\d*\.?\d+/g) || []).map(Number);
259
+ const pts = [];
260
+ for (let i = 0; i < nums.length - 1; i += 2) pts.push({x: nums[i], y: nums[i+1]});
261
+ return pts;
262
+ }
263
+
264
+ _pointsToPath(points) {
265
+ if (!points.length) return this._baseHeadPathD || '';
266
+ let d = `M ${points[0].x.toFixed(2)} ${points[0].y.toFixed(2)}`;
267
+ for (let i = 1; i < points.length; i++) d += ` L ${points[i].x.toFixed(2)} ${points[i].y.toFixed(2)}`;
268
+ return `${d} Z`;
269
+ }
270
+
271
+ _clientToLocal(clientX, clientY) {
272
+ const r = this.getBoundingClientRect();
273
+ return {
274
+ x: clamp(((clientX - r.left) / Math.max(1, r.width)) * 240, 0, 240),
275
+ y: clamp(((clientY - r.top) / Math.max(1, r.height)) * 240, 0, 240),
276
+ };
277
+ }
278
+
279
+ _applyHeadDeform() {
280
+ const j = this._dragJelly;
281
+ if (!this._headShape || !this._baseHeadPoints?.length) return;
282
+ const amount = Math.hypot(j.pullX, j.pullY);
283
+ if (amount < 0.015) {
284
+ this._headShape.setAttribute('d', this._baseHeadPathD);
285
+ return;
286
+ }
287
+ const radius = (50 + amount * 0.8) * 0.92;
288
+ const pts = this._baseHeadPoints.map(p => {
289
+ const dx = p.x - j.hotX;
290
+ const dy = p.y - j.hotY;
291
+ const w = Math.exp(-(dx*dx + dy*dy) / (2 * radius * radius));
292
+ return {x:p.x + j.pullX * w, y:p.y + j.pullY * w};
293
+ });
294
+ this._headShape.setAttribute('d', this._pointsToPath(pts));
295
+ }
296
+
297
+ _onVisibility() {
298
+ this._running = !document.hidden;
299
+ if (this._running) this._resumeFrames();
300
+ else {
301
+ cancelAnimationFrame(this._raf);
302
+ this._raf = 0;
303
+ }
304
+ }
305
+
306
+ _onClickCapture(e) {
307
+ if (!this._suppressClick) return;
308
+ e.preventDefault();
309
+ e.stopImmediatePropagation();
310
+ this._suppressClick = false;
311
+ }
312
+
313
+ _onPointerMove(e) {
314
+ const r = this.getBoundingClientRect();
315
+ const cx = r.left + r.width / 2;
316
+ const cy = r.top + r.height / 2;
317
+ const dx = e.clientX - cx;
318
+ const dy = e.clientY - cy;
319
+ const near = Math.max(r.width, r.height) * 2.55;
320
+ const dist = Math.hypot(dx, dy);
321
+ const influence = clamp(1 - dist / near, 0, 1);
322
+ this._pointer.x = clamp(dx * .42, -48, 48);
323
+ this._pointer.y = clamp(dy * .38, -34, 34);
324
+ this._pointer.active = influence > 0.001;
325
+ this._pointer.influence = influence;
326
+ this._pointer.lastMove = performance.now();
327
+ if ((this._pointer.influence||0) > 0.65 && this._boredRoutine) { this._boredRoutine = null; this._boredLookSpeed = null; }
328
+ }
329
+
330
+ _onDragStart(e) {
331
+ if (e.button != null && e.button !== 0) return;
332
+ const r = this.getBoundingClientRect();
333
+ const j = this._dragJelly;
334
+ j.active = true;
335
+ j.returning = false;
336
+ j.snapped = false;
337
+ j.moved = false;
338
+ j.pointerId = e.pointerId;
339
+ j.startX = e.clientX;
340
+ j.startY = e.clientY;
341
+ const local = this._clientToLocal(e.clientX, e.clientY);
342
+ j.hotX = local.x;
343
+ j.hotY = local.y;
344
+ j.targetX = j.x;
345
+ j.targetY = j.y;
346
+ j.targetStretch = j.stretch;
347
+ j.targetShear = j.shear;
348
+ j.targetPullX = j.pullX;
349
+ j.targetPullY = j.pullY;
350
+ j.threshold = clamp(r.width * 0.40, 24, 46);
351
+ j.maxDist = 0;
352
+ j.pendingReaction = null;
353
+ j.intent = 'angry';
354
+ j.angryThreshold = Math.max(j.threshold * 0.76, 22);
355
+ this._suppressClick = false;
356
+ this._boredRoutine = null;
357
+ this._boredLookSpeed = null;
358
+ this._wanderTarget.x = this._wanderTarget.y = 0;
359
+ try { this.setPointerCapture(e.pointerId); } catch (_) {}
360
+ }
361
+
362
+ _onDragMove(e) {
363
+ const j = this._dragJelly;
364
+ if (!j.active || e.pointerId !== j.pointerId) return;
365
+ const dx = e.clientX - j.startX;
366
+ const dy = e.clientY - j.startY;
367
+ const dist = Math.hypot(dx, dy);
368
+ j.maxDist = Math.max(j.maxDist, dist);
369
+ if (dist > 2.5) {
370
+ j.moved = true;
371
+ this._suppressClick = true;
372
+ }
373
+ const r = this.getBoundingClientRect();
374
+ const size = Math.max(1, Math.max(r.width, r.height));
375
+ const localScale = 240 / size;
376
+ const pdx = dx * localScale;
377
+ const pdy = dy * localScale;
378
+ const cx = 120, cy = 120;
379
+ const rx = j.hotX - cx;
380
+ const ry = j.hotY - cy;
381
+ const rl = Math.max(1, Math.hypot(rx, ry));
382
+ const rux = rx / rl;
383
+ const ruy = ry / rl;
384
+ const radial = pdx * rux + pdy * ruy;
385
+ const tangent = -pdx * ruy + pdy * rux;
386
+ const motionLen = Math.max(1, Math.hypot(pdx, pdy));
387
+ j.ux = pdx / motionLen;
388
+ j.uy = pdy / motionLen;
389
+ j.angle = Math.atan2(j.uy, j.ux);
390
+ j.intent = radial < -6 ? 'success' : 'angry';
391
+ const gain = dist > j.threshold * .60 ? 0.24 : 0.19;
392
+ j.targetPullX = clamp(pdx * gain, -25, 25);
393
+ j.targetPullY = clamp(pdy * gain, -25, 25);
394
+ j.targetX = clamp(dx * 0.065, -5.2, 5.2);
395
+ j.targetY = clamp(dy * 0.065, -5.2, 5.2);
396
+ j.targetStretch = clamp((radial / 240) * .08, -.028, .052);
397
+ j.targetShear = clamp((tangent / 240) * .050, -.022, .022);
398
+ j.anchorX = clamp(50 - (rux * 31), 15, 85);
399
+ j.anchorY = clamp(50 - (ruy * 31), 15, 85);
400
+ }
401
+
402
+ _onDragEnd(e) {
403
+ const j = this._dragJelly;
404
+ if (!j.active || e.pointerId !== j.pointerId) return;
405
+ j.active = false;
406
+ try { this.releasePointerCapture(e.pointerId); } catch (_) {}
407
+ j.pointerId = null;
408
+ const reactionThreshold = Math.max(j.angryThreshold, 22);
409
+ j.pendingReaction = j.maxDist >= reactionThreshold ? j.intent : null;
410
+ j.returning = true;
411
+ j.targetX = 0;
412
+ j.targetY = 0;
413
+ j.targetStretch = 0;
414
+ j.targetShear = 0;
415
+ j.targetPullX = 0;
416
+ j.targetPullY = 0;
417
+ }
418
+
419
+ _finishDragReturn(reaction) {
420
+ const j = this._dragJelly;
421
+ j.returning = false;
422
+ j.pendingReaction = null;
423
+ j.x = j.y = j.vx = j.vy = 0;
424
+ j.stretch = j.stretchVel = 0;
425
+ j.shear = j.shearVel = 0;
426
+ j.pullX = j.pullY = j.pullVX = j.pullVY = 0;
427
+ if (this._dragMotion) this._dragMotion.style.transform = '';
428
+ if (this._headShape) this._headShape.setAttribute('d', this._baseHeadPathD);
429
+ if (reaction === 'angry') this.angry({fromDrag:true});
430
+ else if (reaction === 'success') this.success({fromDrag:true});
431
+ }
432
+
433
+ _updateDragJelly(dt) {
434
+ const j = this._dragJelly;
435
+ const dtS = Math.min(dt, 34) / 1000;
436
+ if (j.active) {
437
+ const fast = Math.min(1, dt / 14);
438
+ const k = 0.36 + 0.30 * fast;
439
+ j.x = lerp(j.x, j.targetX, k);
440
+ j.y = lerp(j.y, j.targetY, k);
441
+ j.stretch = lerp(j.stretch, j.targetStretch, k*.85);
442
+ j.shear = lerp(j.shear, j.targetShear, k*.80);
443
+ j.pullX = lerp(j.pullX, j.targetPullX, k*.95);
444
+ j.pullY = lerp(j.pullY, j.targetPullY, k*.95);
445
+ } else if (j.returning) {
446
+ const kx = 135, dx = 8.2;
447
+ const ax = -j.x * kx - j.vx * dx;
448
+ const ay = -j.y * kx - j.vy * dx;
449
+ j.vx += ax * dtS;
450
+ j.vy += ay * dtS;
451
+ j.x += j.vx * dtS;
452
+ j.y += j.vy * dtS;
453
+ const ks = 128, ds = 7.5;
454
+ const as = -j.stretch * ks - j.stretchVel * ds;
455
+ j.stretchVel += as * dtS;
456
+ j.stretch += j.stretchVel * dtS;
457
+ const kh = 118, dh = 7.2;
458
+ const ah = -j.shear * kh - j.shearVel * dh;
459
+ j.shearVel += ah * dtS;
460
+ j.shear += j.shearVel * dtS;
461
+ const kp = 118, dp = 7.1;
462
+ const apx = -j.pullX * kp - j.pullVX * dp;
463
+ const apy = -j.pullY * kp - j.pullVY * dp;
464
+ j.pullVX += apx * dtS;
465
+ j.pullVY += apy * dtS;
466
+ j.pullX += j.pullVX * dtS;
467
+ j.pullY += j.pullVY * dtS;
468
+
469
+ const visuallyRecovered = Math.abs(j.x) < 0.34 && Math.abs(j.y) < 0.34 && Math.abs(j.stretch) < 0.008 && Math.abs(j.shear) < 0.0035 && Math.abs(j.pullX) < 0.40 && Math.abs(j.pullY) < 0.40;
470
+ if (j.pendingReaction && visuallyRecovered) {
471
+ this._finishDragReturn(j.pendingReaction);
472
+ } else if (Math.abs(j.x) < 0.02 && Math.abs(j.y) < 0.02 && Math.abs(j.vx) < 0.04 && Math.abs(j.vy) < 0.04 && Math.abs(j.stretch) < 0.001 && Math.abs(j.stretchVel) < 0.003 && Math.abs(j.shear) < 0.0005 && Math.abs(j.shearVel) < 0.002 && Math.abs(j.pullX) < 0.03 && Math.abs(j.pullY) < 0.03 && Math.abs(j.pullVX) < 0.06 && Math.abs(j.pullVY) < 0.06) {
473
+ this._finishDragReturn(null);
474
+ }
475
+ }
476
+ j.stretch = clamp(j.stretch, -0.035, 0.055);
477
+ j.shear = clamp(j.shear, -0.026, 0.026);
478
+ j.pullX = clamp(j.pullX, -26, 26);
479
+ j.pullY = clamp(j.pullY, -26, 26);
480
+ this._applyHeadDeform();
481
+ if (this._dragMotion) {
482
+ const sx = 1 + j.stretch;
483
+ const sy = 1 - j.stretch * 0.30;
484
+ const shearDeg = j.shear * 180 / Math.PI;
485
+ this._dragMotion.style.transformOrigin = `${j.anchorX.toFixed(2)}% ${j.anchorY.toFixed(2)}%`;
486
+ this._dragMotion.style.transform = `translate(${j.x.toFixed(2)}px, ${j.y.toFixed(2)}px) rotate(${j.angle.toFixed(4)}rad) scale(${sx.toFixed(4)}, ${sy.toFixed(4)}) skewY(${shearDeg.toFixed(3)}deg) rotate(${(-j.angle).toFixed(4)}rad)`;
487
+ }
488
+ }
489
+
490
+ noteActivity(wake=true) {
491
+ this._lastActivity = performance.now();
492
+ this._resumeFrames();
493
+ if (wake && this._sleeping) this.wake();
494
+ }
495
+
496
+ play(name) {
497
+ const action = String(name || '').trim().toLowerCase();
498
+ switch (action) {
499
+ case 'idle': return this.reset();
500
+ case 'bored': return this.bored();
501
+ case 'input': return this.input(true);
502
+ case 'send': return this.send();
503
+ case 'success': return this.success();
504
+ case 'error': return this.error();
505
+ case 'angry': return this.angry();
506
+ case 'surprise': return this.surprise();
507
+ case 'sleep': return this.sleep();
508
+ case 'wake': return this.wake();
509
+ default: throw new Error(`Unknown Agent Robot Avatar action: ${name}`);
510
+ }
511
+ }
512
+
513
+ reset() {
514
+ this._inputWanted = false;
515
+ ++this._transitionToken;
516
+ this._boredRoutine = null;
517
+ this._boredLookSpeed = null;
518
+ this._sleeping = false;
519
+ this._eyeNod = null;
520
+ this._eyeBob = null;
521
+ this._angryEyeDrop = null;
522
+ this._surpriseShake = null;
523
+ this.setState('idle');
524
+ return this;
525
+ }
526
+
527
+ setState(name, opts={}) {
528
+ if (!POSES[name]) throw new Error(`Unknown AgentFace state: ${name}`);
529
+ this._resumeFrames();
530
+ this._state = name;
531
+ this._sleeping = name === 'sleep';
532
+ if (name === 'idle' && !opts.keepGazeLock) this._releaseExpressionLock();
533
+ this._startMorph(POSES[name], opts.duration ?? (name==='surprise'?170:320), opts.ease || EASE.outQuint);
534
+ this.dispatchEvent(new CustomEvent('face-state', {detail:{state:name}}));
535
+ return this;
536
+ }
537
+
538
+ _startMorph(target, duration, ease) {
539
+ this._fromPose = clonePose(this._pose);
540
+ this._toPose = clonePose(target);
541
+ this._morphStart = performance.now();
542
+ this._morphDuration = Math.max(0, duration);
543
+ this._morphEase = ease;
544
+ }
545
+
546
+ async success(opts={}) {
547
+ const fromDrag = !!opts.fromDrag;
548
+ this.noteActivity();
549
+ this._inputWanted = false;
550
+ if (fromDrag) {
551
+ ++this._transitionToken;
552
+ this._expressionLock = true;
553
+ this._eyeMicro.lX = this._eyeMicro.lY = 0;
554
+ this._eyeMicro.rX = this._eyeMicro.rY = 0;
555
+ this._wanderTarget.x = this._wanderTarget.y = 0;
556
+ this._headCenteringUntil = performance.now() + 120;
557
+ this._look.x = 0;
558
+ this._look.y = 0;
559
+ } else {
560
+ if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
561
+ }
562
+ this.setState('happy', {duration: fromDrag ? 180 : 220, keepGazeLock:true});
563
+ this._eyeBob = {start: performance.now(), duration: 620, amp: 8};
564
+ this._animateHead([
565
+ {transform:'translateY(0px)'},
566
+ {transform:'translateY(-2.4px)', offset:.42},
567
+ {transform:'translateY(0px)'}
568
+ ], 760);
569
+ await this._wait(980);
570
+ this._eyeBob = null;
571
+ if (this._state==='happy') await this._returnToIdle(520);
572
+ }
573
+
574
+ async error() {
575
+ this.noteActivity();
576
+ this._inputWanted = false;
577
+ if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
578
+ this.setState('sad', {duration:320, keepGazeLock:true});
579
+ this._eyeBob = {start: performance.now(), duration: 700, amp: 7};
580
+ this._animateHead([
581
+ {transform:'translateY(0px)'},
582
+ {transform:'translateY(2.6px)', offset:.46},
583
+ {transform:'translateY(0px)'}
584
+ ], 820);
585
+ await this._wait(1180);
586
+ this._eyeBob = null;
587
+ if (this._state==='sad') {
588
+ await this._returnToIdle(560);
589
+ }
590
+ }
591
+
592
+ async angry(opts={}) {
593
+ const fromDrag = !!opts.fromDrag;
594
+ this.noteActivity();
595
+ this._inputWanted = false;
596
+ if (fromDrag) {
597
+ ++this._transitionToken;
598
+ this._expressionLock = true;
599
+ this._eyeMicro.lX = this._eyeMicro.lY = 0;
600
+ this._eyeMicro.rX = this._eyeMicro.rY = 0;
601
+ this._wanderTarget.x = this._wanderTarget.y = 0;
602
+ this._headCenteringUntil = performance.now() + 120;
603
+ this._look.x = 0;
604
+ this._look.y = 0;
605
+ } else {
606
+ if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
607
+ }
608
+ this.setState('angry', {duration: fromDrag ? 180 : 280, keepGazeLock:true});
609
+ this._angryEyeDrop = {start: performance.now(), duration:680, amount:4.8};
610
+ this._animateHead([
611
+ {transform:'translateY(0px)'},
612
+ {transform:'translateY(2.6px)', offset:.46},
613
+ {transform:'translateY(0px)'}
614
+ ], 820);
615
+ await this._wait(980);
616
+ this._angryEyeDrop = null;
617
+ if (this._state==='angry') await this._returnToIdle(500);
618
+ }
619
+
620
+ async surprise() {
621
+ this.noteActivity();
622
+ this._inputWanted = false;
623
+ if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
624
+ this._surpriseShake = null;
625
+ const pre = {...POSES.idle, w:34, h:36, rx:18};
626
+ this._startMorph(pre, 235, EASE.inOutCubic);
627
+ await this._wait(235);
628
+ if (this._state!=='idle') return;
629
+ this.setState('surprise', {duration:175, ease:EASE.outQuint, keepGazeLock:true});
630
+ this._animateHead([{transform:'scale(1)'},{transform:'scale(1.035)'}], 175);
631
+ await this._wait(175);
632
+ if (this._state!=='surprise') return;
633
+ this._surpriseShake = {start: performance.now(), duration:420};
634
+ await this._wait(420);
635
+ this._surpriseShake = null;
636
+ if (this._state==='surprise') {
637
+ this._animateHead([{transform:'scale(1.035)'},{transform:'scale(1)'}], 430);
638
+ await this._returnToIdle(430, EASE.inOutCubic);
639
+ }
640
+ }
641
+
642
+ async send() {
643
+ this.noteActivity();
644
+ this._inputWanted = false;
645
+ if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
646
+ this._eyeNod = {start: performance.now(), duration: 760};
647
+ this._animateHead([
648
+ {transform:'translateY(0px)'},
649
+ {transform:'translateY(5px)', offset:.52},
650
+ {transform:'translateY(0px)'}
651
+ ], 920);
652
+ await this._wait(860);
653
+ this._eyeNod = null;
654
+ this._releaseExpressionLock();
655
+ }
656
+
657
+ async sleep() {
658
+ if (this._sleeping) return;
659
+ if (!(await this._prepareExpression({normalizePose:true, duration:160}))) return;
660
+ this._blinkAnim = null;
661
+ this._blink = 1;
662
+ this._nextBlinkAt = performance.now() + 10000;
663
+ this.setState('sleepy', {duration:940, ease:EASE.inOutCubic, keepGazeLock:true});
664
+ await this._wait(980);
665
+ if (this._state !== 'sleepy') return;
666
+ const reopen = {...POSES.idle, topY:-22, bottomY:36};
667
+ this._startMorph(reopen, 105, EASE.outQuint);
668
+ await this._wait(120);
669
+ if (this._state !== 'sleepy') return;
670
+ this.setState('sleepy', {duration:820, ease:EASE.inOutCubic, keepGazeLock:true});
671
+ await this._wait(870);
672
+ if (this._state !== 'sleepy') return;
673
+ await this._wait(120);
674
+ if (this._state !== 'sleepy') return;
675
+ this._animateHead([{transform:'translateY(0)'},{transform:'translateY(4px)'}],980);
676
+ this._startMorph(POSES.sleep, 980, EASE.inOutCubic);
677
+ this._state = 'sleep';
678
+ this.dispatchEvent(new CustomEvent('face-state', {detail:{state:'sleep'}}));
679
+ this._sleeping = true;
680
+ this._nextBlinkAt = performance.now() + 3200 + Math.random()*2200;
681
+ }
682
+
683
+ async wake() {
684
+ if (!this._sleeping && this._state!=='sleep') return;
685
+ this._expressionLock = true;
686
+ this._sleeping = false;
687
+ this._animateHead([
688
+ {transform:'translateY(4px) scale(1)'},
689
+ {transform:'translateY(-2px) scale(1.015)', offset:.55},
690
+ {transform:'translateY(0) scale(1)'}
691
+ ], 500);
692
+ this.setState('sleepy', {duration:240, keepGazeLock:true});
693
+ await this._wait(260);
694
+ this.setState('surprise', {duration:120, keepGazeLock:true});
695
+ await this._wait(190);
696
+ await this._returnToIdle(300);
697
+ this._lastActivity = performance.now();
698
+ }
699
+
700
+ async input(active=true) {
701
+ this.noteActivity();
702
+ if (active) {
703
+ if (this._inputWanted || this._state==='input') return;
704
+ this._inputWanted = true;
705
+ if (!(await this._prepareExpression({normalizePose:true, duration:140}))) return;
706
+ if (!this._inputWanted) return;
707
+ this.setState('input', {duration:210, keepGazeLock:true});
708
+ } else {
709
+ this._inputWanted = false;
710
+ ++this._transitionToken;
711
+ if (this._state==='input') this.setState('idle', {duration:260});
712
+ else this._releaseExpressionLock();
713
+ }
714
+ }
715
+
716
+ _animateHead(frames, duration) {
717
+ if (!this._headMotion.animate) return;
718
+ const anim = this._headMotion.animate(frames, {duration, easing:'cubic-bezier(.16,1,.3,1)', fill:'forwards'});
719
+ anim.onfinish = () => { this._headMotion.style.transform = frames[frames.length-1].transform || ''; };
720
+ return anim;
721
+ }
722
+
723
+ _wait(ms) { return new Promise(r=>setTimeout(r,ms)); }
724
+
725
+ async _returnToIdle(duration=320, ease=EASE.outQuint) {
726
+ this.setState('idle', {duration, ease, keepGazeLock:true});
727
+ this._headCenteringUntil = Math.max(this._headCenteringUntil || 0, performance.now() + duration);
728
+ await this._wait(duration);
729
+ if (this._state === 'idle') this._releaseExpressionLock();
730
+ }
731
+
732
+ async _prepareExpression({normalizePose=true, duration=180, pause=320}={}) {
733
+ const token = ++this._transitionToken;
734
+ this._expressionLock = true;
735
+ this._eyeMicro.lX = this._eyeMicro.lY = 0;
736
+ this._eyeMicro.rX = this._eyeMicro.rY = 0;
737
+ this._wanderTarget.x = this._wanderTarget.y = 0;
738
+ this._headCenteringUntil = performance.now() + duration + pause;
739
+ if (normalizePose && this._state !== 'idle') {
740
+ this.setState('idle', {duration:Math.min(140, duration), keepGazeLock:true});
741
+ }
742
+ await this._wait(duration);
743
+ if (token !== this._transitionToken) return false;
744
+ if (pause > 0) await this._wait(pause);
745
+ return token === this._transitionToken;
746
+ }
747
+
748
+ async bored() {
749
+ this.noteActivity();
750
+ this._inputWanted = false;
751
+ this._sleeping = false;
752
+ if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:620}))) return;
753
+ this._expressionLock = false;
754
+ this._look.x = 0;
755
+ this._look.y = 0;
756
+ this._wander.x = 0;
757
+ this._wander.y = 0;
758
+ this._startBoredRoutine(performance.now());
759
+ }
760
+
761
+ _startBoredRoutine(now) {
762
+ this._boredRoutine = {
763
+ index: 0,
764
+ nextAt: now,
765
+ steps: [
766
+ {x:-18,y:-11,hold:1400,lookSpeed:2},
767
+ {x:18,y:-11,hold:1400,lookSpeed:2},
768
+ {x:0,y:0,hold:320,lookSpeed:2},
769
+ ]
770
+ };
771
+ }
772
+
773
+ _updateBored(now) {
774
+ if (!this._boredRoutine) return false;
775
+ if (now < this._boredRoutine.nextAt) return true;
776
+ const step = this._boredRoutine.steps[this._boredRoutine.index];
777
+ if (!step) {
778
+ this._boredRoutine = null;
779
+ this._boredLookSpeed = null;
780
+ this._wanderTarget.x = this._wanderTarget.y = 0;
781
+ return false;
782
+ }
783
+ this._wanderTarget.x = step.x;
784
+ this._wanderTarget.y = step.y;
785
+ this._boredLookSpeed = step.lookSpeed ?? null;
786
+ this._boredRoutine.index += 1;
787
+ this._boredRoutine.nextAt = now + step.hold;
788
+ return true;
789
+ }
790
+
791
+ _releaseExpressionLock() {
792
+ this._expressionLock = false;
793
+ this._headCenteringUntil = 0;
794
+ }
795
+
796
+ _updateMorph(now) {
797
+ if (!this._morphDuration) {
798
+ this._pose = clonePose(this._toPose);
799
+ return;
800
+ }
801
+ const t = clamp((now - this._morphStart) / this._morphDuration, 0, 1);
802
+ const k = this._morphEase(t);
803
+ const p = {};
804
+ for (const key of Object.keys(this._toPose)) p[key] = lerp(this._fromPose[key], this._toPose[key], k);
805
+ this._pose = p;
806
+ if (t >= 1) this._morphDuration = 0;
807
+ }
808
+
809
+ _updateBlink(now) {
810
+ if (this._sleeping || this._state === 'input') return;
811
+ if (this._blinkAnim) {
812
+ const t = clamp((now - this._blinkAnim.start) / this._blinkAnim.duration, 0, 1);
813
+ this._blink = t < .5 ? lerp(1,.08,t*2) : lerp(.08,1,(t-.5)*2);
814
+ if (t >= 1) {
815
+ this._blinkAnim = null;
816
+ this._blink = 1;
817
+ this._nextBlinkAt = now + 1900 + Math.random()*2600;
818
+ }
819
+ } else if (!this._expressionLock && now >= this._nextBlinkAt) {
820
+ this._blinkAnim = {start:now,duration:150+Math.random()*70};
821
+ }
822
+ }
823
+
824
+ _updateLook(now, dt) {
825
+ this._updateBored(now);
826
+ const pointerFresh = (now - this._pointer.lastMove) < 900;
827
+ if (!this._expressionLock && !this._boredRoutine && !this._sleeping && this._state!=='input' && now >= this._nextWanderAt) {
828
+ const ampX = 12 + Math.random()*13;
829
+ const ampY = 5 + Math.random()*9;
830
+ this._wanderTarget.x = (Math.random()*2-1)*ampX;
831
+ this._wanderTarget.y = (Math.random()*2-1)*ampY;
832
+ this._nextWanderAt = now + 1150 + Math.random()*1900;
833
+ }
834
+ const draggingJelly = this._dragJelly.active || this._dragJelly.returning;
835
+ const pointerMix = (!this._expressionLock && !this._boredRoutine && pointerFresh && this._pointer.active && !this._sleeping && this._state!=='input')
836
+ ? Math.pow(this._pointer.influence || 0, 1.6)
837
+ : 0;
838
+ const targetX = this._expressionLock ? 0 : lerp(this._wanderTarget.x, this._pointer.x, pointerMix);
839
+ const targetY = this._expressionLock ? 0 : lerp(this._wanderTarget.y, this._pointer.y, pointerMix);
840
+ const lookSpeed = this._expressionLock ? 0.40 : (draggingJelly ? 0.42 : (this._boredRoutine && this._boredLookSpeed != null ? this._boredLookSpeed : (pointerMix > 0.05 ? 0.28 : 0.16)));
841
+ const kL = 1 - Math.pow(1 - clamp(lookSpeed,0.02,.9), dt/16.67);
842
+ this._look.x = lerp(this._look.x, targetX, kL);
843
+ this._look.y = lerp(this._look.y, targetY, kL);
844
+ if (this._sleeping) {
845
+ this._look.x *= .92;
846
+ this._look.y *= .92;
847
+ }
848
+ }
849
+
850
+ _autoSleep(now) {
851
+ if (!this._autoSleepMs || this._sleeping || ['input','happy','sad','surprise'].includes(this._state)) return;
852
+ if (now - this._lastActivity > this._autoSleepMs) this.sleep();
853
+ }
854
+
855
+ _updateHeadFollow(now, dt) {
856
+ const pointerFresh = (now - this._pointer.lastMove) < 900;
857
+ const pointerInRange = pointerFresh && this._pointer.active && (this._pointer.influence || 0) > 0.03;
858
+ const headCentering = now < (this._headCenteringUntil || 0);
859
+ const jellyBusy = this._dragJelly.active || this._dragJelly.returning;
860
+ const canFollowPointer = pointerInRange && !jellyBusy && !this._sleeping && !this._boredRoutine && !headCentering && !this._expressionLock;
861
+ const srcX = canFollowPointer ? this._pointer.x : this._look.x;
862
+ const srcY = canFollowPointer ? this._pointer.y : this._look.y;
863
+ const nx = clamp(srcX / 48, -1, 1);
864
+ const ny = clamp(srcY / 34, -1, 1);
865
+ const diag = clamp(nx * ny, -1, 1);
866
+ const targetRot = diag * 8;
867
+ const targetX = canFollowPointer ? nx * 4.2 : 0;
868
+ const targetY = canFollowPointer ? ny * 4.2 : 0;
869
+
870
+ const dtS = Math.min(dt, 34) / 1000;
871
+ const rotStiffness = canFollowPointer ? 70 : 56;
872
+ const rotDamping = canFollowPointer ? 15 : 13;
873
+ const aRot = (targetRot - this._headFollowPose.rot) * rotStiffness - this._headFollowVel.rot * rotDamping;
874
+ this._headFollowVel.rot += aRot * dtS;
875
+ this._headFollowPose.rot += this._headFollowVel.rot * dtS;
876
+ const stiffness = canFollowPointer ? 95 : 80;
877
+ const damping = canFollowPointer ? 16 : 14;
878
+ const ax = (targetX - this._headFollowPose.x) * stiffness - this._headFollowVel.x * damping;
879
+ const ay = (targetY - this._headFollowPose.y) * stiffness - this._headFollowVel.y * damping;
880
+ this._headFollowVel.x += ax * dtS;
881
+ this._headFollowVel.y += ay * dtS;
882
+ this._headFollowPose.x += this._headFollowVel.x * dtS;
883
+ this._headFollowPose.y += this._headFollowVel.y * dtS;
884
+
885
+ if (!canFollowPointer) {
886
+ if (Math.abs(this._headFollowPose.x) < 0.01 && Math.abs(this._headFollowVel.x) < 0.01) { this._headFollowPose.x = 0; this._headFollowVel.x = 0; }
887
+ if (Math.abs(this._headFollowPose.y) < 0.01 && Math.abs(this._headFollowVel.y) < 0.01) { this._headFollowPose.y = 0; this._headFollowVel.y = 0; }
888
+ if (Math.abs(this._headFollowPose.rot) < 0.01 && Math.abs(this._headFollowVel.rot) < 0.01) { this._headFollowPose.rot = 0; this._headFollowVel.rot = 0; }
889
+ }
890
+
891
+ if (this._headFollow) {
892
+ const a = this._headFollowPose.rot.toFixed(2);
893
+ const tx = this._headFollowPose.x.toFixed(2);
894
+ const ty = this._headFollowPose.y.toFixed(2);
895
+ this._headFollow.setAttribute('transform', `translate(${tx} ${ty}) translate(120 120) rotate(${a}) translate(-120 -120)`);
896
+ }
897
+ }
898
+
899
+ _draw(now) {
900
+ const p = this._pose;
901
+ let nodY = 0;
902
+ let nodScaleY = 1;
903
+ if (this._eyeNod) {
904
+ const elapsed = now - this._eyeNod.start;
905
+ let depth = 0;
906
+ if (elapsed < 150) depth = elapsed / 150;
907
+ else if (elapsed < 300) depth = 1 - (elapsed - 150) / 150;
908
+ else if (elapsed < 450) depth = (elapsed - 300) / 150;
909
+ else if (elapsed < 760) depth = 1 - (elapsed - 450) / 310;
910
+ else { depth = 0; this._eyeNod = null; }
911
+ depth = clamp(depth, 0, 1);
912
+ nodY = 31 * depth;
913
+ nodScaleY = 1 - .30 * depth;
914
+ }
915
+
916
+ let angryDropY = 0;
917
+ if (this._angryEyeDrop) {
918
+ const t = clamp((now - this._angryEyeDrop.start) / this._angryEyeDrop.duration, 0, 1);
919
+ const settle = 1 - Math.exp(-5.2 * t) * Math.cos(8.6 * t);
920
+ angryDropY = this._angryEyeDrop.amount * settle;
921
+ if (t >= 1) angryDropY = this._angryEyeDrop.amount;
922
+ }
923
+
924
+ let emotionBobY = 0;
925
+ if (this._eyeBob) {
926
+ const t = clamp((now - this._eyeBob.start) / this._eyeBob.duration, 0, 1);
927
+ const cycles = 2;
928
+ emotionBobY = Math.sin(t * Math.PI * cycles * 2) * this._eyeBob.amp * (1 - 0.38*t);
929
+ if (t >= 1) this._eyeBob = null;
930
+ }
931
+
932
+ let surpriseScale = 1;
933
+ if (this._surpriseShake) {
934
+ const t = clamp((now - this._surpriseShake.start) / this._surpriseShake.duration, 0, 1);
935
+ surpriseScale = 1 + 0.022 * Math.sin(t * Math.PI * 8) * (1 - 0.18*t);
936
+ if (t >= 1) this._surpriseShake = null;
937
+ }
938
+
939
+ const blinkH = Math.max(2.6, p.h * this._blink * nodScaleY * surpriseScale);
940
+ const yAdjust = (p.h - blinkH) * 0.02;
941
+ const cursorPeriod = 530;
942
+ const inputBlinkOn = this._state==='input' ? ((Math.floor(now / (cursorPeriod/2)) % 2) === 0) : true;
943
+ const inputPulseL = this._state==='input' ? (inputBlinkOn ? 1 : 0) : 1;
944
+ const inputPulseR = inputPulseL;
945
+
946
+ const jellyEyeOffset = (cx, cy) => {
947
+ const j = this._dragJelly;
948
+ const amount = Math.hypot(j.pullX, j.pullY);
949
+ if (amount < 0.05) return {x:0, y:0};
950
+ const radius = (50 + amount * 0.8) * 0.92;
951
+ const dx = cx - j.hotX;
952
+ const dy = cy - j.hotY;
953
+ const w = Math.exp(-(dx*dx + dy*dy) / (2 * radius * radius)) * 0.24;
954
+ return {x: j.pullX * w, y: j.pullY * w};
955
+ };
956
+
957
+ const setEye = (base, inputBase, top, bottom, side, cx, microX, microY, opacity) => {
958
+ const isInput = this._state === 'input';
959
+ const projectedX = cx + this._look.x + microX;
960
+ const edgeDistance = side==='L' ? (projectedX - 32) : (208 - projectedX);
961
+ const edgeFactor = clamp(1 - edgeDistance / 58, 0, 1);
962
+ const baseW = p.w * surpriseScale;
963
+ const baseH = blinkH;
964
+ const w = baseW * (1 - 0.22 * edgeFactor * edgeFactor);
965
+ const h = baseH * (1 - 0.08 * edgeFactor * edgeFactor);
966
+ const inwardShift = ((baseW - w) * 0.58) * (side==='L' ? 1 : -1);
967
+
968
+ if (isInput) {
969
+ base.setAttribute('opacity', '0');
970
+ inputBase.setAttribute('x', (-p.w/2).toFixed(2));
971
+ inputBase.setAttribute('y', (-p.h/2 + yAdjust).toFixed(2));
972
+ inputBase.setAttribute('width', p.w.toFixed(2));
973
+ inputBase.setAttribute('height', p.h.toFixed(2));
974
+ inputBase.setAttribute('rx', '3');
975
+ inputBase.setAttribute('opacity', opacity.toFixed(3));
976
+ } else {
977
+ inputBase.setAttribute('opacity', '0');
978
+ base.setAttribute('cx', '0');
979
+ base.setAttribute('cy', yAdjust.toFixed(2));
980
+ base.setAttribute('rx', (w/2).toFixed(2));
981
+ base.setAttribute('ry', (h/2).toFixed(2));
982
+ base.setAttribute('opacity', opacity.toFixed(3));
983
+ }
984
+ const topY = p.topY;
985
+ const bottomY = p.bottomY;
986
+ const ta = side==='L' ? p.topAL : p.topAR;
987
+ const ba = side==='L' ? p.bottomAL : p.bottomAR;
988
+ top.setAttribute('y', (topY-90).toFixed(2));
989
+ top.setAttribute('height', '90');
990
+ top.setAttribute('transform', `rotate(${ta.toFixed(2)} 0 ${topY.toFixed(2)})`);
991
+ bottom.setAttribute('y', bottomY.toFixed(2));
992
+ bottom.setAttribute('height', '90');
993
+ bottom.setAttribute('transform', `rotate(${ba.toFixed(2)} 0 ${bottomY.toFixed(2)})`);
994
+ const jelly = jellyEyeOffset(cx, 126);
995
+ return `translate(${(cx + this._look.x + microX + jelly.x + (isInput ? 0 : inwardShift)).toFixed(2)} ${(126 + this._look.y + microY + jelly.y + nodY + angryDropY + emotionBobY).toFixed(2)})`;
996
+ };
997
+
998
+ const inputCenter = this._state==='input';
999
+ const surpriseSpread = this._state==='surprise' ? 3.4 * EASE.outQuint(clamp((p.w - 34) / (POSES.surprise.w - 34), 0, 1)) : 0;
1000
+ const leftCX = inputCenter ? 120 : 86 - surpriseSpread;
1001
+ const rightCX = inputCenter ? 120 : 154 + surpriseSpread;
1002
+ this._leftEye.setAttribute('transform', setEye(this._leftBase,this._leftInputBase,this._leftTop,this._leftBottom,'L',leftCX,this._eyeMicro.lX,this._eyeMicro.lY,inputPulseL));
1003
+ this._rightEye.setAttribute('transform', setEye(this._rightBase,this._rightInputBase,this._rightTop,this._rightBottom,'R',rightCX,this._eyeMicro.rX,this._eyeMicro.rY,inputPulseR));
1004
+ }
1005
+
1006
+ _resumeFrames() {
1007
+ if (!this.isConnected || document.hidden || this._raf) return;
1008
+ this._running = true;
1009
+ this._framePaused = false;
1010
+ this._lastFrame = performance.now();
1011
+ this._raf = requestAnimationFrame(this._loop);
1012
+ }
1013
+
1014
+ _canPauseFrames(now) {
1015
+ if (!this._sleeping || this._state !== 'sleep') return false;
1016
+ if (now - this._morphStart < this._morphDuration) return false;
1017
+ if (this._dragJelly.active || this._dragJelly.returning) return false;
1018
+ if (this._waitingFx || this._inspectFx || this._failureFx || this._warningFx || this._systemErrorShake) return false;
1019
+ const animations = this._headMotion?.getAnimations?.() || [];
1020
+ return !animations.some(animation => animation.playState === 'running' || animation.playState === 'pending');
1021
+ }
1022
+
1023
+ _loop = (ts=performance.now()) => {
1024
+ if (!this._running || !this.isConnected) {
1025
+ this._raf = 0;
1026
+ return;
1027
+ }
1028
+ if (ts - this._lastFrame >= 33) {
1029
+ const dt = Math.min(66, ts - (this._lastFrame || ts-33));
1030
+ this._lastFrame = ts;
1031
+ this._updateMorph(ts);
1032
+ this._updateBlink(ts);
1033
+ this._updateLook(ts, dt);
1034
+ this._updateHeadFollow(ts, dt);
1035
+ this._updateDragJelly(dt);
1036
+ this._autoSleep(ts);
1037
+ this._draw(ts);
1038
+ }
1039
+ if (this._canPauseFrames(ts)) {
1040
+ this._framePaused = true;
1041
+ this._raf = 0;
1042
+ return;
1043
+ }
1044
+ this._raf = requestAnimationFrame(this._loop);
1045
+ };
1046
+ }
1047
+
1048
+ if (typeof customElements !== 'undefined' && !customElements.get('agent-robot-avatar')) {
1049
+ customElements.define('agent-robot-avatar', AgentFace);
1050
+ }
1051
+ if (typeof window !== 'undefined') window.AgentRobotAvatar = AgentFace;
1052
+ return AgentFace;
1053
+ })();
1054
+
1055
+ export { AgentRobotAvatar };
1056
+ export default AgentRobotAvatar;