agent-robot-avatar 0.6.0 → 0.7.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +84 -19
  3. package/agent-robot-avatar-lite.js +44 -0
  4. package/agent-robot-avatar.js +2 -1
  5. package/index.d.ts +64 -0
  6. package/lite.d.ts +5 -0
  7. package/package.json +11 -6
  8. package/src/agent-robot-avatar-core.js +5 -2
  9. package/src/agent-robot-avatar-runtime.js +4 -1
  10. package/src/agent-robot-avatar-skin-patterns.js +234 -0
  11. package/src/agent-robot-avatar-skins.js +256 -0
  12. package/src/agent-robot-avatar-version.js +1 -1
  13. package/src/skins/catalog.js +218 -0
  14. package/src/skins/collection-materials.js +118 -0
  15. package/src/skins/eyelids.js +32 -0
  16. package/src/skins/glaze-variants.js +29 -0
  17. package/src/skins/layered-texture.js +162 -0
  18. package/src/skins/material-data.js +111 -0
  19. package/src/skins/material-library.js +302 -0
  20. package/src/skins/metal-tint.js +11 -0
  21. package/src/skins/more-materials.js +69 -0
  22. package/src/skins/native.js +754 -0
  23. package/src/skins/paints.js +3 -0
  24. package/src/skins/revised-materials.js +44 -0
  25. package/src/skins/second-pass-materials.js +71 -0
  26. package/src/skins/soft-materials.js +111 -0
  27. package/src/skins/sports-materials.js +38 -0
  28. package/src/skins/wood-materials.js +68 -0
  29. package/assets/support/alipay.png +0 -0
  30. package/assets/support/wechat-pay.png +0 -0
  31. package/docs/README.de.md +0 -185
  32. package/docs/README.es.md +0 -185
  33. package/docs/README.fr.md +0 -185
  34. package/docs/README.ja.md +0 -185
  35. package/docs/README.ko.md +0 -185
  36. package/docs/README.pt.md +0 -185
  37. package/docs/README.ru.md +0 -185
  38. package/docs/README.zh-CN.md +0 -202
  39. package/docs/README.zh-TW.md +0 -185
package/CHANGELOG.md CHANGED
@@ -4,7 +4,17 @@ All notable user-facing changes to Agent Robot Avatar are documented here.
4
4
 
5
5
  The project follows Semantic Versioning for public releases. Internal development build numbers are not part of the public version history.
6
6
 
7
- ## [Unreleased]
7
+ ## [0.7.0] - 2026-10-29
8
+
9
+ ### Added
10
+
11
+ - 188 skins in 17 series. Pick one by number with `skin="301"` or `setSkin(301)`; `setSkin(null)` goes back to the flat color.
12
+ - Skin numbers are permanent: the series number plus two digits, so 305 is the fifth skin of series 3. New skins take the next free number, and numbers are never reused.
13
+ - Every skin works with every expression and gesture, and its light follows the pointer.
14
+ - `SKINS`, `SKIN_SERIES` and `SKIN_NAMES` list the skins, for building a skin picker.
15
+ - Skin code loads only when a page uses a skin. `loadSkins()` loads it in advance.
16
+ - `agent-robot-avatar/lite`: the same avatar without any skin code.
17
+ - The demo has a Skin Gallery and a page with every skin.
8
18
 
9
19
  ## [0.6.0] - 2026-10-23
10
20
 
package/README.md CHANGED
@@ -26,7 +26,7 @@ Built with SVG and vanilla JavaScript, it works as a native custom element with
26
26
 
27
27
  ## Live Demo
28
28
 
29
- [Open the interactive demo](https://cx-artlab.github.io/agent-robot-avatar/)
29
+ [Open the interactive demo](https://cx-artlab.github.io/agent-robot-avatar/) · [Visit the Skin Gallery](https://cx-artlab.github.io/agent-robot-avatar/skins.html)
30
30
 
31
31
  ## Highlights
32
32
 
@@ -38,19 +38,14 @@ Built with SVG and vanilla JavaScript, it works as a native custom element with
38
38
  - Jelly-style drag deformation with elastic recovery
39
39
  - Press-and-hold squeeze and draggable spring antenna interactions
40
40
  - Programmatically controlled Agent states and expressions
41
- - Waiting, success, failure, warning, review, blocked, and system-error feedback
41
+ - Expressions for waiting, success, failure, affirm, wary, inspect, angry, refuse, love and more
42
42
  - Reduced-motion support
43
43
  - Configurable sleep behavior
44
44
  - Adjustable head roundness
45
- - Optional antenna status flashing
45
+ - 188 numbered skins in 17 series, with light that follows the pointer
46
+ - A lite build without skins for the smallest pages
46
47
  - TypeScript declarations included
47
48
 
48
- ## Embedded eyes (experimental)
49
-
50
- An independent, allocation-free C++11 eyes-only engine draws the same 18 expressions on robot and dev-board screens. Firmware examples cover 26 board and screen configurations across ESP32, ESP32-S3 and RP2040 — ST7789, ST7735, GC9A01 round, ILI9341/9342/9488, CO5300 AMOLED, ST7701S RGB panels and SSD1306/SH1106 OLEDs, including LilyGO, M5Stack, Waveshare, Seeed and Espressif boards. Every example compiles in CI; none is verified on real hardware yet, so please send a hardware report if you try one.
51
-
52
- See the [embedded guide](https://github.com/CX-ArtLab/agent-robot-avatar/tree/main/embedded) and [中文说明](https://github.com/CX-ArtLab/agent-robot-avatar/blob/main/embedded/README.zh-CN.md). The Web component's installation and behavior are unchanged.
53
-
54
49
  ## Install
55
50
 
56
51
  ```bash
@@ -75,6 +70,16 @@ Then add the component:
75
70
 
76
71
  No initialization code is required. The avatar enters its default idle behavior automatically.
77
72
 
73
+ ### Lite build
74
+
75
+ For pages that never use skins, `agent-robot-avatar/lite` is the same avatar without any skin code:
76
+
77
+ ```js
78
+ import 'agent-robot-avatar/lite';
79
+ ```
80
+
81
+ In the lite build the `skin` attribute is ignored, and the console notes that skins need the full build.
82
+
78
83
  ## Basic usage
79
84
 
80
85
  ```js
@@ -89,15 +94,30 @@ avatar.reset();
89
94
 
90
95
  Available actions:
91
96
 
92
- `idle` · `bored` · `waiting-spin` · `waiting-wrap` · `waiting-wave` · `input` · `affirm` · `success` · `failure` · `wary` · `inspect` · `angry` · `refuse` · `surprise` · `love` · `random` · `sleep` · `wake`
93
-
94
- `waiting-wave` plays three 1.1-second waves: the eyes bob vertically with a staggered rhythm and amplitude of 10 SVG units, then settle. Use `startWaiting({ variant: 'wave' })` for seamless continuous waiting, and `stopWaiting()` to stop. It honors the component's reduced-motion setting and reports the canonical `waiting` action lifecycle.
95
-
96
- Action names describe what the avatar does. `refuse` shakes the head, for a request the Agent cannot or will not carry out; `failure` is for a task or connection that did not succeed; `wary` turns the head and gives a guarded up-and-down look, for an action that needs confirmation. Names from 0.5 (`send`, `warning`, `error`, `blocked` and others) still work with a console notice until 1.0; see the [upgrade table](CHANGELOG.md#upgrading-from-05x).
97
-
98
- `love` expresses affection or thanks: the two eyes lean together into a single heart, beat three times, then return.
99
-
100
- `random` plays a slot-machine spin: each eye rolls over a drum, then the left eye slows, bounces and settles, followed by the right. `slot` is an alias.
97
+ | Action | Looks like | Use when |
98
+ | --- | --- | --- |
99
+ | `idle` | Blinks and glances around | Default state |
100
+ | `bored` | Glances slowly from side to side | Nothing has happened for a while |
101
+ | `waiting-spin` | Eyes circle from side to side | Waiting for a reply (default) |
102
+ | `waiting-wrap` | Eyes slide out one side and back in the other | Waiting for a reply |
103
+ | `waiting-wave` | Eyes bob up and down in turn | Waiting for a reply |
104
+ | `input` | Eyes become blinking text cursors | The user is typing |
105
+ | `affirm` | Nods | Got it, confirmed |
106
+ | `success` | Happy, smiling eyes | Task completed |
107
+ | `failure` | Head and eyes droop | Task or connection failed |
108
+ | `wary` | Turns its head and gives a guarded up-and-down look | An action needs confirmation |
109
+ | `inspect` | Narrows its eyes and scans up and down | Checking or reviewing something |
110
+ | `angry` | Lowered, glaring eyes | Content blocked |
111
+ | `refuse` | Shakes its head | Cannot or will not do the request |
112
+ | `surprise` | Wide-open eyes | Something unexpected |
113
+ | `love` | Eyes merge into a beating heart | Thanks, affection |
114
+ | `random` | Eyes spin like slot-machine reels | Random pick, playful moment |
115
+ | `sleep` | Closes its eyes and sleeps | No activity for a long time |
116
+ | `wake` | Opens its eyes and wakes up | Activity resumes |
117
+
118
+ Waiting actions play one round with `play()`. `startWaiting({ variant: 'spin' | 'wrap' | 'wave' })` loops until another action starts or `stopWaiting()` is called.
119
+
120
+ Names from 0.5 (`send`, `warning`, `error`, `blocked`, `slot` and others) still work with a console notice until 1.0; see the [upgrade table](CHANGELOG.md#upgrading-from-05x).
101
121
 
102
122
  For a real Agent request lifecycle:
103
123
 
@@ -139,6 +159,7 @@ try {
139
159
  | `motion` | Motion policy: `auto`, `reduce`, or `full` |
140
160
  | `press-squeeze` | Center press-and-hold squeeze; set to `false` to disable |
141
161
  | `antenna-drag` | Antenna drag, spring return, and angry reaction; set to `false` to disable |
162
+ | `skin` | Skin number, such as `301`; leave it out for the flat color |
142
163
 
143
164
  Common runtime controls:
144
165
 
@@ -151,6 +172,47 @@ avatar.setAntennaDrag(true);
151
172
 
152
173
  The avatar center begins the squeeze interaction. Moving more than 4 CSS pixels transfers the same pointer gesture to the existing head drag. Dragging the antenna uses the same pull limit as head dragging and ends with the existing angry reaction.
153
174
 
175
+ ## Skins · New in 0.7
176
+
177
+ 188 skins in 17 series, with light that follows the pointer. Every skin has a permanent number: the series number followed by two digits, so 305 is the fifth skin of series 3. Titles may be reworded later; numbers never change and are never reused.
178
+
179
+ <img src="./assets/readme/skin-gallery.jpg" alt="Skin Gallery: featured skins, filters and search" width="100%">
180
+
181
+ ```html
182
+ <agent-robot-avatar skin="301"></agent-robot-avatar>
183
+ ```
184
+
185
+ ```js
186
+ avatar.setSkin(102); // or '102'
187
+ avatar.setSkin(null); // back to the flat color
188
+ ```
189
+
190
+ See every skin with its number on the [all skins page](https://cx-artlab.github.io/agent-robot-avatar/materials/).
191
+
192
+ | Series | Numbers |
193
+ | --- | --- |
194
+ | Glaze | 101–106 |
195
+ | Ice glass | 201–206 |
196
+ | Gummy | 301–312 |
197
+ | Candy glass | 401–406 |
198
+ | Acrylic | 501–512 |
199
+ | Jelly | 601–612 |
200
+ | Gradient | 701–712 |
201
+ | Frosted glass | 801–806 |
202
+ | Metal | 901–930 |
203
+ | Gems and ceramics | 1001–1012 |
204
+ | Wood | 1101–1106 |
205
+ | Balls | 1201–1210 |
206
+ | Clear and shimmer | 1301–1305 |
207
+ | Fabric and leather | 1401–1412 |
208
+ | Sweets and fruit | 1501–1512 |
209
+ | Nature | 1601–1612 |
210
+ | Patterns and tech | 1701–1717 |
211
+
212
+ Skin painters download the first time a skin is used, so a page without skins never loads them. `await loadSkins()` loads them in advance. `SKINS` holds each skin’s Chinese and English titles and colors, `SKIN_SERIES` the series, and `SKIN_NAMES` every number.
213
+
214
+ Every expression and gesture works with a skin. When the skin is removed, the `color` attribute applies again.
215
+
154
216
  ## Events and integration
155
217
 
156
218
  The component emits `face-state` for visual state changes and `action-state` for semantic action lifecycle changes.
@@ -165,6 +227,10 @@ Designed for modern browsers with support for ES Modules, Custom Elements, SVG,
165
227
 
166
228
  Automated browser tests cover Chromium, Firefox, and WebKit.
167
229
 
230
+ ## Hardware version (in development)
231
+
232
+ A C++ version that shows the avatar's expressions on robot and dev-board screens (it draws only the eyes) is in development and not released yet. See the [hardware guide](https://github.com/CX-ArtLab/agent-robot-avatar/tree/main/embedded).
233
+
168
234
  ## Contributing
169
235
 
170
236
  Issues and pull requests are welcome. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) before submitting changes.
@@ -195,4 +261,3 @@ If this project is useful to you, you can buy me a coffee.
195
261
 
196
262
  <a href='https://ko-fi.com/P0E625WIOI' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi6.png?v=6' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
197
263
 
198
-
@@ -0,0 +1,44 @@
1
+ // The smallest build: every expression and gesture, without skins. The skin
2
+ // attribute and setSkin() only explain where skins live, so a page written for
3
+ // the full build keeps working with the plain avatar.
4
+ import AgentRobotAvatar from './src/agent-robot-avatar-extension-host.js';
5
+ import './src/agent-robot-avatar-actions.js';
6
+ import './src/agent-robot-avatar-antenna.js';
7
+ import './src/agent-robot-avatar-waiting.js';
8
+ import './src/agent-robot-avatar-inspect.js';
9
+ import './src/agent-robot-avatar-failure.js';
10
+ import './src/agent-robot-avatar-love.js';
11
+ import './src/agent-robot-avatar-random.js';
12
+ import './src/agent-robot-avatar-head-roundness.js';
13
+ import './src/agent-robot-avatar-gestures.js';
14
+ import './src/agent-robot-avatar-runtime.js';
15
+ import { VERSION } from './src/agent-robot-avatar-version.js';
16
+
17
+ const proto = AgentRobotAvatar.prototype;
18
+ let warned = false;
19
+ function explain() {
20
+ if (warned) return;
21
+ warned = true;
22
+ console.warn('agent-robot-avatar: skins are not part of the lite build; import "agent-robot-avatar" instead of "agent-robot-avatar/lite" to use them.');
23
+ }
24
+ if (!proto.setSkin) {
25
+ proto._applySkinAttribute = function() { if (this.getAttribute('skin')) explain(); };
26
+ proto.setSkin = function(value = null) {
27
+ if (value !== null && value !== 'none') explain();
28
+ return this;
29
+ };
30
+ proto.getSkin = () => null;
31
+ }
32
+
33
+ // Elements already present in markup can upgrade while the core module is
34
+ // loading, before runtime extensions finish installing their prototype hooks.
35
+ if (typeof document !== 'undefined') {
36
+ document.querySelectorAll('agent-robot-avatar').forEach(avatar => avatar._resumeFrames?.());
37
+ }
38
+
39
+ if (typeof window !== 'undefined') {
40
+ window.AgentRobotAvatarVersion = VERSION;
41
+ }
42
+
43
+ export { AgentRobotAvatar, VERSION };
44
+ export default AgentRobotAvatar;
@@ -8,6 +8,7 @@ import './src/agent-robot-avatar-love.js';
8
8
  import './src/agent-robot-avatar-random.js';
9
9
  import './src/agent-robot-avatar-head-roundness.js';
10
10
  import './src/agent-robot-avatar-gestures.js';
11
+ import { SKIN_NAMES, SKINS, SKIN_SERIES, loadSkins } from './src/agent-robot-avatar-skins.js';
11
12
  import './src/agent-robot-avatar-runtime.js';
12
13
  import { VERSION } from './src/agent-robot-avatar-version.js';
13
14
 
@@ -23,5 +24,5 @@ if (typeof window !== 'undefined') {
23
24
  window.AgentRobotAvatarVersion = VERSION;
24
25
  }
25
26
 
26
- export { AgentRobotAvatar, VERSION };
27
+ export { AgentRobotAvatar, SKIN_NAMES, SKINS, SKIN_SERIES, VERSION, loadSkins };
27
28
  export default AgentRobotAvatar;
package/index.d.ts CHANGED
@@ -39,6 +39,63 @@ export type AgentRobotAvatarDeprecatedAction =
39
39
  | 'connection-error'
40
40
  | 'slot';
41
41
 
42
+ /**
43
+ * Skin numbers for the `skin` attribute and `setSkin()`: the series number
44
+ * followed by two digits, so `305` is the fifth skin of series 3. A number is
45
+ * permanent; titles may be reworded, numbers never change and are never reused.
46
+ */
47
+ export type AgentRobotAvatarSkin =
48
+ | '101' | '102' | '103' | '104' | '105' | '106' | '201' | '202' | '203' | '204' | '205' | '206'
49
+ | '301' | '302' | '303' | '304' | '305' | '306' | '307' | '308' | '309' | '310' | '311' | '312'
50
+ | '401' | '402' | '403' | '404' | '405' | '406' | '501' | '502' | '503' | '504' | '505' | '506'
51
+ | '507' | '508' | '509' | '510' | '511' | '512' | '601' | '602' | '603' | '604' | '605' | '606'
52
+ | '607' | '608' | '609' | '610' | '611' | '612' | '701' | '702' | '703' | '704' | '705' | '706'
53
+ | '707' | '708' | '709' | '710' | '711' | '712' | '801' | '802' | '803' | '804' | '805' | '806'
54
+ | '901' | '902' | '903' | '904' | '905' | '906' | '907' | '908' | '909' | '910' | '911' | '912'
55
+ | '913' | '914' | '915' | '916' | '917' | '918' | '919' | '920' | '921' | '922' | '923' | '924'
56
+ | '925' | '926' | '927' | '928' | '929' | '930' | '1001' | '1002' | '1003' | '1004' | '1005' | '1006'
57
+ | '1007' | '1008' | '1009' | '1010' | '1011' | '1012' | '1101' | '1102' | '1103' | '1104' | '1105' | '1106'
58
+ | '1201' | '1202' | '1203' | '1204' | '1205' | '1206' | '1207' | '1208' | '1209' | '1210' | '1301' | '1302'
59
+ | '1303' | '1304' | '1305' | '1401' | '1402' | '1403' | '1404' | '1405' | '1406' | '1407' | '1408' | '1409'
60
+ | '1410' | '1411' | '1412' | '1501' | '1502' | '1503' | '1504' | '1505' | '1506' | '1507' | '1508' | '1509'
61
+ | '1510' | '1511' | '1512' | '1601' | '1602' | '1603' | '1604' | '1605' | '1606' | '1607' | '1608' | '1609'
62
+ | '1610' | '1611' | '1612' | '1701' | '1702' | '1703' | '1704' | '1705' | '1706' | '1707' | '1708' | '1709'
63
+ | '1710' | '1711' | '1712' | '1713' | '1714' | '1715' | '1716' | '1717';
64
+
65
+ /** Display information for one skin. */
66
+ export interface AgentRobotAvatarSkinInfo {
67
+ /** The series id, such as `'glaze'` or `'metal'`. */
68
+ readonly series: string;
69
+ /** Chinese title. */
70
+ readonly title: string;
71
+ /** English title. */
72
+ readonly titleEn: string;
73
+ /** Representative colors, light to dark. */
74
+ readonly base: readonly string[];
75
+ }
76
+
77
+ /** A group of related skins, in gallery order. */
78
+ export interface AgentRobotAvatarSkinSeries {
79
+ readonly id: string;
80
+ /** The first digits of every skin number in the series. */
81
+ readonly number: number;
82
+ readonly title: string;
83
+ readonly titleEn: string;
84
+ readonly skins: readonly AgentRobotAvatarSkin[];
85
+ }
86
+
87
+ /** Every skin number, in gallery order. */
88
+ export declare const SKIN_NAMES: readonly AgentRobotAvatarSkin[];
89
+ /** Titles and colors for every skin, by number. */
90
+ export declare const SKINS: Readonly<Record<AgentRobotAvatarSkin, AgentRobotAvatarSkinInfo>>;
91
+ /** The skin series, in gallery order. */
92
+ export declare const SKIN_SERIES: readonly AgentRobotAvatarSkinSeries[];
93
+ /**
94
+ * Skin painters download the first time a skin is used, so the first skin shows
95
+ * a moment after it is set. Await this to load them in advance.
96
+ */
97
+ export declare function loadSkins(): Promise<void>;
98
+
42
99
  export type AgentRobotAvatarState =
43
100
  | 'idle'
44
101
  | 'happy'
@@ -114,6 +171,13 @@ export declare class AgentRobotAvatar extends HTMLElement {
114
171
  setAntennaDrag(enabled?: boolean): this;
115
172
  setHeadRoundness(value?: number): this;
116
173
  getHeadRoundness(): number;
174
+ /**
175
+ * Dresses the head in a numbered skin with light that follows the pointer.
176
+ * Same as the `skin` attribute; `null` or `'none'` returns to the flat color.
177
+ * Throws for an unknown number.
178
+ */
179
+ setSkin(skin?: AgentRobotAvatarSkin | number | 'none' | null): this;
180
+ getSkin(): AgentRobotAvatarSkin | null;
117
181
  noteActivity(wake?: boolean): void;
118
182
 
119
183
  addEventListener<K extends keyof AgentRobotAvatarEventMap>(
package/lite.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ // The lite build: the same avatar without skins. setSkin() keeps its type so a
2
+ // page can switch builds, but it only logs that skins need the full build.
3
+ export { AgentRobotAvatar, VERSION } from './index.js';
4
+ export { default } from './index.js';
5
+ export type * from './index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-robot-avatar",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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",
@@ -14,6 +14,11 @@
14
14
  "types": "./index.d.ts",
15
15
  "import": "./agent-robot-avatar.js",
16
16
  "default": "./agent-robot-avatar.js"
17
+ },
18
+ "./lite": {
19
+ "types": "./lite.d.ts",
20
+ "import": "./agent-robot-avatar-lite.js",
21
+ "default": "./agent-robot-avatar-lite.js"
17
22
  }
18
23
  },
19
24
  "sideEffects": true,
@@ -21,21 +26,22 @@
21
26
  "dev": "node scripts/serve.mjs",
22
27
  "build:pages": "node scripts/build-pages.mjs",
23
28
  "check": "node scripts/check-source.mjs",
24
- "test": "npm run check && npm run test:types && npm run test:release && npm run test:package && npm run test:waiting && npm run test:browser && npm run build:pages",
29
+ "test": "npm run check && npm run test:types && npm run test:release && npm run test:package && npm run test:waiting && npm run test:browser && npm run test:pages",
25
30
  "release:check": "npm test && npm publish --dry-run --ignore-scripts --tag latest",
26
31
  "prepublishOnly": "npm test && node scripts/check-publish-tag.mjs",
27
32
  "test:types": "tsc -p tsconfig.types.json",
28
33
  "test:release": "node --test tests/release.test.mjs",
29
34
  "test:package": "node --test tests/package.test.mjs",
30
35
  "test:waiting": "node --test tests/waiting-wave.test.mjs",
31
- "test:browser": "playwright test"
36
+ "test:browser": "playwright test",
37
+ "test:pages": "node --test tests/pages-public.test.mjs"
32
38
  },
33
39
  "files": [
34
40
  "agent-robot-avatar.js",
41
+ "agent-robot-avatar-lite.js",
35
42
  "index.d.ts",
43
+ "lite.d.ts",
36
44
  "src/",
37
- "assets/support/",
38
- "docs/",
39
45
  "README.md",
40
46
  "CHANGELOG.md",
41
47
  "LICENSE"
@@ -76,4 +82,3 @@
76
82
  "typescript": "7.0.2"
77
83
  }
78
84
  }
79
-
@@ -374,8 +374,11 @@ const AgentRobotAvatar = (() => {
374
374
  const near = Math.max(r.width, r.height) * 2.55;
375
375
  const dist = Math.hypot(dx, dy);
376
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);
377
+ // Measure the pointer offset as if the avatar were demo-sized, so a large
378
+ // avatar does not look further than a small one for the same gesture.
379
+ const scale = Math.min(1, 120 / Math.max(1, r.width, r.height));
380
+ this._pointer.x = clamp(dx * .42 * scale, -48, 48);
381
+ this._pointer.y = clamp(dy * .38 * scale, -34, 34);
379
382
  this._pointer.active = influence > 0.001;
380
383
  this._pointer.influence = influence;
381
384
  this._pointer.lastMove = performance.now();
@@ -444,15 +444,18 @@ function ensureRuntime(instance) {
444
444
  if (record.attributeName === 'size') applySize(instance);
445
445
  if (record.attributeName === 'motion') motionChanged = true;
446
446
  if (record.attributeName === 'auto-sleep') autoSleepChanged = true;
447
+ if (record.attributeName === 'skin') instance._applySkinAttribute?.();
447
448
  }
448
449
  if (motionChanged) handleMotionChange(instance);
449
450
  if (autoSleepChanged) scheduleAutoSleep(instance);
450
451
  });
451
452
  instance._runtimeAttributeObserver.observe(instance, {
452
453
  attributes: true,
453
- attributeFilter: ['size', 'motion', 'wake-on', 'auto-sleep'],
454
+ attributeFilter: ['size', 'motion', 'wake-on', 'auto-sleep', 'skin'],
454
455
  });
455
456
  }
457
+ // A skin set with setSkin() survives a reconnect; markup only applies when present.
458
+ if (instance.hasAttribute('skin')) instance._applySkinAttribute?.();
456
459
 
457
460
  if (typeof IntersectionObserver !== 'undefined') {
458
461
  instance._runtimeIntersectionObserver = new IntersectionObserver(entries => {
@@ -0,0 +1,234 @@
1
+ // Pattern tiles for skins. Each generator returns the tile size, an optional
2
+ // pattern transform and the SVG markup of one tile, in the head's 240-unit
3
+ // coordinates. Scattered patterns use a seeded random source so every avatar
4
+ // shows the same tile.
5
+
6
+ const rng = seed => {
7
+ let state = (seed % 2147483647) || 1;
8
+ return () => (state = state * 16807 % 2147483647) / 2147483647;
9
+ };
10
+ const n = value => Number(value.toFixed(2));
11
+ const pick = (random, list) => list[Math.floor(random() * list.length) % list.length];
12
+
13
+ // Repeats an element at the tile edges so scattered shapes wrap seamlessly.
14
+ function wrap(size, x, y, r, draw) {
15
+ let out = '';
16
+ for (const dx of [-size, 0, size]) for (const dy of [-size, 0, size]) {
17
+ if (x + dx + r < 0 || x + dx - r > size || y + dy + r < 0 || y + dy - r > size) continue;
18
+ out += draw(x + dx, y + dy);
19
+ }
20
+ return out;
21
+ }
22
+
23
+ // Turns a point on the unit sphere, for the patterns drawn on balls.
24
+ function rotate([x, y, z], [yaw, pitch, roll]) {
25
+ let c = Math.cos(yaw), s = Math.sin(yaw);
26
+ [x, z] = [x * c + z * s, -x * s + z * c];
27
+ c = Math.cos(pitch); s = Math.sin(pitch);
28
+ [y, z] = [y * c - z * s, y * s + z * c];
29
+ c = Math.cos(roll); s = Math.sin(roll);
30
+ [x, y] = [x * c - y * s, x * s + y * c];
31
+ return [x, y, z];
32
+ }
33
+
34
+
35
+
36
+
37
+
38
+ const PATTERNS = {
39
+ tartan({ colors: [a, b, c], size = 56 }) {
40
+ const bands = [[0, 18, a, .55], [26, 3, b, .8], [38, 2, c, .8], [44, 6, a, .35]];
41
+ let body = '';
42
+ for (const [x, w, color, o] of bands) {
43
+ body += `<rect x="${x}" width="${w}" height="${size}" fill="${color}" opacity="${o}"/><rect y="${x}" width="${size}" height="${w}" fill="${color}" opacity="${o}"/>`;
44
+ }
45
+ return { size: [size, size], body };
46
+ },
47
+ dots({ colors: [c], r = 5, size = 26, opacity = 1 }) {
48
+ const q = size / 4;
49
+ return { size: [size, size], body: `<circle cx="${q}" cy="${q}" r="${r}" fill="${c}" opacity="${opacity}"/><circle cx="${q * 3}" cy="${q * 3}" r="${r}" fill="${c}" opacity="${opacity}"/>` };
50
+ },
51
+ terrazzo({ colors, count = 44, size = 120, seed = 5 }) {
52
+ const random = rng(seed);
53
+ let body = '';
54
+ for (let i = 0; i < count; i++) {
55
+ const x = random() * size, y = random() * size, k = 2.5 + random() * 6, sides = 3 + Math.floor(random() * 3), color = pick(random, colors), turn = random() * Math.PI;
56
+ let d = '';
57
+ for (let j = 0; j < sides; j++) {
58
+ const a = turn + j / sides * Math.PI * 2, rr = k * (.6 + random() * .5);
59
+ d += `${j ? 'L' : 'M'}${n(Math.cos(a) * rr)} ${n(Math.sin(a) * rr)}`;
60
+ }
61
+ body += wrap(size, x, y, k, (px, py) => `<path d="${d}Z" fill="${color}" transform="translate(${n(px)} ${n(py)})"/>`);
62
+ }
63
+ return { size: [size, size], body };
64
+ },
65
+ marble({ colors: [a, b], seed = 21 }) {
66
+ const random = rng(seed);
67
+ let body = '';
68
+ for (let i = 0; i < 7; i++) {
69
+ const color = i % 3 === 2 ? b : a;
70
+ let x = -20, y = random() * 240, d = `M${n(x)} ${n(y)}`;
71
+ while (x < 260) {
72
+ const nx = x + 30 + random() * 40, ny = y + (random() - .5) * 70;
73
+ d += `Q${n(x + 15 + random() * 20)} ${n(y + (random() - .5) * 60)} ${n(nx)} ${n(ny)}`;
74
+ x = nx; y = ny;
75
+ }
76
+ body += `<path d="${d}" fill="none" stroke="${color}" stroke-width="${n(.6 + random() * 1.8)}" opacity="${n(.35 + random() * .35)}"/>`;
77
+ body += `<path d="${d}" fill="none" stroke="${color}" stroke-width="${n(5 + random() * 6)}" opacity=".07"/>`;
78
+ }
79
+ return { size: [240, 240], body };
80
+ },
81
+ pixel({ colors, cell = 10, size = 80, seed = 31, opacity = .9 }) {
82
+ const random = rng(seed);
83
+ let body = '';
84
+ for (let y = 0; y < size; y += cell) for (let x = 0; x < size; x += cell) {
85
+ if (random() < .45) continue;
86
+ body += `<rect x="${x}" y="${y}" width="${cell}" height="${cell}" fill="${pick(random, colors)}" opacity="${opacity}"/>`;
87
+ }
88
+ return { size: [size, size], body };
89
+ },
90
+ confetti({ colors, count = 38, size = 120, seed = 37 }) {
91
+ const random = rng(seed);
92
+ let body = '';
93
+ for (let i = 0; i < count; i++) {
94
+ const x = random() * size, y = random() * size, color = pick(random, colors), turn = random() * 180, strip = random() < .5;
95
+ body += wrap(size, x, y, 6, (px, py) => strip
96
+ ? `<rect x="${n(px - 1.6)}" y="${n(py - 4)}" width="3.2" height="8" rx="1" fill="${color}" transform="rotate(${n(turn)} ${n(px)} ${n(py)})"/>`
97
+ : `<circle cx="${n(px)}" cy="${n(py)}" r="2.2" fill="${color}"/>`);
98
+ }
99
+ return { size: [size, size], body };
100
+ },
101
+ halftone({ colors: [c], step = 12, opacity = .7 }) {
102
+ let body = '';
103
+ for (let y = 6; y < 240; y += step) for (let x = 6 + ((y / step) % 2) * step / 2; x < 240; x += step) {
104
+ body += `<circle cx="${n(x)}" cy="${n(y)}" r="${n(.6 + 4.4 * (y / 240))}" fill="${c}" opacity="${opacity}"/>`;
105
+ }
106
+ return { size: [240, 240], body };
107
+ },
108
+ scatter({ colors, shapes, count = 12, size = 120, k = [6, 10], seed = 47 }) {
109
+ const random = rng(seed);
110
+ let body = '';
111
+ for (let i = 0; i < count; i++) {
112
+ const x = random() * size, y = random() * size, r = k[0] + random() * (k[1] - k[0]), turn = (random() - .5) * 70;
113
+ const shape = pick(random, shapes), color = colors[i % colors.length], accent = colors[(i + 1) % colors.length];
114
+ const art = SHAPES[shape](r, color, accent);
115
+ body += wrap(size, x, y, r * 1.3, (px, py) => `<g transform="translate(${n(px)} ${n(py)}) rotate(${n(turn)})">${art}</g>`);
116
+ }
117
+ return { size: [size, size], body };
118
+ },
119
+ grooves({ colors: [line, shadow], radius = 138, angles = [.5, -.45, .2] }) {
120
+ // A modern ball: rings of fine grooves wrapping the surface in three directions.
121
+ let body = '';
122
+ for (const axis of [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) {
123
+ for (const lat of [-.42, 0, .42]) {
124
+ const pts = [];
125
+ for (let k = 0; k <= 72; k++) {
126
+ const a = k / 72 * Math.PI * 2, r = Math.cos(lat), h = Math.sin(lat);
127
+ const p = axis[0] ? [h, r * Math.cos(a), r * Math.sin(a)] : axis[1] ? [r * Math.cos(a), h, r * Math.sin(a)] : [r * Math.cos(a), r * Math.sin(a), h];
128
+ pts.push(rotate(p, angles));
129
+ }
130
+ let d = '';
131
+ pts.forEach(([x, y, z], i) => { d += `${i && z > 0 && pts[i - 1][2] > 0 ? 'L' : 'M'}${n(120 + x * radius)} ${n(120 + y * radius)}`; });
132
+ body += `<path d="${d}" fill="none" stroke="${shadow}" stroke-width="3" opacity=".6" transform="translate(.8 1)"/><path d="${d}" fill="none" stroke="${line}" stroke-width="1.6"/>`;
133
+ }
134
+ }
135
+ return { size: [240, 240], body };
136
+ },
137
+ // Balls: each is drawn once across the whole head instead of repeating.
138
+ basketball({ colors: [seam, grain] }) {
139
+ return {
140
+ size: [240, 240],
141
+ defs: tex => `<pattern id="${tex}" patternUnits="userSpaceOnUse" width="6" height="6"><circle cx="3" cy="3" r=".9" fill="${grain}" opacity=".32"/></pattern>`,
142
+ body: tex => `<rect width="240" height="240" fill="url(#${tex})"/><path d="M120 0V240M0 120H240M52 18Q100 120 52 222M188 18Q140 120 188 222" fill="none" stroke="#ffd2a6" stroke-width="2.4" stroke-linecap="round" opacity=".55" transform="translate(1.2 1.6)"/><path d="M120 0V240M0 120H240M52 18Q100 120 52 222M188 18Q140 120 188 222" fill="none" stroke="${seam}" stroke-width="3.4" stroke-linecap="round"/>`,
143
+ };
144
+ },
145
+
146
+
147
+
148
+
149
+
150
+ tennis({ colors: [seam, fuzz] }) {
151
+ return {
152
+ size: [240, 240],
153
+ defs: tex => `<pattern id="${tex}" patternUnits="userSpaceOnUse" width="7" height="7"><circle cx="1.5" cy="2" r=".7" fill="${fuzz}" opacity=".35"/><circle cx="5" cy="5.5" r=".6" fill="#ffffff" opacity=".3"/></pattern>`,
154
+ body: tex => `<rect width="240" height="240" fill="url(#${tex})"/><path d="M14 62C92 34 92 206 14 178M226 62C148 34 148 206 226 178" fill="none" stroke="#f7ffd0" stroke-width="6.5" stroke-linecap="round" opacity=".45" transform="translate(1 1.6)"/><path d="M14 62C92 34 92 206 14 178M226 62C148 34 148 206 226 178" fill="none" stroke="#4a6000" stroke-width="6.5" stroke-linecap="round" opacity=".38" transform="translate(-1 -1.6)"/><path d="M14 62C92 34 92 206 14 178M226 62C148 34 148 206 226 178" fill="none" stroke="${seam}" stroke-width="5" stroke-linecap="round"/>`,
155
+ };
156
+ },
157
+ baseball({ colors: [seam, stitch] }) {
158
+ const arc = (x0, cx) => {
159
+ let stitches = '';
160
+ for (let t = .06; t < .96; t += .055) {
161
+ const x = (1 - t) ** 2 * x0 + 2 * (1 - t) * t * cx + t * t * x0, y = (1 - t) ** 2 * 26 + 2 * (1 - t) * t * 120 + t * t * 214;
162
+ const dx = 2 * (1 - t) * (cx - x0) + 2 * t * (x0 - cx), dy = 2 * (1 - t) * 94 + 2 * t * 94, len = Math.hypot(dx, dy);
163
+ const nx = -dy / len * 5, ny = dx / len * 5, tx = dx / len * 2.2, ty = dy / len * 2.2;
164
+ stitches += `M${n(x - nx - tx)} ${n(y - ny - ty)}L${n(x)} ${n(y)}L${n(x + nx - tx)} ${n(y + ny - ty)}`;
165
+ }
166
+ return `<path d="M${x0} 26Q${cx} 120 ${x0} 214" fill="none" stroke="${seam}" stroke-width="1.3"/><path d="${stitches}" fill="none" stroke="${stitch}" stroke-width="2" stroke-linecap="round"/>`;
167
+ };
168
+ return { size: [240, 240], body: arc(44, 104) + arc(196, 136) };
169
+ },
170
+ cricket({ colors: [seam, stitch] }) {
171
+ // A raised seam arching over the forehead with three rows of stitches each side.
172
+ const y = (x, lift) => 84 - Math.sin(x / 240 * Math.PI) * 26 + lift;
173
+ const curve = lift => {
174
+ let d = '';
175
+ for (let x = -4; x <= 244; x += 8) d += `${x < 0 ? 'M' : 'L'}${x} ${n(y(x, lift))}`;
176
+ return d;
177
+ };
178
+ let rows = '';
179
+ for (const lift of [-11, -7, -3, 3, 7, 11]) {
180
+ for (let x = 2; x < 240; x += 6) rows += `M${x} ${n(y(x, lift))}l2.6 -1.2`;
181
+ }
182
+ return { size: [240, 240], body: `<path d="${curve(0)}" fill="none" stroke="${seam}" stroke-width="15" stroke-linecap="round" opacity=".55"/><path d="${curve(0)}" fill="none" stroke="${seam}" stroke-width="2"/><path d="${rows}" fill="none" stroke="${stitch}" stroke-width="1.5" stroke-linecap="round"/>` };
183
+ },
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+ golf({ colors: [shade, shine], r = 5.6 }) {
194
+ const w = 15, h = w * Math.sqrt(3);
195
+ const dimple = (x, y) => `<circle cx="${n(x)}" cy="${n(y)}" r="${r}" fill="${shade}" opacity=".55"/><circle cx="${n(x + .9)}" cy="${n(y + 1)}" r="${n(r * .55)}" fill="${shine}" opacity=".8"/>`;
196
+ return { size: [w, n(h)], body: dimple(0, 0) + dimple(w, 0) + dimple(w / 2, h / 2) + dimple(0, h) + dimple(w, h) };
197
+ },
198
+ football({ colors: [lace, grain, seam] }) {
199
+ // Lying on its side: laces run along the top, stripes ring both ends.
200
+ let stitches = '';
201
+ for (let x = 90; x <= 150; x += 10) stitches += `M${x} 44V60`;
202
+ return {
203
+ size: [240, 240],
204
+ defs: tex => `<pattern id="${tex}" patternUnits="userSpaceOnUse" width="5" height="5"><circle cx="2.5" cy="2.5" r=".8" fill="${grain}" opacity=".35"/></pattern>`,
205
+ body: tex => `<rect width="240" height="240" fill="url(#${tex})"/>`
206
+ + `<path d="M-10 40Q120 62 250 40M-10 210Q120 188 250 210" fill="none" stroke="${seam}" stroke-width="1.4" opacity=".7"/>`
207
+ + `<g transform="translate(1 1.6)" opacity=".35"><path d="M84 52H156" stroke="#1a0800" stroke-width="2"/><path d="${stitches}" stroke="#1a0800" stroke-width="3.6" stroke-linecap="round"/></g>`
208
+ + `<path d="M84 52H156" stroke="${lace}" stroke-width="2"/><path d="${stitches}" stroke="${lace}" stroke-width="3.4" stroke-linecap="round"/>`
209
+ + `<path d="M34 20Q46 120 34 220M46 20Q58 120 46 220M206 20Q194 120 206 220M194 20Q182 120 194 220" fill="none" stroke="${lace}" stroke-width="5"/>`,
210
+ };
211
+ },
212
+ };
213
+
214
+ const SHAPES = {
215
+ leaf: (k, c, accent) => `<path d="M0 ${n(-k)}Q${n(.65 * k)} 0 0 ${n(k)}Q${n(-.65 * k)} 0 0 ${n(-k)}Z" fill="${c}"/><path d="M0 ${n(-.8 * k)}V${n(.9 * k)}" stroke="${accent}" stroke-width="${n(.08 * k)}"/>`,
216
+ blossom: (k, c, accent) => `${[0, 72, 144, 216, 288].map(a => `<ellipse cy="${n(-.5 * k)}" rx="${n(.3 * k)}" ry="${n(.45 * k)}" fill="${c}" transform="rotate(${a})"/>`).join('')}<circle r="${n(.2 * k)}" fill="${accent}"/>`,
217
+ };
218
+
219
+ // A generator may also return defs (for example a fine texture tile) that its
220
+ // body refers to; both receive the texture id.
221
+ // Generated tiles are cached per spec; ball layouts take a moment to work out.
222
+ const tiles = new Map();
223
+ function patternMarkup(id, spec) {
224
+ const make = PATTERNS[spec.kind];
225
+ if (!make) return '';
226
+ const key = JSON.stringify(spec);
227
+ if (!tiles.has(key)) tiles.set(key, make(spec));
228
+ const { size: [w, h], transform, body, defs } = tiles.get(key);
229
+ const tex = `${id}-texture`;
230
+ const inner = typeof body === 'function' ? body(tex) : body;
231
+ return `${defs ? defs(tex) : ''}<pattern id="${id}" patternUnits="userSpaceOnUse" width="${w}" height="${h}"${transform ? ` patternTransform="${transform}"` : ''}>${inner}</pattern>`;
232
+ }
233
+
234
+ export { PATTERNS, patternMarkup };