agent-robot-avatar 0.5.2 → 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 (43) hide show
  1. package/CHANGELOG.md +56 -7
  2. package/README.md +93 -16
  3. package/agent-robot-avatar-lite.js +44 -0
  4. package/agent-robot-avatar.js +2 -2
  5. package/index.d.ts +94 -17
  6. package/lite.d.ts +5 -0
  7. package/package.json +12 -5
  8. package/src/agent-robot-avatar-actions.js +17 -14
  9. package/src/agent-robot-avatar-core.js +5 -2
  10. package/src/agent-robot-avatar-gestures.js +9 -0
  11. package/src/agent-robot-avatar-runtime.js +58 -12
  12. package/src/agent-robot-avatar-skin-patterns.js +234 -0
  13. package/src/agent-robot-avatar-skins.js +256 -0
  14. package/src/agent-robot-avatar-version.js +1 -1
  15. package/src/agent-robot-avatar-waiting.js +30 -6
  16. package/src/skins/catalog.js +218 -0
  17. package/src/skins/collection-materials.js +118 -0
  18. package/src/skins/eyelids.js +32 -0
  19. package/src/skins/glaze-variants.js +29 -0
  20. package/src/skins/layered-texture.js +162 -0
  21. package/src/skins/material-data.js +111 -0
  22. package/src/skins/material-library.js +302 -0
  23. package/src/skins/metal-tint.js +11 -0
  24. package/src/skins/more-materials.js +69 -0
  25. package/src/skins/native.js +754 -0
  26. package/src/skins/paints.js +3 -0
  27. package/src/skins/revised-materials.js +44 -0
  28. package/src/skins/second-pass-materials.js +71 -0
  29. package/src/skins/soft-materials.js +111 -0
  30. package/src/skins/sports-materials.js +38 -0
  31. package/src/skins/wood-materials.js +68 -0
  32. package/assets/support/alipay.png +0 -0
  33. package/assets/support/wechat-pay.png +0 -0
  34. package/docs/README.de.md +0 -186
  35. package/docs/README.es.md +0 -186
  36. package/docs/README.fr.md +0 -186
  37. package/docs/README.ja.md +0 -186
  38. package/docs/README.ko.md +0 -186
  39. package/docs/README.pt.md +0 -186
  40. package/docs/README.ru.md +0 -186
  41. package/docs/README.zh-CN.md +0 -190
  42. package/docs/README.zh-TW.md +0 -186
  43. package/src/agent-robot-avatar-antenna-flash.js +0 -127
@@ -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();
@@ -42,6 +42,15 @@ proto.setPressSqueeze=function(enabled=true){
42
42
  if(!enabled && this._gestureFx?.kind==='squeeze')this._cancelGesture();
43
43
  return this;
44
44
  };
45
+ // Antenna blinking was removed in 0.6. The method stays so existing calls keep working.
46
+ let warnedAntennaFlash=false;
47
+ proto.setAntennaFlash=function(){
48
+ if(!warnedAntennaFlash){
49
+ warnedAntennaFlash=true;
50
+ console.warn('agent-robot-avatar: setAntennaFlash() was removed in 0.6 and does nothing. It will be deleted in 1.0.');
51
+ }
52
+ return this;
53
+ };
45
54
  proto.setAntennaDrag=function(enabled=true){
46
55
  this.setAttribute('antenna-drag',String(Boolean(enabled)));
47
56
  if(!enabled && this._gestureFx?.kind==='antenna-drag')this._cancelGesture();
@@ -24,28 +24,36 @@ const baseStopWaiting = proto.stopWaiting;
24
24
  const baseDraw = proto._draw;
25
25
 
26
26
  const DEFAULT_SIZE = 112;
27
+ // Canonical action names describe what the avatar does on screen. Older names that
28
+ // described a situation (send, warning, blocked, error, ...) still resolve to the same
29
+ // action, but print a one-time deprecation notice; they are removed in 1.0.
27
30
  const ACTION_ALIASES = Object.freeze({
28
31
  idle: 'idle',
29
32
  bored: 'bored',
30
33
  waiting: 'waiting',
34
+ 'waiting-spin': 'waiting',
31
35
  wait: 'waiting',
32
36
  'waiting-wrap': 'waiting',
37
+ 'waiting-wave': 'waiting',
33
38
  input: 'input',
34
- send: 'send',
39
+ affirm: 'affirm',
40
+ send: 'affirm',
35
41
  success: 'success',
36
42
  failure: 'failure',
37
43
  failed: 'failure',
38
44
  fail: 'failure',
39
- warning: 'warning',
45
+ wary: 'wary',
46
+ warning: 'wary',
40
47
  inspect: 'inspect',
41
48
  verify: 'inspect',
42
49
  review: 'inspect',
43
50
  angry: 'angry',
44
- blocked: 'blocked',
45
- 'policy-blocked': 'blocked',
46
- error: 'error',
47
- 'system-error': 'error',
48
- 'connection-error': 'error',
51
+ blocked: 'angry',
52
+ 'policy-blocked': 'angry',
53
+ refuse: 'refuse',
54
+ error: 'refuse',
55
+ 'system-error': 'refuse',
56
+ 'connection-error': 'refuse',
49
57
  surprise: 'surprise',
50
58
  love: 'love',
51
59
  random: 'random',
@@ -54,10 +62,44 @@ const ACTION_ALIASES = Object.freeze({
54
62
  wake: 'wake',
55
63
  });
56
64
 
65
+ const DEPRECATED_ACTIONS = Object.freeze({
66
+ wait: 'waiting-spin',
67
+ send: 'affirm',
68
+ failed: 'failure',
69
+ fail: 'failure',
70
+ warning: 'wary',
71
+ verify: 'inspect',
72
+ review: 'inspect',
73
+ blocked: 'angry',
74
+ 'policy-blocked': 'angry',
75
+ error: 'refuse',
76
+ 'system-error': 'refuse',
77
+ 'connection-error': 'refuse',
78
+ slot: 'random',
79
+ });
80
+
81
+ // The renamed actions still run on the original implementations.
82
+ const ACTION_IMPLEMENTATIONS = Object.freeze({
83
+ 'waiting-spin': 'waiting',
84
+ affirm: 'send',
85
+ wary: 'warning',
86
+ refuse: 'error',
87
+ });
88
+
89
+ const warnedDeprecations = new Set();
90
+
91
+ function warnDeprecatedAction(raw) {
92
+ const replacement = DEPRECATED_ACTIONS[raw];
93
+ if (!replacement || warnedDeprecations.has(raw)) return;
94
+ warnedDeprecations.add(raw);
95
+ console.warn(`agent-robot-avatar: action "${raw}" is deprecated, use "${replacement}". Old names stop working in 1.0.`);
96
+ }
97
+
57
98
  function normalizeAction(name) {
58
99
  const raw = String(name || '').trim().toLowerCase();
59
100
  const canonical = Object.hasOwn(ACTION_ALIASES, raw) ? ACTION_ALIASES[raw] : null;
60
- return { raw, canonical };
101
+ if (canonical) warnDeprecatedAction(raw);
102
+ return { raw, canonical, implementation: ACTION_IMPLEMENTATIONS[raw] ?? raw };
61
103
  }
62
104
 
63
105
  function wakePolicy(instance) {
@@ -402,15 +444,18 @@ function ensureRuntime(instance) {
402
444
  if (record.attributeName === 'size') applySize(instance);
403
445
  if (record.attributeName === 'motion') motionChanged = true;
404
446
  if (record.attributeName === 'auto-sleep') autoSleepChanged = true;
447
+ if (record.attributeName === 'skin') instance._applySkinAttribute?.();
405
448
  }
406
449
  if (motionChanged) handleMotionChange(instance);
407
450
  if (autoSleepChanged) scheduleAutoSleep(instance);
408
451
  });
409
452
  instance._runtimeAttributeObserver.observe(instance, {
410
453
  attributes: true,
411
- attributeFilter: ['size', 'motion', 'wake-on', 'auto-sleep'],
454
+ attributeFilter: ['size', 'motion', 'wake-on', 'auto-sleep', 'skin'],
412
455
  });
413
456
  }
457
+ // A skin set with setSkin() survives a reconnect; markup only applies when present.
458
+ if (instance.hasAttribute('skin')) instance._applySkinAttribute?.();
414
459
 
415
460
  if (typeof IntersectionObserver !== 'undefined') {
416
461
  instance._runtimeIntersectionObserver = new IntersectionObserver(entries => {
@@ -717,17 +762,17 @@ function prepareReplacement(instance, canonical) {
717
762
 
718
763
  proto.play = function(name) {
719
764
  ensureRuntime(this);
720
- const { raw, canonical } = normalizeAction(name);
765
+ const { canonical, implementation } = normalizeAction(name);
721
766
  if (!canonical) throw new Error(`Unknown Agent Robot Avatar action: ${name}`);
722
767
  if (playNoOp(this, canonical)) return this;
723
- if (canonical === 'idle') return basePlay.call(this, raw);
768
+ if (canonical === 'idle') return basePlay.call(this, implementation);
724
769
 
725
770
  prepareReplacement(this, canonical);
726
771
 
727
772
  let result;
728
773
  this._runtimeInPlay = true;
729
774
  try {
730
- result = basePlay.call(this, raw);
775
+ result = basePlay.call(this, implementation);
731
776
  } finally {
732
777
  this._runtimeInPlay = false;
733
778
  }
@@ -903,3 +948,4 @@ export {
903
948
  DEFAULT_SIZE,
904
949
  };
905
950
  export default AgentRobotAvatar;
951
+
@@ -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 };
@@ -0,0 +1,256 @@
1
+ import AgentRobotAvatar, { registerAvatarExtension } from './agent-robot-avatar-extension-host.js';
2
+ import { SKIN_CATALOG, SKIN_SERIES } from './skins/catalog.js';
3
+
4
+ // The painters are large, so they load the first time a skin needs them. A page
5
+ // that never uses a skin never downloads them.
6
+ const painters = {};
7
+ const loading = {};
8
+ function loadPainter(kind) {
9
+ loading[kind] ??= Promise.all([
10
+ import('./skins/paints.js'),
11
+ kind === 'native' ? import('./skins/native.js') : import('./skins/material-library.js'),
12
+ import('./skins/eyelids.js'),
13
+ ]).then(([paints, painter, eyelids]) => {
14
+ painters.paints = paints.SKIN_PAINTS;
15
+ painters.maskEyelids = eyelids.maskEyelids;
16
+ painters[kind] = painter;
17
+ });
18
+ return loading[kind];
19
+ }
20
+
21
+ /** Loads every skin painter now, so later skins show on the same frame they are set. */
22
+ function loadSkins() {
23
+ return Promise.all([loadPainter('native'), loadPainter('material')]).then(() => undefined);
24
+ }
25
+
26
+ const proto = AgentRobotAvatar.prototype;
27
+ const baseApplyColor = proto._applyColor;
28
+
29
+ // Skins are numbered. The number is the permanent public name; the Chinese and
30
+ // English titles are for display and may be reworded later. SKIN_NAMES lists
31
+ // them in display order, series by series.
32
+ const SKIN_NAMES = Object.freeze(SKIN_SERIES.flatMap(series => series.skins));
33
+ const SKINS = Object.freeze(Object.fromEntries(Object.entries(SKIN_CATALOG).map(([id, skin]) => [id,
34
+ Object.freeze({ series: skin.series, title: skin.zh, titleEn: skin.en, base: Object.freeze([...skin.base]) })])));
35
+ const SERIES = Object.freeze(SKIN_SERIES.map(series => Object.freeze({
36
+ id: series.id, number: series.number, title: series.zh, titleEn: series.en, skins: Object.freeze([...series.skins]),
37
+ })));
38
+
39
+ const REST_LIGHT = Object.freeze({ x: -0.5, y: -0.5 });
40
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
41
+
42
+ function normalizeSkin(value) {
43
+ const text = String(value ?? '').trim().toLowerCase();
44
+ if (!text || text === 'none') return null;
45
+ const name = /^\d+$/.test(text) ? String(Number(text)) : text;
46
+ return Object.hasOwn(SKIN_CATALOG, name) ? name : undefined;
47
+ }
48
+
49
+ // Hand adjustments from the skin workshop, applied after a skin is painted.
50
+ // A record names an element by id or selector, optionally a child path, and the
51
+ // attributes to set on it.
52
+ function findPainted(root, record) {
53
+ let node = record.id ? root.getElementById(record.id) : root.querySelector(record.selector);
54
+ for (const index of record.path || []) node = node?.children[index];
55
+ return node;
56
+ }
57
+
58
+ // Some adjustments touch layers every skin shares, so each change is recorded
59
+ // and undone before the next skin is painted.
60
+ function paintAdjustments(instance, name) {
61
+ const records = painters.paints[name]?.paint || [];
62
+ const root = instance.shadowRoot;
63
+ const undo = instance._skinUndo ??= [];
64
+ for (const record of records) {
65
+ const node = findPainted(root, record);
66
+ if (!node) continue;
67
+ for (const [attribute, value] of Object.entries(record.attributes)) {
68
+ undo.push(() => {
69
+ if (old === null) node.removeAttribute(attribute);
70
+ else node.setAttribute(attribute, old);
71
+ });
72
+ const old = node.getAttribute(attribute);
73
+ node.setAttribute(attribute, value);
74
+ }
75
+ }
76
+ for (const [property, value] of Object.entries(painters.paints[name]?.properties || {})) {
77
+ const old = instance.style.getPropertyValue(property);
78
+ undo.push(() => old ? instance.style.setProperty(property, old) : instance.style.removeProperty(property));
79
+ instance.style.setProperty(property, value);
80
+ }
81
+ }
82
+
83
+ function undoAdjustments(instance) {
84
+ const undo = instance._skinUndo;
85
+ while (undo?.length) undo.pop()();
86
+ }
87
+
88
+ // Adjustments to light-driven attributes were made with the light at rest. They
89
+ // become offsets so they move with the light instead of being overwritten.
90
+ function adjustNative(instance, skin, name) {
91
+ const { lightNative } = painters.native;
92
+ const { x, y } = skin.light;
93
+ lightNative(instance, REST_LIGHT.x, REST_LIGHT.y);
94
+ const resting = Object.entries(skin.nodes).map(([key, node]) => {
95
+ const attribute = key.slice(key.lastIndexOf('.') + 1);
96
+ return { key, node, attribute, value: parseFloat(node.getAttribute(attribute)) };
97
+ });
98
+ paintAdjustments(instance, name);
99
+ for (const { key, node, attribute, value } of resting) {
100
+ const adjusted = parseFloat(node.getAttribute(attribute));
101
+ if (Number.isFinite(adjusted) && Number.isFinite(value) && adjusted !== value) skin.offsets[key] = adjusted - value;
102
+ }
103
+ skin.shown = {};
104
+ lightNative(instance, x, y);
105
+ }
106
+
107
+ function adjustMaterial(instance, material, name) {
108
+ const records = painters.paints[name]?.paint || [];
109
+ const root = instance.shadowRoot;
110
+ const key = SKIN_CATALOG[name].material;
111
+ const offsets = [];
112
+ for (const lightKey of material.lightKeys || []) {
113
+ const [suffix, attribute] = lightKey.split(':');
114
+ const record = records.find(r => r.id === `material-${key}-${suffix}` && !r.path?.length);
115
+ const node = record && findPainted(root, record);
116
+ const value = record?.attributes[attribute];
117
+ if (node && value != null) offsets.push({ node, attribute, delta: parseFloat(value) - parseFloat(node.getAttribute(attribute)) });
118
+ }
119
+ paintAdjustments(instance, name);
120
+ const light = material.light.bind(material);
121
+ material.light = (x, y) => {
122
+ light(x, y);
123
+ for (const { node, attribute, delta } of offsets) node.setAttribute(attribute, parseFloat(node.getAttribute(attribute)) + delta);
124
+ };
125
+ }
126
+
127
+ function render(instance) {
128
+ const name = instance._skinName ?? null;
129
+ if (instance._skinShown === name || !instance._head) return;
130
+ const entry = name ? SKIN_CATALOG[name] : null;
131
+ const kind = entry ? (entry.native ? 'native' : 'material') : null;
132
+ if (kind && !painters[kind]) {
133
+ // Draw it once the painter arrives, unless another skin was chosen meanwhile.
134
+ if (instance._skinPending !== name) {
135
+ instance._skinPending = name;
136
+ loadPainter(kind).then(() => {
137
+ instance._skinPending = undefined;
138
+ if ((instance._skinName ?? null) === name) render(instance);
139
+ });
140
+ }
141
+ return;
142
+ }
143
+ const { showNative } = painters.native || {};
144
+ undoAdjustments(instance);
145
+ instance._material?.dispose();
146
+ instance._material = null;
147
+ if (!entry?.native) showNative?.(instance, null);
148
+ instance._skinShown = name;
149
+ // Without a skin the head takes the current color attribute again.
150
+ if (!entry) {
151
+ baseApplyColor.call(instance, instance.getAttribute('color'));
152
+ return;
153
+ }
154
+ if (entry.native) {
155
+ // Frames may stop as soon as love ends, so the joined heart is split here.
156
+ if (!instance._skinLoveEnd) {
157
+ instance._skinLoveEnd = event => {
158
+ if (event.detail?.state !== 'love') painters.native?.separateNativeEyes(instance);
159
+ };
160
+ instance.addEventListener('face-state', instance._skinLoveEnd);
161
+ }
162
+ const skin = showNative(instance, entry.native);
163
+ if (skin) {
164
+ const light = instance._skinLight;
165
+ if (light) { skin.light.x = light.x; skin.light.y = light.y; }
166
+ adjustNative(instance, skin, name);
167
+ }
168
+ } else {
169
+ // The lids cut the eyes through a mask, so the head paint shows behind them.
170
+ painters.maskEyelids(instance);
171
+ const material = painters.material.applyMaterial(instance, entry.material);
172
+ adjustMaterial(instance, material, name);
173
+ instance._material = material;
174
+ instance._materialLight = null;
175
+ }
176
+ }
177
+
178
+ // The light follows the pointer the same way the eyes do, and rests at the
179
+ // upper left when the pointer is away, following is off, or motion is reduced.
180
+ function updateLight(instance) {
181
+ const name = instance._skinShown;
182
+ if (!name) return;
183
+ const pointer = instance._pointer;
184
+ const reduced = instance._isReducedMotion?.();
185
+ const follows = instance._pointerFollowEnabled !== false && !reduced && pointer?.active && pointer.lastMove > 0;
186
+ const pull = follows ? Math.pow(clamp(pointer.influence || 0, 0, 1), .7) : 0;
187
+ // Pages can pin the light with _skinFixedLight = { x, y } in [-1, 1].
188
+ const fixed = instance._skinFixedLight;
189
+ const tx = fixed ? fixed.x : REST_LIGHT.x + (clamp(pointer?.x / 48, -1, 1) - REST_LIGHT.x) * pull;
190
+ const ty = fixed ? fixed.y : REST_LIGHT.y + (clamp(pointer?.y / 34, -1, 1) - REST_LIGHT.y) * pull;
191
+ const light = instance._skinLight ??= { ...REST_LIGHT };
192
+ if (reduced) {
193
+ light.x = tx;
194
+ light.y = ty;
195
+ } else {
196
+ light.x += ((Number.isFinite(tx) ? tx : REST_LIGHT.x) - light.x) * .18;
197
+ light.y += ((Number.isFinite(ty) ? ty : REST_LIGHT.y) - light.y) * .18;
198
+ }
199
+ if (SKIN_CATALOG[name].native) {
200
+ painters.native.joinNativeEyes(instance);
201
+ painters.native.lightNative(instance, light.x, light.y);
202
+ } else if (instance._material) {
203
+ const last = instance._materialLight;
204
+ if (last && Math.abs(last.x - light.x) < .0005 && Math.abs(last.y - light.y) < .0005) return;
205
+ instance._materialLight = { x: light.x, y: light.y };
206
+ instance._material.light(light.x, light.y);
207
+ }
208
+ }
209
+
210
+ // The runtime's attribute observer calls this when the skin attribute changes
211
+ // and once on connect. An unknown name in markup falls back to no skin with a
212
+ // console notice, so an invalid attribute never breaks the page; setSkin()
213
+ // throws instead.
214
+ proto._applySkinAttribute = function() {
215
+ const value = this.getAttribute('skin');
216
+ if (normalizeSkin(value) === undefined) {
217
+ console.warn(`agent-robot-avatar: unknown skin "${value}". Use a skin number such as ${SKIN_NAMES[0]}, or none.`);
218
+ this.setSkin(null);
219
+ } else {
220
+ this.setSkin(value);
221
+ }
222
+ };
223
+
224
+ proto.setSkin = function(value = null) {
225
+ const name = normalizeSkin(value);
226
+ if (name === undefined) throw new Error(`Unknown Agent Robot Avatar skin: ${value}`);
227
+ this._skinName = name;
228
+ render(this);
229
+ this._resumeFrames?.();
230
+ return this;
231
+ };
232
+
233
+ proto.getSkin = function() {
234
+ return this._skinName ?? null;
235
+ };
236
+
237
+ // A skin keeps the head paint even when the color attribute changes; removing
238
+ // the skin brings the color back.
239
+ proto._applyColor = function(value) {
240
+ baseApplyColor.call(this, value);
241
+ const entry = this._skinShown ? SKIN_CATALOG[this._skinShown] : null;
242
+ if (!entry || !this._head) return;
243
+ this._head.setAttribute('fill', entry.native ? 'url(#skin-base)' : `url(#material-${entry.material}-base)`);
244
+ };
245
+
246
+ registerAvatarExtension({
247
+ name: 'skins',
248
+ draw() {
249
+ // A skin set before the avatar was ready is shown on its first frame.
250
+ if ((this._skinName ?? null) !== (this._skinShown ?? null)) render(this);
251
+ updateLight(this);
252
+ },
253
+ });
254
+
255
+ export { AgentRobotAvatar, SKINS, SKIN_NAMES, SERIES as SKIN_SERIES, loadSkins };
256
+ export default AgentRobotAvatar;
@@ -1,3 +1,3 @@
1
- const VERSION = '0.5.2';
1
+ const VERSION = '0.7.0';
2
2
 
3
3
  export { VERSION };