agent-robot-avatar 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +193 -33
- package/agent-robot-avatar.js +11 -1
- package/docs/README.de.md +22 -8
- package/docs/README.es.md +19 -11
- package/docs/README.fr.md +21 -11
- package/docs/README.ja.md +19 -11
- package/docs/README.ko.md +19 -11
- package/docs/README.pt.md +19 -11
- package/docs/README.zh-CN.md +202 -92
- package/docs/README.zh-TW.md +19 -11
- package/index.d.ts +30 -0
- package/package.json +3 -2
- package/src/agent-robot-avatar-actions.js +8 -2
- package/src/agent-robot-avatar-antenna.js +24 -45
- package/src/agent-robot-avatar-core.js +80 -17
- package/src/agent-robot-avatar-extension-host.js +6 -1
- package/src/agent-robot-avatar-failure.js +5 -1
- package/src/agent-robot-avatar-geometry.js +40 -0
- package/src/agent-robot-avatar-head-roundness.js +2 -35
- package/src/agent-robot-avatar-inspect.js +41 -30
- package/src/agent-robot-avatar-runtime-fixes.js +178 -0
- package/src/agent-robot-avatar-runtime.js +744 -0
- package/src/agent-robot-avatar-version.js +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import AgentRobotAvatar, { registerAvatarExtension } from './agent-robot-avatar-extension-host.js';
|
|
2
|
+
import { DEFAULT_HEAD_HEIGHT_SCALE, flattenHeadPoints } from './agent-robot-avatar-geometry.js';
|
|
2
3
|
|
|
3
4
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
4
|
-
const HEAD_HEIGHT_SCALE = 0.9;
|
|
5
5
|
const ANTENNA_DEFAULTS = Object.freeze({
|
|
6
6
|
x: 120,
|
|
7
7
|
y: 12,
|
|
@@ -35,40 +35,10 @@ function antennaHome() {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function smooth01(t) {
|
|
39
|
-
t = Math.max(0, Math.min(1, t));
|
|
40
|
-
return t * t * (3 - 2 * t);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
38
|
function flattenHead(instance) {
|
|
44
39
|
if (instance._headFlattenedR32 || !instance._headShape || !instance._baseHeadPoints?.length) return;
|
|
45
40
|
|
|
46
|
-
const
|
|
47
|
-
const minY = Math.min(...source.map(point => point.y));
|
|
48
|
-
const maxY = Math.max(...source.map(point => point.y));
|
|
49
|
-
const height = maxY - minY;
|
|
50
|
-
const centerY = (minY + maxY) / 2;
|
|
51
|
-
const inset = height * (1 - HEAD_HEIGHT_SCALE) / 2;
|
|
52
|
-
|
|
53
|
-
const upperLockY = minY + height * 0.40;
|
|
54
|
-
const lowerLockY = maxY - height * 0.40;
|
|
55
|
-
|
|
56
|
-
const flattened = source.map(point => {
|
|
57
|
-
let shiftY = 0;
|
|
58
|
-
if (point.y <= upperLockY) {
|
|
59
|
-
shiftY = inset;
|
|
60
|
-
} else if (point.y < centerY) {
|
|
61
|
-
const t = (centerY - point.y) / Math.max(0.001, centerY - upperLockY);
|
|
62
|
-
shiftY = inset * smooth01(t);
|
|
63
|
-
} else if (point.y >= lowerLockY) {
|
|
64
|
-
shiftY = -inset;
|
|
65
|
-
} else if (point.y > centerY) {
|
|
66
|
-
const t = (point.y - centerY) / Math.max(0.001, lowerLockY - centerY);
|
|
67
|
-
shiftY = -inset * smooth01(t);
|
|
68
|
-
}
|
|
69
|
-
return { x: point.x, y: point.y + shiftY };
|
|
70
|
-
});
|
|
71
|
-
|
|
41
|
+
const flattened = flattenHeadPoints(instance._baseHeadPoints, DEFAULT_HEAD_HEIGHT_SCALE);
|
|
72
42
|
const path = typeof instance._pointsToPath === 'function'
|
|
73
43
|
? instance._pointsToPath(flattened)
|
|
74
44
|
: `M ${flattened.map((point, index) => `${index ? 'L ' : ''}${point.x.toFixed(2)} ${point.y.toFixed(2)}`).join(' ')} Z`;
|
|
@@ -135,24 +105,33 @@ function updateAntenna(instance, now) {
|
|
|
135
105
|
const dot = instance._antennaDot;
|
|
136
106
|
const target = antennaTarget(instance);
|
|
137
107
|
|
|
138
|
-
if (
|
|
108
|
+
if (instance._isReducedMotion?.()) {
|
|
139
109
|
spring.x = target.x;
|
|
140
110
|
spring.y = target.y;
|
|
111
|
+
spring.vx = 0;
|
|
112
|
+
spring.vy = 0;
|
|
141
113
|
spring.last = now;
|
|
142
114
|
spring.initialized = true;
|
|
143
|
-
}
|
|
115
|
+
} else {
|
|
116
|
+
if (!spring.initialized) {
|
|
117
|
+
spring.x = target.x;
|
|
118
|
+
spring.y = target.y;
|
|
119
|
+
spring.last = now;
|
|
120
|
+
spring.initialized = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const dt = Math.max(0.001, Math.min(0.05, (now - spring.last) / 1000 || 0.033));
|
|
124
|
+
spring.last = now;
|
|
144
125
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
spring.x += spring.vx * dt;
|
|
155
|
-
spring.y += spring.vy * dt;
|
|
126
|
+
const stiffness = configNumber('stiffness', ANTENNA_DEFAULTS.stiffness, 5, 300);
|
|
127
|
+
const damping = configNumber('damping', ANTENNA_DEFAULTS.damping, 0.5, 40);
|
|
128
|
+
const ax = (target.x - spring.x) * stiffness - spring.vx * damping;
|
|
129
|
+
const ay = (target.y - spring.y) * stiffness - spring.vy * damping;
|
|
130
|
+
spring.vx += ax * dt;
|
|
131
|
+
spring.vy += ay * dt;
|
|
132
|
+
spring.x += spring.vx * dt;
|
|
133
|
+
spring.y += spring.vy * dt;
|
|
134
|
+
}
|
|
156
135
|
|
|
157
136
|
dot.setAttribute('cx', spring.x.toFixed(2));
|
|
158
137
|
dot.setAttribute('cy', spring.y.toFixed(2));
|
|
@@ -142,6 +142,7 @@ const AgentRobotAvatar = (() => {
|
|
|
142
142
|
this._inputWanted = false;
|
|
143
143
|
this._sleeping = false;
|
|
144
144
|
this._idleTimer = null;
|
|
145
|
+
this._pendingWaits = new Map();
|
|
145
146
|
this._boredRoutine = null;
|
|
146
147
|
this._boredLookSpeed = null;
|
|
147
148
|
this._autoSleepMs = Number(this.getAttribute('auto-sleep')) || 0;
|
|
@@ -169,6 +170,7 @@ const AgentRobotAvatar = (() => {
|
|
|
169
170
|
this.addEventListener('pointerdown', this._boundDragStart);
|
|
170
171
|
this.addEventListener('click', this._boundClickCapture, true);
|
|
171
172
|
this._running = !document.hidden;
|
|
173
|
+
this._lastActivity = performance.now();
|
|
172
174
|
this._resumeFrames();
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -176,6 +178,20 @@ const AgentRobotAvatar = (() => {
|
|
|
176
178
|
unregisterConnectedFace(this);
|
|
177
179
|
this.removeEventListener('pointerdown', this._boundDragStart);
|
|
178
180
|
this.removeEventListener('click', this._boundClickCapture, true);
|
|
181
|
+
// Removal cancels work; reattachment starts idle with appearance settings intact.
|
|
182
|
+
this._resetToIdle(false);
|
|
183
|
+
this._blinkAnim = null;
|
|
184
|
+
this._blink = 1;
|
|
185
|
+
this._pointer.active = false;
|
|
186
|
+
this._pointer.influence = 0;
|
|
187
|
+
const drag = this._dragJelly;
|
|
188
|
+
if (drag.pointerId != null) {
|
|
189
|
+
try { this.releasePointerCapture(drag.pointerId); } catch (_) {}
|
|
190
|
+
}
|
|
191
|
+
drag.active = false;
|
|
192
|
+
drag.pointerId = null;
|
|
193
|
+
this._finishDragReturn(null);
|
|
194
|
+
this._suppressClick = false;
|
|
179
195
|
this._running = false;
|
|
180
196
|
cancelAnimationFrame(this._raf);
|
|
181
197
|
this._raf = 0;
|
|
@@ -184,11 +200,18 @@ const AgentRobotAvatar = (() => {
|
|
|
184
200
|
static get observedAttributes() { return ['color','size','auto-sleep']; }
|
|
185
201
|
attributeChangedCallback(name, oldV, newV) {
|
|
186
202
|
if (oldV === newV) return;
|
|
187
|
-
if (name === 'color'
|
|
203
|
+
if (name === 'color') this._applyColor(newV);
|
|
188
204
|
if (name === 'size') this.style.setProperty('--face-size', `${Number(newV)||112}px`);
|
|
189
205
|
if (name === 'auto-sleep') this._autoSleepMs = Number(newV) || 0;
|
|
190
206
|
}
|
|
191
207
|
|
|
208
|
+
_applyColor(value) {
|
|
209
|
+
const color = value || '#08090b';
|
|
210
|
+
for (const part of [this._head, this._leftTop, this._rightTop, this._leftBottom, this._rightBottom, this._antennaDot]) {
|
|
211
|
+
if (part) part.setAttribute('fill', color);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
_renderShell() {
|
|
193
216
|
this.shadowRoot.innerHTML = `
|
|
194
217
|
<style>
|
|
@@ -246,10 +269,7 @@ const AgentRobotAvatar = (() => {
|
|
|
246
269
|
this._rightTop = this.shadowRoot.getElementById('rightTop');
|
|
247
270
|
this._leftBottom = this.shadowRoot.getElementById('leftBottom');
|
|
248
271
|
this._rightBottom = this.shadowRoot.getElementById('rightBottom');
|
|
249
|
-
|
|
250
|
-
this._head.setAttribute('fill', c);
|
|
251
|
-
[this._leftTop,this._rightTop,this._leftBottom,this._rightBottom].forEach(el=>el.setAttribute('fill',c));
|
|
252
|
-
this.style.setProperty('--face-size', `${Number(this.getAttribute('size'))||112}px`);
|
|
272
|
+
this._applyColor(this.getAttribute('color'));
|
|
253
273
|
this._baseHeadPathD = this._headShape.getAttribute('d');
|
|
254
274
|
this._baseHeadPoints = this._parseHeadPoints(this._baseHeadPathD);
|
|
255
275
|
}
|
|
@@ -511,8 +531,14 @@ const AgentRobotAvatar = (() => {
|
|
|
511
531
|
}
|
|
512
532
|
|
|
513
533
|
reset() {
|
|
534
|
+
return this._resetToIdle();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_resetToIdle(notify = true) {
|
|
538
|
+
this._resetExtensionEffects?.();
|
|
514
539
|
this._inputWanted = false;
|
|
515
540
|
++this._transitionToken;
|
|
541
|
+
this._cancelPendingWaits();
|
|
516
542
|
this._boredRoutine = null;
|
|
517
543
|
this._boredLookSpeed = null;
|
|
518
544
|
this._sleeping = false;
|
|
@@ -520,7 +546,7 @@ const AgentRobotAvatar = (() => {
|
|
|
520
546
|
this._eyeBob = null;
|
|
521
547
|
this._angryEyeDrop = null;
|
|
522
548
|
this._surpriseShake = null;
|
|
523
|
-
this.setState('idle');
|
|
549
|
+
this.setState('idle', { notify, duration: notify ? 320 : 0 });
|
|
524
550
|
return this;
|
|
525
551
|
}
|
|
526
552
|
|
|
@@ -531,7 +557,7 @@ const AgentRobotAvatar = (() => {
|
|
|
531
557
|
this._sleeping = name === 'sleep';
|
|
532
558
|
if (name === 'idle' && !opts.keepGazeLock) this._releaseExpressionLock();
|
|
533
559
|
this._startMorph(POSES[name], opts.duration ?? (name==='surprise'?170:320), opts.ease || EASE.outQuint);
|
|
534
|
-
this.dispatchEvent(new CustomEvent('face-state', {detail:{state:name}}));
|
|
560
|
+
if (opts.notify !== false) this.dispatchEvent(new CustomEvent('face-state', {detail:{state:name}}));
|
|
535
561
|
return this;
|
|
536
562
|
}
|
|
537
563
|
|
|
@@ -559,6 +585,7 @@ const AgentRobotAvatar = (() => {
|
|
|
559
585
|
} else {
|
|
560
586
|
if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
|
|
561
587
|
}
|
|
588
|
+
const token = this._transitionToken;
|
|
562
589
|
this.setState('happy', {duration: fromDrag ? 180 : 220, keepGazeLock:true});
|
|
563
590
|
this._eyeBob = {start: performance.now(), duration: 620, amp: 8};
|
|
564
591
|
this._animateHead([
|
|
@@ -567,6 +594,7 @@ const AgentRobotAvatar = (() => {
|
|
|
567
594
|
{transform:'translateY(0px)'}
|
|
568
595
|
], 760);
|
|
569
596
|
await this._wait(980);
|
|
597
|
+
if (token !== this._transitionToken) return;
|
|
570
598
|
this._eyeBob = null;
|
|
571
599
|
if (this._state==='happy') await this._returnToIdle(520);
|
|
572
600
|
}
|
|
@@ -575,6 +603,7 @@ const AgentRobotAvatar = (() => {
|
|
|
575
603
|
this.noteActivity();
|
|
576
604
|
this._inputWanted = false;
|
|
577
605
|
if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
|
|
606
|
+
const token = this._transitionToken;
|
|
578
607
|
this.setState('sad', {duration:320, keepGazeLock:true});
|
|
579
608
|
this._eyeBob = {start: performance.now(), duration: 700, amp: 7};
|
|
580
609
|
this._animateHead([
|
|
@@ -583,6 +612,7 @@ const AgentRobotAvatar = (() => {
|
|
|
583
612
|
{transform:'translateY(0px)'}
|
|
584
613
|
], 820);
|
|
585
614
|
await this._wait(1180);
|
|
615
|
+
if (token !== this._transitionToken) return;
|
|
586
616
|
this._eyeBob = null;
|
|
587
617
|
if (this._state==='sad') {
|
|
588
618
|
await this._returnToIdle(560);
|
|
@@ -605,6 +635,7 @@ const AgentRobotAvatar = (() => {
|
|
|
605
635
|
} else {
|
|
606
636
|
if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
|
|
607
637
|
}
|
|
638
|
+
const token = this._transitionToken;
|
|
608
639
|
this.setState('angry', {duration: fromDrag ? 180 : 280, keepGazeLock:true});
|
|
609
640
|
this._angryEyeDrop = {start: performance.now(), duration:680, amount:4.8};
|
|
610
641
|
this._animateHead([
|
|
@@ -613,6 +644,7 @@ const AgentRobotAvatar = (() => {
|
|
|
613
644
|
{transform:'translateY(0px)'}
|
|
614
645
|
], 820);
|
|
615
646
|
await this._wait(980);
|
|
647
|
+
if (token !== this._transitionToken) return;
|
|
616
648
|
this._angryEyeDrop = null;
|
|
617
649
|
if (this._state==='angry') await this._returnToIdle(500);
|
|
618
650
|
}
|
|
@@ -621,17 +653,19 @@ const AgentRobotAvatar = (() => {
|
|
|
621
653
|
this.noteActivity();
|
|
622
654
|
this._inputWanted = false;
|
|
623
655
|
if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
|
|
656
|
+
const token = this._transitionToken;
|
|
624
657
|
this._surpriseShake = null;
|
|
625
658
|
const pre = {...POSES.idle, w:34, h:36, rx:18};
|
|
626
659
|
this._startMorph(pre, 235, EASE.inOutCubic);
|
|
627
660
|
await this._wait(235);
|
|
628
|
-
if (this._state!=='idle') return;
|
|
661
|
+
if (token !== this._transitionToken || this._state!=='idle') return;
|
|
629
662
|
this.setState('surprise', {duration:175, ease:EASE.outQuint, keepGazeLock:true});
|
|
630
663
|
this._animateHead([{transform:'scale(1)'},{transform:'scale(1.035)'}], 175);
|
|
631
664
|
await this._wait(175);
|
|
632
|
-
if (this._state!=='surprise') return;
|
|
665
|
+
if (token !== this._transitionToken || this._state!=='surprise') return;
|
|
633
666
|
this._surpriseShake = {start: performance.now(), duration:420};
|
|
634
667
|
await this._wait(420);
|
|
668
|
+
if (token !== this._transitionToken) return;
|
|
635
669
|
this._surpriseShake = null;
|
|
636
670
|
if (this._state==='surprise') {
|
|
637
671
|
this._animateHead([{transform:'scale(1.035)'},{transform:'scale(1)'}], 430);
|
|
@@ -643,6 +677,7 @@ const AgentRobotAvatar = (() => {
|
|
|
643
677
|
this.noteActivity();
|
|
644
678
|
this._inputWanted = false;
|
|
645
679
|
if (!(await this._prepareExpression({normalizePose:true, duration:180, pause:320}))) return;
|
|
680
|
+
const token = this._transitionToken;
|
|
646
681
|
this._eyeNod = {start: performance.now(), duration: 760};
|
|
647
682
|
this._animateHead([
|
|
648
683
|
{transform:'translateY(0px)'},
|
|
@@ -650,6 +685,7 @@ const AgentRobotAvatar = (() => {
|
|
|
650
685
|
{transform:'translateY(0px)'}
|
|
651
686
|
], 920);
|
|
652
687
|
await this._wait(860);
|
|
688
|
+
if (token !== this._transitionToken) return;
|
|
653
689
|
this._eyeNod = null;
|
|
654
690
|
this._releaseExpressionLock();
|
|
655
691
|
}
|
|
@@ -657,21 +693,22 @@ const AgentRobotAvatar = (() => {
|
|
|
657
693
|
async sleep() {
|
|
658
694
|
if (this._sleeping) return;
|
|
659
695
|
if (!(await this._prepareExpression({normalizePose:true, duration:160}))) return;
|
|
696
|
+
const token = this._transitionToken;
|
|
660
697
|
this._blinkAnim = null;
|
|
661
698
|
this._blink = 1;
|
|
662
699
|
this._nextBlinkAt = performance.now() + 10000;
|
|
663
700
|
this.setState('sleepy', {duration:940, ease:EASE.inOutCubic, keepGazeLock:true});
|
|
664
701
|
await this._wait(980);
|
|
665
|
-
if (this._state !== 'sleepy') return;
|
|
702
|
+
if (token !== this._transitionToken || this._state !== 'sleepy') return;
|
|
666
703
|
const reopen = {...POSES.idle, topY:-22, bottomY:36};
|
|
667
704
|
this._startMorph(reopen, 105, EASE.outQuint);
|
|
668
705
|
await this._wait(120);
|
|
669
|
-
if (this._state !== 'sleepy') return;
|
|
706
|
+
if (token !== this._transitionToken || this._state !== 'sleepy') return;
|
|
670
707
|
this.setState('sleepy', {duration:820, ease:EASE.inOutCubic, keepGazeLock:true});
|
|
671
708
|
await this._wait(870);
|
|
672
|
-
if (this._state !== 'sleepy') return;
|
|
709
|
+
if (token !== this._transitionToken || this._state !== 'sleepy') return;
|
|
673
710
|
await this._wait(120);
|
|
674
|
-
if (this._state !== 'sleepy') return;
|
|
711
|
+
if (token !== this._transitionToken || this._state !== 'sleepy') return;
|
|
675
712
|
this._animateHead([{transform:'translateY(0)'},{transform:'translateY(4px)'}],980);
|
|
676
713
|
this._startMorph(POSES.sleep, 980, EASE.inOutCubic);
|
|
677
714
|
this._state = 'sleep';
|
|
@@ -682,6 +719,7 @@ const AgentRobotAvatar = (() => {
|
|
|
682
719
|
|
|
683
720
|
async wake() {
|
|
684
721
|
if (!this._sleeping && this._state!=='sleep') return;
|
|
722
|
+
const token = ++this._transitionToken;
|
|
685
723
|
this._expressionLock = true;
|
|
686
724
|
this._sleeping = false;
|
|
687
725
|
this._animateHead([
|
|
@@ -691,9 +729,12 @@ const AgentRobotAvatar = (() => {
|
|
|
691
729
|
], 500);
|
|
692
730
|
this.setState('sleepy', {duration:240, keepGazeLock:true});
|
|
693
731
|
await this._wait(260);
|
|
732
|
+
if (token !== this._transitionToken) return;
|
|
694
733
|
this.setState('surprise', {duration:120, keepGazeLock:true});
|
|
695
734
|
await this._wait(190);
|
|
735
|
+
if (token !== this._transitionToken) return;
|
|
696
736
|
await this._returnToIdle(300);
|
|
737
|
+
if (token !== this._transitionToken) return;
|
|
697
738
|
this._lastActivity = performance.now();
|
|
698
739
|
}
|
|
699
740
|
|
|
@@ -715,18 +756,40 @@ const AgentRobotAvatar = (() => {
|
|
|
715
756
|
|
|
716
757
|
_animateHead(frames, duration) {
|
|
717
758
|
if (!this._headMotion.animate) return;
|
|
759
|
+
const token = this._transitionToken;
|
|
718
760
|
const anim = this._headMotion.animate(frames, {duration, easing:'cubic-bezier(.16,1,.3,1)', fill:'forwards'});
|
|
719
|
-
anim.onfinish = () => {
|
|
761
|
+
anim.onfinish = () => {
|
|
762
|
+
if (token === this._transitionToken) {
|
|
763
|
+
this._headMotion.style.transform = frames[frames.length-1].transform || '';
|
|
764
|
+
}
|
|
765
|
+
};
|
|
720
766
|
return anim;
|
|
721
767
|
}
|
|
722
768
|
|
|
723
|
-
_wait(ms) {
|
|
769
|
+
_wait(ms) {
|
|
770
|
+
return new Promise(resolve => {
|
|
771
|
+
const timer = setTimeout(() => {
|
|
772
|
+
this._pendingWaits.delete(timer);
|
|
773
|
+
resolve();
|
|
774
|
+
}, ms);
|
|
775
|
+
this._pendingWaits.set(timer, resolve);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
_cancelPendingWaits() {
|
|
780
|
+
for (const [timer, resolve] of this._pendingWaits) {
|
|
781
|
+
clearTimeout(timer);
|
|
782
|
+
resolve();
|
|
783
|
+
}
|
|
784
|
+
this._pendingWaits.clear();
|
|
785
|
+
}
|
|
724
786
|
|
|
725
787
|
async _returnToIdle(duration=320, ease=EASE.outQuint) {
|
|
788
|
+
const token = this._transitionToken;
|
|
726
789
|
this.setState('idle', {duration, ease, keepGazeLock:true});
|
|
727
790
|
this._headCenteringUntil = Math.max(this._headCenteringUntil || 0, performance.now() + duration);
|
|
728
791
|
await this._wait(duration);
|
|
729
|
-
if (this._state === 'idle') this._releaseExpressionLock();
|
|
792
|
+
if (token === this._transitionToken && this._state === 'idle') this._releaseExpressionLock();
|
|
730
793
|
}
|
|
731
794
|
|
|
732
795
|
async _prepareExpression({normalizePose=true, duration=180, pause=320}={}) {
|
|
@@ -848,7 +911,7 @@ const AgentRobotAvatar = (() => {
|
|
|
848
911
|
}
|
|
849
912
|
|
|
850
913
|
_autoSleep(now) {
|
|
851
|
-
if (!this._autoSleepMs || this._sleeping || ['input','happy','sad','surprise'].includes(this._state)) return;
|
|
914
|
+
if (!this._autoSleepMs || this._sleeping || this._expressionLock || ['input','happy','sad','surprise'].includes(this._state)) return;
|
|
852
915
|
if (now - this._lastActivity > this._autoSleepMs) this.sleep();
|
|
853
916
|
}
|
|
854
917
|
|
|
@@ -58,9 +58,14 @@ proto.play = function(name) {
|
|
|
58
58
|
return result;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
// Called dynamically by core reset and disconnect; custom-element lifecycle
|
|
62
|
+
// callbacks themselves were already registered before extensions loaded.
|
|
63
|
+
proto._resetExtensionEffects = function() {
|
|
64
|
+
for (const hook of resetHooks) hook.call(this);
|
|
65
|
+
};
|
|
66
|
+
|
|
61
67
|
proto.reset = function() {
|
|
62
68
|
this._resumeFrames?.();
|
|
63
|
-
for (const hook of resetHooks) hook.call(this);
|
|
64
69
|
return baseReset.call(this);
|
|
65
70
|
};
|
|
66
71
|
|
|
@@ -13,6 +13,7 @@ function dispatchFailureState(instance, state) {
|
|
|
13
13
|
|
|
14
14
|
function animateFailureHead(instance, frames, duration) {
|
|
15
15
|
if (!instance._headMotion?.animate) return null;
|
|
16
|
+
const token = instance._transitionToken;
|
|
16
17
|
instance._headMotion.style.transformBox = 'view-box';
|
|
17
18
|
instance._headMotion.style.transformOrigin = '120px 120px';
|
|
18
19
|
const anim = instance._headMotion.animate(frames, {
|
|
@@ -21,7 +22,9 @@ function animateFailureHead(instance, frames, duration) {
|
|
|
21
22
|
fill: 'forwards',
|
|
22
23
|
});
|
|
23
24
|
anim.onfinish = () => {
|
|
24
|
-
|
|
25
|
+
if (token === instance._transitionToken) {
|
|
26
|
+
instance._headMotion.style.transform = frames[frames.length - 1].transform || '';
|
|
27
|
+
}
|
|
25
28
|
};
|
|
26
29
|
return anim;
|
|
27
30
|
}
|
|
@@ -95,6 +98,7 @@ proto.failure = async function() {
|
|
|
95
98
|
{ transform: 'translateY(0px)' },
|
|
96
99
|
], recover);
|
|
97
100
|
if (this._state === 'sad') await this._returnToIdle(recover);
|
|
101
|
+
if (token !== this._transitionToken) return;
|
|
98
102
|
this._failureFx = null;
|
|
99
103
|
};
|
|
100
104
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const DEFAULT_HEAD_HEIGHT_SCALE = 0.9;
|
|
2
|
+
|
|
3
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
4
|
+
const smooth01 = value => {
|
|
5
|
+
const t = clamp(value, 0, 1);
|
|
6
|
+
return t * t * (3 - 2 * t);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function flattenHeadPoints(points, heightScale = DEFAULT_HEAD_HEIGHT_SCALE) {
|
|
10
|
+
if (!points?.length) return [];
|
|
11
|
+
const source = points.map(point => ({ x: point.x, y: point.y }));
|
|
12
|
+
const minY = Math.min(...source.map(point => point.y));
|
|
13
|
+
const maxY = Math.max(...source.map(point => point.y));
|
|
14
|
+
const height = maxY - minY;
|
|
15
|
+
if (!(height > 0)) return source;
|
|
16
|
+
|
|
17
|
+
const scale = clamp(Number(heightScale), 0.1, 1);
|
|
18
|
+
const centerY = (minY + maxY) / 2;
|
|
19
|
+
const inset = height * (1 - scale) / 2;
|
|
20
|
+
const upperLockY = minY + height * 0.40;
|
|
21
|
+
const lowerLockY = maxY - height * 0.40;
|
|
22
|
+
|
|
23
|
+
return source.map(point => {
|
|
24
|
+
let shiftY = 0;
|
|
25
|
+
if (point.y <= upperLockY) {
|
|
26
|
+
shiftY = inset;
|
|
27
|
+
} else if (point.y < centerY) {
|
|
28
|
+
const t = (centerY - point.y) / Math.max(0.001, centerY - upperLockY);
|
|
29
|
+
shiftY = inset * smooth01(t);
|
|
30
|
+
} else if (point.y >= lowerLockY) {
|
|
31
|
+
shiftY = -inset;
|
|
32
|
+
} else if (point.y > centerY) {
|
|
33
|
+
const t = (point.y - centerY) / Math.max(0.001, lowerLockY - centerY);
|
|
34
|
+
shiftY = -inset * smooth01(t);
|
|
35
|
+
}
|
|
36
|
+
return { x: point.x, y: point.y + shiftY };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { DEFAULT_HEAD_HEIGHT_SCALE, flattenHeadPoints };
|
|
@@ -1,47 +1,14 @@
|
|
|
1
1
|
import AgentRobotAvatar from './agent-robot-avatar-extension-host.js';
|
|
2
|
+
import { flattenHeadPoints } from './agent-robot-avatar-geometry.js';
|
|
2
3
|
import { VERSION } from './agent-robot-avatar-version.js';
|
|
3
4
|
|
|
4
5
|
const proto = AgentRobotAvatar.prototype;
|
|
5
|
-
const HEAD_HEIGHT_SCALE = 0.9;
|
|
6
6
|
const DEFAULT_ROUNDNESS = 50;
|
|
7
7
|
|
|
8
8
|
function clamp(value, min, max) {
|
|
9
9
|
return Math.max(min, Math.min(max, value));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function smooth01(t) {
|
|
13
|
-
t = clamp(t, 0, 1);
|
|
14
|
-
return t * t * (3 - 2 * t);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function flattenPoints(points) {
|
|
18
|
-
if (!points?.length) return [];
|
|
19
|
-
const source = points.map(point => ({ x: point.x, y: point.y }));
|
|
20
|
-
const minY = Math.min(...source.map(point => point.y));
|
|
21
|
-
const maxY = Math.max(...source.map(point => point.y));
|
|
22
|
-
const height = maxY - minY;
|
|
23
|
-
const centerY = (minY + maxY) / 2;
|
|
24
|
-
const inset = height * (1 - HEAD_HEIGHT_SCALE) / 2;
|
|
25
|
-
const upperLockY = minY + height * 0.40;
|
|
26
|
-
const lowerLockY = maxY - height * 0.40;
|
|
27
|
-
|
|
28
|
-
return source.map(point => {
|
|
29
|
-
let shiftY = 0;
|
|
30
|
-
if (point.y <= upperLockY) {
|
|
31
|
-
shiftY = inset;
|
|
32
|
-
} else if (point.y < centerY) {
|
|
33
|
-
const t = (centerY - point.y) / Math.max(0.001, centerY - upperLockY);
|
|
34
|
-
shiftY = inset * smooth01(t);
|
|
35
|
-
} else if (point.y >= lowerLockY) {
|
|
36
|
-
shiftY = -inset;
|
|
37
|
-
} else if (point.y > centerY) {
|
|
38
|
-
const t = (point.y - centerY) / Math.max(0.001, lowerLockY - centerY);
|
|
39
|
-
shiftY = -inset * smooth01(t);
|
|
40
|
-
}
|
|
41
|
-
return { x: point.x, y: point.y + shiftY };
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
12
|
function ensureDefaultGeometry(instance) {
|
|
46
13
|
if (!instance?._headShape || !instance._baseHeadPoints?.length) return false;
|
|
47
14
|
if (instance._headRoundnessDefaultPoints?.length) return true;
|
|
@@ -49,7 +16,7 @@ function ensureDefaultGeometry(instance) {
|
|
|
49
16
|
let points = instance._baseHeadPoints.map(point => ({ x: point.x, y: point.y }));
|
|
50
17
|
|
|
51
18
|
if (!instance._headFlattenedR32) {
|
|
52
|
-
points =
|
|
19
|
+
points = flattenHeadPoints(points);
|
|
53
20
|
instance._headFlattenedR32 = true;
|
|
54
21
|
instance._baseHeadPoints = points.map(point => ({ ...point }));
|
|
55
22
|
instance._baseHeadPathD = instance._pointsToPath(points);
|
|
@@ -31,46 +31,54 @@ const INSPECT_DEFAULTS = Object.freeze({
|
|
|
31
31
|
open: 260,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
const INSPECT_BOUNDS = Object.freeze({
|
|
35
|
+
aperture: [6, 40],
|
|
36
|
+
scanOffset: [0, 24],
|
|
37
|
+
prepPause: [0, 2400],
|
|
38
|
+
close: [40, 2400],
|
|
39
|
+
holdClosed: [0, 2400],
|
|
40
|
+
down1: [40, 2400],
|
|
41
|
+
holdDown1: [0, 2400],
|
|
42
|
+
up1: [40, 2400],
|
|
43
|
+
holdUp1: [0, 2400],
|
|
44
|
+
center1: [40, 2400],
|
|
45
|
+
holdCenter: [0, 2400],
|
|
46
|
+
down2: [40, 2400],
|
|
47
|
+
holdDown2: [0, 2400],
|
|
48
|
+
up2: [40, 2400],
|
|
49
|
+
holdUp2: [0, 2400],
|
|
50
|
+
center2: [40, 2400],
|
|
51
|
+
holdAfter: [0, 2400],
|
|
52
|
+
open: [40, 2400],
|
|
53
|
+
});
|
|
54
|
+
|
|
34
55
|
const runtimeWindow = typeof window !== 'undefined' ? window : null;
|
|
35
56
|
const inspectConfig = (runtimeWindow?.AgentRobotAvatarInspectConfig && typeof runtimeWindow.AgentRobotAvatarInspectConfig === 'object')
|
|
36
57
|
? runtimeWindow.AgentRobotAvatarInspectConfig
|
|
37
58
|
: {};
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
|
|
60
|
+
function normalizeConfigValue(key, value) {
|
|
61
|
+
const fallback = INSPECT_DEFAULTS[key];
|
|
62
|
+
const numeric = Number(value);
|
|
63
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
64
|
+
const [min, max] = INSPECT_BOUNDS[key];
|
|
65
|
+
return Math.max(min, Math.min(max, numeric));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const key of Object.keys(INSPECT_DEFAULTS)) {
|
|
69
|
+
inspectConfig[key] = normalizeConfigValue(key, inspectConfig[key]);
|
|
40
70
|
}
|
|
41
71
|
if (runtimeWindow) {
|
|
42
72
|
runtimeWindow.AgentRobotAvatarInspectConfig = inspectConfig;
|
|
43
73
|
runtimeWindow.AgentRobotAvatarInspectDefaults = INSPECT_DEFAULTS;
|
|
44
74
|
}
|
|
45
75
|
|
|
46
|
-
function configNumber(key, fallback, min, max) {
|
|
47
|
-
const value = Number(inspectConfig[key]);
|
|
48
|
-
if (!Number.isFinite(value)) return fallback;
|
|
49
|
-
return Math.max(min, Math.min(max, value));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
76
|
function currentInspectConfig() {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
close: Math.max(40, duration('close')),
|
|
59
|
-
holdClosed: duration('holdClosed'),
|
|
60
|
-
down1: Math.max(40, duration('down1')),
|
|
61
|
-
holdDown1: duration('holdDown1'),
|
|
62
|
-
up1: Math.max(40, duration('up1')),
|
|
63
|
-
holdUp1: duration('holdUp1'),
|
|
64
|
-
center1: Math.max(40, duration('center1')),
|
|
65
|
-
holdCenter: duration('holdCenter'),
|
|
66
|
-
down2: Math.max(40, duration('down2')),
|
|
67
|
-
holdDown2: duration('holdDown2'),
|
|
68
|
-
up2: Math.max(40, duration('up2')),
|
|
69
|
-
holdUp2: duration('holdUp2'),
|
|
70
|
-
center2: Math.max(40, duration('center2')),
|
|
71
|
-
holdAfter: duration('holdAfter'),
|
|
72
|
-
open: Math.max(40, duration('open')),
|
|
73
|
-
};
|
|
77
|
+
const config = {};
|
|
78
|
+
for (const key of Object.keys(INSPECT_DEFAULTS)) {
|
|
79
|
+
config[key] = normalizeConfigValue(key, inspectConfig[key]);
|
|
80
|
+
}
|
|
81
|
+
return config;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
function dispatchInspectState(instance, state) {
|
|
@@ -122,6 +130,7 @@ function verticalShift(elapsed, phases, offset) {
|
|
|
122
130
|
// Keep the R60 head motion unchanged: down, up past center, then back to center.
|
|
123
131
|
function animateInspectHead(instance, phases) {
|
|
124
132
|
if (!instance._headMotion?.animate) return;
|
|
133
|
+
const token = instance._transitionToken;
|
|
125
134
|
const start = phases.down1Start;
|
|
126
135
|
const end = phases.center1End;
|
|
127
136
|
const duration = Math.max(1, end - start);
|
|
@@ -142,7 +151,9 @@ function animateInspectHead(instance, phases) {
|
|
|
142
151
|
easing: 'linear',
|
|
143
152
|
fill: 'forwards',
|
|
144
153
|
});
|
|
145
|
-
anim.onfinish = () => {
|
|
154
|
+
anim.onfinish = () => {
|
|
155
|
+
if (token === instance._transitionToken) instance._headMotion.style.transform = 'translateY(0px)';
|
|
156
|
+
};
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
proto.inspect = async function() {
|