agent-robot-avatar 0.3.1 → 0.5.2

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