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.
- package/CHANGELOG.md +56 -7
- package/README.md +93 -16
- package/agent-robot-avatar-lite.js +44 -0
- package/agent-robot-avatar.js +2 -2
- package/index.d.ts +94 -17
- package/lite.d.ts +5 -0
- package/package.json +12 -5
- package/src/agent-robot-avatar-actions.js +17 -14
- package/src/agent-robot-avatar-core.js +5 -2
- package/src/agent-robot-avatar-gestures.js +9 -0
- package/src/agent-robot-avatar-runtime.js +58 -12
- package/src/agent-robot-avatar-skin-patterns.js +234 -0
- package/src/agent-robot-avatar-skins.js +256 -0
- package/src/agent-robot-avatar-version.js +1 -1
- package/src/agent-robot-avatar-waiting.js +30 -6
- package/src/skins/catalog.js +218 -0
- package/src/skins/collection-materials.js +118 -0
- package/src/skins/eyelids.js +32 -0
- package/src/skins/glaze-variants.js +29 -0
- package/src/skins/layered-texture.js +162 -0
- package/src/skins/material-data.js +111 -0
- package/src/skins/material-library.js +302 -0
- package/src/skins/metal-tint.js +11 -0
- package/src/skins/more-materials.js +69 -0
- package/src/skins/native.js +754 -0
- package/src/skins/paints.js +3 -0
- package/src/skins/revised-materials.js +44 -0
- package/src/skins/second-pass-materials.js +71 -0
- package/src/skins/soft-materials.js +111 -0
- package/src/skins/sports-materials.js +38 -0
- package/src/skins/wood-materials.js +68 -0
- package/assets/support/alipay.png +0 -0
- package/assets/support/wechat-pay.png +0 -0
- package/docs/README.de.md +0 -186
- package/docs/README.es.md +0 -186
- package/docs/README.fr.md +0 -186
- package/docs/README.ja.md +0 -186
- package/docs/README.ko.md +0 -186
- package/docs/README.pt.md +0 -186
- package/docs/README.ru.md +0 -186
- package/docs/README.zh-CN.md +0 -190
- package/docs/README.zh-TW.md +0 -186
- 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 };
|