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,3 +1,3 @@
1
- const VERSION = '0.3.1';
2
-
3
- export { VERSION };
1
+ const VERSION = '0.5.2';
2
+
3
+ export { VERSION };
@@ -1,121 +1,130 @@
1
- import AgentRobotAvatar, { registerAvatarExtension } from './agent-robot-avatar-extension-host.js';
2
-
3
- const WAITING_CYCLE = 3200;
4
- const proto = AgentRobotAvatar.prototype;
5
-
6
- const clamp01 = value => Math.max(0, Math.min(1, value));
7
-
8
- function dispatchWaitingState(instance, state) {
9
- instance.dispatchEvent(new CustomEvent('face-state', {
10
- detail: { state }
11
- }));
12
- }
13
-
14
- function readEyeY(eye) {
15
- const raw = eye?.getAttribute('transform') || '';
16
- const match = raw.match(/translate\(\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\)/);
17
- return match ? Number(match[2]) : 126;
18
- }
19
-
20
- function setEyeOrbitTransform(eye, x, scaleX, scaleY) {
21
- if (!eye) return;
22
- const y = readEyeY(eye);
23
- eye.setAttribute(
24
- 'transform',
25
- `translate(${x.toFixed(2)} ${y.toFixed(2)}) scale(${scaleX.toFixed(4)} ${scaleY.toFixed(4)})`
26
- );
27
- }
28
-
29
- async function enterWaiting(instance, continuous, source) {
30
- instance.noteActivity();
31
- instance._inputWanted = false;
32
-
33
- instance.reset();
34
- instance._waitingRequested = true;
35
- if (!(await instance._prepareExpression({ normalizePose: true, duration: 120, pause: 0 }))) return null;
36
- if (!instance._waitingRequested) return null;
37
-
38
- const token = instance._transitionToken;
39
- instance._expressionLock = true;
40
- instance._look.x = 0;
41
- instance._look.y = 0;
42
- instance._waitingFx = {
43
- start: performance.now(),
44
- duration: WAITING_CYCLE,
45
- continuous: continuous === true,
46
- source,
47
- };
48
- dispatchWaitingState(instance, 'waiting');
49
- return token;
50
- }
51
-
52
- proto.waiting = async function() {
53
- const token = await enterWaiting(this, false, 'play');
54
- if (token == null) return;
55
-
56
- await this._wait(WAITING_CYCLE);
57
- if (token !== this._transitionToken || !this._waitingRequested) return;
58
- this._waitingRequested = false;
59
- this._waitingFx = null;
60
- this._releaseExpressionLock();
61
- dispatchWaitingState(this, 'idle');
62
- };
63
-
64
- // Runtime/Agent behavior: call when a request has been sent and the reply has not arrived yet.
65
- // It stays seamless until another action or stopWaiting() occurs.
66
- proto.startWaiting = async function() {
67
- await enterWaiting(this, true, 'runtime');
68
- return this;
69
- };
70
-
71
- proto.stopWaiting = function() {
72
- return this.reset();
73
- };
74
-
75
- function drawWaiting(now) {
76
- const fx = this._waitingFx;
77
- if (!fx) return;
78
-
79
- const elapsed = Math.max(0, now - fx.start);
80
- const phaseMs = fx.continuous
81
- ? (elapsed % fx.duration)
82
- : Math.min(fx.duration, elapsed);
83
- const t = clamp01(phaseMs / fx.duration);
84
-
85
- // Original R43 waiting-eye motion: two continuous horizontal orbits per 3.2 s cycle.
86
- const angle = t * Math.PI * 4;
87
- const cosA = Math.cos(angle);
88
- const sinA = Math.sin(angle);
89
-
90
- const leftX = 120 - 34 * cosA;
91
- const rightX = 120 + 34 * cosA;
92
-
93
- // Original R43 perspective/size treatment retained exactly.
94
- const foreshorten = 0.72 + 0.28 * Math.abs(cosA);
95
- const leftDepth = 1 + 0.08 * sinA;
96
- const rightDepth = 1 - 0.08 * sinA;
97
-
98
- setEyeOrbitTransform(this._leftEye, leftX, foreshorten * leftDepth, leftDepth);
99
- setEyeOrbitTransform(this._rightEye, rightX, foreshorten * rightDepth, rightDepth);
100
- }
101
-
102
- registerAvatarExtension({
103
- name: 'waiting',
104
- actions: {
105
- waiting() { return this.waiting(); },
106
- wait() { return this.waiting(); },
107
- },
108
- beforePlay(action) {
109
- if (action === 'waiting' || action === 'wait') return;
110
- this._waitingRequested = false;
111
- this._waitingFx = null;
112
- },
113
- reset() {
114
- this._waitingRequested = false;
115
- this._waitingFx = null;
116
- },
117
- draw: drawWaiting,
118
- });
119
-
120
- export { AgentRobotAvatar, WAITING_CYCLE };
121
- export default AgentRobotAvatar;
1
+ import AgentRobotAvatar, { registerAvatarExtension } from './agent-robot-avatar-extension-host.js';
2
+ import { WRAP_CYCLE, drawWrapWaiting } from './agent-robot-avatar-wrap.js';
3
+
4
+ const WAITING_CYCLE = 3200;
5
+ const proto = AgentRobotAvatar.prototype;
6
+
7
+ const clamp01 = value => Math.max(0, Math.min(1, value));
8
+
9
+ function dispatchWaitingState(instance, state) {
10
+ instance.dispatchEvent(new CustomEvent('face-state', {
11
+ detail: { state }
12
+ }));
13
+ }
14
+
15
+ function readEyeY(eye) {
16
+ const raw = eye?.getAttribute('transform') || '';
17
+ const match = raw.match(/translate\(\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\)/);
18
+ return match ? Number(match[2]) : 126;
19
+ }
20
+
21
+ function setEyeOrbitTransform(eye, x, scaleX, scaleY) {
22
+ if (!eye) return;
23
+ const y = readEyeY(eye);
24
+ eye.setAttribute(
25
+ 'transform',
26
+ `translate(${x.toFixed(2)} ${y.toFixed(2)}) scale(${scaleX.toFixed(4)} ${scaleY.toFixed(4)})`
27
+ );
28
+ }
29
+
30
+ async function enterWaiting(instance, continuous, source, options = {}) {
31
+ const variant = options?.variant ?? 'default';
32
+ if (variant !== 'default' && variant !== 'wrap') throw new TypeError(`Unknown waiting variant: ${variant}`);
33
+ instance.noteActivity();
34
+ instance._inputWanted = false;
35
+
36
+ instance.reset();
37
+ instance._waitingRequested = true;
38
+ if (!(await instance._prepareExpression({ normalizePose: true, duration: 120, pause: 0 }))) return null;
39
+ if (!instance._waitingRequested) return null;
40
+
41
+ const token = instance._transitionToken;
42
+ instance._expressionLock = true;
43
+ instance._look.x = 0;
44
+ instance._look.y = 0;
45
+ instance._waitingFx = {
46
+ start: performance.now(),
47
+ duration: variant === 'wrap' ? WRAP_CYCLE : WAITING_CYCLE,
48
+ variant,
49
+ continuous: continuous === true,
50
+ source,
51
+ };
52
+ dispatchWaitingState(instance, 'waiting');
53
+ return token;
54
+ }
55
+
56
+ proto.waiting = async function(options = {}) {
57
+ const token = await enterWaiting(this, false, 'play', options);
58
+ if (token == null) return;
59
+
60
+ await this._wait(options?.variant === 'wrap' ? WRAP_CYCLE : WAITING_CYCLE);
61
+ if (token !== this._transitionToken || !this._waitingRequested) return;
62
+ this._waitingRequested = false;
63
+ this._waitingFx = null;
64
+ this._releaseExpressionLock();
65
+ dispatchWaitingState(this, 'idle');
66
+ };
67
+
68
+ // Runtime/Agent behavior: call when a request has been sent and the reply has not arrived yet.
69
+ // It stays seamless until another action or stopWaiting() occurs.
70
+ proto.startWaiting = async function(options = {}) {
71
+ await enterWaiting(this, true, 'runtime', options);
72
+ return this;
73
+ };
74
+
75
+ proto.stopWaiting = function() {
76
+ return this.reset();
77
+ };
78
+
79
+ function drawWaiting(now) {
80
+ const fx = this._waitingFx;
81
+ if (!fx) return;
82
+
83
+ const elapsed = Math.max(0, now - fx.start);
84
+ const phaseMs = fx.continuous
85
+ ? (elapsed % fx.duration)
86
+ : Math.min(fx.duration, elapsed);
87
+ const t = clamp01(phaseMs / fx.duration);
88
+ if (fx.variant === 'wrap') {
89
+ drawWrapWaiting(this, phaseMs);
90
+ return;
91
+ }
92
+
93
+ // Original R43 waiting-eye motion: two continuous horizontal orbits per 3.2 s cycle.
94
+ const angle = t * Math.PI * 4;
95
+ const cosA = Math.cos(angle);
96
+ const sinA = Math.sin(angle);
97
+
98
+ const leftX = 120 - 34 * cosA;
99
+ const rightX = 120 + 34 * cosA;
100
+
101
+ // Original R43 perspective/size treatment retained exactly.
102
+ const foreshorten = 0.72 + 0.28 * Math.abs(cosA);
103
+ const leftDepth = 1 + 0.08 * sinA;
104
+ const rightDepth = 1 - 0.08 * sinA;
105
+
106
+ setEyeOrbitTransform(this._leftEye, leftX, foreshorten * leftDepth, leftDepth);
107
+ setEyeOrbitTransform(this._rightEye, rightX, foreshorten * rightDepth, rightDepth);
108
+ }
109
+
110
+ registerAvatarExtension({
111
+ name: 'waiting',
112
+ actions: {
113
+ waiting() { return this.waiting(); },
114
+ wait() { return this.waiting(); },
115
+ 'waiting-wrap'() { return this.waiting({ variant: 'wrap' }); },
116
+ },
117
+ beforePlay(action) {
118
+ if (action === 'waiting' || action === 'wait' || action === 'waiting-wrap') return;
119
+ this._waitingRequested = false;
120
+ this._waitingFx = null;
121
+ },
122
+ reset() {
123
+ this._waitingRequested = false;
124
+ this._waitingFx = null;
125
+ },
126
+ draw: drawWaiting,
127
+ });
128
+
129
+ export { AgentRobotAvatar, WAITING_CYCLE };
130
+ export default AgentRobotAvatar;
@@ -0,0 +1,66 @@
1
+ // Both eyes lie on the same rotating surface. Position and foreshortening
2
+ // must be projected together, otherwise the eyes overlap or hit the head edge.
3
+ export const WRAP_CYCLE = 2400;
4
+ const RADIUS = 85;
5
+ const EYE_ANGLE = Math.asin(34 / RADIUS);
6
+ const HALF_WIDTH = 27 / Math.cos(EYE_ANGLE);
7
+ const FRAMES = [
8
+ [0, 0, null],
9
+ [834, -Math.PI / 2 + EYE_ANGLE, [.6123122262631361,0,.8763337613684284,.3403039133642485]],
10
+ [996, -Math.PI / 2 - EYE_ANGLE, null],
11
+ [1260, -3 * Math.PI / 2 + EYE_ANGLE, null],
12
+ [1390, -3 * Math.PI / 2 - EYE_ANGLE, null],
13
+ [2400, -2 * Math.PI, [.16771539711108435,.7245980352977247,.32366613843706765,1]],
14
+ ];
15
+
16
+ function progress(t, curve) {
17
+ if (!curve || t <= 0 || t >= 1) return t;
18
+ const b = (s,a,z) => 3*(1-s)*(1-s)*s*a + 3*(1-s)*s*s*z + s*s*s;
19
+ let lo=0,hi=1;
20
+ for (let i=0;i<24;i++) { const mid=(lo+hi)/2; if(b(mid,curve[0],curve[2])<t)lo=mid;else hi=mid; }
21
+ return b((lo+hi)/2,curve[1],curve[3]);
22
+ }
23
+
24
+ export function wrapEyeHeight(width) {
25
+ // Optical compensation: a narrow eye reads taller than a round one.
26
+ // Keep the frontal height, then shorten smoothly toward the side profile.
27
+ const side=Math.max(0,Math.min(1,1-width/54));
28
+ return 58-18*side*side*(3-2*side);
29
+ }
30
+
31
+ export function wrapEyeX(x, width) {
32
+ // Close the gap to the head's 94-unit silhouette only as the eye narrows.
33
+ // The cubic keeps the round frontal eye untouched and approaches the edge
34
+ // tangentially, instead of pushing a full-size ellipse through the clip.
35
+ const side=Math.max(0,Math.min(1,1-width/54));
36
+ return x+Math.sign(x-120)*9*side**3;
37
+ }
38
+
39
+ export function sampleWrapPose(elapsed) {
40
+ const time=Math.max(0,Math.min(WRAP_CYCLE,elapsed));
41
+ const index=Math.max(1,FRAMES.findIndex(frame=>frame[0]>=time));
42
+ const a=FRAMES[index-1],b=FRAMES[index],t=progress((time-a[0])/(b[0]-a[0]),b[2]);
43
+ const angle=a[1]+(b[1]-a[1])*t;
44
+ const pose={};
45
+ for(const [side,offset] of [['left',-EYE_ANGLE],['right',EYE_ANGLE]]) {
46
+ const eyeAngle=angle+offset;
47
+ const front=Math.cos(eyeAngle);
48
+ pose[side+'Y']=126;
49
+ pose[side+'Width']=front>1e-10?2*HALF_WIDTH*front:0;
50
+ pose[side+'X']=wrapEyeX(120+RADIUS*Math.sin(eyeAngle),pose[side+'Width']);
51
+ pose[side+'Height']=wrapEyeHeight(pose[side+'Width']);
52
+ pose[side+'Opacity']=1;
53
+ }
54
+ return pose;
55
+ }
56
+
57
+ export function drawWrapWaiting(instance, elapsed) {
58
+ const pose=sampleWrapPose(elapsed);
59
+ for(const side of ['left','right']) {
60
+ instance['_'+side+'Eye'].setAttribute('transform',`translate(${pose[side+'X']} 126)`);
61
+ const eye=instance['_'+side+'Base'];
62
+ eye.setAttribute('rx',pose[side+'Width']/2);eye.setAttribute('ry',pose[side+'Height']/2);
63
+ eye.setAttribute('opacity','1');
64
+ instance['_'+side+'InputBase'].setAttribute('opacity','0');
65
+ }
66
+ }