agent-robot-avatar 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ import AgentRobotAvatar from './agent-robot-avatar-core.js';
2
+
3
+ const proto = AgentRobotAvatar.prototype;
4
+ const basePlay = proto.play;
5
+ const baseReset = proto.reset;
6
+ const baseDraw = proto._draw;
7
+ const extensionNames = new Set();
8
+ const actionHandlers = new Map();
9
+ const beforePlayHooks = [];
10
+ const afterPlayHooks = [];
11
+ const resetHooks = [];
12
+ const drawHooks = [];
13
+
14
+ function registerAvatarExtension({
15
+ name,
16
+ actions = {},
17
+ beforePlay,
18
+ afterPlay,
19
+ reset,
20
+ draw,
21
+ }) {
22
+ if (!name || extensionNames.has(name)) {
23
+ throw new Error(`Agent Robot Avatar extension already registered: ${name || '(unnamed)'}`);
24
+ }
25
+ extensionNames.add(name);
26
+
27
+ for (const [action, handler] of Object.entries(actions)) {
28
+ if (actionHandlers.has(action)) {
29
+ throw new Error(`Agent Robot Avatar action already registered: ${action}`);
30
+ }
31
+ actionHandlers.set(action, handler);
32
+ }
33
+
34
+ if (beforePlay) beforePlayHooks.push(beforePlay);
35
+ if (afterPlay) afterPlayHooks.push(afterPlay);
36
+ if (reset) resetHooks.push(reset);
37
+ if (draw) drawHooks.push(draw);
38
+ }
39
+
40
+ proto.play = function(name) {
41
+ this._resumeFrames?.();
42
+ const action = String(name || '').trim().toLowerCase();
43
+ const context = {};
44
+
45
+ for (const hook of beforePlayHooks) hook.call(this, action, context);
46
+
47
+ let result;
48
+ try {
49
+ const handler = actionHandlers.get(action);
50
+ result = handler ? handler.call(this, action) : basePlay.call(this, action);
51
+ } catch (error) {
52
+ context.error = error;
53
+ for (const hook of afterPlayHooks) hook.call(this, action, undefined, context);
54
+ throw error;
55
+ }
56
+
57
+ for (const hook of afterPlayHooks) hook.call(this, action, result, context);
58
+ return result;
59
+ };
60
+
61
+ proto.reset = function() {
62
+ this._resumeFrames?.();
63
+ for (const hook of resetHooks) hook.call(this);
64
+ return baseReset.call(this);
65
+ };
66
+
67
+ proto._draw = function(now) {
68
+ baseDraw.call(this, now);
69
+ for (const hook of drawHooks) hook.call(this, now);
70
+ };
71
+
72
+ export { AgentRobotAvatar, registerAvatarExtension };
73
+ export default AgentRobotAvatar;
@@ -0,0 +1,127 @@
1
+ import AgentRobotAvatar, { registerAvatarExtension } from './agent-robot-avatar-extension-host.js';
2
+
3
+ const proto = AgentRobotAvatar.prototype;
4
+
5
+ const FAILURE_HEAD_DROP = 5.5;
6
+ const FAILURE_EYE_DROP = 4.8;
7
+
8
+ function dispatchFailureState(instance, state) {
9
+ instance.dispatchEvent(new CustomEvent('face-state', {
10
+ detail: { state }
11
+ }));
12
+ }
13
+
14
+ function animateFailureHead(instance, frames, duration) {
15
+ if (!instance._headMotion?.animate) return null;
16
+ instance._headMotion.style.transformBox = 'view-box';
17
+ instance._headMotion.style.transformOrigin = '120px 120px';
18
+ const anim = instance._headMotion.animate(frames, {
19
+ duration,
20
+ easing: 'cubic-bezier(.32,0,.24,1)',
21
+ fill: 'forwards',
22
+ });
23
+ anim.onfinish = () => {
24
+ instance._headMotion.style.transform = frames[frames.length - 1].transform || '';
25
+ };
26
+ return anim;
27
+ }
28
+
29
+ function smooth01(t) {
30
+ t = Math.max(0, Math.min(1, t));
31
+ return t * t * (3 - 2 * t);
32
+ }
33
+
34
+ function failureEyeDrop(fx, now) {
35
+ const elapsed = Math.max(0, now - fx.start);
36
+ if (elapsed < fx.headDown) {
37
+ return FAILURE_EYE_DROP * smooth01(elapsed / Math.max(1, fx.headDown));
38
+ }
39
+ if (elapsed < fx.headDown + fx.hold) return FAILURE_EYE_DROP;
40
+ const recoverElapsed = elapsed - fx.headDown - fx.hold;
41
+ if (recoverElapsed < fx.recover) {
42
+ return FAILURE_EYE_DROP * (1 - smooth01(recoverElapsed / Math.max(1, fx.recover)));
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ function addEyeDrop(eye, amount) {
48
+ if (!eye || Math.abs(amount) < 0.001) return;
49
+ const current = eye.getAttribute('transform') || '';
50
+ eye.setAttribute('transform', `${current} translate(0 ${amount.toFixed(2)})`);
51
+ }
52
+
53
+ proto.failure = async function() {
54
+ this.noteActivity();
55
+ this._inputWanted = false;
56
+ this._failureFx = null;
57
+
58
+ // Clear waiting/inspect/flash or any other active extension before entering failure.
59
+ this.reset();
60
+ if (!(await this._prepareExpression({ normalizePose: true, duration: 180, pause: 260 }))) return;
61
+ const token = this._transitionToken;
62
+
63
+ // First finish the sad/failure eye deformation completely.
64
+ const eyeIn = 320;
65
+ this.setState('sad', { duration: eyeIn, keepGazeLock: true });
66
+ dispatchFailureState(this, 'failure');
67
+ await this._wait(eyeIn);
68
+ if (token !== this._transitionToken || this._state !== 'sad') return;
69
+
70
+ // Only after the eyes have settled, lower the head and press the eyes down inside it together.
71
+ const headDown = 720;
72
+ const loweredHold = 720;
73
+ const recover = 520;
74
+ this._failureFx = {
75
+ start: performance.now(),
76
+ headDown,
77
+ hold: loweredHold,
78
+ recover,
79
+ };
80
+
81
+ animateFailureHead(this, [
82
+ { transform: 'translateY(0px)' },
83
+ { transform: `translateY(${FAILURE_HEAD_DROP}px)` },
84
+ ], headDown);
85
+ await this._wait(headDown);
86
+ if (token !== this._transitionToken) return;
87
+
88
+ // Hold the lowered, sad pose longer before recovering.
89
+ await this._wait(loweredHold);
90
+ if (token !== this._transitionToken) return;
91
+
92
+ // Head, eyes and sad expression recover together.
93
+ animateFailureHead(this, [
94
+ { transform: `translateY(${FAILURE_HEAD_DROP}px)` },
95
+ { transform: 'translateY(0px)' },
96
+ ], recover);
97
+ if (this._state === 'sad') await this._returnToIdle(recover);
98
+ this._failureFx = null;
99
+ };
100
+
101
+ function drawFailure(now) {
102
+ const fx = this._failureFx;
103
+ if (!fx) return;
104
+ const drop = failureEyeDrop(fx, now);
105
+ addEyeDrop(this._leftEye, drop);
106
+ addEyeDrop(this._rightEye, drop);
107
+ }
108
+
109
+ registerAvatarExtension({
110
+ name: 'failure',
111
+ actions: {
112
+ failure() { return this.failure(); },
113
+ failed() { return this.failure(); },
114
+ fail() { return this.failure(); },
115
+ },
116
+ beforePlay(action) {
117
+ if (action === 'failure' || action === 'failed' || action === 'fail') return;
118
+ this._failureFx = null;
119
+ },
120
+ reset() {
121
+ this._failureFx = null;
122
+ },
123
+ draw: drawFailure,
124
+ });
125
+
126
+ export { AgentRobotAvatar };
127
+ export default AgentRobotAvatar;
@@ -0,0 +1,136 @@
1
+ import AgentRobotAvatar from './agent-robot-avatar-extension-host.js';
2
+ import { VERSION } from './agent-robot-avatar-version.js';
3
+
4
+ const proto = AgentRobotAvatar.prototype;
5
+ const HEAD_HEIGHT_SCALE = 0.9;
6
+ const DEFAULT_ROUNDNESS = 50;
7
+
8
+ function clamp(value, min, max) {
9
+ return Math.max(min, Math.min(max, value));
10
+ }
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
+ function ensureDefaultGeometry(instance) {
46
+ if (!instance?._headShape || !instance._baseHeadPoints?.length) return false;
47
+ if (instance._headRoundnessDefaultPoints?.length) return true;
48
+
49
+ let points = instance._baseHeadPoints.map(point => ({ x: point.x, y: point.y }));
50
+
51
+ if (!instance._headFlattenedR32) {
52
+ points = flattenPoints(points);
53
+ instance._headFlattenedR32 = true;
54
+ instance._baseHeadPoints = points.map(point => ({ ...point }));
55
+ instance._baseHeadPathD = instance._pointsToPath(points);
56
+ instance._headShape.setAttribute('d', instance._baseHeadPathD);
57
+ }
58
+
59
+ instance._headRoundnessDefaultPoints = instance._baseHeadPoints.map(point => ({ x: point.x, y: point.y }));
60
+ instance._headRoundness = DEFAULT_ROUNDNESS;
61
+ return true;
62
+ }
63
+
64
+ function superellipsePoint(angle, cx, cy, halfW, halfH, exponent) {
65
+ const cos = Math.cos(angle);
66
+ const sin = Math.sin(angle);
67
+ const xTerm = Math.pow(Math.abs(cos) / Math.max(0.001, halfW), exponent);
68
+ const yTerm = Math.pow(Math.abs(sin) / Math.max(0.001, halfH), exponent);
69
+ const denom = Math.pow(xTerm + yTerm, 1 / exponent);
70
+ const radius = denom > 0 ? 1 / denom : 0;
71
+ return { x: cx + cos * radius, y: cy + sin * radius };
72
+ }
73
+
74
+ function targetShape(points, exponent) {
75
+ const minX = Math.min(...points.map(point => point.x));
76
+ const maxX = Math.max(...points.map(point => point.x));
77
+ const minY = Math.min(...points.map(point => point.y));
78
+ const maxY = Math.max(...points.map(point => point.y));
79
+ const cx = (minX + maxX) / 2;
80
+ const cy = (minY + maxY) / 2;
81
+ const halfW = (maxX - minX) / 2;
82
+ const halfH = (maxY - minY) / 2;
83
+
84
+ return points.map(point => {
85
+ const angle = Math.atan2(point.y - cy, point.x - cx);
86
+ return superellipsePoint(angle, cx, cy, halfW, halfH, exponent);
87
+ });
88
+ }
89
+
90
+ function mixPoints(from, to, amount) {
91
+ const t = clamp(amount, 0, 1);
92
+ return from.map((point, index) => ({
93
+ x: point.x + (to[index].x - point.x) * t,
94
+ y: point.y + (to[index].y - point.y) * t,
95
+ }));
96
+ }
97
+
98
+ proto.setHeadRoundness = function(value = DEFAULT_ROUNDNESS) {
99
+ if (!ensureDefaultGeometry(this)) return this;
100
+
101
+ const roundness = clamp(Number(value) || 0, 0, 100);
102
+ const baseline = this._headRoundnessDefaultPoints;
103
+ let points = baseline.map(point => ({ ...point }));
104
+
105
+ if (roundness < DEFAULT_ROUNDNESS) {
106
+ const squarer = targetShape(baseline, 10);
107
+ points = mixPoints(baseline, squarer, (DEFAULT_ROUNDNESS - roundness) / DEFAULT_ROUNDNESS);
108
+ } else if (roundness > DEFAULT_ROUNDNESS) {
109
+ const rounder = targetShape(baseline, 2.2);
110
+ points = mixPoints(baseline, rounder, (roundness - DEFAULT_ROUNDNESS) / DEFAULT_ROUNDNESS);
111
+ }
112
+
113
+ this._headRoundness = roundness;
114
+ this._baseHeadPoints = points;
115
+ this._baseHeadPathD = this._pointsToPath(points);
116
+
117
+ if (this._dragJelly?.active || this._dragJelly?.returning) this._applyHeadDeform?.();
118
+ else this._headShape.setAttribute('d', this._baseHeadPathD);
119
+
120
+ this.dispatchEvent(new CustomEvent('head-roundness-change', {
121
+ detail: { value: roundness, version: VERSION }
122
+ }));
123
+ return this;
124
+ };
125
+
126
+ proto.getHeadRoundness = function() {
127
+ return Number.isFinite(this._headRoundness) ? this._headRoundness : DEFAULT_ROUNDNESS;
128
+ };
129
+
130
+ if (typeof window !== 'undefined') {
131
+ window.AgentRobotAvatarVersion = VERSION;
132
+ window.AgentRobotAvatarHeadRoundnessDefault = DEFAULT_ROUNDNESS;
133
+ }
134
+
135
+ export { AgentRobotAvatar, DEFAULT_ROUNDNESS, VERSION };
136
+ export default AgentRobotAvatar;
@@ -0,0 +1,231 @@
1
+ import AgentRobotAvatar, { registerAvatarExtension } from './agent-robot-avatar-extension-host.js';
2
+
3
+ const proto = AgentRobotAvatar.prototype;
4
+
5
+ const clamp01 = value => Math.max(0, Math.min(1, value));
6
+ const lerp = (a, b, t) => a + (b - a) * t;
7
+ const smooth = t => {
8
+ t = clamp01(t);
9
+ return t * t * (3 - 2 * t);
10
+ };
11
+
12
+ // Finalized from the R59 tuner values supplied by the user.
13
+ const INSPECT_DEFAULTS = Object.freeze({
14
+ aperture: 16,
15
+ scanOffset: 16,
16
+ prepPause: 120,
17
+ close: 220,
18
+ holdClosed: 880,
19
+ down1: 400,
20
+ holdDown1: 560,
21
+ up1: 160,
22
+ holdUp1: 580,
23
+ center1: 140,
24
+ holdCenter: 760,
25
+ down2: 140,
26
+ holdDown2: 620,
27
+ up2: 180,
28
+ holdUp2: 620,
29
+ center2: 100,
30
+ holdAfter: 940,
31
+ open: 260,
32
+ });
33
+
34
+ const runtimeWindow = typeof window !== 'undefined' ? window : null;
35
+ const inspectConfig = (runtimeWindow?.AgentRobotAvatarInspectConfig && typeof runtimeWindow.AgentRobotAvatarInspectConfig === 'object')
36
+ ? runtimeWindow.AgentRobotAvatarInspectConfig
37
+ : {};
38
+ for (const [key, value] of Object.entries(INSPECT_DEFAULTS)) {
39
+ inspectConfig[key] = value;
40
+ }
41
+ if (runtimeWindow) {
42
+ runtimeWindow.AgentRobotAvatarInspectConfig = inspectConfig;
43
+ runtimeWindow.AgentRobotAvatarInspectDefaults = INSPECT_DEFAULTS;
44
+ }
45
+
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
+ function currentInspectConfig() {
53
+ const duration = key => configNumber(key, INSPECT_DEFAULTS[key], 0, 2400);
54
+ return {
55
+ aperture: configNumber('aperture', INSPECT_DEFAULTS.aperture, 6, 40),
56
+ scanOffset: configNumber('scanOffset', INSPECT_DEFAULTS.scanOffset, 0, 24),
57
+ prepPause: duration('prepPause'),
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
+ };
74
+ }
75
+
76
+ function dispatchInspectState(instance, state) {
77
+ instance.dispatchEvent(new CustomEvent('face-state', {
78
+ detail: { state }
79
+ }));
80
+ }
81
+
82
+ function makeTimeline(config) {
83
+ let cursor = 0;
84
+ const phases = {};
85
+ const add = key => {
86
+ phases[`${key}Start`] = cursor;
87
+ cursor += config[key];
88
+ phases[`${key}End`] = cursor;
89
+ };
90
+ for (const key of [
91
+ 'close','holdClosed','down1','holdDown1','up1','holdUp1','center1','holdCenter',
92
+ 'down2','holdDown2','up2','holdUp2','center2','holdAfter','open'
93
+ ]) add(key);
94
+ phases.duration = cursor;
95
+ return phases;
96
+ }
97
+
98
+ function move(elapsed, start, end, from, to) {
99
+ if (elapsed <= start) return from;
100
+ if (elapsed >= end) return to;
101
+ const span = Math.max(1, end - start);
102
+ return lerp(from, to, smooth((elapsed - start) / span));
103
+ }
104
+
105
+ function verticalShift(elapsed, phases, offset) {
106
+ if (elapsed < phases.down1Start) return 0;
107
+ if (elapsed < phases.down1End) return move(elapsed, phases.down1Start, phases.down1End, 0, offset);
108
+ if (elapsed < phases.holdDown1End) return offset;
109
+ if (elapsed < phases.up1End) return move(elapsed, phases.up1Start, phases.up1End, offset, -offset);
110
+ if (elapsed < phases.holdUp1End) return -offset;
111
+ if (elapsed < phases.center1End) return move(elapsed, phases.center1Start, phases.center1End, -offset, 0);
112
+ if (elapsed < phases.holdCenterEnd) return 0;
113
+
114
+ // Second pass: down to the lowest point, hold, then return directly to center.
115
+ // Do not overshoot upward again.
116
+ if (elapsed < phases.down2End) return move(elapsed, phases.down2Start, phases.down2End, 0, offset);
117
+ if (elapsed < phases.holdDown2End) return offset;
118
+ if (elapsed < phases.up2End) return move(elapsed, phases.up2Start, phases.up2End, offset, 0);
119
+ return 0;
120
+ }
121
+
122
+ // Keep the R60 head motion unchanged: down, up past center, then back to center.
123
+ function animateInspectHead(instance, phases) {
124
+ if (!instance._headMotion?.animate) return;
125
+ const start = phases.down1Start;
126
+ const end = phases.center1End;
127
+ const duration = Math.max(1, end - start);
128
+ const offset = time => Math.max(0, Math.min(1, (time - start) / duration));
129
+
130
+ instance._headMotion.style.transformBox = 'view-box';
131
+ instance._headMotion.style.transformOrigin = '120px 120px';
132
+ const anim = instance._headMotion.animate([
133
+ { transform: 'translateY(0px)', offset: 0, easing: 'cubic-bezier(.38,0,.25,1)' },
134
+ { transform: 'translateY(5px)', offset: offset(phases.down1End) },
135
+ { transform: 'translateY(5px)', offset: offset(phases.holdDown1End), easing: 'cubic-bezier(.35,0,.22,1)' },
136
+ { transform: 'translateY(-4px)', offset: offset(phases.up1End) },
137
+ { transform: 'translateY(-4px)', offset: offset(phases.holdUp1End), easing: 'cubic-bezier(.35,0,.22,1)' },
138
+ { transform: 'translateY(0px)', offset: 1 },
139
+ ], {
140
+ duration,
141
+ delay: start,
142
+ easing: 'linear',
143
+ fill: 'forwards',
144
+ });
145
+ anim.onfinish = () => { instance._headMotion.style.transform = 'translateY(0px)'; };
146
+ }
147
+
148
+ proto.inspect = async function() {
149
+ this.noteActivity();
150
+ this._inputWanted = false;
151
+
152
+ const config = currentInspectConfig();
153
+ this.reset();
154
+ if (!(await this._prepareExpression({ normalizePose: true, duration: 160, pause: config.prepPause }))) return;
155
+
156
+ const token = this._transitionToken;
157
+ this._expressionLock = true;
158
+ this._look.x = 0;
159
+ this._look.y = 0;
160
+
161
+ const phases = makeTimeline(config);
162
+ this._inspectFx = { start: performance.now(), duration: phases.duration, config, phases };
163
+ dispatchInspectState(this, 'inspect');
164
+ animateInspectHead(this, phases);
165
+
166
+ await this._wait(phases.duration + 30);
167
+ if (token !== this._transitionToken) return;
168
+
169
+ this._inspectFx = null;
170
+ this._look.x = 0;
171
+ this._look.y = 0;
172
+ this._releaseExpressionLock();
173
+ dispatchInspectState(this, 'idle');
174
+ };
175
+
176
+ function drawInspect(now) {
177
+ const fx = this._inspectFx;
178
+ if (!fx) return;
179
+
180
+ const config = fx.config || currentInspectConfig();
181
+ const phases = fx.phases || makeTimeline(config);
182
+ const elapsed = Math.max(0, Math.min(phases.duration, now - fx.start));
183
+
184
+ let lidBlend = 1;
185
+ if (elapsed < phases.closeEnd) {
186
+ lidBlend = smooth(elapsed / Math.max(1, config.close));
187
+ } else if (elapsed >= phases.openStart) {
188
+ lidBlend = 1 - smooth((elapsed - phases.openStart) / Math.max(1, config.open));
189
+ }
190
+
191
+ const yShift = verticalShift(elapsed, phases, config.scanOffset);
192
+
193
+ // Keep the original eye geometry. The squint is made only by the top/bottom masks.
194
+ this._leftEye.setAttribute('transform', `translate(86 ${(126 + yShift).toFixed(2)}) scale(1 1)`);
195
+ this._rightEye.setAttribute('transform', `translate(154 ${(126 + yShift).toFixed(2)}) scale(1 1)`);
196
+
197
+ const apertureHalf = config.aperture / 2;
198
+ const topEdge = lerp(-36, -apertureHalf, lidBlend);
199
+ const bottomEdge = lerp(36, apertureHalf, lidBlend);
200
+
201
+ for (const top of [this._leftTop, this._rightTop]) {
202
+ top.setAttribute('y', (topEdge - 90).toFixed(2));
203
+ top.setAttribute('height', '90');
204
+ top.setAttribute('transform', `rotate(0 0 ${topEdge.toFixed(2)})`);
205
+ }
206
+ for (const bottom of [this._leftBottom, this._rightBottom]) {
207
+ bottom.setAttribute('y', bottomEdge.toFixed(2));
208
+ bottom.setAttribute('height', '90');
209
+ bottom.setAttribute('transform', `rotate(0 0 ${bottomEdge.toFixed(2)})`);
210
+ }
211
+ }
212
+
213
+ registerAvatarExtension({
214
+ name: 'inspect',
215
+ actions: {
216
+ inspect() { return this.inspect(); },
217
+ verify() { return this.inspect(); },
218
+ review() { return this.inspect(); },
219
+ },
220
+ beforePlay(action) {
221
+ if (action === 'inspect' || action === 'verify' || action === 'review') return;
222
+ this._inspectFx = null;
223
+ },
224
+ reset() {
225
+ this._inspectFx = null;
226
+ },
227
+ draw: drawInspect,
228
+ });
229
+
230
+ export { AgentRobotAvatar, INSPECT_DEFAULTS };
231
+ export default AgentRobotAvatar;
@@ -0,0 +1,3 @@
1
+ const VERSION = '0.2.0';
2
+
3
+ export { VERSION };
@@ -0,0 +1,121 @@
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;