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,754 @@
1
+ // Paints the skins built from layered gradients: glass, acrylic, jelly, gems,
2
+ // gradients, printed patterns and balls. The public skin module decides which
3
+ // skin is shown and drives the light; this module only paints.
4
+ import AgentRobotAvatar from '../agent-robot-avatar-extension-host.js';
5
+ import { patternMarkup } from '../agent-robot-avatar-skin-patterns.js';
6
+ import { maskEyelids } from './eyelids.js';
7
+
8
+ const NS = 'http://www.w3.org/2000/svg';
9
+ const runtimeWindow = typeof window !== 'undefined' ? window : null;
10
+ const baseApplyColor = AgentRobotAvatar.prototype._applyColor;
11
+
12
+ const SKINS={"glow-blue":{"base":["#90b9fe","#619bff","#4287ff","#5295ff"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#90b9fe","#619bff","#5295ff"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaHighlight":1,"antennaHighlightSize":0.36,"antennaMid":"#70acff","antennaDark":"#9fc7fe"}},"glow-red":{"base":["#fe9590","#ff6961","#ff4c42","#ff5752"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#fe9590","#ff6961","#ff5752"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaHighlight":1,"antennaHighlightSize":0.36,"antennaMid":"#ff7070","antennaDark":"#fe9f9f"}},"custom-1793262923292":{"base":["#b9ff94","#92ec55","#72d232","#57c200"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#b9ff94","#92ec55","#57c200"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaMid":"#b4ff42","antennaDark":"#b5e44e","antennaHighlight":1,"antennaHighlightSize":0.47}},"glow-purple":{"base":["#ca90fe","#b561ff","#a742ff","#ab52ff"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ca90fe","#b561ff","#ab52ff"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaHighlight":1,"antennaHighlightSize":0.36,"antennaMid":"#b570ff","antennaDark":"#cd9ffe"}},"glow-pink":{"base":["#fe90c8","#ff61b0","#ff42a0","#ff52ac"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#fe90c8","#ff61b0","#ff52ac"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaHighlight":1,"antennaHighlightSize":0.36,"antennaMid":"#ff70bf","antennaDark":"#fe9fd3"}},"glow-orange":{"base":["#febb89","#ffa159","#ff8f39","#ff9449"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#febb89","#ffa159","#ff9449"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaHighlight":1,"antennaHighlightSize":0.36,"antennaMid":"#ffa268","antennaDark":"#febf98"}},"custom-1793262553386":{"base":["#90d4fe","#80d9ff","#4dc9ff","#38a9ff"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#90d4fe","#80d9ff","#38a9ff"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaMid":"#94d6ff","antennaDark":"#7adeff","antennaHighlight":1,"antennaHighlightSize":0.47}},"custom-1793262790531":{"base":["#b9cad4","#9bb8c5","#8198a2","#8a959e"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#b9cad4","#9bb8c5","#8a959e"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaMid":"#aec3d0","antennaDark":"#a1b0b5","antennaHighlight":1,"antennaHighlightSize":0.47}},"custom-1793263007260":{"base":["#ff94fb","#ff1f1f","#fe5881","#c20000"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff94fb","#ff1f1f","#c20000"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaMid":"#fe9a9a","antennaDark":"#ffadca","antennaHighlight":1,"antennaHighlightSize":0.47}},"custom-1793263221394":{"base":["#47c8ff","#27ecbb","#30cfb4","#0f4bff"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#47c8ff","#27ecbb","#0f4bff"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaMid":"#42d9ff","antennaDark":"#c2f3ff","antennaHighlight":1,"antennaHighlightSize":0.62}},"custom-1793263313834":{"base":["#ffe014","#ffd642","#ffbd2e","#ffd35c"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ffe014","#ffd642","#ffd35c"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaMid":"#ffe042","antennaDark":"#fffac2","antennaHighlight":1,"antennaHighlightSize":0.62}},"glow-green":{"base":["#90d89e","#61c876","#42bd5b","#52c267"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#90d89e","#61c876","#52c267"],"fixedTune":{"depth":0.6,"faceGlow":0.8,"faceX":0.43,"faceY":0.05,"faceSize":0.69,"bandWidth":46,"bandLight":0.41,"bandOpacity":0.77,"bandMiddle":1,"lineOpacity":0,"glintWidth":3.2,"glintLower":0.85,"eyeTint":0.42,"shadowBlur":4,"shadowOpacity":0.89,"shadowDarkness":0.3,"gradient":"radial","centerX":0.32,"centerY":0.08,"radius":0.81,"stopMid":0.333,"stopEdge":0.667,"bandBlur":12,"glintInset":0.955,"glintBlur":2.7,"glintAngle":94,"glintDash":0,"upperEnd":0,"topGloss":1,"topReach":0.25,"topBlur":6.5,"eyeLitPart":0.25,"antennaHighlight":1,"antennaHighlightSize":0.36,"antennaMid":"#70cd7e","antennaDark":"#9fdda8"}},"gem-blue":{"base":["#2e7bff","#337aff","#0056f5","#023bca"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#2e7bff","#337aff","#023bca"],"fixedTune":{"faceGlow":0.67,"bandWidth":20,"lineOpacity":0.41,"glintUpper":1,"glintLower":0.43,"eyeTint":0.2,"shadowOpacity":0.8,"shadowDarkness":0.34,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"shadowAngle":117,"antennaHighlight":1,"antennaHighlightSize":0.5,"antennaRimOpacity":1,"eyeHighlight":1,"eyeHighlightSize":26.5,"eyeHighlightSoft":0.98,"eyeHighlightX":-1,"eyeHighlightY":-9,"eyeHighlightFollow":11.5,"eyeSpark":0,"colors":["#2e7bff","#337aff","#0056f5","#023bca"]}},"gem-red":{"base":["#ff312e","#ff3a33","#f50700","#ca1502"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff312e","#ff3a33","#ca1502"],"fixedTune":{"faceGlow":0.67,"bandWidth":20,"lineOpacity":0.41,"glintUpper":1,"glintLower":0.43,"eyeTint":0.2,"shadowOpacity":0.8,"shadowDarkness":0.34,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"shadowAngle":117,"antennaHighlight":1,"antennaHighlightSize":0.5,"antennaRimOpacity":1,"eyeHighlight":1,"eyeHighlightSize":26.5,"eyeHighlightSoft":0.98,"eyeHighlightX":-1,"eyeHighlightY":-9,"eyeHighlightFollow":11.5,"eyeSpark":0,"colors":["#ff312e","#ff3a33","#f50700","#ca1502"]}},"gem-green":{"base":["#14ce29","#15d42e","#0f9620","#0d7621"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#14ce29","#15d42e","#0d7621"],"fixedTune":{"faceGlow":0.67,"bandWidth":20,"lineOpacity":0.41,"glintUpper":1,"glintLower":0.43,"eyeTint":0.2,"shadowOpacity":0.8,"shadowDarkness":0.34,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"shadowAngle":117,"antennaHighlight":1,"antennaHighlightSize":0.5,"antennaRimOpacity":1,"eyeHighlight":1,"eyeHighlightSize":26.5,"eyeHighlightSoft":0.98,"eyeHighlightX":-1,"eyeHighlightY":-9,"eyeHighlightFollow":11.5,"eyeSpark":0,"colors":["#14ce29","#15d42e","#0f9620","#0d7621"]}},"gem-purple":{"base":["#992eff","#a033ff","#8200f5","#7902ca"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#992eff","#a033ff","#7902ca"],"fixedTune":{"faceGlow":0.67,"bandWidth":20,"lineOpacity":0.41,"glintUpper":1,"glintLower":0.43,"eyeTint":0.2,"shadowOpacity":0.8,"shadowDarkness":0.34,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"shadowAngle":117,"antennaHighlight":1,"antennaHighlightSize":0.5,"antennaRimOpacity":1,"eyeHighlight":1,"eyeHighlightSize":26.5,"eyeHighlightSoft":0.98,"eyeHighlightX":-1,"eyeHighlightY":-9,"eyeHighlightFollow":11.5,"eyeSpark":0,"colors":["#992eff","#a033ff","#8200f5","#7902ca"]}},"gem-pink":{"base":["#ff2e94","#ff3392","#f50073","#ca0253"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff2e94","#ff3392","#ca0253"],"fixedTune":{"faceGlow":0.67,"bandWidth":20,"lineOpacity":0.41,"glintUpper":1,"glintLower":0.43,"eyeTint":0.2,"shadowOpacity":0.8,"shadowDarkness":0.34,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"shadowAngle":117,"antennaHighlight":1,"antennaHighlightSize":0.5,"antennaRimOpacity":1,"eyeHighlight":1,"eyeHighlightSize":26.5,"eyeHighlightSoft":0.98,"eyeHighlightX":-1,"eyeHighlightY":-9,"eyeHighlightFollow":11.5,"eyeSpark":0,"colors":["#ff2e94","#ff3392","#f50073","#ca0253"]}},"gem-orange":{"base":["#ff7f25","#ff862a","#eb6500","#c16102"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff7f25","#ff862a","#c16102"],"fixedTune":{"faceGlow":0.67,"bandWidth":20,"lineOpacity":0.41,"glintUpper":1,"glintLower":0.43,"eyeTint":0.2,"shadowOpacity":0.8,"shadowDarkness":0.34,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"shadowAngle":117,"antennaHighlight":1,"antennaHighlightSize":0.5,"antennaRimOpacity":1,"eyeHighlight":1,"eyeHighlightSize":26.5,"eyeHighlightSoft":0.98,"eyeHighlightX":-1,"eyeHighlightY":-9,"eyeHighlightFollow":11.5,"eyeSpark":0,"colors":["#ff7f25","#ff862a","#eb6500","#c16102"]}},"gleam-blue":{"base":["#5a9dff","#2f7cf6","#286fea","#1c5ed8"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#5a9dff","#2f7cf6","#1c5ed8"],"fixedTune":{"depth":0.12,"faceGlow":0,"bandWidth":26,"bandLight":0.4,"bandOpacity":0.6,"bandMiddle":0.24,"lineOpacity":1,"glintWidth":3.6,"glintLower":0.85,"eyeTint":0.6,"shadowDistance":8,"shadowBlur":6,"gradient":"radial","centerX":0.5,"centerY":-0.28,"radius":0.92,"stopMid":0.84,"stopEdge":0.85,"bandBlur":1.1,"lineInset":0.87,"lineWidth":1.9,"lineAngle":218,"glintInset":0.95,"glintBlur":0.5,"glintAngle":44,"glintDash":0,"upperEnd":0.2,"lowerStart":0.73,"topGloss":0.75,"topReach":0.58,"topBlur":12,"eyeLitPart":0.38}},"gleam-red":{"base":["#ff5c5a","#f6362f","#ea3328","#d8291c"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff5c5a","#f6362f","#d8291c"],"fixedTune":{"depth":0.12,"faceGlow":0,"bandWidth":26,"bandLight":0.4,"bandOpacity":0.6,"bandMiddle":0.24,"lineOpacity":1,"glintWidth":3.6,"glintLower":0.85,"eyeTint":0.6,"shadowDistance":8,"shadowBlur":6,"gradient":"radial","centerX":0.5,"centerY":-0.28,"radius":0.92,"stopMid":0.84,"stopEdge":0.85,"bandBlur":1.1,"lineInset":0.87,"lineWidth":1.9,"lineAngle":218,"glintInset":0.95,"glintBlur":0.5,"glintAngle":44,"glintDash":0,"upperEnd":0.2,"lowerStart":0.73,"topGloss":0.75,"topReach":0.58,"topBlur":12,"eyeLitPart":0.38}},"gleam-green":{"base":["#2cea41","#1bbf31","#20a434","#1e8630"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#2cea41","#1bbf31","#1e8630"],"fixedTune":{"depth":0.12,"faceGlow":0,"bandWidth":26,"bandLight":0.4,"bandOpacity":0.6,"bandMiddle":0.24,"lineOpacity":1,"glintWidth":3.6,"glintLower":0.85,"eyeTint":0.6,"shadowDistance":8,"shadowBlur":6,"gradient":"radial","centerX":0.5,"centerY":-0.28,"radius":0.92,"stopMid":0.84,"stopEdge":0.85,"bandBlur":1.1,"lineInset":0.87,"lineWidth":1.9,"lineAngle":218,"glintInset":0.95,"glintBlur":0.5,"glintAngle":44,"glintDash":0,"upperEnd":0.2,"lowerStart":0.73,"topGloss":0.75,"topReach":0.58,"topBlur":12,"eyeLitPart":0.38}},"gleam-yellow":{"base":["#ffe35a","#f6d82f","#ead128","#d8c31c"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ffe35a","#f6d82f","#d8c31c"],"fixedTune":{"depth":0.12,"faceGlow":0,"bandWidth":26,"bandLight":0.4,"bandOpacity":0.6,"bandMiddle":0.24,"lineOpacity":1,"glintWidth":3.6,"glintLower":0.85,"eyeTint":0.6,"shadowDistance":8,"shadowBlur":6,"gradient":"radial","centerX":0.5,"centerY":-0.28,"radius":0.92,"stopMid":0.84,"stopEdge":0.85,"bandBlur":1.1,"lineInset":0.87,"lineWidth":1.9,"lineAngle":218,"glintInset":0.95,"glintBlur":0.5,"glintAngle":44,"glintDash":0,"upperEnd":0.2,"lowerStart":0.73,"topGloss":0.75,"topReach":0.58,"topBlur":12,"eyeLitPart":0.38}},"gleam-purple":{"base":["#af5aff","#992ff6","#9428ea","#871cd8"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#af5aff","#992ff6","#871cd8"],"fixedTune":{"depth":0.12,"faceGlow":0,"bandWidth":26,"bandLight":0.4,"bandOpacity":0.6,"bandMiddle":0.24,"lineOpacity":1,"glintWidth":3.6,"glintLower":0.85,"eyeTint":0.6,"shadowDistance":8,"shadowBlur":6,"gradient":"radial","centerX":0.5,"centerY":-0.28,"radius":0.92,"stopMid":0.84,"stopEdge":0.85,"bandBlur":1.1,"lineInset":0.87,"lineWidth":1.9,"lineAngle":218,"glintInset":0.95,"glintBlur":0.5,"glintAngle":44,"glintDash":0,"upperEnd":0.2,"lowerStart":0.73,"topGloss":0.75,"topReach":0.58,"topBlur":12,"eyeLitPart":0.38}},"gleam-pink":{"base":["#ff5ab0","#f62f92","#ea2885","#d81c73"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff5ab0","#f62f92","#d81c73"],"fixedTune":{"depth":0.12,"faceGlow":0,"bandWidth":26,"bandLight":0.4,"bandOpacity":0.6,"bandMiddle":0.24,"lineOpacity":1,"glintWidth":3.6,"glintLower":0.85,"eyeTint":0.6,"shadowDistance":8,"shadowBlur":6,"gradient":"radial","centerX":0.5,"centerY":-0.28,"radius":0.92,"stopMid":0.84,"stopEdge":0.85,"bandBlur":1.1,"lineInset":0.87,"lineWidth":1.9,"lineAngle":218,"glintInset":0.95,"glintBlur":0.5,"glintAngle":44,"glintDash":0,"upperEnd":0.2,"lowerStart":0.73,"topGloss":0.75,"topReach":0.58,"topBlur":12,"eyeLitPart":0.38}},"clear-blue":{"base":["#75acff","#428eff","#337aff","#001442"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#75acff","#428eff","#001442"],"fixedTune":{"depth":0.4,"faceGlow":0.8,"faceX":0.24,"faceY":0.01,"faceSize":0.94,"bandWidth":18,"bandOpacity":0.64,"bandMiddle":0,"lineOpacity":0.81,"glintWidth":3.2,"glintUpper":0.95,"glintLower":0.85,"eyeTint":0.38,"shadowDistance":5,"shadowBlur":1.9,"gradient":"radial","centerX":0.32,"centerY":0.17,"radius":0.95,"stopMid":0.333,"stopEdge":0.67,"depthStart":0.66,"bandBlur":2.6,"lineInset":0.865,"lineWidth":8,"lineBlur":4.4,"lineAngle":243,"glintInset":0.955,"glintBlur":2.6,"glintAngle":89,"glintDash":0,"eyeLitPart":0.38}},"clear-red":{"base":["#ff7a75","#ff4842","#ff4533","#420900"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff7a75","#ff4842","#420900"],"fixedTune":{"depth":0.4,"faceGlow":0.8,"faceX":0.24,"faceY":0.01,"faceSize":0.94,"bandWidth":18,"bandOpacity":0.64,"bandMiddle":0,"lineOpacity":0.81,"glintWidth":3.2,"glintUpper":0.95,"glintLower":0.85,"eyeTint":0.38,"shadowDistance":5,"shadowBlur":1.9,"gradient":"radial","centerX":0.32,"centerY":0.17,"radius":0.95,"stopMid":0.333,"stopEdge":0.67,"depthStart":0.66,"bandBlur":2.6,"lineInset":0.865,"lineWidth":8,"lineBlur":4.4,"lineAngle":243,"glintInset":0.955,"glintBlur":2.6,"glintAngle":89,"glintDash":0,"eyeLitPart":0.38}},"clear-green":{"base":["#48ed5f","#16e332","#15d439","#010d04"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#48ed5f","#16e332","#010d04"],"fixedTune":{"depth":0.4,"faceGlow":0.8,"faceX":0.24,"faceY":0.01,"faceSize":0.94,"bandWidth":18,"bandOpacity":0.64,"bandMiddle":0,"lineOpacity":0.81,"glintWidth":3.2,"glintUpper":0.95,"glintLower":0.85,"eyeTint":0.38,"shadowDistance":5,"shadowBlur":1.9,"shadowOpacity":0.8,"gradient":"radial","centerX":0.32,"centerY":0.17,"radius":0.95,"stopMid":0.333,"stopEdge":0.67,"depthStart":0.66,"bandBlur":2.6,"lineInset":0.865,"lineWidth":8,"lineBlur":4.4,"lineAngle":243,"glintInset":0.955,"glintBlur":2.6,"glintAngle":89,"glintDash":0,"glintMiddle":0,"rimOpacity":0,"topGloss":0,"sideThickness":0,"eyeLitPart":0.38,"shadowSpread":0,"colors":["#48ed5f","#16e332","#15d439","#010d04"]}},"clear-purple":{"base":["#bf75ff","#a742ff","#ab33ff","#2a0042"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#bf75ff","#a742ff","#2a0042"],"fixedTune":{"depth":0.4,"faceGlow":0.8,"faceX":0.24,"faceY":0.01,"faceSize":0.94,"bandWidth":18,"bandOpacity":0.64,"bandMiddle":0,"lineOpacity":0.81,"glintWidth":3.2,"glintUpper":0.95,"glintLower":0.85,"eyeTint":0.38,"shadowDistance":5,"shadowBlur":1.9,"gradient":"radial","centerX":0.32,"centerY":0.17,"radius":0.95,"stopMid":0.333,"stopEdge":0.67,"depthStart":0.66,"bandBlur":2.6,"lineInset":0.865,"lineWidth":8,"lineBlur":4.4,"lineAngle":243,"glintInset":0.955,"glintBlur":2.6,"glintAngle":89,"glintDash":0,"eyeLitPart":0.38}},"clear-pink":{"base":["#ff75b5","#ff429a","#ff3387","#420018"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ff75b5","#ff429a","#420018"],"fixedTune":{"depth":0.4,"faceGlow":0.8,"faceX":0.24,"faceY":0.01,"faceSize":0.94,"bandWidth":18,"bandOpacity":0.64,"bandMiddle":0,"lineOpacity":0.81,"glintWidth":3.2,"glintUpper":0.95,"glintLower":0.85,"eyeTint":0.38,"shadowDistance":5,"shadowBlur":1.9,"gradient":"radial","centerX":0.32,"centerY":0.17,"radius":0.95,"stopMid":0.333,"stopEdge":0.67,"depthStart":0.66,"bandBlur":2.6,"lineInset":0.865,"lineWidth":8,"lineBlur":4.4,"lineAngle":243,"glintInset":0.955,"glintBlur":2.6,"glintAngle":89,"glintDash":0,"eyeLitPart":0.38}},"clear-orange":{"base":["#ffad6d","#ff8f39","#ff922a","#3c2000"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ffad6d","#ff8f39","#3c2000"],"fixedTune":{"depth":0.4,"faceGlow":0.8,"faceX":0.24,"faceY":0.01,"faceSize":0.94,"bandWidth":18,"bandOpacity":0.64,"bandMiddle":0,"lineOpacity":0.81,"glintWidth":3.2,"glintUpper":0.95,"glintLower":0.85,"eyeTint":0.38,"shadowDistance":5,"shadowBlur":1.9,"gradient":"radial","centerX":0.32,"centerY":0.17,"radius":0.95,"stopMid":0.333,"stopEdge":0.67,"depthStart":0.66,"bandBlur":2.6,"lineInset":0.865,"lineWidth":8,"lineBlur":4.4,"lineAngle":243,"glintInset":0.955,"glintBlur":2.6,"glintAngle":89,"glintDash":0,"eyeLitPart":0.38}},"slab-blue":{"base":["#3cabec","#2a67d0","#1e3cb4","#12228a"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#3cabec","#2a67d0","#12228a"],"fixedTune":{"depth":0.79,"faceGlow":0.18,"faceX":0.24,"faceY":0,"faceSize":0.83,"bandWidth":66,"bandOpacity":0.42,"bandMiddle":0.46,"lineOpacity":1,"glintWidth":10.5,"glintLower":0.46,"eyeTint":0.32,"shadowDistance":5.5,"shadowBlur":0,"shadowOpacity":0.78,"angle":45,"stopMid":0.51,"stopEdge":1,"depthStart":0.29,"bandBlur":9.8,"lineInset":0.98,"lineWidth":5.5,"lineAngle":22,"lineOffsetX":-2.1,"lineOffsetY":0.4,"line1Pos":0.28,"line1Opacity":0.82,"line2Pos":0.44,"line2Opacity":0.69,"line3Pos":0.7,"line3Opacity":0.6,"line4Pos":0.44,"line4Opacity":0.47,"line5Pos":0.32,"line5Opacity":0.71,"glintInset":0.8,"glintAngle":87,"glintDash":0,"upperEnd":0.24,"topGloss":0.78,"topReach":0.5,"topBlur":5.2,"sideThickness":16.5,"sideOpacity":0.69,"sideLine":1,"sideLineWidth":3.2,"sideLineScaleX":0.94,"sideLineScaleY":1.045,"sideLineX":0.5,"sideLineY":-4.5,"sideLineFrom":0.72,"sideLineFade":0.13,"eyeLitPart":0.3,"sideColor":"#5ccbff","sideLineColor":"#cceeff"}},"slab-red":{"base":["#ec3c64","#d0302a","#b43c1e","#8a3212"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ec3c64","#d0302a","#8a3212"],"fixedTune":{"depth":0.79,"faceGlow":0.18,"faceX":0.24,"faceY":0,"faceSize":0.83,"bandWidth":66,"bandOpacity":0.42,"bandMiddle":0.46,"lineOpacity":1,"glintWidth":10.5,"glintLower":0.46,"eyeTint":0.32,"shadowDistance":5.5,"shadowBlur":0,"shadowOpacity":0.78,"angle":45,"stopMid":0.51,"stopEdge":1,"depthStart":0.29,"bandBlur":9.8,"lineInset":0.98,"lineWidth":5.5,"lineAngle":22,"lineOffsetX":-2.1,"lineOffsetY":0.4,"line1Pos":0.28,"line1Opacity":0.82,"line2Pos":0.44,"line2Opacity":0.69,"line3Pos":0.7,"line3Opacity":0.6,"line4Pos":0.44,"line4Opacity":0.47,"line5Pos":0.32,"line5Opacity":0.71,"glintInset":0.8,"glintAngle":87,"glintDash":0,"upperEnd":0.24,"topGloss":0.78,"topReach":0.5,"topBlur":5.2,"sideThickness":16.5,"sideOpacity":0.69,"sideLine":1,"sideLineWidth":3.2,"sideLineScaleX":0.94,"sideLineScaleY":1.045,"sideLineX":0.5,"sideLineY":-4.5,"sideLineFrom":0.72,"sideLineFade":0.13,"eyeLitPart":0.3,"sideColor":"#ff5c8a","sideLineColor":"#ffccda"}},"slab-green":{"base":["#a6ec3c","#62d02a","#38b41e","#1e8a12"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#a6ec3c","#62d02a","#1e8a12"],"fixedTune":{"depth":0.79,"faceGlow":0.18,"faceX":0.24,"faceY":0,"faceSize":0.83,"bandWidth":66,"bandOpacity":0.42,"bandMiddle":0.46,"lineOpacity":1,"glintWidth":10.5,"glintLower":0.46,"eyeTint":0.32,"shadowDistance":5.5,"shadowBlur":0,"shadowOpacity":0.78,"angle":45,"stopMid":0.51,"stopEdge":1,"depthStart":0.29,"bandBlur":9.8,"lineInset":0.98,"lineWidth":5.5,"lineAngle":22,"lineOffsetX":-2.1,"lineOffsetY":0.4,"line1Pos":0.28,"line1Opacity":0.82,"line2Pos":0.44,"line2Opacity":0.69,"line3Pos":0.7,"line3Opacity":0.6,"line4Pos":0.44,"line4Opacity":0.47,"line5Pos":0.32,"line5Opacity":0.71,"glintInset":0.8,"glintAngle":87,"glintDash":0,"upperEnd":0.24,"topGloss":0.78,"topReach":0.5,"topBlur":5.2,"sideThickness":16.5,"sideOpacity":0.69,"sideLine":1,"sideLineWidth":3.2,"sideLineScaleX":0.94,"sideLineScaleY":1.045,"sideLineX":0.5,"sideLineY":-4.5,"sideLineFrom":0.72,"sideLineFade":0.13,"eyeLitPart":0.3,"sideColor":"#c6ff5c","sideLineColor":"#edffcc"}},"slab-purple":{"base":["#6b3cec","#832ad0","#871eb4","#6e128a"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#6b3cec","#832ad0","#6e128a"],"fixedTune":{"depth":0.79,"faceGlow":0.18,"faceX":0.24,"faceY":0,"faceSize":0.83,"bandWidth":66,"bandOpacity":0.42,"bandMiddle":0.46,"lineOpacity":1,"glintWidth":10.5,"glintLower":0.46,"eyeTint":0.32,"shadowDistance":5.5,"shadowBlur":0,"shadowOpacity":0.78,"angle":45,"stopMid":0.51,"stopEdge":1,"depthStart":0.29,"bandBlur":9.8,"lineInset":0.98,"lineWidth":5.5,"lineAngle":22,"lineOffsetX":-2.1,"lineOffsetY":0.4,"line1Pos":0.28,"line1Opacity":0.82,"line2Pos":0.44,"line2Opacity":0.69,"line3Pos":0.7,"line3Opacity":0.6,"line4Pos":0.44,"line4Opacity":0.47,"line5Pos":0.32,"line5Opacity":0.71,"glintInset":0.8,"glintAngle":87,"glintDash":0,"upperEnd":0.24,"topGloss":0.78,"topReach":0.5,"topBlur":5.2,"sideThickness":16.5,"sideOpacity":0.69,"sideLine":1,"sideLineWidth":3.2,"sideLineScaleX":0.94,"sideLineScaleY":1.045,"sideLineX":0.5,"sideLineY":-4.5,"sideLineFrom":0.72,"sideLineFade":0.13,"eyeLitPart":0.3,"sideColor":"#805cff","sideLineColor":"#d7ccff"}},"slab-pink":{"base":["#ec3cc3","#d02a7d","#b41e50","#8a1232"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#ec3cc3","#d02a7d","#8a1232"],"fixedTune":{"depth":0.79,"faceGlow":0.18,"faceX":0.24,"faceY":0,"faceSize":0.83,"bandWidth":66,"bandOpacity":0.42,"bandMiddle":0.46,"lineOpacity":1,"glintWidth":10.5,"glintLower":0.46,"eyeTint":0.32,"shadowDistance":5.5,"shadowBlur":0,"shadowOpacity":0.78,"angle":45,"stopMid":0.51,"stopEdge":1,"depthStart":0.29,"bandBlur":9.8,"lineInset":0.98,"lineWidth":5.5,"lineAngle":22,"lineOffsetX":-2.1,"lineOffsetY":0.4,"line1Pos":0.28,"line1Opacity":0.82,"line2Pos":0.44,"line2Opacity":0.69,"line3Pos":0.7,"line3Opacity":0.6,"line4Pos":0.44,"line4Opacity":0.47,"line5Pos":0.32,"line5Opacity":0.71,"glintInset":0.8,"glintAngle":87,"glintDash":0,"upperEnd":0.24,"topGloss":0.78,"topReach":0.5,"topBlur":5.2,"sideThickness":16.5,"sideOpacity":0.69,"sideLine":1,"sideLineWidth":3.2,"sideLineScaleX":0.94,"sideLineScaleY":1.045,"sideLineX":0.5,"sideLineY":-4.5,"sideLineFrom":0.72,"sideLineFade":0.13,"eyeLitPart":0.3,"sideColor":"#ff5ce1","sideLineColor":"#ffccf5"}},"slab-orange":{"base":["#f1856f","#de9259","#dc962e","#b68218"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#f1856f","#de9259","#b68218"],"fixedTune":{"depth":0.79,"faceGlow":0.18,"faceX":0.24,"faceY":0,"faceSize":0.83,"bandWidth":66,"bandOpacity":0.42,"bandMiddle":0.46,"lineOpacity":1,"glintWidth":10.5,"glintLower":0.46,"eyeTint":0.32,"shadowDistance":5.5,"shadowBlur":0,"shadowOpacity":0.78,"angle":45,"stopMid":0.51,"stopEdge":1,"depthStart":0.29,"bandBlur":9.8,"lineInset":0.98,"lineWidth":5.5,"lineAngle":22,"lineOffsetX":-2.1,"lineOffsetY":0.4,"line1Pos":0.28,"line1Opacity":0.82,"line2Pos":0.44,"line2Opacity":0.69,"line3Pos":0.7,"line3Opacity":0.6,"line4Pos":0.44,"line4Opacity":0.47,"line5Pos":0.32,"line5Opacity":0.71,"glintInset":0.8,"glintAngle":87,"glintDash":0,"upperEnd":0.24,"topGloss":0.78,"topReach":0.5,"topBlur":5.2,"sideThickness":16.5,"sideOpacity":0.69,"sideLine":1,"sideLineWidth":3.2,"sideLineScaleX":0.94,"sideLineScaleY":1.045,"sideLineX":0.5,"sideLineY":-4.5,"sideLineFrom":0.72,"sideLineFade":0.13,"eyeLitPart":0.3,"sideColor":"#ff9c8e","sideLineColor":"#fff2f1"}},"prism-indigo":{"base":["#3f81f0","#4034c9","#bb30e8","#ff1f8b"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#3f81f0","#4034c9","#ff1f8b"],"fixedTune":{"faceGlow":0,"bandWidth":0,"lineOpacity":1,"glintUpper":1,"glintLower":0.61,"eyeTint":0.44,"shadowDistance":8,"shadowBlur":0,"shadowOpacity":1,"shadowDarkness":0,"angle":0,"depthStart":0.54,"lineInset":0.93,"lineWidth":1.9,"lineAngle":293,"lineOffsetY":-10,"line1Pos":0.44,"line1Opacity":1,"line2Pos":0.28,"line3Pos":1,"line4Opacity":0.49,"line5Pos":0.57,"glintInset":0.8,"glintAngle":90,"glintOffsetY":-4.4,"glintDash":0,"upperEnd":0.22,"lowerStart":1,"glintMiddle":0.29,"middleEnd":0.43,"rimOpacity":0,"topGloss":0.81,"topReach":0.54,"topBlur":10.1,"sideThickness":15,"sideTone":-0.24,"sideOpacity":1,"sideLine":1,"sideLineTone":0.88,"sideLineWidth":2.7,"sideLineScaleX":0.925,"sideLineScaleY":0.935,"sideLineFrom":0.77,"sideLineFade":0.28,"eyeLitPart":0.25,"shadowSpread":0.1,"antennaHighlight":1,"antennaHighlightSize":0.65,"antennaHighlightX":0.25,"antennaHighlightY":0.26,"eyeHighlight":0.76,"eyeHighlightSize":27,"eyeHighlightSoft":0.98,"eyeHighlightX":-5,"eyeHighlightY":5.5,"eyeHighlightFollow":12.5,"eyeSpark":0,"colors":["#3f81f0","#4034c9","#bb30e8","#ff1f8b"],"shadowColor":"#341a9e","antennaMid":"#748cd8","antennaDark":"#fba7f2","line2Color":"#ffffff","line3Color":"#ffffff"}},"prism-red":{"base":["#f03f8a","#c93934","#e8b230","#97ff1f"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#f03f8a","#c93934","#97ff1f"],"fixedTune":{"faceGlow":0,"bandWidth":0,"lineOpacity":1,"glintUpper":1,"glintLower":0.61,"eyeTint":0.44,"shadowDistance":8,"shadowBlur":0,"shadowOpacity":1,"shadowDarkness":0,"angle":0,"depthStart":0.54,"lineInset":0.93,"lineWidth":1.9,"lineAngle":293,"lineOffsetY":-10,"line1Pos":0.44,"line1Opacity":1,"line2Pos":0.28,"line3Pos":1,"line4Opacity":0.49,"line5Pos":0.57,"glintInset":0.8,"glintAngle":90,"glintOffsetY":-4.4,"glintDash":0,"upperEnd":0.22,"lowerStart":1,"glintMiddle":0.29,"middleEnd":0.43,"rimOpacity":0,"topGloss":0.81,"topReach":0.54,"topBlur":10.1,"sideThickness":15,"sideTone":-0.24,"sideOpacity":1,"sideLine":1,"sideLineTone":0.88,"sideLineWidth":2.7,"sideLineScaleX":0.925,"sideLineScaleY":0.935,"sideLineFrom":0.77,"sideLineFade":0.28,"eyeLitPart":0.25,"shadowSpread":0.1,"antennaHighlight":1,"antennaHighlightSize":0.65,"antennaHighlightX":0.25,"antennaHighlightY":0.26,"eyeHighlight":0.76,"eyeHighlightSize":27,"eyeHighlightSoft":0.98,"eyeHighlightX":-5,"eyeHighlightY":5.5,"eyeHighlightFollow":12.5,"eyeSpark":0,"colors":["#f03f8a","#c93934","#e8b230","#97ff1f"],"shadowColor":"#9e2d1a","antennaMid":"#d87491","antennaDark":"#f6fba7","line2Color":"#ffffff","line3Color":"#ffffff"}},"prism-lime":{"base":["#cfe537","#6ab237","#29dc57","#18eff3"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#cfe537","#6ab237","#18eff3"],"fixedTune":{"faceGlow":0,"bandWidth":0,"lineOpacity":1,"glintUpper":1,"glintLower":0.61,"eyeTint":0.44,"shadowDistance":8,"shadowBlur":0,"shadowOpacity":1,"shadowDarkness":0,"angle":0,"depthStart":0.54,"lineInset":0.93,"lineWidth":1.9,"lineAngle":293,"lineOffsetY":-10,"line1Pos":0.44,"line1Opacity":1,"line2Pos":0.28,"line3Pos":1,"line4Opacity":0.49,"line5Pos":0.57,"glintInset":0.8,"glintAngle":90,"glintOffsetY":-4.4,"glintDash":0,"upperEnd":0.22,"lowerStart":1,"glintMiddle":0.29,"middleEnd":0.43,"rimOpacity":0,"topGloss":0.81,"topReach":0.54,"topBlur":10.1,"sideThickness":15,"sideTone":-0.24,"sideOpacity":1,"sideLine":1,"sideLineTone":0.88,"sideLineWidth":2.7,"sideLineScaleX":0.925,"sideLineScaleY":0.935,"sideLineFrom":0.77,"sideLineFade":0.28,"eyeLitPart":0.25,"shadowSpread":0.1,"antennaHighlight":1,"antennaHighlightSize":0.65,"antennaHighlightX":0.25,"antennaHighlightY":0.26,"eyeHighlight":0.76,"eyeHighlightSize":27,"eyeHighlightSoft":0.98,"eyeHighlightX":-5,"eyeHighlightY":5.5,"eyeHighlightFollow":12.5,"eyeSpark":0,"colors":["#cfe537","#6ab237","#29dc57","#18eff3"],"shadowColor":"#3e891d","antennaMid":"#b5cf6c","antennaDark":"#9ff6d4","line2Color":"#ffffff","line3Color":"#ffffff"}},"prism-rose":{"base":["#f03fbc","#c93459","#e87e30","#d6ff1f"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#f03fbc","#c93459","#d6ff1f"],"fixedTune":{"faceGlow":0,"bandWidth":0,"lineOpacity":1,"glintUpper":1,"glintLower":0.61,"eyeTint":0.44,"shadowDistance":8,"shadowBlur":0,"shadowOpacity":1,"shadowDarkness":0,"angle":0,"depthStart":0.54,"lineInset":0.93,"lineWidth":1.9,"lineAngle":293,"lineOffsetY":-10,"line1Pos":0.44,"line1Opacity":1,"line2Pos":0.28,"line3Pos":1,"line4Opacity":0.49,"line5Pos":0.57,"glintInset":0.8,"glintAngle":90,"glintOffsetY":-4.4,"glintDash":0,"upperEnd":0.22,"lowerStart":1,"glintMiddle":0.29,"middleEnd":0.43,"rimOpacity":0,"topGloss":0.81,"topReach":0.54,"topBlur":10.1,"sideThickness":15,"sideTone":-0.24,"sideOpacity":1,"sideLine":1,"sideLineTone":0.88,"sideLineWidth":2.7,"sideLineScaleX":0.925,"sideLineScaleY":0.935,"sideLineFrom":0.77,"sideLineFade":0.28,"eyeLitPart":0.25,"shadowSpread":0.1,"antennaHighlight":1,"antennaHighlightSize":0.65,"antennaHighlightX":0.25,"antennaHighlightY":0.26,"eyeHighlight":0.76,"eyeHighlightSize":27,"eyeHighlightSoft":0.98,"eyeHighlightX":-5,"eyeHighlightY":5.5,"eyeHighlightFollow":12.5,"eyeSpark":0,"colors":["#f03fbc","#c93459","#e87e30","#d6ff1f"],"shadowColor":"#9e1a2c","antennaMid":"#d874ad","antennaDark":"#fbe8a7","line2Color":"#ffffff","line3Color":"#ffffff"}},"prism-sky":{"base":["#3ff0da","#3497c9","#3130e8","#cb1fff"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#3ff0da","#3497c9","#cb1fff"],"fixedTune":{"faceGlow":0,"bandWidth":0,"lineOpacity":1,"glintUpper":1,"glintLower":0.61,"eyeTint":0.44,"shadowDistance":8,"shadowBlur":0,"shadowOpacity":1,"shadowDarkness":0,"angle":0,"depthStart":0.54,"lineInset":0.93,"lineWidth":1.9,"lineAngle":293,"lineOffsetY":-10,"line1Pos":0.44,"line1Opacity":1,"line2Pos":0.28,"line3Pos":1,"line4Opacity":0.49,"line5Pos":0.57,"glintInset":0.8,"glintAngle":90,"glintOffsetY":-4.4,"glintDash":0,"upperEnd":0.22,"lowerStart":1,"glintMiddle":0.29,"middleEnd":0.43,"rimOpacity":0,"topGloss":0.81,"topReach":0.54,"topBlur":10.1,"sideThickness":15,"sideTone":-0.24,"sideOpacity":1,"sideLine":1,"sideLineTone":0.88,"sideLineWidth":2.7,"sideLineScaleX":0.925,"sideLineScaleY":0.935,"sideLineFrom":0.77,"sideLineFade":0.28,"eyeLitPart":0.25,"shadowSpread":0.1,"antennaHighlight":1,"antennaHighlightSize":0.65,"antennaHighlightX":0.25,"antennaHighlightY":0.26,"eyeHighlight":0.76,"eyeHighlightSize":27,"eyeHighlightSoft":0.98,"eyeHighlightX":-5,"eyeHighlightY":5.5,"eyeHighlightFollow":12.5,"eyeSpark":0,"colors":["#3ff0da","#3497c9","#3130e8","#cb1fff"],"shadowColor":"#1a639e","antennaMid":"#74d7d8","antennaDark":"#c5a7fb","line2Color":"#ffffff","line3Color":"#ffffff"}},"prism-purple":{"base":["#9a3ff0","#c534c9","#e83071","#ff7a1f"],"sheen":"slab","acrylic":true,"faithful":true,"plain":true,"eye":"#ffffff","rim":"#ffffff","shine":0,"shade":0,"antenna":["#9a3ff0","#c534c9","#ff7a1f"],"fixedTune":{"faceGlow":0,"bandWidth":0,"lineOpacity":1,"glintUpper":1,"glintLower":0.61,"eyeTint":0.44,"shadowDistance":8,"shadowBlur":0,"shadowOpacity":1,"shadowDarkness":0,"angle":0,"depthStart":0.54,"lineInset":0.93,"lineWidth":1.9,"lineAngle":293,"lineOffsetY":-10,"line1Pos":0.44,"line1Opacity":1,"line2Pos":0.28,"line3Pos":1,"line4Opacity":0.49,"line5Pos":0.57,"glintInset":0.8,"glintAngle":90,"glintOffsetY":-4.4,"glintDash":0,"upperEnd":0.22,"lowerStart":1,"glintMiddle":0.29,"middleEnd":0.43,"rimOpacity":0,"topGloss":0.81,"topReach":0.54,"topBlur":10.1,"sideThickness":15,"sideTone":-0.24,"sideOpacity":1,"sideLine":1,"sideLineTone":0.88,"sideLineWidth":2.7,"sideLineScaleX":0.925,"sideLineScaleY":0.935,"sideLineFrom":0.77,"sideLineFade":0.28,"eyeLitPart":0.25,"shadowSpread":0.1,"antennaHighlight":1,"antennaHighlightSize":0.65,"antennaHighlightX":0.25,"antennaHighlightY":0.26,"eyeHighlight":0.76,"eyeHighlightSize":27,"eyeHighlightSoft":0.98,"eyeHighlightX":-5,"eyeHighlightY":5.5,"eyeHighlightFollow":12.5,"eyeSpark":0,"colors":["#9a3ff0","#c534c9","#e83071","#ff7a1f"],"shadowColor":"#9e1a93","antennaMid":"#b574d8","antennaDark":"#fba7a7","line2Color":"#ffffff","line3Color":"#ffffff"}},"ocean":{"base":["#7ff0e4","#3cc3e0","#2b7fd4","#3b3fb0"],"eye":"#132a4a","rim":"#e8fffd","shine":0.8,"shade":0.3,"antenna":["#e8fffd","#3cc3e0","#3b3fb0"],"linear":true},"forest":{"base":["#d9f99d","#7bd389","#2f9e7a","#1e5f6b"],"eye":"#12302a","rim":"#f4ffe8","shine":0.75,"shade":0.3,"antenna":["#f4ffe8","#7bd389","#1e5f6b"],"linear":true},"gradient-yellow":{"base":["#f0bf7f","#e0c73c","#b6d42b","#65b03b"],"eye":"#3c4a13","rim":"#fff5e8","shine":0.8,"shade":0.3,"antenna":["#fff5e8","#e0c73c","#65b03b"],"linear":true},"gradient-purple":{"base":["#a29aeb","#c592f1","#eb95cf","#e7c49b"],"eye":"#522d51","rim":"#fff6f4","shine":0.82,"shade":0.3,"antenna":["#efedff","#e99bec","#a5567e"],"linear":true},"candy":{"base":["#ffe1f0","#ff9cc8","#b18cff","#7fd4ff"],"eye":"#3a2152","rim":"#fff4fb","shine":0.85,"shade":0.26,"antenna":["#fff4fb","#ff9cc8","#7f6bd6"],"linear":true},"aurora":{"base":["#9aebea","#92c9f1","#a795eb","#e79bc7"],"eye":"#2d3052","rim":"#fff4fe","shine":0.82,"shade":0.3,"antenna":["#edffff","#9ba7ec","#7456a5"],"linear":true},"pearl":{"base":["#ffffff","#eaf2ff","#f4e9ff","#e2f6ee"],"eye":"#3b3a4a","rim":"#ffffff","shine":0.92,"shade":0.16,"antenna":["#ffffff","#ece6fb","#b9b4cf"],"linear":true},"sakura":{"base":["#ffffff","#fdeaf0","#f3c7d4","#d697ab"],"eye":"#4b2f3b","rim":"#ffffff","shine":0.9,"shade":0.2,"antenna":["#ffffff","#f7d3de","#cf8ea3"]},"lavender":{"base":["#ffffff","#ece6ff","#cbbcf2","#9c88d6"],"eye":"#33294d","rim":"#ffffff","shine":0.9,"shade":0.2,"antenna":["#ffffff","#ddd3fb","#9c88d6"]},"lemon":{"base":["#fffef0","#fff2a8","#f5d757","#c9a21d"],"eye":"#3d3510","rim":"#fffbe0","shine":0.9,"shade":0.22,"antenna":["#ffffff","#fbe58a","#c9a21d"]},"sky":{"base":["#ffffff","#dff1ff","#a9d6f7","#5ea4d8"],"eye":"#1d3a52","rim":"#ffffff","shine":0.9,"shade":0.22,"antenna":["#ffffff","#c7e6fb","#5ea4d8"]},"mint":{"base":["#f5fffb","#cdf3e5","#93dcc3","#52ad90"],"eye":"#1f3f36","rim":"#f2fffa","shine":0.85,"shade":0.24,"antenna":["#ffffff","#b5ead6","#4fa88b"]},"obsidian":{"base":["#7b8796","#313a47","#101722","#05080e"],"eye":"#f0f7ff","rim":"#c1d5f4","shine":0.57,"shade":0.68,"antenna":["#c3cfdf","#384452","#080e19"]},"midnight":{"base":["#7f98d4","#2c4381","#111e4a","#060b22"],"eye":"#eaf2ff","rim":"#a9c2ff","shine":0.55,"shade":0.62,"antenna":["#c9d7ff","#33498a","#070d26"]},"jade":{"base":["#7fb5a3","#2f6d5b","#123e33","#06201a"],"eye":"#ecfff7","rim":"#aee8d3","shine":0.5,"shade":0.6,"antenna":["#c8f0e2","#2f6d5b","#05201a"]},"amber":{"base":["#ffe2a6","#f2a73c","#c46812","#6b3005"],"eye":"#fff7e8","rim":"#ffdba0","shine":0.6,"shade":0.5,"antenna":["#fff0cc","#e08c22","#6b3005"]},"ruby":{"base":["#ff8a9a","#d0243f","#7a0d22","#3a0410"],"eye":"#fff0f2","rim":"#ffb3bf","shine":0.7,"shade":0.6,"antenna":["#ffd0d8","#d0243f","#3a0410"]},"sapphire":{"base":["#8fc0ff","#2a6fd6","#103f8c","#071d44"],"eye":"#eef6ff","rim":"#bcd8ff","shine":0.7,"shade":0.6,"antenna":["#d4e7ff","#2a6fd6","#071d44"]},"emerald":{"base":["#8ff0c0","#1fae74","#0b6a45","#033321"],"eye":"#effff7","rim":"#b8f7d9","shine":0.7,"shade":0.55,"antenna":["#d2fbe6","#1fae74","#033321"]},"soccer-grooves":{"base":["#4a4d55","#24262b","#131417","#060607"],"ball":true,"eye":"#ffffff","rim":"#e8eef8","shine":0.7,"shade":0.5,"antenna":["#4a4d55","#24262b","#060607"],"pattern":{"kind":"grooves","colors":["#c6ff2e","#000000"]}},"basketball":{"base":["#ffb066","#f07a1e","#c95a0e","#7a3305"],"ball":true,"eye":"#fff3e6","ring":"#2a1408","rim":"#ffffff","shine":0.55,"shade":0.45,"antenna":["#ffb066","#f07a1e","#7a3305"],"pattern":{"kind":"basketball","colors":["#2a1408","#7a3305"]}},"football":{"base":["#c07a4a","#8f4a22","#6a3112","#3a1606"],"ball":true,"eye":"#fff3e6","ring":"#2a1004","rim":"#e8eef8","shine":0.5,"shade":0.5,"antenna":["#c07a4a","#8f4a22","#3a1606"],"pattern":{"kind":"football","colors":["#ffffff","#3a1606","#2a1004"]}},"tennis":{"base":["#f4ff8a","#d7f43a","#aacb10","#66800a"],"ball":true,"eye":"#2f3a08","rim":"#ffffff","shine":0.6,"shade":0.3,"antenna":["#f4ff8a","#d7f43a","#66800a"],"pattern":{"kind":"tennis","colors":["#ffffff","#6a8a00"]}},"baseball":{"base":["#fffdf7","#f6f1e6","#e2d9c7","#b5a98f"],"ball":true,"eye":"#2a2a2e","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#fffdf7","#f6f1e6","#b5a98f"],"pattern":{"kind":"baseball","colors":["#c9a9a0","#d6333a"]}},"softball":{"base":["#fbff9a","#eef55a","#cfd61f","#8a8f0a"],"ball":true,"eye":"#3a3a10","rim":"#ffffff","shine":0.75,"shade":0.3,"antenna":["#fbff9a","#eef55a","#8a8f0a"],"pattern":{"kind":"baseball","colors":["#b8a640","#d6333a"]}},"cricket":{"base":["#e05a4f","#b52a22","#851812","#470806"],"ball":true,"eye":"#fff3ee","rim":"#e8eef8","shine":0.6,"shade":0.5,"antenna":["#e05a4f","#b52a22","#470806"],"pattern":{"kind":"cricket","colors":["#5a0c08","#f6ead6"]}},"golf":{"base":["#ffffff","#f6f6f4","#e2e2de","#acaca6"],"ball":true,"eye":"#2a2f36","rim":"#ffffff","shine":0.95,"shade":0.25,"antenna":["#ffffff","#f6f6f4","#acaca6"],"pattern":{"kind":"golf","colors":["#c3c9d2","#ffffff"]}},"tartan":{"base":["#d04848","#a82d30","#7d1c20","#4a0e12"],"eye":"#fff6e8","ring":"#2a0b0e","rim":"#e8eef8","shine":0.5,"shade":0.55,"antenna":["#d04848","#a82d30","#4a0e12"],"pattern":{"kind":"tartan","colors":["#1b2a4a","#f2d36b","#0f3d2a"]}},"marble":{"base":["#ffffff","#f6f6f4","#e6e6e2","#bcbcb6"],"eye":"#2a2a2e","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#ffffff","#f6f6f4","#bcbcb6"],"pattern":{"kind":"marble","colors":["#8d8d93","#c7a76b"]}},"black-marble":{"base":["#4a4a4f","#25252a","#141417","#050506"],"eye":"#f7f2e6","rim":"#e8eef8","shine":0.5,"shade":0.55,"antenna":["#4a4a4f","#25252a","#050506"],"pattern":{"kind":"marble","colors":["#d9b46a","#f2f2f2"],"seed":33}},"leaves":{"base":["#f0fbe6","#cfeeb2","#97d37b","#4f9a45"],"eye":"#173a17","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#f0fbe6","#cfeeb2","#4f9a45"],"pattern":{"kind":"scatter","shapes":["leaf"],"colors":["#3e8a3a","#7cc46a"],"count":18,"k":[6,10]}},"blossoms":{"base":["#fffafd","#ffeaf2","#ffd0e0","#f0a3c0"],"eye":"#4b2032","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#fffafd","#ffeaf2","#f0a3c0"],"pattern":{"kind":"scatter","shapes":["blossom"],"colors":["#ff9fbf","#ffe1ec"],"count":12,"k":[6,9]}},"neon":{"base":["#5b3df5","#2a1673","#120a3a","#06041a"],"eye":"#7ff9ff","rim":"#7ff9ff","shine":0.45,"shade":0.6,"antenna":["#c9fbff","#5b3df5","#06041a"]},"polka":{"base":["#ff9db2","#ff5d84","#e03a64","#9b1c3e"],"eye":"#ffffff","ring":"#7a1430","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#ff9db2","#ff5d84","#9b1c3e"],"pattern":{"kind":"dots","colors":["#ffffff"],"r":6,"size":30,"opacity":0.9}},"confetti":{"base":["#ffffff","#fbfaf7","#efece6","#c9c4bb"],"eye":"#2a2a35","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#ffffff","#fbfaf7","#c9c4bb"],"pattern":{"kind":"confetti","colors":["#ff5d8a","#3da9fc","#ffd23f","#2cb67d","#7f5af0"]}},"halftone":{"base":["#ffe6ee","#ffb3c7","#ff7aa0","#d6406c"],"eye":"#4a1428","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#ffe6ee","#ffb3c7","#d6406c"],"pattern":{"kind":"halftone","colors":["#ffffff"]}},"terrazzo":{"base":["#ffffff","#fbfaf7","#efece6","#c9c4bb"],"eye":"#2d2a26","rim":"#ffffff","shine":0.8,"shade":0.28,"antenna":["#ffffff","#fbfaf7","#c9c4bb"],"pattern":{"kind":"terrazzo","colors":["#e46a4f","#2e7d6b","#f2b33d","#3d5aa8","#9aa0a6"]}},"pixel":{"base":["#3a3a4a","#24242f","#16161d","#0a0a10"],"eye":"#ffffff","ring":"#16161a","rim":"#e8eef8","shine":0.5,"shade":0.55,"antenna":["#3a3a4a","#24242f","#0a0a10"],"pattern":{"kind":"pixel","colors":["#7f5af0","#2cb67d","#ff8906","#f25f4c","#3da9fc"]}}};
13
+ const SHEENS = Object.freeze({
14
+ gloss: { top: .78, reach: .36, bottom: .3 },
15
+ slab: { top: .26, reach: .3, bottom: 0, slab: true },
16
+ });
17
+
18
+
19
+ // Live tuning for the faithful acrylic, read each time the skin is painted. A
20
+ // tuner page can set window.AgentRobotAvatarAcrylicConfig and repaint.
21
+ // Defaults are the look tuned by hand in demo/acrylic-tuner.html.
22
+ const ACRYLIC_DEFAULTS = Object.freeze({
23
+ colors: null, // [top, middle, edge, shadow] to override the skin's own colours
24
+ vertical: false, // colour runs top to bottom instead of corner to corner
25
+ depth: 0.8, // darkening toward the edge
26
+ faceGlow: 0.67, // soft light on the face
27
+ faceX: 0.25, faceY: 0.03, faceSize: 0.66,
28
+ bandWidth: 20, // width of the bevel band along the edge
29
+ bandLight: 0, // how much lighter the bevel is than the colour
30
+ bandOpacity: 1, // bevel strength at the top and bottom
31
+ bandMiddle: 0.98, // bevel strength along the sides
32
+ edgeBlur: 0.6, // softness of the bevel and glints
33
+ lineOpacity: 0.41, // inner bevel line
34
+ glintWidth: 12, // white glint lines
35
+ glintUpper: 1, // glint on the upper right
36
+ glintLower: 0.21, // glint along the bottom
37
+ eyeTint: 0.37, // colour picked up by the eyes' shaded side
38
+ eyeSoftness: 0, // blur of the eyes seen through the acrylic
39
+ shadowDistance: 6,
40
+ shadowBlur: 0.6,
41
+ shadowOpacity: 0.8,
42
+ shadowDarkness: 0.25, // 0 = middle colour, 1 = the shadow colour
43
+ // Finer controls, applied by applyAcrylicTuning() after the skin is painted.
44
+ gradient: 'linear', // 'auto' keeps the skin's own; 'radial' or 'linear'
45
+ angle: 80, // linear direction in degrees, 90 = top to bottom
46
+ centerX: 0.75, centerY: 0.46, radius: 1.11,
47
+ stopMid: 0.71, stopEdge: 0.96,
48
+ depthStart: 0.59, // where the edge darkening begins, 0..1 from the middle
49
+ bandColor: '', // bevel colour; empty = a lighter version of the top colour
50
+ bandBlur: 6.2,
51
+ lineInset: 0.855, lineWidth: 4.4, lineBlur: 0, lineAngle: 250, lineOffsetX: 0, lineOffsetY: 0,
52
+ // Five colour stops along the inner line; an empty colour is a light tint of
53
+ // the skin, never a dark one.
54
+ line1Pos: 0.64, line1Color: '#ffffff', line1Opacity: 0.29,
55
+ line2Pos: 0.36, line2Color: '', line2Opacity: 0,
56
+ line3Pos: 0.43, line3Color: '', line3Opacity: 0.67,
57
+ line4Pos: 0.51, line4Color: '', line4Opacity: 0.95,
58
+ line5Pos: 0.68, line5Color: '', line5Opacity: 0,
59
+ lineFollow: false, // turn the line's bright end toward the light
60
+ glintInset: 0.82, glintBlur: 0, glintColor: '#ffffff', glintAngle: 40, glintFollow: 0,
61
+ glintOffsetX: 0, glintOffsetY: 0, glintDash: 30.5, glintGap: 14.5,
62
+ upperStart: 0, upperEnd: 0.19, lowerStart: 0.81, lowerEnd: 1,
63
+ glintMiddle: 0, middleStart: 1, middleEnd: 0.02,
64
+ rimOpacity: 0, rimWidth: 0, rimColor: '#ffffff',
65
+ topGloss: 0, topReach: 0.32, topBlur: 3.4,
66
+ // sideTone: positive lightens the side wall, negative darkens it.
67
+ sideThickness: 0, sideTone: 0.45, sideOpacity: 0.75, sideColor: '',
68
+ // sideLineTone: 1 = white, 0 = the skin colour, -1 = the darkest tone.
69
+ sideLine: 0.85, sideLineTone: 1, sideLineColor: '', sideLineWidth: 2.4,
70
+ // The edge line is a copy of the head outline: scale and move it freely,
71
+ // choose where along the edge it shows, and fade its ends.
72
+ sideLineScaleX: 1, sideLineScaleY: 1, sideLineX: 0, sideLineY: 0,
73
+ sideLineFrom: .7, sideLineTo: 1, sideLineFade: .12, sideLineSideFade: 0,
74
+ sideLineBlur: 0, sideLineDash: 0, sideLineGap: 0, sideLineCap: 'round',
75
+ eyeColor: '#ffffff', eyeOpacity: 1, eyeLitPart: 0.34, eyeRimWidth: 0, eyeRimOpacity: 1,
76
+ shadowColor: '', shadowSpread: 0.3, shadowFollow: true, shadowAngle: 135,
77
+ // Antenna sphere.
78
+ antennaLight: '', antennaMid: '', antennaDark: '', // empty = from the skin colours
79
+ antennaHighlight: .9, antennaHighlightSize: .22, // white spot on the sphere
80
+ antennaHighlightX: .3, antennaHighlightY: .25, antennaFollow: true,
81
+ antennaOpacity: 1, antennaSize: 1,
82
+ antennaRimColor: '#ffffff', antennaRimWidth: 0, antennaRimOpacity: .6,
83
+ // Highlight on the eyes, in each eye's own units (the eye is about 54 wide).
84
+ // Eyes stay plain unless a look adds its own highlight.
85
+ eyeHighlight: 0, eyeHighlightSize: 7, eyeHighlightSoft: .75,
86
+ eyeHighlightX: -9, eyeHighlightY: -10, eyeHighlightFollow: 6,
87
+ eyeSpark: 0, eyeSparkSize: 3.5, eyeSparkX: 8, eyeSparkY: 9,
88
+ });
89
+ function tunedAcrylic(look, own) {
90
+ // An avatar with its own tuning is fixed: the page-wide config (the tuner's
91
+ // live sliders) never reaches it, so editing one look leaves the others alone.
92
+ const tune = { ...ACRYLIC_DEFAULTS, ...own };
93
+ const base = Array.isArray(tune.colors) && tune.colors.length === 4 ? tune.colors : look.base;
94
+ return { ...look, base, antenna: [base[0], base[1], base[3]], linear: tune.vertical ? 'vertical' : look.linear, shade: tune.depth, tune };
95
+ }
96
+
97
+ // Applies the fine acrylic controls on top of the painted skin.
98
+ function applyAcrylicTuning(skin, look) {
99
+ const t = look.tune, base = look.base, p = skin.sheenParts, defs = skin.defs;
100
+ const white = '#ffffff';
101
+ const replace = (id, tag, attrs, stops) => {
102
+ defs.querySelector(`#${id}`)?.remove();
103
+ return gradient(defs, tag, id, attrs, stops.sort((a, b) => a[0] - b[0]).map(([o, c, a]) => [Math.min(1, Math.max(0, o)), c, a]));
104
+ };
105
+ // Layers and filters that would not change the picture are left out, so a
106
+ // look only pays for what it actually uses.
107
+ const softly = (node, id, std) => {
108
+ defs.querySelector(`#${id}`)?.remove();
109
+ if (!(std > 0)) { node.removeAttribute('filter'); return; }
110
+ defs.insertAdjacentHTML('beforeend', `<filter id="${id}" filterUnits="userSpaceOnUse" x="-30" y="-30" width="300" height="300"><feGaussianBlur stdDeviation="${std}"/></filter>`);
111
+ node.setAttribute('filter', `url(#${id})`);
112
+ };
113
+ const show = (node, on) => { node.style.display = on ? '' : 'none'; };
114
+ const inset = k => `translate(${120 * (1 - k)} ${120 * (1 - k)}) scale(${k}) ${skin.headTransform}`;
115
+
116
+ // Body colour.
117
+ if (t.gradient !== 'auto') {
118
+ const stops = [[0, base[0]], [t.stopMid, base[1]], [t.stopEdge, base[2]], [1, base[2]]];
119
+ if (t.gradient === 'linear') {
120
+ const a = t.angle * Math.PI / 180, dx = Math.cos(a) / 2, dy = Math.sin(a) / 2;
121
+ skin.parts.base = replace('skin-base', 'linearGradient', { x1: .5 - dx, y1: .5 - dy, x2: .5 + dx, y2: .5 + dy }, stops);
122
+ } else {
123
+ skin.parts.base = replace('skin-base', 'radialGradient', { cx: t.centerX, cy: t.centerY, r: t.radius }, stops);
124
+ }
125
+ skin.tunedBase = true;
126
+ }
127
+ skin.parts.volume = replace('skin-volume', 'radialGradient', { cx: '.43', cy: '.30', r: '.76' },
128
+ [[0, base[3], 0], [t.depthStart, base[3], 0], [1, base[3], t.depth]]);
129
+ show(skin.layers.children[1], t.depth > 0);
130
+ show(p.face, t.faceGlow > 0);
131
+
132
+ // Bevel band.
133
+ const [band, line, glint] = p.bevel.children;
134
+ const bandColor = t.bandColor || mixHex(base[0], white, t.bandLight);
135
+ const bandTop = t.bandColor || mixHex(base[0], white, Math.min(1, t.bandLight * 2));
136
+ replace('skin-sheen-band', 'linearGradient', { x1: '0', y1: '0', x2: '0', y2: '1' },
137
+ [[0, bandTop, t.bandOpacity * 1.2], [.06, bandColor, t.bandOpacity], [.5, bandColor, t.bandMiddle], [.94, bandColor, t.bandOpacity], [1, bandTop, t.bandOpacity * 1.3]]);
138
+ p.bevel.removeAttribute('filter');
139
+ band.setAttribute('stroke-width', String(t.bandWidth));
140
+ softly(band, 'skin-band-blur', t.bandBlur);
141
+ show(band, t.bandWidth > 0 && (t.bandOpacity > 0 || t.bandMiddle > 0));
142
+
143
+ // Inner bevel line.
144
+ // A gradient axis through the middle of the head at the given angle; k keeps
145
+ // 135 degrees running exactly from the top-right corner to the bottom-left.
146
+ const axis = degrees => {
147
+ const a = degrees * Math.PI / 180, k = Math.SQRT1_2;
148
+ return { x1: .5 - Math.cos(a) * k, y1: .5 - Math.sin(a) * k, x2: .5 + Math.cos(a) * k, y2: .5 + Math.sin(a) * k };
149
+ };
150
+ const tint = mixHex(base[0], white, .5);
151
+ skin.lineGradient = replace('skin-sheen-line', 'linearGradient', axis(t.lineAngle),
152
+ [1, 2, 3, 4, 5].map(i => [t[`line${i}Pos`], t[`line${i}Color`] || tint, t[`line${i}Opacity`]]));
153
+ skin.lineFollow = t.lineFollow;
154
+ line.style.display = t.lineOpacity > 0 ? '' : 'none';
155
+ line.setAttribute('transform', `translate(${t.lineOffsetX} ${t.lineOffsetY}) ${inset(t.lineInset)}`);
156
+ line.setAttribute('stroke-width', String(t.lineWidth));
157
+ line.setAttribute('opacity', String(t.lineOpacity));
158
+ softly(line, 'skin-line-blur', t.lineBlur);
159
+ if (t.lineWidth <= 0 || [1, 2, 3, 4, 5].every(i => !(t[`line${i}Opacity`] > 0))) show(line, false);
160
+
161
+ // White glints: one stretch on the upper right, one along the bottom.
162
+ // Up to three stretches of glint along the gradient axis: upper, middle, lower.
163
+ const g = t.glintColor;
164
+ const stretch = (start, end, bright) => bright > 0 && end > start
165
+ ? [[start, g, 0], [Math.min(end, start + .06), g, bright], [Math.max(start + .06, end - .06), g, bright * .9], [end, g, 0]]
166
+ : [];
167
+ replace('skin-sheen-glint', 'linearGradient', axis(t.glintAngle), [
168
+ [0, g, 0], ...stretch(t.upperStart, t.upperEnd, t.glintUpper), ...stretch(t.middleStart, t.middleEnd, t.glintMiddle),
169
+ ...stretch(t.lowerStart, t.lowerEnd, t.glintLower), [1, g, 0],
170
+ ]);
171
+ skin.glintTransform = `translate(${t.glintOffsetX} ${t.glintOffsetY}) ${inset(t.glintInset)}`;
172
+ skin.glintFollow = t.glintFollow;
173
+ glint.setAttribute('transform', skin.glintTransform);
174
+ glint.setAttribute('stroke-width', String(t.glintWidth));
175
+ softly(glint, 'skin-glint-blur', t.glintBlur);
176
+ show(glint, t.glintWidth > 0 && (t.glintUpper > 0 || t.glintMiddle > 0 || t.glintLower > 0));
177
+ if (t.glintDash > 0) glint.setAttribute('stroke-dasharray', `${t.glintDash} ${t.glintGap}`);
178
+ else glint.removeAttribute('stroke-dasharray');
179
+
180
+ // Outer rim line.
181
+ const rim = skin.layers.children[3];
182
+ rim.style.display = t.rimOpacity > 0 ? '' : 'none';
183
+ rim.setAttribute('stroke', t.rimColor);
184
+ rim.setAttribute('stroke-width', String(t.rimWidth));
185
+ rim.setAttribute('opacity', String(t.rimOpacity));
186
+
187
+ // Gloss band across the top.
188
+ p.top.style.display = t.topGloss > 0 ? '' : 'none';
189
+ replace('skin-sheen-top', 'linearGradient', { x1: '0', y1: '0', x2: '0', y2: '1' },
190
+ [[0, white, t.topGloss], [t.topReach * .5, white, t.topGloss * .45], [t.topReach, white, 0], [1, white, 0]]);
191
+ softly(p.top, 'skin-top-blur', t.topBlur);
192
+
193
+ // Side wall along the bottom.
194
+ p.side.style.display = t.sideThickness > 0 ? '' : 'none';
195
+ const lifted = `translate(0 ${-t.sideThickness}) ${skin.headTransform}`;
196
+ skin.sideMaskShape.setAttribute('transform', lifted);
197
+ // Scale the line about the head centre, then move it.
198
+ const sx = t.sideLineScaleX, sy = t.sideLineScaleY;
199
+ skin.sideLine.setAttribute('transform', `translate(${t.sideLineX} ${t.sideLineY - t.sideThickness}) translate(${120 * (1 - sx)} ${120 * (1 - sy)}) scale(${sx} ${sy}) ${skin.headTransform}`);
200
+ skin.sideLine.setAttribute('stroke-width', String(t.sideLineWidth / Math.sqrt(sx * sy)));
201
+ skin.sideLine.setAttribute('stroke-linecap', t.sideLineCap);
202
+ if (t.sideLineDash > 0) skin.sideLine.setAttribute('stroke-dasharray', `${t.sideLineDash} ${t.sideLineGap}`);
203
+ else skin.sideLine.removeAttribute('stroke-dasharray');
204
+ softly(skin.sideLine, 'skin-sideline-blur', t.sideLineBlur);
205
+ show(skin.sideLine, t.sideLine > 0 && t.sideLineWidth > 0);
206
+ const black = '#000000';
207
+ // Older saved looks used sideLight (always lighter).
208
+ const tone = t.sideTone ?? t.sideLight ?? .45;
209
+ const shade = (color, k) => tone >= 0 ? mixHex(color, white, tone * k) : mixHex(color, black, -tone * k);
210
+ const wall = t.sideColor ? [t.sideColor, t.sideColor, t.sideColor] : [shade(base[0], 1), shade(base[2], 1), shade(base[3], .8)];
211
+ replace('skin-side-fill', 'linearGradient', { x1: '0', y1: '0', x2: '1', y2: '0' },
212
+ [[0, wall[0], t.sideOpacity], [.5, wall[1], t.sideOpacity * .93], [1, wall[2], Math.min(1, t.sideOpacity * 1.07)]]);
213
+ const lt = t.sideLineTone;
214
+ const lineColor = t.sideLineColor || (lt >= 0 ? mixHex(base[1], white, lt) : mixHex(base[1], mixHex(base[3], black, .4), -lt));
215
+ // Vertical range: visible from sideLineFrom to sideLineTo (0 = top, 1 = bottom),
216
+ // fading in over sideLineFade.
217
+ const from = Math.min(t.sideLineFrom, t.sideLineTo), to = Math.max(t.sideLineFrom, t.sideLineTo);
218
+ const fade = Math.min(t.sideLineFade, (to - from) / 2);
219
+ replace('skin-side-line', 'linearGradient', { x1: '0', y1: '0', x2: '0', y2: '1' },
220
+ [[0, lineColor, 0], [from, lineColor, 0], [from + fade, lineColor, t.sideLine], [to - fade, lineColor, t.sideLine], [to, lineColor, 0], [1, lineColor, 0]]);
221
+ // Fade toward the left and right sides with a mask.
222
+ defs.querySelector('#skin-sideline-mask')?.remove();
223
+ defs.querySelector('#skin-sideline-mask-grad')?.remove();
224
+ skin.sideLine.removeAttribute('mask');
225
+
226
+ // Eyes.
227
+ skin.parts.eyeGlass = replace('skin-eye-glass', 'linearGradient', { x1: '1', y1: '0', x2: '0', y2: '1' },
228
+ [[0, t.eyeColor], [t.eyeLitPart, t.eyeColor], [1, mixHex(t.eyeColor, base[1], t.eyeTint)]]);
229
+ skin.style.textContent += `
230
+ #leftBase,#rightBase,#leftInputBase,#rightInputBase{fill-opacity:${t.eyeOpacity};${t.eyeRimWidth > 0 ? `stroke:rgba(255,255,255,${t.eyeRimOpacity});stroke-width:${t.eyeRimWidth}px;` : ''}}`;
231
+ skin.eyePaint.opacity = t.eyeOpacity;
232
+ skin.eyePaint.outline = t.eyeRimWidth > 0 ? { color: '#ffffff', width: t.eyeRimWidth, opacity: t.eyeRimOpacity } : null;
233
+
234
+ // Eye shadow.
235
+ defs.querySelector('#skin-eye-sink')?.remove();
236
+ const shadowColor = t.shadowColor || mixHex(base[2], base[3], t.shadowDarkness);
237
+ defs.insertAdjacentHTML('beforeend', `<filter id="skin-eye-sink" filterUnits="userSpaceOnUse" x="-90" y="-90" width="180" height="180">`
238
+ + `<feMorphology in="SourceAlpha" operator="dilate" radius="${t.shadowSpread}" result="m"/>`
239
+ + `<feOffset id="skin-eye-sink-offset" in="m" dx="0" dy="0" result="o"/><feGaussianBlur in="o" stdDeviation="${t.shadowBlur}" result="b"/>`
240
+ + `<feFlood flood-color="${shadowColor}" flood-opacity="${t.shadowOpacity}"/><feComposite in2="b" operator="in" result="s"/>`
241
+ + `<feGaussianBlur in="SourceGraphic" stdDeviation="${t.eyeSoftness}" result="g"/><feMerge><feMergeNode in="s"/><feMergeNode in="g"/></feMerge></filter>`);
242
+ skin.parts.eyeSink = defs.querySelector('#skin-eye-sink-offset');
243
+ skin.fixedShadowAngle = t.shadowFollow ? null : t.shadowAngle;
244
+ skin.shadowDistance = t.shadowDistance;
245
+
246
+ // Eye highlight and a smaller spark, both drifting toward the light.
247
+ const showHighlight = t.eyeHighlight > 0 || t.eyeSpark > 0;
248
+ for (const layer of skin.eyeHighlights) layer.style.display = showHighlight ? '' : 'none';
249
+ if (showHighlight) {
250
+ const highlight = replace('skin-eye-hl', 'radialGradient', { gradientUnits: 'userSpaceOnUse', cx: t.eyeHighlightX, cy: t.eyeHighlightY, r: t.eyeHighlightSize },
251
+ [[0, white, t.eyeHighlight], [t.eyeHighlightSoft, white, t.eyeHighlight], [1, white, 0]]);
252
+ const spark = replace('skin-eye-spark', 'radialGradient', { gradientUnits: 'userSpaceOnUse', cx: t.eyeSparkX, cy: t.eyeSparkY, r: t.eyeSparkSize },
253
+ [[0, white, t.eyeSpark], [.4, white, t.eyeSpark], [1, white, 0]]);
254
+ skin.eyeHighlight = { highlight, spark, t };
255
+ }
256
+
257
+ // Antenna sphere.
258
+ const sphere = [t.antennaLight || base[0], t.antennaMid || base[1], t.antennaDark || base[3]];
259
+ skin.parts.antenna = replace('skin-antenna', 'radialGradient', { cx: t.antennaHighlightX, cy: t.antennaHighlightY, r: '.85' },
260
+ [[0, white, t.antennaHighlight], [t.antennaHighlightSize * .5, mixHex(sphere[0], white, t.antennaHighlight * .6), 1], [t.antennaHighlightSize, sphere[0], 1], [.5, sphere[1], 1], [1, sphere[2], 1]]);
261
+ skin.antennaFixed = !t.antennaFollow;
262
+ skin.style.textContent += `
263
+ #antennaDot{fill:url(#skin-antenna);fill-opacity:${t.antennaOpacity};${t.antennaRimWidth > 0 ? `stroke:${t.antennaRimColor};stroke-width:${t.antennaRimWidth}px;stroke-opacity:${t.antennaRimOpacity};` : 'stroke:none;'}${t.antennaSize !== 1 ? `transform-box:fill-box;transform-origin:center;scale:${t.antennaSize};` : ''}}`;
264
+ }
265
+
266
+ // Mixes two #rrggbb colours; t = 0 keeps the first.
267
+ function mixHex(a, b, t) {
268
+ const channel = (hex, i) => parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16);
269
+ return '#' + [0, 1, 2].map(i => Math.round(channel(a, i) * (1 - t) + channel(b, i) * t).toString(16).padStart(2, '0')).join('');
270
+ }
271
+ function saturation(hex) {
272
+ const c = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255);
273
+ const max = Math.max(...c), min = Math.min(...c);
274
+ return max === 0 ? 0 : (max - min) / max;
275
+ }
276
+ const REST_LIGHT = Object.freeze({ x: -0.5, y: -0.5 });
277
+
278
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
279
+
280
+ function svg(tag, attributes = {}, parent) {
281
+ const element = document.createElementNS(NS, tag);
282
+ for (const [name, value] of Object.entries(attributes)) element.setAttribute(name, String(value));
283
+ parent?.append(element);
284
+ return element;
285
+ }
286
+
287
+ function gradient(parent, tag, id, attributes, stops) {
288
+ const element = svg(tag, { id, ...attributes }, parent);
289
+ for (const [offset, color, opacity = 1] of stops) {
290
+ svg('stop', { offset, 'stop-color': color, 'stop-opacity': opacity }, element);
291
+ }
292
+ return element;
293
+ }
294
+
295
+ function build(instance) {
296
+ const root = instance.shadowRoot;
297
+ const head = root?.getElementById('head');
298
+ const drag = root?.getElementById('dragMotion');
299
+ if (!head || !drag) return null;
300
+ maskEyelids(instance);
301
+
302
+ const defs = svg('g', { id: 'skinDefs' });
303
+ root.querySelector('defs').append(defs);
304
+ const layers = svg('g', { 'pointer-events': 'none', 'aria-hidden': 'true' });
305
+ const shape = { href: '#headShape', transform: head.getAttribute('transform') };
306
+ // A printed pattern sits on the base paint, under the shine and shading.
307
+ const pattern = svg('use', { ...shape, fill: 'url(#skin-pattern)', 'pointer-events': 'none' });
308
+ head.after(pattern, layers);
309
+ for (const paint of ['shine', 'volume', 'edge']) svg('use', { ...shape, fill: `url(#skin-${paint})` }, layers);
310
+ svg('use', { ...shape, fill: 'none', stroke: 'url(#skin-rim)', 'stroke-width': 1.25 }, layers);
311
+ // Balls get extra round shading: darker toward the edge and a small hot spot.
312
+ const ball = svg('g', {}, layers);
313
+ for (const paint of ['ball-shade', 'ball-hot']) svg('use', { ...shape, fill: `url(#skin-${paint})` }, ball);
314
+ // Glossy sheen: copies of the head outline drawn a little inside it, faded so
315
+ // only a band under the top edge and a faint one above the bottom remain. They
316
+ // are the head's own shape, so they bend with every squeeze and drag.
317
+ const sheen = svg('g', { 'clip-path': 'url(#headClip)' }, layers);
318
+ const inset = scale => `translate(${120 * (1 - scale)} ${120 * (1 - scale)}) scale(${scale}) ${shape.transform}`;
319
+ const glow = svg('use', { href: '#headShape', transform: shape.transform, fill: 'url(#skin-sheen-glow)' }, sheen);
320
+ const face = svg('use', { href: '#headShape', transform: shape.transform, fill: 'url(#skin-sheen-face)' }, sheen);
321
+ // Slab bevel: a light band just inside the edge, a bevel line, and glints.
322
+ const bevel = svg('g', {}, sheen);
323
+ svg('use', { href: '#headShape', transform: shape.transform, fill: 'none', stroke: 'url(#skin-sheen-band)', 'stroke-width': 26 }, bevel);
324
+ const bevelLine = svg('use', { href: '#headShape', transform: inset(.865), fill: 'none', stroke: 'url(#skin-sheen-line)', 'stroke-width': 1.6 }, bevel);
325
+ svg('use', { href: '#headShape', transform: inset(.955), fill: 'none', stroke: 'url(#skin-sheen-glint)', 'stroke-width': 3.2, 'stroke-linecap': 'round' }, bevel);
326
+ // Side wall of a thick slab: the strip between the head outline and the same
327
+ // outline lifted a little, so its upper edge follows the curve of the face.
328
+ const lifted = `translate(0 -13) ${shape.transform}`;
329
+ const sideMask = svg('mask', { id: 'skin-side-mask', maskUnits: 'userSpaceOnUse', maskContentUnits: 'userSpaceOnUse', x: -20, y: -20, width: 280, height: 280 }, root.querySelector('defs'));
330
+ svg('rect', { x: -20, y: -20, width: 280, height: 280, fill: '#fff' }, sideMask);
331
+ const sideMaskShape = svg('use', { href: '#headShape', transform: lifted, fill: '#000' }, sideMask);
332
+ const side = svg('g', {}, sheen);
333
+ svg('use', { href: '#headShape', transform: shape.transform, fill: 'url(#skin-side-fill)', mask: 'url(#skin-side-mask)' }, side);
334
+ const sideLine = svg('use', { href: '#headShape', transform: lifted, fill: 'none', stroke: 'url(#skin-side-line)', 'stroke-width': 2.4 }, side);
335
+ // The reflections drift with the light inside the head's clip.
336
+ const moving = svg('g', {}, sheen);
337
+ const sheenParts = {
338
+ glow,
339
+ face,
340
+ bevel,
341
+ bevelLine,
342
+ side,
343
+ moving,
344
+ top: svg('use', { href: '#headShape', transform: inset(.9), fill: 'url(#skin-sheen-top)' }, moving),
345
+ bottom: svg('use', { href: '#headShape', transform: inset(.93), fill: 'url(#skin-sheen-bottom)' }, moving),
346
+ };
347
+
348
+ const style = document.createElement('style');
349
+ root.append(style);
350
+
351
+ // The sink shadow goes on the whole eye group, outside the eyelid mask, so
352
+ // the lids never cut it off.
353
+ const eyeGroups = ['left', 'right'].map(side => root.getElementById(`${side}Eye`)).filter(Boolean);
354
+ // Eye highlights: a highlight layer cut to each eye's own shape, so it follows
355
+ // blinks and expressions and the eyelids still close over it.
356
+ const eyeHighlights = ['left', 'right'].map(side => {
357
+ const paint = root.querySelector(`[mask="url(#skin-${side}-lids)"]`);
358
+ const mask = svg('mask', { id: `skin-${side}-eye-shape`, maskUnits: 'userSpaceOnUse', maskContentUnits: 'userSpaceOnUse', x: -90, y: -90, width: 180, height: 180, style: 'mask-type:alpha' }, root.querySelector('defs'));
359
+ svg('use', { href: `#${side}Base` }, mask);
360
+ const layer = svg('g', { mask: `url(#skin-${side}-eye-shape)`, 'pointer-events': 'none' }, paint);
361
+ svg('rect', { x: -90, y: -90, width: 180, height: 180, fill: 'url(#skin-eye-hl)' }, layer);
362
+ svg('rect', { x: -90, y: -90, width: 180, height: 180, fill: 'url(#skin-eye-spark)' }, layer);
363
+ layer.style.display = 'none';
364
+ return layer;
365
+ });
366
+ // Tuned looks restyle these shared layers; every paint starts from this state.
367
+ const pristine = [pattern, layers, sideMask, ...eyeHighlights].flatMap(node => [node, ...node.querySelectorAll('*')])
368
+ .map(node => [node, [...node.attributes].map(({ name, value }) => [name, value])]);
369
+ return { instance, defs, layers, pattern, ball, sheen, sheenParts, sideMaskShape, sideLine, headTransform: shape.transform, eyeGroups, eyeHighlights, style, pristine, name: null, parts: null, light: { ...REST_LIGHT }, shown: {}, nodes: {}, offsets: {} };
370
+ }
371
+
372
+ function restorePristine(skin) {
373
+ for (const [node, attributes] of skin.pristine) {
374
+ for (const name of node.getAttributeNames()) if (!attributes.some(([n]) => n === name)) node.removeAttribute(name);
375
+ for (const [name, value] of attributes) if (node.getAttribute(name) !== value) node.setAttribute(name, value);
376
+ }
377
+ }
378
+
379
+ function paintSkin(skin, name) {
380
+ const look = SKINS[name].faithful ? tunedAcrylic(SKINS[name], SKINS[name].fixedTune) : SKINS[name];
381
+ const tune = look.tune;
382
+ joinEyes(skin, false);
383
+ restorePristine(skin);
384
+ skin.defs.replaceChildren();
385
+ // Balls shade and reflect in their own colours, so a warm ball never picks up
386
+ // a cool grey cast at its edge.
387
+ const dark = look.ball || look.sheen ? look.base[3] : '#091126';
388
+ const reflect = look.ball ? look.base[0] : look.rim;
389
+ skin.parts = {
390
+ base: gradient(skin.defs, look.linear ? 'linearGradient' : 'radialGradient', 'skin-base',
391
+ look.linear === 'vertical' ? { x1: '0', y1: '0', x2: '0', y2: '1' } : look.linear ? { x1: '.05', y1: '.05', x2: '.85', y2: '.98' } : { cx: '.32', cy: '.17', r: '.95' },
392
+ // Faithful acrylic keeps its colour clear right to the edge; the darkest
393
+ // tone is only used for the eye shadow, so the bevel never turns muddy.
394
+ (look.faithful ? [...look.base.slice(0, 3), look.base[2]] : look.base).map((color, index) => [index / 3, color])),
395
+ // A glossy skin narrows the shine into a small, bright specular spot; it is
396
+ // still a fill of the head, so it bends and squashes with it.
397
+ shine: look.spec
398
+ ? gradient(skin.defs, 'radialGradient', 'skin-shine', { cx: '.30', cy: '.12', r: String(look.spec) },
399
+ [[0, '#fff', 1], [.22, '#fff', .9], [.5, '#fff', .28], [.78, '#fff', .05], [1, '#fff', 0]])
400
+ : gradient(skin.defs, 'radialGradient', 'skin-shine', { cx: '.30', cy: '.12', r: '.70' },
401
+ [[0, '#fff', look.shine], [.23, '#fff', look.shine * .30], [.65, '#fff', .035], [1, '#fff', 0]]),
402
+ volume: gradient(skin.defs, 'radialGradient', 'skin-volume', { cx: '.43', cy: '.30', r: '.76' },
403
+ [[0, dark, 0], [.58, dark, 0], [.82, dark, look.shade * .27], [1, dark, look.shade]]),
404
+ rim: gradient(skin.defs, 'linearGradient', 'skin-rim', { x1: '0', y1: '0', x2: '1', y2: '1' },
405
+ [[0, '#fff', .95], [.28, look.rim, .60], [.52, look.rim, .06], [.82, look.rim, .13], [1, look.rim, .65]]),
406
+ edge: gradient(skin.defs, 'radialGradient', 'skin-edge', { cx: '.88', cy: '.83', r: '.5' },
407
+ [[0, reflect, look.spec ? .7 : .5], [.45, reflect, look.spec ? .24 : .16], [1, reflect, 0]]),
408
+ antenna: gradient(skin.defs, 'radialGradient', 'skin-antenna', { cx: '.28', cy: '.20', r: '.85' },
409
+ [[0, look.antenna[0]], [.43, look.antenna[1]], [1, look.antenna[2]]]),
410
+ };
411
+ if (look.ball) {
412
+ gradient(skin.defs, 'radialGradient', 'skin-ball-shade', { cx: '.46', cy: '.4', r: '.74' },
413
+ [[0, dark, 0], [.55, dark, 0], [.84, dark, .2], [1, dark, .5]]);
414
+ skin.parts.hot = gradient(skin.defs, 'radialGradient', 'skin-ball-hot', { cx: '.32', cy: '.22', r: '.2' },
415
+ [[0, '#ffffff', .62], [.45, '#ffffff', .22], [1, '#ffffff', 0]]);
416
+ }
417
+ skin.ball.style.display = look.ball ? '' : 'none';
418
+ // A plain skin keeps only the base paint and its shading, with no highlight,
419
+ // reflection or rim, as a clean starting point for hand-drawn highlights.
420
+ const [shineLayer, , edgeLayer, rimLayer] = skin.layers.children;
421
+ for (const node of [shineLayer, edgeLayer, rimLayer]) node.style.display = look.plain ? 'none' : '';
422
+ if (look.plain && skin.parts.hot) skin.ball.lastElementChild.style.display = 'none';
423
+ else if (skin.ball.lastElementChild) skin.ball.lastElementChild.style.display = '';
424
+
425
+ const sheen = look.sheen ? SHEENS[look.sheen] : null;
426
+ if (sheen) {
427
+ // Pure white over a strong colour washes it toward grey, so coloured skins
428
+ // reflect a light tint of their own colour; neutral ones stay white.
429
+ const tinted = saturation(look.base[1]) > .35;
430
+ const light = tinted ? mixHex('#ffffff', look.base[0], .28) : '#ffffff';
431
+ const topAlpha = look.wet ? .62 : look.bright ? .5 : sheen.top;
432
+ const bounce = tinted ? mixHex('#ffffff', look.base[0], .5) : '#ffffff';
433
+ gradient(skin.defs, 'linearGradient', 'skin-sheen-top', { x1: '0', y1: '0', x2: '0', y2: '1' },
434
+ [[0, light, topAlpha], [sheen.reach * .6, light, topAlpha * .45], [sheen.reach * .93, light, topAlpha * .2], [sheen.reach, light, 0], [1, light, 0]]);
435
+ gradient(skin.defs, 'linearGradient', 'skin-sheen-bottom', { x1: '0', y1: '0', x2: '0', y2: '1' },
436
+ [[0, bounce, 0], [.8, bounce, 0], [.94, bounce, sheen.bottom * .6], [1, bounce, sheen.bottom]]);
437
+ if (sheen.slab) {
438
+ // The bevel is lighter at the top and bottom, where the edge turns toward
439
+ // the light and toward the bright floor; glints sit on the upper left and
440
+ // the right side.
441
+ const edge = look.metal ? '#ffffff' : mixHex('#ffffff', look.base[0], .45);
442
+ // Metal is a mirror: the top of the bevel catches the sky, the lower bevel
443
+ // falls into a dark band, and its very lip reflects the bright floor.
444
+ gradient(skin.defs, 'linearGradient', 'skin-sheen-band', { x1: '0', y1: '0', x2: '0', y2: '1' }, look.metal
445
+ ? [[0, '#ffffff', .95], [.07, '#ffffff', .7], [.16, '#ffffff', .12], [.5, '#ffffff', 0], [.8, look.base[3], .2], [.9, look.base[3], .75], [.955, look.base[3], .5], [.985, '#ffffff', .9], [1, '#ffffff', .95]]
446
+ : [[0, edge, .55], [.3, edge, .22], [.7, edge, .16], [1, edge, .48]]);
447
+ if (look.acrylic) {
448
+ // Acrylic: a deep colour whose edges are lit through the material, with a
449
+ // bright lip along the bottom; the middle sits a little darker.
450
+ const lit = mixHex('#ffffff', look.base[0], .4);
451
+ gradient(skin.defs, 'linearGradient', 'skin-sheen-band', { x1: '0', y1: '0', x2: '0', y2: '1' },
452
+ look.faithful
453
+ // Pale white over a strong colour reads as grey, so the bevel is lit
454
+ // with a lighter, still saturated version of the colour itself.
455
+ ? [[0, mixHex(look.base[0], '#ffffff', tune.bandLight * 2), tune.bandOpacity * 1.2], [.06, mixHex(look.base[0], '#ffffff', tune.bandLight), tune.bandOpacity], [.5, mixHex(look.base[0], '#ffffff', tune.bandLight * .8), tune.bandMiddle], [.94, mixHex(look.base[0], '#ffffff', tune.bandLight), tune.bandOpacity], [1, mixHex(look.base[0], '#ffffff', tune.bandLight * 2), tune.bandOpacity * 1.3]]
456
+ : look.wet
457
+ ? [[0, mixHex(look.base[0], '#ffffff', .6), .75], [.15, mixHex(look.base[0], '#ffffff', .45), .55], [.5, mixHex(look.base[1], '#ffffff', .4), .45], [.85, mixHex(look.base[2], '#ffffff', .45), .55], [1, mixHex(look.base[3], '#ffffff', .6), .8]]
458
+ : look.bright
459
+ ? [[0, '#ffffff', .75], [.1, lit, .5], [.3, lit, .22], [.7, lit, .26], [.9, '#ffffff', .75], [1, '#ffffff', .95]]
460
+ : [[0, lit, .55], [.12, lit, .25], [.5, lit, .08], [.8, lit, .16], [.93, lit, .7], [1, lit, .9]]);
461
+ if (look.faithful) {
462
+ gradient(skin.defs, 'radialGradient', 'skin-sheen-face', { cx: String(tune.faceX), cy: String(tune.faceY), r: String(tune.faceSize) },
463
+ [[0, '#ffffff', tune.faceGlow], [1, '#ffffff', 0]]);
464
+ } else {
465
+ gradient(skin.defs, 'radialGradient', 'skin-sheen-face', { cx: '.5', cy: '.48', r: '.6' },
466
+ [[0, look.base[3], .14], [.7, look.base[3], .05], [1, look.base[3], 0]]);
467
+ }
468
+ // The eyes sit under the acrylic: a soft coloured shadow falls to the
469
+ // lower left, and their lower edge picks up a tint of the colour.
470
+ skin.parts.eyeGlass = gradient(skin.defs, 'linearGradient', 'skin-eye-glass', { x1: '1', y1: '0', x2: '0', y2: '1' },
471
+ look.wet
472
+ ? [[0, '#ffffff'], [.3, mixHex('#ffffff', look.base[1], .1)], [.75, mixHex('#ffffff', look.base[1], .32)], [1, mixHex('#ffffff', look.base[1], .5)]]
473
+ : look.faithful
474
+ ? [[0, '#ffffff'], [.55, '#ffffff'], [1, mixHex('#ffffff', look.base[1], tune.eyeTint)]]
475
+ : [[0, '#ffffff'], [.38, '#ffffff'], [.72, mixHex('#ffffff', look.base[1], .25)], [1, mixHex('#ffffff', look.base[1], .6)]]);
476
+ skin.defs.insertAdjacentHTML('beforeend', `<filter id="skin-soft-edge" filterUnits="userSpaceOnUse" x="-20" y="-20" width="280" height="280"><feGaussianBlur stdDeviation="${look.wet ? 6 : look.faithful ? tune.edgeBlur : look.bright ? 1.1 : 2.6}"/></filter>`);
477
+ skin.defs.insertAdjacentHTML('beforeend', '<filter id="skin-soft-top" filterUnits="userSpaceOnUse" x="-20" y="-20" width="280" height="280"><feGaussianBlur stdDeviation="4"/></filter>');
478
+ skin.defs.insertAdjacentHTML('beforeend', `<filter id="skin-eye-sink" filterUnits="userSpaceOnUse" x="-90" y="-90" width="180" height="180"><feOffset id="skin-eye-sink-offset" in="SourceAlpha" dx="-6" dy="8" result="o"/><feGaussianBlur in="o" stdDeviation="${look.faithful ? tune.shadowBlur : look.wet ? 3 : 6}" result="b"/><feFlood flood-color="${look.faithful ? mixHex(look.base[2], look.base[3], tune.shadowDarkness) : look.base[3]}" flood-opacity="${look.faithful ? tune.shadowOpacity : look.wet ? .45 : .8}"/><feComposite in2="b" operator="in" result="s"/><feGaussianBlur in="SourceGraphic" stdDeviation="${look.faithful ? tune.eyeSoftness : 0}" result="g"/><feMerge><feMergeNode in="s"/><feMergeNode in="g"/></feMerge></filter>`);
479
+ skin.parts.eyeSink = skin.defs.querySelector('#skin-eye-sink-offset');
480
+ }
481
+ gradient(skin.defs, 'linearGradient', 'skin-sheen-line', { x1: '0', y1: '0', x2: '0', y2: '1' },
482
+ [[0, '#ffffff', .55], [.5, look.faithful ? look.base[2] : look.base[3], .25], [1, look.faithful ? mixHex(look.base[0], '#ffffff', .5) : '#ffffff', .35]]);
483
+ gradient(skin.defs, 'linearGradient', 'skin-sheen-glint', look.wet ? { x1: '0', y1: '0', x2: '1', y2: '1' } : look.faithful ? { x1: '1', y1: '0', x2: '0', y2: '1' } : { x1: '0', y1: '0', x2: '1', y2: '1' }, look.wet
484
+ ? [[0, '#ffffff', 1], [.3, '#ffffff', .95], [.44, '#ffffff', .5], [.52, '#ffffff', 0], [.8, '#ffffff', 0], [.9, '#ffffff', .7], [1, '#ffffff', .95]]
485
+ : look.faithful
486
+ ? [[0, '#ffffff', 0], [.05, '#ffffff', tune.glintUpper], [.3, '#ffffff', tune.glintUpper * .9], [.4, '#ffffff', 0], [.55, '#ffffff', 0], [.65, '#ffffff', tune.glintLower], [.85, '#ffffff', tune.glintLower * .85], [.95, '#ffffff', 0], [1, '#ffffff', 0]]
487
+ : [[0, '#ffffff', 0], [.08, '#ffffff', .95], [.3, '#ffffff', .9], [.42, '#ffffff', 0], [.6, '#ffffff', 0], [.72, '#ffffff', .85], [.82, '#ffffff', 0], [1, '#ffffff', 0]]);
488
+ }
489
+ const p = skin.sheenParts;
490
+ p.bottom.style.display = sheen.bottom ? '' : 'none';
491
+ p.bevel.style.display = sheen.slab ? '' : 'none';
492
+ // Acrylic has no crisp edges: its rim light is soft and has no bevel line.
493
+ p.bevelLine.style.display = look.acrylic && !look.faithful ? 'none' : '';
494
+ p.top.style.display = look.faithful ? 'none' : '';
495
+ const [band] = p.bevel.children;
496
+ band.removeAttribute('filter');
497
+ band.setAttribute('stroke-width', String(look.faithful ? tune.bandWidth : 26));
498
+ p.bevel.children[2].setAttribute('stroke-width', String(look.faithful ? tune.glintWidth : 3.2));
499
+ p.bevelLine.setAttribute('opacity', String(look.faithful ? tune.lineOpacity : 1));
500
+ p.top.removeAttribute('filter');
501
+ if (look.acrylic) p.bevel.setAttribute('filter', 'url(#skin-soft-edge)');
502
+ else p.bevel.removeAttribute('filter');
503
+ p.glow.style.display = sheen.slab && look.glow ? '' : 'none';
504
+ p.face.style.display = sheen.slab && (look.metal || (look.acrylic && !look.bright)) ? '' : 'none';
505
+ p.side.style.display = 'none';
506
+
507
+ }
508
+ skin.sheen.style.display = sheen ? '' : 'none';
509
+ skin.shadowDistance = look.faithful ? tune.shadowDistance : 8;
510
+ if (look.pattern) skin.defs.insertAdjacentHTML('beforeend', patternMarkup('skin-pattern', look.pattern));
511
+ skin.pattern.style.display = look.pattern ? '' : 'none';
512
+ skin.shown = {};
513
+ skin.nodes = {};
514
+ skin.offsets = {};
515
+ // On busy patterns a thin ring keeps the eyes readable.
516
+ const ring = look.ring ? `stroke:${look.ring};stroke-width:3px;` : '';
517
+ // The sink shadow goes on each eye's group: WebKit clips a filter set on the
518
+ // eye shape itself to a rectangle.
519
+ for (const group of skin.eyeGroups) {
520
+ if (look.acrylic) group.setAttribute('filter', 'url(#skin-eye-sink)');
521
+ else group.removeAttribute('filter');
522
+ }
523
+ skin.tunedBase = false;
524
+ skin.fixedShadowAngle = null;
525
+ skin.glintFollow = 0;
526
+ skin.lineFollow = false;
527
+ skin.antennaFixed = false;
528
+ skin.eyeHighlight = null;
529
+ for (const layer of skin.eyeHighlights) layer.style.display = 'none';
530
+ skin.eyePaint = {
531
+ gradient: look.acrylic, color: look.eye, opacity: look.wet ? .8 : 1,
532
+ outline: look.ring ? { color: look.ring, width: 3, opacity: 1 } : look.wet ? { color: '#ffffff', width: 1.6, opacity: .95 } : null,
533
+ };
534
+ skin.style.textContent = `
535
+ #leftBase,#rightBase,#leftInputBase,#rightInputBase{fill:${look.acrylic ? 'url(#skin-eye-glass)' : look.eye};${ring}${look.wet ? 'fill-opacity:.8;stroke:rgba(255,255,255,.95);stroke-width:1.6px;' : ''}}
536
+ #antennaDot{fill:url(#skin-antenna);${look.plain ? '' : `stroke:${look.rim};stroke-width:.7px`}}`;
537
+ if (look.faithful) applyAcrylicTuning(skin, look);
538
+ }
539
+
540
+ // Light-driven attributes are written only when they change. A tuned skin can
541
+ // shift any of them by a fixed amount (skin.offsets), which then moves with the
542
+ // light instead of being overwritten by it.
543
+ function set(skin, node, key, attribute, value) {
544
+ const id = `${key}.${attribute}`;
545
+ skin.nodes[id] = node;
546
+ const text = (value + (skin.offsets[id] || 0)).toFixed(3);
547
+ if (skin.shown[id] === text) return;
548
+ skin.shown[id] = text;
549
+ node.setAttribute(attribute, text);
550
+ }
551
+
552
+ function applyLight(skin, x, y) {
553
+ const { parts } = skin;
554
+ const cx = .5 + x * .30;
555
+ const cy = .30 + y * .17;
556
+ if (!SKINS[skin.name].linear && !skin.tunedBase) {
557
+ set(skin, parts.base, 'base', 'cx', cx);
558
+ set(skin, parts.base, 'base', 'cy', cy);
559
+ }
560
+ set(skin, parts.shine, 'shine', 'cx', cx);
561
+ set(skin, parts.shine, 'shine', 'cy', cy - .10);
562
+ set(skin, parts.volume, 'volume', 'cx', .5 + x * .12);
563
+ set(skin, parts.volume, 'volume', 'cy', .35 + y * .08);
564
+ set(skin, parts.edge, 'edge', 'cx', .5 - x * .42);
565
+ set(skin, parts.edge, 'edge', 'cy', .75 - y * .12);
566
+ set(skin, parts.rim, 'rim', 'x1', .5 + x * .5);
567
+ set(skin, parts.rim, 'rim', 'x2', .5 - x * .5);
568
+ if (!skin.antennaFixed) {
569
+ set(skin, parts.antenna, 'antenna', 'cx', cx);
570
+ set(skin, parts.antenna, 'antenna', 'cy', Math.max(.1, cy));
571
+ }
572
+ // Eyes set into acrylic: the lit side faces the light and the shaded side and
573
+ // the shadow fall opposite it, so they turn as the pointer moves around.
574
+ if (parts.eyeGlass) {
575
+ // Direction only; how far the pointer is changes nothing. A fixed shadow
576
+ // angle points the shadow one way regardless of the light.
577
+ const len = Math.hypot(x, y) || 1;
578
+ const fixed = skin.fixedShadowAngle;
579
+ const ux = fixed == null ? x / len : -Math.cos(fixed * Math.PI / 180);
580
+ const uy = fixed == null ? y / len : -Math.sin(fixed * Math.PI / 180);
581
+ set(skin, parts.eyeGlass, 'eyeGlass', 'x1', .5 + ux * .5);
582
+ set(skin, parts.eyeGlass, 'eyeGlass', 'y1', .5 + uy * .5);
583
+ set(skin, parts.eyeGlass, 'eyeGlass', 'x2', .5 - ux * .5);
584
+ set(skin, parts.eyeGlass, 'eyeGlass', 'y2', .5 - uy * .5);
585
+ if (parts.eyeSink) {
586
+ set(skin, parts.eyeSink, 'eyeSink', 'dx', -ux * (skin.shadowDistance ?? 8));
587
+ set(skin, parts.eyeSink, 'eyeSink', 'dy', -uy * (skin.shadowDistance ?? 8));
588
+ }
589
+ }
590
+ // Eye highlights slide toward the light; the spark sits opposite.
591
+ if (skin.eyeHighlight) {
592
+ const { highlight, spark, t } = skin.eyeHighlight;
593
+ const dx = (x - REST_LIGHT.x) * t.eyeHighlightFollow, dy = (y - REST_LIGHT.y) * t.eyeHighlightFollow;
594
+ set(skin, highlight, 'eyeHl', 'cx', t.eyeHighlightX + dx);
595
+ set(skin, highlight, 'eyeHl', 'cy', t.eyeHighlightY + dy);
596
+ set(skin, spark, 'eyeSpark', 'cx', t.eyeSparkX - dx);
597
+ set(skin, spark, 'eyeSpark', 'cy', t.eyeSparkY - dy);
598
+ }
599
+ // A following inner line keeps its bright end toward the light.
600
+ // Tuned glints can slide toward the light.
601
+ // The sheen drifts a little with the light.
602
+ if (SKINS[skin.name].sheen) {
603
+ const t = `translate(${((x - REST_LIGHT.x) * 7).toFixed(2)} ${((y - REST_LIGHT.y) * 4).toFixed(2)})`;
604
+ if (skin.shown.sheen !== t) { skin.shown.sheen = t; skin.sheenParts.moving.setAttribute('transform', t); }
605
+ }
606
+ // The hot spot sits in the middle of the shine, so the two read as one highlight.
607
+ if (parts.hot) {
608
+ set(skin, parts.hot, 'hot', 'cx', cx);
609
+ set(skin, parts.hot, 'hot', 'cy', cy - .10);
610
+ }
611
+ }
612
+
613
+ // Shows the named skin, or none, on an avatar. The layers are built on first use
614
+ // and kept, so switching skins only repaints them.
615
+ export function showNative(instance, name) {
616
+ if (!name && !instance._nativeSkin) return null;
617
+ if (!instance._nativeSkin) instance._nativeSkin = build(instance);
618
+ const skin = instance._nativeSkin;
619
+ if (!skin || skin.name === name) return skin;
620
+ skin.name = name;
621
+ const head = instance._head;
622
+ if (name) {
623
+ paintSkin(skin, name);
624
+ head.setAttribute('fill', 'url(#skin-base)');
625
+ skin.layers.style.display = '';
626
+ applyLight(skin, skin.light.x, skin.light.y);
627
+ } else {
628
+ joinEyes(skin, false);
629
+ skin.style.textContent = '';
630
+ for (const group of skin.eyeGroups) group.removeAttribute('filter');
631
+ for (const layer of skin.eyeHighlights) layer.style.display = 'none';
632
+ skin.eyeHighlight = null;
633
+ skin.layers.style.display = 'none';
634
+ skin.pattern.style.display = 'none';
635
+ baseApplyColor.call(instance, instance.getAttribute('color'));
636
+ }
637
+ return skin;
638
+ }
639
+
640
+ // Love slides the two eyes into one heart. Each eye has its own shadow, rim and
641
+ // gradient, so where they overlap one would show on top of the other. While
642
+ // they overlap, both are hidden and their union is painted once instead.
643
+ function eyeBox(eye, parent) {
644
+ const box = eye.getBBox();
645
+ const m = parent.getCTM().inverse().multiply(eye.getCTM());
646
+ const xs = [], ys = [];
647
+ for (const [x, y] of [[box.x, box.y], [box.x + box.width, box.y], [box.x, box.y + box.height], [box.x + box.width, box.y + box.height]]) {
648
+ xs.push(m.a * x + m.c * y + m.e);
649
+ ys.push(m.b * x + m.d * y + m.f);
650
+ }
651
+ return { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) };
652
+ }
653
+
654
+ function joinedSurface(skin) {
655
+ if (skin.joined) return skin.joined;
656
+ const root = skin.instance.shadowRoot;
657
+ const defs = root.querySelector('defs');
658
+ const parent = skin.eyeGroups[0].parentElement;
659
+ const mask = svg('mask', { id: 'skin-joined-eyes', maskUnits: 'userSpaceOnUse', maskContentUnits: 'userSpaceOnUse', x: 0, y: 0, width: 240, height: 240, style: 'mask-type:alpha' }, defs);
660
+ for (const side of ['left', 'right']) svg('use', { href: `#${side}Eye` }, mask);
661
+ const outline = svg('filter', { id: 'skin-joined-outline', x: '-20%', y: '-20%', width: '140%', height: '140%' }, defs);
662
+ const dilate = svg('feMorphology', { in: 'SourceAlpha', operator: 'dilate', radius: 1, result: 'grown' }, outline);
663
+ const flood = svg('feFlood', {}, outline);
664
+ svg('feComposite', { in2: 'grown', operator: 'in' }, outline);
665
+ const gradientNode = svg('linearGradient', { id: 'skin-eye-joined', gradientUnits: 'userSpaceOnUse' }, defs);
666
+ const group = svg('g', { 'pointer-events': 'none', 'aria-hidden': 'true', 'clip-path': parent.getAttribute('clip-path') || 'none' });
667
+ parent.after(group);
668
+ const ring = svg('g', { filter: 'url(#skin-joined-outline)' }, group);
669
+ for (const side of ['left', 'right']) svg('use', { href: `#${side}Eye` }, ring);
670
+ const surface = svg('g', {}, group);
671
+ const paint = svg('rect', { x: 0, y: 0, width: 240, height: 240, mask: 'url(#skin-joined-eyes)' }, surface);
672
+ group.style.display = 'none';
673
+ skin.joined = { parent, group, ring, surface, paint, gradient: gradientNode, dilate, flood, on: false, opacity: parent.style.opacity };
674
+ return skin.joined;
675
+ }
676
+
677
+ function joinEyes(skin, on) {
678
+ const joined = on ? joinedSurface(skin) : skin.joined;
679
+ if (!joined || joined.on === on) return;
680
+ joined.on = on;
681
+ joined.parent.style.opacity = on ? '0' : joined.opacity;
682
+ joined.group.style.display = on ? '' : 'none';
683
+ // The copies in the mask must not carry each eye's own shadow.
684
+ for (const group of skin.eyeGroups) {
685
+ if (on) {
686
+ skin.eyeFilter = group.getAttribute('filter');
687
+ group.removeAttribute('filter');
688
+ } else if (skin.eyeFilter) group.setAttribute('filter', skin.eyeFilter);
689
+ }
690
+ if (!on) { skin.eyeFilter = null; return; }
691
+ const paint = skin.eyePaint || { color: '#ffffff', opacity: 1, outline: null };
692
+ // The eye filter's region is set in one eye's own units; the joined copy
693
+ // covers the whole face instead.
694
+ const root = skin.instance.shadowRoot;
695
+ root.getElementById('skin-eye-sink-joined')?.remove();
696
+ joined.offset = null;
697
+ const source = skin.eyeFilter && root.getElementById('skin-eye-sink');
698
+ if (source) {
699
+ const copy = source.cloneNode(true);
700
+ copy.id = 'skin-eye-sink-joined';
701
+ for (const [name, value] of [['x', 0], ['y', 0], ['width', 240], ['height', 240]]) copy.setAttribute(name, value);
702
+ joined.offset = copy.querySelector('#skin-eye-sink-offset');
703
+ joined.offset?.removeAttribute('id');
704
+ source.after(copy);
705
+ joined.surface.setAttribute('filter', 'url(#skin-eye-sink-joined)');
706
+ } else joined.surface.removeAttribute('filter');
707
+ if (paint.gradient) {
708
+ const source = skin.instance.shadowRoot.getElementById('skin-eye-glass');
709
+ joined.gradient.replaceChildren(...[...(source?.children || [])].map(stop => stop.cloneNode()));
710
+ joined.paint.setAttribute('fill', 'url(#skin-eye-joined)');
711
+ } else joined.paint.setAttribute('fill', paint.color);
712
+ joined.paint.setAttribute('fill-opacity', String(paint.opacity));
713
+ joined.ring.style.display = paint.outline ? '' : 'none';
714
+ if (paint.outline) {
715
+ joined.dilate.setAttribute('radius', String(paint.outline.width / 2));
716
+ joined.flood.setAttribute('flood-color', paint.outline.color);
717
+ joined.flood.setAttribute('flood-opacity', String(paint.outline.opacity));
718
+ }
719
+ }
720
+
721
+ export function separateNativeEyes(instance) {
722
+ if (instance._nativeSkin) joinEyes(instance._nativeSkin, false);
723
+ }
724
+
725
+ // Called every frame: joins the eyes while they overlap and keeps the joined
726
+ // gradient spanning both, the way each eye's gradient spans that eye.
727
+ export function joinNativeEyes(instance) {
728
+ const skin = instance._nativeSkin;
729
+ if (!skin?.name) return;
730
+ const [left, right] = skin.eyeGroups;
731
+ if (!instance._loveFx || !left || !right) { joinEyes(skin, false); return; }
732
+ const parent = left.parentElement;
733
+ const a = eyeBox(left, parent), b = eyeBox(right, parent);
734
+ const overlap = a.x1 > b.x0 && b.x1 > a.x0 && a.y1 > b.y0 && b.y1 > a.y0;
735
+ joinEyes(skin, overlap);
736
+ if (!overlap) return;
737
+ const x0 = Math.min(a.x0, b.x0), y0 = Math.min(a.y0, b.y0), x1 = Math.max(a.x1, b.x1), y1 = Math.max(a.y1, b.y1);
738
+ // The shadow keeps following the light.
739
+ const offset = skin.joined.offset, live = skin.parts?.eyeSink;
740
+ if (offset && live) for (const name of ['dx', 'dy']) offset.setAttribute(name, live.getAttribute(name));
741
+ const g = skin.joined.gradient;
742
+ g.setAttribute('x1', x1.toFixed(2)); g.setAttribute('y1', y0.toFixed(2));
743
+ g.setAttribute('x2', x0.toFixed(2)); g.setAttribute('y2', y1.toFixed(2));
744
+ }
745
+
746
+ export function lightNative(instance, x, y) {
747
+ const skin = instance._nativeSkin;
748
+ if (!skin?.name) return;
749
+ skin.light.x = x;
750
+ skin.light.y = y;
751
+ applyLight(skin, x, y);
752
+ }
753
+
754
+ export { SKINS as NATIVE_SKINS };