agent-robot-avatar 0.2.1 → 0.3.1

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/index.d.ts CHANGED
@@ -38,8 +38,38 @@ export type AgentRobotAvatarState =
38
38
  | 'inspect'
39
39
  | 'failure';
40
40
 
41
+ export type AgentRobotAvatarCanonicalAction =
42
+ | 'idle'
43
+ | 'bored'
44
+ | 'waiting'
45
+ | 'input'
46
+ | 'send'
47
+ | 'success'
48
+ | 'failure'
49
+ | 'warning'
50
+ | 'inspect'
51
+ | 'angry'
52
+ | 'blocked'
53
+ | 'error'
54
+ | 'surprise'
55
+ | 'sleep'
56
+ | 'wake'
57
+ | 'reaction';
58
+
59
+ export type AgentRobotAvatarActionPhase = 'start' | 'end' | 'cancel';
60
+ export type AgentRobotAvatarActionSource = 'api' | 'interaction' | 'automatic';
61
+ export type AgentRobotAvatarWakeOn = 'activity' | 'interaction' | 'manual';
62
+ export type AgentRobotAvatarMotion = 'auto' | 'reduce' | 'full';
63
+
64
+ export interface AgentRobotAvatarActionStateDetail {
65
+ action: AgentRobotAvatarCanonicalAction;
66
+ phase: AgentRobotAvatarActionPhase;
67
+ source: AgentRobotAvatarActionSource;
68
+ }
69
+
41
70
  export interface AgentRobotAvatarEventMap extends HTMLElementEventMap {
42
71
  'face-state': CustomEvent<{ state: AgentRobotAvatarState }>;
72
+ 'action-state': CustomEvent<AgentRobotAvatarActionStateDetail>;
43
73
  'head-roundness-change': CustomEvent<{ value: number; version: string }>;
44
74
  }
45
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-robot-avatar",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "A lightweight expressive robot avatar Web Component for AI agents and interactive applications.",
5
5
  "type": "module",
6
6
  "main": "./agent-robot-avatar.js",
@@ -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 source = instance._baseHeadPoints.map(point => ({ x: point.x, y: point.y }));
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 (!spring.initialized) {
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
- const dt = Math.max(0.001, Math.min(0.05, (now - spring.last) / 1000 || 0.033));
146
- spring.last = now;
147
-
148
- const stiffness = configNumber('stiffness', ANTENNA_DEFAULTS.stiffness, 5, 300);
149
- const damping = configNumber('damping', ANTENNA_DEFAULTS.damping, 0.5, 40);
150
- const ax = (target.x - spring.x) * stiffness - spring.vx * damping;
151
- const ay = (target.y - spring.y) * stiffness - spring.vy * damping;
152
- spring.vx += ax * dt;
153
- spring.vy += ay * dt;
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));
@@ -73,8 +73,14 @@ const AgentRobotAvatar = (() => {
73
73
  if (face._dragJelly.active) face._onDragMove(event);
74
74
  });
75
75
  };
76
- const handleGlobalPointerDown = () => forEachConnectedFace(face => face.noteActivity());
77
- const handleGlobalKeyDown = () => forEachConnectedFace(face => face.noteActivity());
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
+ });
78
84
  const handleGlobalPointerEnd = event => {
79
85
  forEachConnectedFace(face => {
80
86
  if (face._dragJelly.active) face._onDragEnd(event);
@@ -175,26 +181,34 @@ const AgentRobotAvatar = (() => {
175
181
  }
176
182
 
177
183
  disconnectedCallback() {
178
- unregisterConnectedFace(this);
179
- this.removeEventListener('pointerdown', this._boundDragStart);
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 (_) {}
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;
190
211
  }
191
- drag.active = false;
192
- drag.pointerId = null;
193
- this._finishDragReturn(null);
194
- this._suppressClick = false;
195
- this._running = false;
196
- cancelAnimationFrame(this._raf);
197
- this._raf = 0;
198
212
  }
199
213
 
200
214
  static get observedAttributes() { return ['color','size','auto-sleep']; }
@@ -202,7 +216,10 @@ const AgentRobotAvatar = (() => {
202
216
  if (oldV === newV) return;
203
217
  if (name === 'color') this._applyColor(newV);
204
218
  if (name === 'size') this.style.setProperty('--face-size', `${Number(newV)||112}px`);
205
- if (name === 'auto-sleep') this._autoSleepMs = Number(newV) || 0;
219
+ if (name === 'auto-sleep') {
220
+ this._autoSleepMs = Number(newV) || 0;
221
+ this._scheduleAutoSleep?.();
222
+ }
206
223
  }
207
224
 
208
225
  _applyColor(value) {
@@ -1096,7 +1113,6 @@ const AgentRobotAvatar = (() => {
1096
1113
  this._updateLook(ts, dt);
1097
1114
  this._updateHeadFollow(ts, dt);
1098
1115
  this._updateDragJelly(dt);
1099
- this._autoSleep(ts);
1100
1116
  this._draw(ts);
1101
1117
  }
1102
1118
  if (this._canPauseFrames(ts)) {
@@ -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 = flattenPoints(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
- for (const [key, value] of Object.entries(INSPECT_DEFAULTS)) {
39
- inspectConfig[key] = value;
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 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
- };
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) {