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
@@ -0,0 +1,302 @@
1
+ // Paints the material skins: glazes, frosted glass, metals, wood, fabrics,
2
+ // sweets and the rest. Each material is a base paint, light layers and an
3
+ // optional texture drawn by one of the texture modules.
4
+ import { MATERIALS as skins } from './material-data.js';
5
+ import { extraTexture } from './more-materials.js';
6
+ import { collectionTexture } from './collection-materials.js';
7
+ import { revisionTexture } from './revised-materials.js';
8
+ import { secondPassTexture } from './second-pass-materials.js';
9
+ import { sportsTexture } from './sports-materials.js';
10
+ import { softTexture } from './soft-materials.js';
11
+ import { layerDefaults } from './layered-texture.js';
12
+
13
+ const NS = 'http://www.w3.org/2000/svg';
14
+
15
+ function svgElement(tag, attributes = {}, parent) {
16
+ const element = document.createElementNS(NS, tag);
17
+ for (const [name, value] of Object.entries(attributes)) element.setAttribute(name, value);
18
+ parent?.append(element);
19
+ return element;
20
+ }
21
+
22
+ function gradient(defs, tag, id, attributes, stops) {
23
+ const element = svgElement(tag, { id, ...attributes }, defs);
24
+ for (const [offset, color, opacity = 1] of stops) {
25
+ svgElement('stop', { offset, 'stop-color': color, 'stop-opacity': opacity }, element);
26
+ }
27
+ return element;
28
+ }
29
+
30
+ function texture(defs, id, kind) {
31
+ const extra = softTexture(defs, id, kind, svgElement, gradient) || sportsTexture(defs, id, kind, svgElement, gradient) || secondPassTexture(defs, id, kind, svgElement, gradient) || revisionTexture(defs, id, kind, svgElement, gradient) || collectionTexture(defs, id, kind, svgElement, gradient) || extraTexture(defs, id, kind, svgElement, gradient);
32
+ if (extra) return extra;
33
+ const tileSize = ({brushed:8, grain:48, wood:120, marble:220, denim:8, carbon:240,
34
+ galaxy:220, lava:120, terrazzo:80, cow:160, seeds:52, honey:48, blueprint:80})[kind] || 32;
35
+ const pattern = svgElement('pattern', {
36
+ id, width: tileSize, height: tileSize, patternUnits: 'userSpaceOnUse',
37
+ ...(kind === 'stripes' ? { patternTransform: 'rotate(-32)' } : {}),
38
+ }, defs);
39
+ if (kind === 'stripes') {
40
+ svgElement('rect', { width: 14, height: 32, fill: '#fff4f6', opacity: .88 }, pattern);
41
+ svgElement('rect', { x: 20, width: 3, height: 32, fill: '#fff4f6', opacity: .48 }, pattern);
42
+ } else if (kind === 'grain') {
43
+ // A fixed small tile gives the matte finish texture without image assets.
44
+ for (let i = 0; i < 64; i++) {
45
+ svgElement('circle', {
46
+ cx: (i * 17.37) % 48, cy: (i * i * 7.13) % 48,
47
+ r: .35 + (i % 3) * .18, fill: i % 2 ? '#fff4de' : '#6f3a27', opacity: .18,
48
+ }, pattern);
49
+ }
50
+ } else if (kind === 'dots') {
51
+ for (const [cx, cy] of [[8, 8], [24, 24]]) {
52
+ svgElement('circle', { cx, cy, r: 4.6, fill: '#f6db7b' }, pattern);
53
+ }
54
+ } else if (kind === 'denim') {
55
+ svgElement('path',{d:'M-4 8L8-4M0 12L12 0',stroke:'#b9d2df','stroke-width':1,opacity:.34},pattern);
56
+ svgElement('path',{d:'M0 1H8M0 5H8',stroke:'#0a2a49','stroke-width':.6,opacity:.23},pattern);
57
+ } else if (kind === 'carbon') {
58
+ gradient(defs,'linearGradient',`${id}-warp`,{x1:'0',y1:'0',x2:'1',y2:'0'},
59
+ [[0,'#142024'],[.22,'#37464a'],[.48,'#526064'],[.72,'#354347'],[1,'#18252a']]);
60
+ gradient(defs,'linearGradient',`${id}-weft`,{x1:'0',y1:'0',x2:'0',y2:'1'},
61
+ [[0,'#0d191f'],[.25,'#27353b'],[.52,'#3c4c52'],[.78,'#26363c'],[1,'#101c23']]);
62
+ const weave = svgElement('pattern',{
63
+ id:`${id}-weave`,width:32,height:32,patternUnits:'userSpaceOnUse',
64
+ patternTransform:'rotate(-18 120 120)',
65
+ },defs);
66
+ svgElement('rect',{width:32,height:32,fill:'#101b20'},weave);
67
+ // Two over, two under; the shift in each row produces the diagonal twill.
68
+ for(let row=0;row<4;row++) for(let col=0;col<4;col++) {
69
+ const x=col*8,y=row*8,vertical=(row-col+4)%4<2;
70
+ svgElement('rect',{x:x+.35,y:y+.35,width:7.3,height:7.3,rx:.65,
71
+ fill:`url(#${id}-${vertical?'warp':'weft'})`},weave);
72
+ for(const offset of [1.7,3.3,4.9,6.5]) {
73
+ svgElement('path',{
74
+ d:vertical?`M${x+offset} ${y+.7}v6.6`:`M${x+.7} ${y+offset}h6.6`,
75
+ stroke:vertical?'#bdc8c6':'#91a6ab','stroke-width':.36,opacity:vertical?.24:.18,
76
+ },weave);
77
+ }
78
+ }
79
+ svgElement('rect',{width:240,height:240,fill:`url(#${id}-weave)`},pattern);
80
+ gradient(defs,'linearGradient',`${id}-resin`,{x1:'.05',y1:'.1',x2:'.95',y2:'.8'},
81
+ [[0,'#e3efec',0],[.24,'#e3efec',.02],[.42,'#e3efec',.18],
82
+ [.55,'#e3efec',.05],[.72,'#e3efec',0],[1,'#031119',.12]]);
83
+ svgElement('rect',{width:240,height:240,fill:`url(#${id}-resin)`},pattern);
84
+ } else if (kind === 'galaxy') {
85
+ // Retain the original two-nebula composition; emphasize color and starlight.
86
+ gradient(defs,'radialGradient',`${id}-cloud`,{},[[0,'#d77cff',.83],[.35,'#ad59ed',.56],[.65,'#8452d9',.25],[1,'#8452d9',0]]);
87
+ svgElement('ellipse',{cx:72,cy:106,rx:110,ry:51,fill:`url(#${id}-cloud)`,transform:'rotate(-35 72 106)'},pattern);
88
+ gradient(defs,'radialGradient',`${id}-blue`,{},[[0,'#70d4ff',.68],[.4,'#4aabf0',.35],[1,'#4aabf0',0]]);
89
+ svgElement('ellipse',{cx:150,cy:64,rx:72,ry:65,fill:`url(#${id}-blue)`},pattern);
90
+ gradient(defs,'radialGradient',`${id}-starlight`,{},[[0,'#eef8ff',.6],[.25,'#d0e6ff',.2],[1,'#bdd9ff',0]]);
91
+ for(let i=0;i<84;i++) {
92
+ const cx=(i*37.13)%220,cy=(i*i*11.79)%220,bright=i%10===0;
93
+ if(bright) svgElement('circle',{cx,cy,r:5.5,fill:`url(#${id}-starlight)`},pattern);
94
+ svgElement('circle',{
95
+ cx,cy,r:bright?1.85:.75+(i%3)*.22,
96
+ fill:i%4?'#fff5e5':'#cdefff',opacity:.64+(i%4)*.11,
97
+ },pattern);
98
+ }
99
+ svgElement('path',{d:'M156 164h14m-7-7v14M44 40h11m-5.5-5.5v11M177 72h9m-4.5-4.5v9',stroke:'#fff2ce','stroke-width':1,opacity:.95},pattern);
100
+ } else if (kind === 'terrazzo') {
101
+ const colors=['#a47d73','#789b91','#d3a963','#a9a1b8','#e0b2a4'];
102
+ for(let i=0;i<24;i++) {
103
+ const x=(i*29.17)%80,y=(i*i*13.31)%80,s=3+(i%4)*1.4;
104
+ svgElement('path',{d:`M${x} ${y}l${s} ${s*.4} ${-s*.3} ${s} ${-s*.9} ${-s*.3}Z`,fill:colors[i%colors.length],opacity:.75},pattern);
105
+ }
106
+ } else if (kind === 'seeds') {
107
+ for(const [cx,cy,angle] of [[13,13,-20],[39,39,25]]) svgElement('ellipse',{cx,cy,rx:1.5,ry:3.2,fill:'#824248',opacity:.75,transform:`rotate(${angle} ${cx} ${cy})`},pattern);
108
+ } else if (kind === 'blueprint') {
109
+ for(let p=0;p<=80;p+=10) svgElement('path',{d:`M${p} 0v80M0 ${p}h80`,stroke:'#d0eeff','stroke-width':p%40===0?.7:.35,opacity:p%40===0?.37:.18},pattern);
110
+ svgElement('circle',{cx:40,cy:40,r:21,fill:'none',stroke:'#dbf3ff','stroke-width':.6,opacity:.36},pattern);
111
+ svgElement('path',{d:'M10 10h12m-6-6v12M60 63h12m-6-6v12',stroke:'#dbf3ff','stroke-width':.8,opacity:.55},pattern);
112
+ }
113
+ return pattern;
114
+ }
115
+
116
+ // Paints one material over the avatar and returns how to light and remove it.
117
+ // The skin module masks the eyelids beforehand, so they already cut the eyes.
118
+ export function applyMaterial(avatar, name) {
119
+ const skin = skins[name];
120
+ const root = avatar.shadowRoot;
121
+ const defs = root.querySelector('svg defs');
122
+ const head = root.getElementById('head');
123
+ const originalDefs = new Set(defs.children);
124
+ const originalFill = head.getAttribute('fill');
125
+ const originalOpacity = head.getAttribute('fill-opacity');
126
+ const properties = ['--material-eye', '--material-eye-right', '--material-eye-relief', '--material-separate-eye-relief', '--material-antenna', '--material-antenna-rim'];
127
+ const originalProperties = properties.map(key => [key, avatar.style.getPropertyValue(key)]);
128
+ const id = `material-${name}`;
129
+ const fill = suffix => `url(#${id}-${suffix})`;
130
+
131
+ let eyeShadowOffset;
132
+ if (skin.raisedEyes) {
133
+ // The workshop tints the porcelain relief with each design's eye color.
134
+ // Keep that paint in the public renderer as well as the authoring tool.
135
+ const eyeColor = skin.simpleLayers ? layerDefaults(name).eyeColor : '#ffffff';
136
+ const tintEye = original => '#' + [1,3,5].map(offset =>
137
+ Math.round(parseInt(original.slice(offset,offset+2),16) * parseInt(eyeColor.slice(offset,offset+2),16) / 255).toString(16).padStart(2,'0')
138
+ ).join('');
139
+ gradient(defs, 'radialGradient', `${id}-eye-porcelain`, {cx:'.32',cy:'.22',r:'.88'},
140
+ [[0,tintEye('#ffffff')],[.66,tintEye('#f2f3f8')],[.88,tintEye('#e3e8f1')],[1,tintEye('#b5c4d9')]]);
141
+ const relief = svgElement('filter', {id:`${id}-eye-relief`,x:'-60%',y:'-60%',width:'240%',height:'250%','color-interpolation-filters':'sRGB'}, defs);
142
+ svgElement('feGaussianBlur', {in:'SourceAlpha',stdDeviation:4,result:'soft-shadow'}, relief);
143
+ eyeShadowOffset = svgElement('feOffset', {in:'soft-shadow',dx:5,dy:8,result:'offset-shadow'}, relief);
144
+ svgElement('feFlood', {'flood-color':skin.eyeShadow || (name==='blueglaze'?'#102674':'#53759f'),'flood-opacity':.34,result:'shadow-color'}, relief);
145
+ svgElement('feComposite', {in:'shadow-color',in2:'offset-shadow',operator:'in',result:'shadow'}, relief);
146
+ svgElement('feOffset', {in:'SourceAlpha',dx:1,dy:2,result:'raised-edge'}, relief);
147
+ svgElement('feFlood', {'flood-color':tintEye('#a9bad1'),result:'edge-color'}, relief);
148
+ svgElement('feComposite', {in:'edge-color',in2:'raised-edge',operator:'in',result:'edge'}, relief);
149
+ const merge = svgElement('feMerge', {}, relief);
150
+ for (const source of ['shadow','edge','SourceGraphic']) svgElement('feMergeNode', {in:source}, merge);
151
+ }
152
+
153
+ const linear = name === 'aurora' || skin.linear;
154
+ const base = gradient(defs, linear ? 'linearGradient' : 'radialGradient', `${id}-base`,
155
+ linear
156
+ ? { x1: '.05', y1: '.05', x2: '.85', y2: '.98' }
157
+ : { cx: '.32', cy: '.17', r: '.95' },
158
+ skin.simpleLayers ? [[0,skin.darkColor],[1,skin.darkColor]] : skin.base.map((color, index) => [index / (skin.base.length - 1), color]));
159
+ const specular = gradient(defs, 'radialGradient', `${id}-shine`, { cx: '.30', cy: '.12', r: '.70' },
160
+ [[0, '#fff', skin.shine], [.23, '#fff', skin.shine * .30], [.65, '#fff', .035], [1, '#fff', 0]]);
161
+ const volume = gradient(defs, 'radialGradient', `${id}-volume`, { cx: '.43', cy: '.30', r: '.76' },
162
+ [[0, '#091126', 0], [.58, '#091126', 0], [.82, '#091126', skin.shade * .27], [1, '#091126', skin.shade]]);
163
+ const rim = gradient(defs, 'linearGradient', `${id}-rim`, { x1: '0', y1: '0', x2: '1', y2: '1' },
164
+ [[0, '#fff', .95], [.28, skin.rim, .60], [.52, skin.rim, .06], [.82, skin.rim, .13], [1, skin.rim, .65]]);
165
+ const edge = gradient(defs, 'radialGradient', `${id}-edge`, { cx: '.88', cy: '.83', r: '.55' },
166
+ [[0, skin.rim, .25], [.65, skin.rim, .035], [1, skin.rim, 0]]);
167
+ const antenna = gradient(defs, 'radialGradient', `${id}-antenna`, { cx: '.28', cy: '.20', r: '.85' },
168
+ [[0, skin.antenna[0]], [.43, skin.antenna[1]], [1, skin.antenna[2]]]);
169
+ if (skin.pattern) texture(defs, `${id}-texture`, skin.pattern);
170
+
171
+ const layers = svgElement('g', { 'pointer-events': 'none', 'aria-hidden': 'true' });
172
+ head.after(layers);
173
+ const shape = { href: '#headShape', transform: head.getAttribute('transform') };
174
+ if (skin.pattern) svgElement('use', { ...shape, fill: fill('texture') }, layers);
175
+ if (!skin.simpleLayers) {
176
+ if (skin.translucent) {
177
+ gradient(defs, 'linearGradient', `${id}-glass`, { x1: '0', y1: '0', x2: '.6', y2: '1' },
178
+ [[0, '#fff', .6], [.14, '#fff', .03], [.77, '#fff', .02], [1, skin.glassTint || '#c5fff2', .3]]);
179
+ svgElement('use', { ...shape, fill: fill('glass'), stroke: skin.glassRim || '#d6fff5', 'stroke-width': 5, 'stroke-opacity': skin.glassEdgeOpacity ?? .25 }, layers);
180
+ }
181
+ svgElement('use', { ...shape, fill: fill('shine') }, layers);
182
+ svgElement('use', { ...shape, fill: fill('volume') }, layers);
183
+ svgElement('use', { ...shape, fill: fill('edge') }, layers);
184
+ svgElement('use', { ...shape, fill: 'none', stroke: fill('rim'), 'stroke-width': 1.25, opacity: skin.rimOpacity ?? (skin.matte ? .3 : 1) }, layers);
185
+ }
186
+ if(skin.stitched) svgElement('use',{href:'#headShape',transform:'translate(18 18) scale(.85)',fill:'none',stroke:'#ead09d','stroke-width':3,'stroke-dasharray':'6 6','stroke-linecap':'round',opacity:.9},layers);
187
+ if(skin.edgeColor) svgElement('use',{...shape,fill:'none',stroke:skin.edgeColor,'stroke-width':7,opacity:.9},layers);
188
+
189
+ // Love intentionally overlaps the two animated eyes into one heart. Paint
190
+ // that union once: independent porcelain gradients and drop shadows otherwise
191
+ // leave a seam and make one half look stacked on top of the other.
192
+ const eyeParent = root.getElementById('leftEye').parentElement;
193
+ const originalEyeOpacity = eyeParent.style.opacity;
194
+ let mergedSurface;
195
+ if (skin.raisedEyes) {
196
+ const silhouette = svgElement('mask', {
197
+ id: `${id}-joined-eyes`, maskUnits: 'userSpaceOnUse', maskContentUnits: 'userSpaceOnUse',
198
+ x: 0, y: 0, width: 240, height: 240, 'mask-type': 'alpha',
199
+ }, defs);
200
+ for (const side of ['left', 'right']) svgElement('use', { href: `#${side}Eye` }, silhouette);
201
+ mergedSurface = svgElement('g', { id: `${id}-joined-eye-surface`, 'pointer-events': 'none', 'aria-hidden': 'true',
202
+ 'clip-path': eyeParent.getAttribute('clip-path') || 'none' });
203
+ eyeParent.after(mergedSurface);
204
+ const reliefSurface = svgElement('g', { style: 'filter:var(--material-eye-relief,none)' }, mergedSurface);
205
+ svgElement('rect', { x: 0, y: 0, width: 240, height: 240,
206
+ fill: 'var(--material-eye,#fff)', mask: `url(#${id}-joined-eyes)` }, reliefSurface);
207
+ }
208
+ function mergeEyes(merged) {
209
+ if (!mergedSurface) return;
210
+ // Opacity on the parent hides the original paint without hiding the eye
211
+ // nodes referenced by the union mask. Their live transforms still animate.
212
+ eyeParent.style.opacity = merged ? '0' : originalEyeOpacity;
213
+ mergedSurface.style.display = merged ? '' : 'none';
214
+ if (merged) avatar.style.setProperty('--material-separate-eye-relief', 'none');
215
+ else avatar.style.removeProperty('--material-separate-eye-relief');
216
+ }
217
+ const onFaceState = event => mergeEyes(event.detail?.state === 'love');
218
+ const onActionState = event => {
219
+ if (event.detail?.action === 'love' && event.detail.phase !== 'start') mergeEyes(false);
220
+ };
221
+ if (mergedSurface) {
222
+ avatar.addEventListener('face-state', onFaceState);
223
+ avatar.addEventListener('action-state', onActionState);
224
+ mergeEyes(Boolean(avatar._loveFx));
225
+ }
226
+
227
+ const style = document.createElement('style');
228
+ style.textContent = `
229
+ #leftBase,#rightBase,#leftInputBase,#rightInputBase{fill:var(--material-eye,#fff)}
230
+ #rightBase,#rightInputBase{fill:var(--material-eye-right,var(--material-eye,#fff))}
231
+ #antennaDot{fill:var(--material-antenna);stroke:var(--material-antenna-rim);stroke-width:.7px}
232
+ ${skin.raisedEyes ? '#leftEye,#rightEye{filter:var(--material-separate-eye-relief,var(--material-eye-relief,none))}' : ''}
233
+ `;
234
+ root.append(style);
235
+ const addedDefs = [...defs.children].filter(node => !originalDefs.has(node));
236
+ let disposed = false;
237
+
238
+ const material = {
239
+ dispose() {
240
+ if (disposed) return;
241
+ disposed = true;
242
+ avatar.removeEventListener('face-state', onFaceState);
243
+ avatar.removeEventListener('action-state', onActionState);
244
+ eyeParent.style.opacity = originalEyeOpacity;
245
+ mergedSurface?.remove();
246
+ for (const node of addedDefs) node.remove();
247
+ layers.remove();
248
+ style.remove();
249
+ if (originalFill === null) head.removeAttribute('fill');
250
+ else head.setAttribute('fill', originalFill);
251
+ if (originalOpacity === null) head.removeAttribute('fill-opacity');
252
+ else head.setAttribute('fill-opacity', originalOpacity);
253
+ for (const [key, value] of originalProperties) {
254
+ if (value) avatar.style.setProperty(key, value);
255
+ else avatar.style.removeProperty(key);
256
+ }
257
+ },
258
+ // Only these coordinates are driven by light(), not the saved material.
259
+ lightKeys: [
260
+ ...(!linear ? ['base:cx','base:cy'] : skin.linear ? ['base:x1','base:x2'] : []),
261
+ 'shine:cx','shine:cy','volume:cx','volume:cy','edge:cx','edge:cy',
262
+ 'rim:x1','rim:x2','antenna:cx','antenna:cy',
263
+ ],
264
+ light(x, y) {
265
+ if (eyeShadowOffset) {
266
+ eyeShadowOffset.setAttribute('dx', -x * 8);
267
+ eyeShadowOffset.setAttribute('dy', 5 - y * 6);
268
+ }
269
+ const cx = .5 + x * .30;
270
+ const cy = .30 + y * .17;
271
+ if (!linear) {
272
+ base.setAttribute('cx', cx);
273
+ base.setAttribute('cy', cy);
274
+ } else if (skin.linear) {
275
+ base.setAttribute('x1', .1 + x * .2);
276
+ base.setAttribute('x2', .9 + x * .2);
277
+ }
278
+ specular.setAttribute('cx', cx);
279
+ specular.setAttribute('cy', cy - .10);
280
+ volume.setAttribute('cx', .5 + x * .12);
281
+ volume.setAttribute('cy', .35 + y * .08);
282
+ edge.setAttribute('cx', .5 - x * .42);
283
+ edge.setAttribute('cy', .75 - y * .12);
284
+ rim.setAttribute('x1', .5 + x * .5);
285
+ rim.setAttribute('x2', .5 - x * .5);
286
+ antenna.setAttribute('cx', cx);
287
+ antenna.setAttribute('cy', Math.max(.1, cy));
288
+ },
289
+ };
290
+ head.setAttribute('fill', fill('base'));
291
+ head.setAttribute('fill-opacity', skin.translucent ? (skin.bodyOpacity ?? .58) : 1);
292
+ avatar.style.setProperty('--material-eye', skin.raisedEyes ? fill('eye-porcelain') : skin.eye);
293
+ avatar.style.setProperty('--material-eye-right', skin.raisedEyes ? fill('eye-porcelain') : skin.eyeRight || skin.eye);
294
+ if (skin.raisedEyes) avatar.style.setProperty('--material-eye-relief', fill('eye-relief'));
295
+ avatar.style.setProperty('--material-antenna', fill('antenna'));
296
+ avatar.style.setProperty('--material-antenna-rim', skin.rim);
297
+ material.light(-.5, -.5);
298
+ return material;
299
+ }
300
+
301
+
302
+ export { skins };
@@ -0,0 +1,11 @@
1
+ // Change hue while keeping each silver color's HSL lightness. White highlights
2
+ // stay white; reflection positions, stop offsets and opacity are untouched.
3
+ export function metalTint(color,family) {
4
+ const channels = [1,3,5].map(offset => parseInt(color.slice(offset,offset+2),16)/255);
5
+ const lightness = (Math.max(...channels)+Math.min(...channels))/2;
6
+ const hue = family === 'gold' ? 44/60 : 22/60;
7
+ const saturation = family === 'gold' ? .58 : .48;
8
+ const c = (1-Math.abs(2*lightness-1))*saturation;
9
+ const x = c*(1-Math.abs(hue%2-1)), m = lightness-c/2;
10
+ return '#' + [c,x,0].map(value => Math.round((value+m)*255).toString(16).padStart(2,'0')).join('');
11
+ }
@@ -0,0 +1,69 @@
1
+ // Native SVG swatches: no image files, external libraries or remote requests.
2
+ const EXTRA = new Set(['cork', 'pearl', 'hologram', 'ice', 'soap', 'leather', 'lace', 'bamboo', 'wave', 'origami', 'circuit', 'topo']);
3
+
4
+ export function extraTexture(defs, id, kind, el, gradient) {
5
+ if (!EXTRA.has(kind)) return null;
6
+ const size = {pearl:240,hologram:120,ice:240,soap:240,copper:100,leather:24,cork:64,knit:18,bamboo:32,origami:120,stained:80,circuit:80,koi:200,sakura:72,lace:64,checker:48,tiger:120,camo:120,topo:160,wave:48,moon:160}[kind];
7
+ const p = el('pattern', {id,width:size,height:size,patternUnits:'userSpaceOnUse'}, defs);
8
+ const path = (d,stroke,width=1,opacity=1,fill='none',parent=p) => el('path',{d,stroke,'stroke-width':width,opacity,fill,'stroke-linejoin':'round','stroke-linecap':'round'},parent);
9
+ const circle = (cx,cy,r,fill,opacity=1,parent=p) => el('circle',{cx,cy,r,fill,opacity},parent);
10
+ if (kind === 'pearl') {
11
+ for(let i=0;i<20;i++) path(`M-20 ${i*15}Q90 ${i*15-100} 260 ${i*15-22}`,i%2?'#d5c1e2':'#fff9e4',2,.35);
12
+ } else if (kind === 'hologram') {
13
+ const colors=['#fff7bc','#96ffe6','#d9bcff','#ffb8de'];
14
+ for(let i=0;i<4;i++) path(`M${i*30-60} 0l60 120h18L${i*30-42} 0Z`,'none',0,.28,colors[i]);
15
+ for(const [x,y] of [[25,26],[91,83]]) path(`M${x-6} ${y}h12m-6-6v12`,'#fff',1,.85);
16
+ } else if (kind === 'ice') {
17
+ const facets=['M0 0L110 30 42 119Z','M110 30L240 0 195 143Z','M42 119L195 143 121 240Z','M195 143L240 0 240 240Z'];
18
+ facets.forEach((d,i)=>path(d,'#f3ffff',.8,.18+i*.035,i%2?'#efffff':'#5b9fc2'));
19
+ path('M110 30L99 80 131 107 121 162M42 119L63 167 50 210','#f4ffff',1.1,.7);
20
+ for(let i=0;i<22;i++) circle((i*43.7)%240,(i*i*19.3)%240,.8+i%3*.5,'#fff',.35);
21
+ } else if (kind === 'soap') {
22
+ ['#efb4e5','#94ddd9','#faf0b4','#b9b1e9'].forEach((c,i)=>{
23
+ path(`M${25+i*3} ${155-i*5}C${-5+i*3} 80 40 ${20+i*4} ${133+i*3} ${20+i*4}`,c,3,.55);
24
+ path(`M${210-i*3} 82Q${253-i*4} 190 ${120-i*2} ${213-i*3}`,c,2,.5);
25
+ });
26
+ for(const [x,y,r] of [[58,173,9],[181,43,5],[193,168,13]]) el('circle',{cx:x,cy:y,r,fill:'none',stroke:'#fff','stroke-width':1.4,opacity:.5},p);
27
+ } else if (kind === 'leather') {
28
+ path('M0 3L7 1 12 6 22 3 24 9M0 16L8 11 16 15 24 12M7 1L8 11 4 24M16 15L19 24','#402719',.6,.22);
29
+ path('M1 4L7 3 11 7M9 12L15 16 23 13','#f0d6ad',.5,.25);
30
+ } else if (kind === 'cork') {
31
+ for(let i=0;i<65;i++) {
32
+ const x=(i*23.7)%64,y=(i*i*11.3)%64,r=1+i%3;
33
+ el('rect',{x,y,width:r*1.7,height:r,rx:.5,fill:['#76542e','#fff0c6','#aa743a'][i%3],opacity:.3,transform:`rotate(${i*31} ${x} ${y})`},p);
34
+ }
35
+ } else if (kind === 'bamboo') {
36
+ for(let i=-2;i<4;i++) {
37
+ path(`M${i*16} -16l64 64`,'#796335',6,.3);
38
+ path(`M${i*16+2} -16l64 64`,'#f1dfad',3,.65);
39
+ path(`M${i*16} 48l64-64`,'#c6a76c',6,.8);
40
+ path(`M${i*16+2} 48l64-64`,'#f9eac0',1,.55);
41
+ }
42
+ } else if (kind === 'origami' || kind === 'stained') {
43
+ const s=size, m=s*.43;
44
+ const shapes=[`M0 0H${s}L${m} ${m}Z`,`M${s} 0V${s}L${m} ${m}Z`,`M${s} ${s}H0L${m} ${m}Z`,`M0 ${s}V0L${m} ${m}Z`];
45
+ const colors=kind==='stained'?['#c29a57','#508e8d','#9279a7','#698bb0']:['#fff5dc','#8ca6ae','#b5b7d5','#eab69d'];
46
+ shapes.forEach((d,i)=>path(d,kind==='stained'?'#303951':'#fff7ef',kind==='stained'?2.7:.7,kind==='stained'?.85:.6,colors[i]));
47
+ } else if (kind === 'circuit') {
48
+ for(const d of ['M0 12H22L38 28H80','M8 80V53L24 37V0','M44 80V57L63 38H80','M52 0V12L68 28']) path(d,'#d1bc73',1.3,.7);
49
+ for(const [x,y] of [[38,28],[24,37],[44,57],[68,28]]) {
50
+ circle(x,y,3,'#dac98c',.9);circle(x,y,1.3,'#285348');
51
+ }
52
+ el('rect',{x:47,y:7,width:14,height:10,rx:2,fill:'#102f2c',stroke:'#819d76','stroke-width':.7},p);
53
+ } else if (kind === 'lace') {
54
+ for(const [x,y] of [[0,0],[32,32],[64,0],[0,64],[64,64]]) {
55
+ const g=el('g',{transform:`translate(${x} ${y})`},p);
56
+ for(let i=0;i<8;i++) el('ellipse',{cx:0,cy:10,rx:4,ry:10,fill:'none',stroke:'#eee1ce','stroke-width':.7,opacity:.65,transform:`rotate(${i*45})`},g);
57
+ circle(0,0,2,'#eee1ce',.7,g);
58
+ for(let i=0;i<12;i++) circle(Math.cos(i*Math.PI/6)*24,Math.sin(i*Math.PI/6)*24,.7,'#eee1ce',.6,g);
59
+ }
60
+ } else if (kind === 'topo') {
61
+ for(let i=0;i<11;i++) {
62
+ const r=9+i*7;
63
+ path(`M${80-r} 80C${80-r*1.3} ${80-r} ${80+r*.6} ${80-r*1.4} ${80+r} ${80-r*.4}S${80+r*.4} ${80+r*1.4} ${80-r*.4} ${80+r}S${80-r} 95 ${80-r} 80Z`,'#d8edc9',i%3===0?1.2:.6,.48);
64
+ }
65
+ } else if (kind === 'wave') {
66
+ for(const [x,y] of [[0,24],[48,24],[24,48],[0,72],[48,72]]) for(const r of [6,12,18,24]) path(`M${x-r} ${y}a${r} ${r} 0 0 1 ${r*2} 0`,'#e7e7cf',1,.55);
67
+ }
68
+ return p;
69
+ }