Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 43s
554 lines
38 KiB
JavaScript
554 lines
38 KiB
JavaScript
/* teach - personage (avatar): eigen figuur samenstellen (soort, kleuren, haar,
|
|
accessoires), gedeeld tussen de Instellingen-sectie "Mijn personage" en het
|
|
avatar-widget. Getekend als één SVG rond vaste anatomische ankers; de tekenvolgorde =
|
|
laagvolgorde - geen z-index-gepuzzel zoals bij de eerdere CSS-laagjesversie)
|
|
zodat elk onderdeel z'n vaste plek op het silhouet overlapt in plaats van
|
|
los ernaast te zweven. */
|
|
"use strict";
|
|
const AVATAR_SPECIES = ["dino", "monster", "animal"];
|
|
const AVATAR_HAIR = ["none", "tuft", "spiky", "curly", "wavy", "bob", "ponytail", "braids", "mohawk"];
|
|
const AVATAR_HEAD_ACC = ["none", "cap", "beanie", "crown", "wizardhat", "glasses", "sunglasses", "eyepatch", "bow", "flower", "headphones", "partyhat"];
|
|
const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge", "necklace", "bowtie", "belt", "backpack", "satchel", "wings"];
|
|
const AVATAR_DEFAULT = Object.freeze({
|
|
species: "dino", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29",
|
|
accessoryHead: "none", accessoryBody: "none",
|
|
});
|
|
const AVATAR_SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
function avatarSanitize(config){
|
|
const c = config || {};
|
|
return {
|
|
species: AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species,
|
|
bodyColor: /^#[0-9a-fA-F]{6}$/.test(c.bodyColor||"") ? c.bodyColor : AVATAR_DEFAULT.bodyColor,
|
|
hairStyle: AVATAR_HAIR.includes(c.hairStyle) ? c.hairStyle : AVATAR_DEFAULT.hairStyle,
|
|
hairColor: /^#[0-9a-fA-F]{6}$/.test(c.hairColor||"") ? c.hairColor : AVATAR_DEFAULT.hairColor,
|
|
accessoryHead: AVATAR_HEAD_ACC.includes(c.accessoryHead) ? c.accessoryHead : AVATAR_DEFAULT.accessoryHead,
|
|
accessoryBody: AVATAR_BODY_ACC.includes(c.accessoryBody) ? c.accessoryBody : AVATAR_DEFAULT.accessoryBody,
|
|
};
|
|
}
|
|
|
|
/* mengt een hex-kleur amt (-1..1) richting zwart (negatief) of wit (positief) -
|
|
gebruikt voor buik/snuit-highlights en pootjes/staart-schaduw, zodat die
|
|
altijd bij de gekozen lichaamskleur passen zonder een aparte kleurkiezer. */
|
|
function avatarShade(hex, amt){
|
|
const n = parseInt(hex.slice(1), 16);
|
|
let r = (n>>16)&255, g = (n>>8)&255, b = n&255;
|
|
if(amt < 0){ r *= 1+amt; g *= 1+amt; b *= 1+amt; }
|
|
else{ r += (255-r)*amt; g += (255-g)*amt; b += (255-b)*amt; }
|
|
const clamp = v => Math.max(0, Math.min(255, Math.round(v)));
|
|
return "#" + [r,g,b].map(v=>clamp(v).toString(16).padStart(2,"0")).join("");
|
|
}
|
|
|
|
/* Groot hoofd en compact, soortspecifiek lijf. De kop beslaat circa twee
|
|
derde van de figuurbreedte; romp en ledematen blijven kort en sluiten onder
|
|
de wangen aan. Alle soorten delen dezelfde accessoire-ankers rond hals y=61,
|
|
borst y=72 en middel y=87. */
|
|
function avatarBodyMarkup(species, bodyColor){
|
|
const outline = avatarShade(bodyColor, -0.48);
|
|
const dark = avatarShade(bodyColor, -0.24);
|
|
const light = avatarShade(bodyColor, 0.4);
|
|
const highlight = avatarShade(bodyColor, 0.72);
|
|
let m = "";
|
|
|
|
/* Soortdetails achter kop en lijf. */
|
|
if(species === "dino"){
|
|
m += `<path class="av-part av-tail" d="M 39 79 C 25 83 14 81 3 72 C 10 88 24 94 43 87 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-crest" d="M 73 10 L 85 3 L 81 17 L 91 15 L 84 29" fill="${dark}" stroke="${outline}" stroke-width="1.3" stroke-linejoin="round"/>`;
|
|
}
|
|
if(species === "animal"){
|
|
m += `<path class="av-part av-ear av-ear-left" d="M 29 16 C 13 15 10 1 18 -4 C 29 -8 38 4 37 17 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5"/>`;
|
|
m += `<path d="M 27 11 C 19 11 17 4 21 1 C 27 -1 32 6 32 12 Z" fill="${light}"/>`;
|
|
m += `<path class="av-part av-ear av-ear-right" d="M 71 16 C 87 15 90 1 82 -4 C 71 -8 62 4 63 17 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5"/>`;
|
|
m += `<path d="M 73 11 C 81 11 83 4 79 1 C 73 -1 68 6 68 12 Z" fill="${light}"/>`;
|
|
}
|
|
if(species === "monster"){
|
|
m += `<path class="av-part av-horn" d="M 28 15 C 19 8 21 -3 28 -10 C 28 2 34 7 40 13 Z" fill="${outline}"/>`;
|
|
m += `<path class="av-part av-horn" d="M 72 15 C 81 8 79 -3 72 -10 C 72 2 66 7 60 13 Z" fill="${outline}"/>`;
|
|
}
|
|
|
|
/* Korte ledematen verschillen per wezen en verdwijnen onder de romp. */
|
|
if(species === "dino"){
|
|
m += `<path class="av-part av-leg av-leg-left" d="M 38 84 C 35 90 35 98 35 100 C 29 101 27 104 30 106 C 35 108 42 106 43 102 L 45 86 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-leg av-leg-right" d="M 62 84 C 65 90 65 98 65 100 C 71 101 73 104 70 106 C 65 108 58 106 57 102 L 55 86 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-arm av-arm-left" d="M 39 65 C 33 63 29 67 26 72 C 23 73 21 76 23 79 C 26 81 30 78 31 75 C 34 73 36 75 39 77 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-arm av-arm-right" d="M 61 65 C 67 63 71 67 74 72 C 77 73 79 76 77 79 C 74 81 70 78 69 75 C 66 73 64 75 61 77 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path d="M 23 76 L 27 77 M 77 76 L 73 77" stroke="${outline}" stroke-width="1" stroke-linecap="round"/>`;
|
|
}else if(species === "monster"){
|
|
m += `<path class="av-part av-leg av-leg-left" d="M 36 84 C 32 91 32 98 28 102 C 27 105 34 108 42 104 L 44 86 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-leg av-leg-right" d="M 64 84 C 68 91 68 98 72 102 C 73 105 66 108 58 104 L 56 86 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-arm av-arm-left" d="M 36 64 C 28 61 23 67 19 74 C 14 76 14 82 18 83 C 23 84 26 78 29 74 C 32 72 34 75 37 78 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-arm av-arm-right" d="M 64 64 C 72 61 77 67 81 74 C 86 76 86 82 82 83 C 77 84 74 78 71 74 C 68 72 66 75 63 78 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path d="M 16 79 L 21 80 M 84 79 L 79 80 M 29 103 L 34 102 M 71 103 L 66 102" stroke="${outline}" stroke-width="1.1" stroke-linecap="round"/>`;
|
|
}else{
|
|
m += `<path class="av-part av-leg av-leg-left" d="M 38 84 C 35 91 35 98 33 101 C 31 104 35 107 41 106 C 45 104 44 94 45 86 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-leg av-leg-right" d="M 62 84 C 65 91 65 98 67 101 C 69 104 65 107 59 106 C 55 104 56 94 55 86 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-arm av-arm-left" d="M 38 65 C 32 63 27 68 25 75 C 22 78 24 82 28 81 C 32 79 32 73 38 76 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-arm av-arm-right" d="M 62 65 C 68 63 73 68 75 75 C 78 78 76 82 72 81 C 68 79 68 73 62 76 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path d="M 25 78 Q 28 76 31 78 M 75 78 Q 72 76 69 78" stroke="${outline}" stroke-width="1" fill="none" stroke-linecap="round"/>`;
|
|
}
|
|
|
|
m += `<path class="av-part av-neck" d="M 42 52 C 43 59 40 62 37 64 L 63 64 C 60 62 57 59 58 52 Z" fill="${dark}" stroke="${outline}" stroke-width="1.3"/>`;
|
|
const torsoPath = species === "dino"
|
|
? "M 39 61 C 45 58 55 58 61 61 C 67 68 67 83 62 91 C 56 96 44 96 38 91 C 33 83 33 68 39 61 Z"
|
|
: species === "monster"
|
|
? "M 35 62 C 42 57 58 57 65 62 C 71 70 70 85 64 92 C 56 97 44 97 36 92 C 30 85 29 70 35 62 Z"
|
|
: "M 38 61 C 44 58 56 58 62 61 C 67 69 66 84 61 93 C 54 97 46 97 39 93 C 34 84 33 69 38 61 Z";
|
|
m += `<path class="av-part av-torso av-torso-${species}" d="${torsoPath}" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
|
if(species === "dino"){
|
|
m += `<path class="av-part av-belly" d="M 43 66 C 47 63 53 63 57 66 C 61 73 60 86 56 91 C 52 93 48 93 44 91 C 40 86 39 73 43 66 Z" fill="${light}" opacity=".9"/>`;
|
|
m += `<circle cx="38" cy="80" r="1.7" fill="${dark}"/><circle cx="63" cy="74" r="1.5" fill="${dark}"/>`;
|
|
}else if(species === "monster"){
|
|
m += `<ellipse class="av-part av-belly" cx="50" cy="77" rx="12" ry="14" fill="${light}" opacity=".72"/>`;
|
|
m += `<circle cx="43" cy="71" r="2" fill="${dark}"/><circle cx="58" cy="82" r="2.5" fill="${dark}"/>`;
|
|
}else{
|
|
m += `<path class="av-part av-chest-fluff" d="M 39 64 L 45 67 L 50 64 L 55 67 L 61 64 C 60 76 57 85 50 90 C 43 85 40 76 39 64 Z" fill="${light}" opacity=".9"/>`;
|
|
}
|
|
|
|
/* De forse kop overlapt de hals en de bovenkant van het kleine lijf. */
|
|
const headPath = species === "monster"
|
|
? "M 50 -3 C 29 -5 16 8 16 30 C 15 51 29 63 48 64 C 69 66 85 53 84 30 C 84 8 71 -2 50 -3 Z"
|
|
: species === "dino"
|
|
? "M 47 -3 C 28 -3 15 10 16 31 C 16 49 28 60 45 63 C 64 67 83 55 85 35 C 87 15 73 -1 55 -2 Z"
|
|
: "M 50 -3 C 30 -3 17 10 17 31 C 17 52 30 64 50 64 C 70 64 83 52 83 31 C 83 10 70 -3 50 -3 Z";
|
|
m += `<path class="av-part av-head av-head-large" d="${headPath}" fill="${bodyColor}" stroke="${outline}" stroke-width="1.6" stroke-linejoin="round"/>`;
|
|
m += `<path d="M 27 12 C 33 4 41 1 49 1" stroke="${highlight}" stroke-width="2.5" fill="none" stroke-linecap="round" opacity=".68"/>`;
|
|
|
|
if(species === "dino"){
|
|
m += `<path class="av-part av-muzzle" d="M 28 39 C 37 34 67 34 75 41 C 79 51 67 59 51 59 C 35 59 24 51 28 39 Z" fill="${light}"/>`;
|
|
m += `<ellipse cx="41" cy="45" rx="2.2" ry="3" fill="${outline}"/><ellipse cx="63" cy="45" rx="2.2" ry="3" fill="${outline}"/>`;
|
|
}else if(species === "animal"){
|
|
m += `<path class="av-part av-muzzle" d="M 33 40 C 38 35 46 36 50 41 C 54 36 62 35 67 40 C 73 49 64 59 50 59 C 36 59 27 49 33 40 Z" fill="${light}"/>`;
|
|
m += `<path d="M 45 40 Q 50 36 55 40 Q 54 46 50 47 Q 46 46 45 40 Z" fill="#3a2a18"/>`;
|
|
}
|
|
|
|
if(species === "monster"){
|
|
m += `<ellipse class="av-part av-eye" cx="35" cy="27" rx="10" ry="10.8" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<ellipse class="av-part av-eye" cx="66" cy="27" rx="8.2" ry="9" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<circle cx="37" cy="29" r="4" fill="#232733"/><circle cx="67" cy="29" r="3.4" fill="#232733"/>`;
|
|
}else{
|
|
m += `<ellipse class="av-part av-eye" cx="35" cy="27" rx="8.5" ry="9.5" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<ellipse class="av-part av-eye" cx="65" cy="27" rx="8.5" ry="9.5" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<circle cx="37" cy="29" r="3.7" fill="#232733"/><circle cx="63" cy="29" r="3.7" fill="#232733"/>`;
|
|
}
|
|
m += `<circle cx="38.2" cy="27.5" r="1.2" fill="#fff"/><circle cx="64.2" cy="27.5" r="1.2" fill="#fff"/>`;
|
|
|
|
if(species === "dino"){
|
|
m += `<path d="M 42 54 Q 51 59 60 54" stroke="${outline}" stroke-width="1.7" fill="none" stroke-linecap="round"/>`;
|
|
}else if(species === "animal"){
|
|
m += `<path d="M 50 47 L 50 51 M 40 51 Q 45 57 50 51 Q 55 57 60 51" stroke="#3a2a18" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
}else{
|
|
m += `<path class="av-part av-mouth" d="M 29 43 Q 50 60 71 43 Q 66 58 50 59 Q 34 58 29 43 Z" fill="#3c2158" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<path d="M 39 46 L 43 54 L 47 46 Z M 53 46 L 57 54 L 61 46 Z" fill="#fff"/>`;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
const AVATAR_HAIR_MARKUP = {
|
|
none: { back: () => "", front: () => "" },
|
|
tuft: {
|
|
back: () => "",
|
|
front: (c) => `<g class="av-part av-hair av-hair-tuft" fill="${c}" stroke="${avatarShade(c,-0.4)}" stroke-linejoin="round">
|
|
<path d="M 37 11 C 39 5 42 2 47 1 C 43 -3 45 -9 50 -11 C 52 -6 53 -2 52 2 C 56 -3 62 -3 65 1 C 61 2 59 5 58 8 C 53 6 47 7 42 12 Z" stroke-width="1.25"/>
|
|
<path class="av-hair-texture" d="M 48 0 Q 50 4 48 7 M 56 1 Q 54 4 54 7" fill="none" stroke="${avatarShade(c,0.28)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
</g>`,
|
|
},
|
|
spiky: {
|
|
back: () => "",
|
|
front: (c) => `<g class="av-part av-hair av-hair-spiky" fill="${c}" stroke="${avatarShade(c,-0.4)}" stroke-linejoin="round">
|
|
<path d="M 18 22 C 18 12 21 5 27 1 L 28 -7 L 36 -2 L 41 -11 L 48 -3 L 55 -13 L 61 -3 L 71 -9 L 70 0 L 79 -3 L 76 7 C 80 11 82 16 82 22 C 70 16 62 14 51 14 C 39 14 29 17 18 22 Z" stroke-width="1.35"/>
|
|
<path class="av-hairline" d="M 20 20 C 35 13 65 11 80 20" fill="none" stroke="${avatarShade(c,-0.18)}" stroke-width="1.2" stroke-linecap="round"/>
|
|
<path class="av-hair-texture" d="M 31 2 L 35 9 M 43 -3 L 45 8 M 57 -4 L 55 8 M 69 0 L 64 10" fill="none" stroke="${avatarShade(c,0.25)}" stroke-width="1.05" stroke-linecap="round"/>
|
|
</g>`,
|
|
},
|
|
curly: {
|
|
back: (c) => `<path class="av-part av-hair-back av-hair-curly-back" d="M 13 21 C 11 12 16 5 23 2 C 22 -6 31 -11 38 -8 C 43 -16 55 -16 61 -9 C 69 -12 78 -6 77 2 C 85 5 89 14 86 23 C 83 31 77 35 72 38 L 28 38 C 21 35 15 30 13 21 Z" fill="${avatarShade(c,-0.08)}" stroke="${avatarShade(c,-0.42)}" stroke-width="1.35"/>`,
|
|
front: (c) => `<g class="av-part av-hair av-hair-curly" fill="${c}" stroke="${avatarShade(c,-0.34)}" stroke-width="1.05">
|
|
<circle cx="19" cy="18" r="7.2"/><circle cx="23" cy="8" r="7.7"/><circle cx="31" cy="1" r="8.2"/>
|
|
<circle cx="41" cy="-3" r="8.5"/><circle cx="51" cy="-4" r="8.8"/><circle cx="61" cy="-2" r="8.4"/>
|
|
<circle cx="70" cy="2" r="8.1"/><circle cx="78" cy="9" r="7.7"/><circle cx="82" cy="19" r="7.1"/>
|
|
<path class="av-hairline" d="M 18 21 C 25 18 27 13 29 10 C 34 16 40 15 43 10 C 48 16 54 16 58 10 C 63 15 70 16 73 11 C 75 16 78 19 82 21" fill="none" stroke="${avatarShade(c,-0.3)}" stroke-width="1.3" stroke-linecap="round"/>
|
|
<path class="av-hair-texture" d="M 28 3 Q 34 -1 38 4 M 46 -5 Q 52 -1 55 -6 M 64 0 Q 70 3 72 8" fill="none" stroke="${avatarShade(c,0.3)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
</g>`,
|
|
},
|
|
wavy: {
|
|
back: (c) => `<path class="av-part av-hair-back av-hair-wavy" d="M 14 19 C 13 0 28 -12 49 -12 C 71 -12 86 1 86 22 C 88 31 85 39 81 45 C 85 51 82 58 75 62 C 76 54 70 50 72 43 C 66 48 61 51 57 52 L 43 52 C 37 50 32 47 28 43 C 30 51 24 55 25 62 C 17 58 14 51 18 44 C 14 37 12 28 14 19 Z" fill="${avatarShade(c,-0.07)}" stroke="${avatarShade(c,-0.42)}" stroke-width="1.4" stroke-linejoin="round"/>`,
|
|
front: (c) => `<g class="av-part av-hair av-hair-wavy-fringe" fill="${c}" stroke="${avatarShade(c,-0.38)}" stroke-linejoin="round">
|
|
<path d="M 17 21 C 17 2 31 -9 50 -9 C 69 -9 82 2 83 20 C 76 18 71 14 67 9 C 63 15 58 18 52 19 C 47 20 42 17 40 12 C 34 17 26 20 17 21 Z" stroke-width="1.3"/>
|
|
<path class="av-side-lock av-side-lock-left" d="M 18 18 C 16 25 18 31 23 35 C 21 29 25 25 28 20 Z" stroke-width="1.15"/>
|
|
<path class="av-side-lock av-side-lock-right" d="M 82 17 C 85 25 82 31 77 35 C 79 29 75 25 72 20 Z" stroke-width="1.15"/>
|
|
<path class="av-hair-part" d="M 57 -5 C 53 2 48 8 40 12" fill="none" stroke="${avatarShade(c,0.35)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
<path class="av-hair-texture" d="M 29 2 Q 34 6 31 11 M 69 1 Q 73 6 70 11" fill="none" stroke="${avatarShade(c,0.24)}" stroke-width="1" stroke-linecap="round"/>
|
|
</g>`,
|
|
},
|
|
bob: {
|
|
back: (c) => `<path class="av-part av-hair-back av-hair-bob" d="M 14 20 C 14 0 29 -12 50 -12 C 71 -12 86 0 86 21 C 87 34 85 47 81 57 C 77 61 72 61 67 58 L 66 42 C 61 46 57 48 50 48 C 43 48 38 46 33 42 L 32 58 C 27 61 21 61 17 57 C 14 46 13 33 14 20 Z" fill="${avatarShade(c,-0.06)}" stroke="${avatarShade(c,-0.42)}" stroke-width="1.4"/>`,
|
|
front: (c) => `<g class="av-part av-hair av-hair-bob-fringe" fill="${c}" stroke="${avatarShade(c,-0.38)}" stroke-linejoin="round">
|
|
<path d="M 16 21 C 16 2 31 -9 50 -9 C 69 -9 83 2 84 21 C 76 19 69 15 63 8 C 58 13 53 17 47 18 C 39 20 31 17 24 13 C 23 17 21 20 16 21 Z" stroke-width="1.3"/>
|
|
<path class="av-side-lock av-side-lock-left" d="M 17 18 C 15 28 17 39 23 47 C 27 44 29 39 28 32 C 25 27 23 23 24 15 Z" stroke-width="1.15"/>
|
|
<path class="av-side-lock av-side-lock-right" d="M 83 18 C 85 28 83 39 77 47 C 73 44 71 39 72 32 C 75 27 77 23 76 15 Z" stroke-width="1.15"/>
|
|
<path class="av-hair-part" d="M 62 -4 C 59 2 58 5 63 8" fill="none" stroke="${avatarShade(c,0.34)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
<path class="av-hair-texture" d="M 24 23 Q 21 34 24 40 M 76 23 Q 79 34 76 40" fill="none" stroke="${avatarShade(c,0.24)}" stroke-width="1" stroke-linecap="round"/>
|
|
</g>`,
|
|
},
|
|
ponytail: {
|
|
back: (c) => `<g class="av-part av-hair-back av-hair-ponytail" stroke="${avatarShade(c,-0.42)}" stroke-linejoin="round">
|
|
<path d="M 72 8 C 83 1 94 7 96 18 C 99 32 91 47 78 58 C 82 45 80 34 74 29 C 86 31 88 19 76 17 Z" fill="${avatarShade(c,-0.06)}" stroke-width="1.4"/>
|
|
<path class="av-hair-texture" d="M 83 13 C 92 22 89 36 83 45" fill="none" stroke="${avatarShade(c,0.22)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
<ellipse class="av-hair-tie" cx="75" cy="13" rx="5.2" ry="4.2" fill="#d84f78" stroke="#96304f" stroke-width="1"/>
|
|
</g>`,
|
|
front: (c) => `<g class="av-part av-hair av-hair-ponytail-front" fill="${c}" stroke="${avatarShade(c,-0.38)}" stroke-linejoin="round">
|
|
<path d="M 16 21 C 17 1 31 -10 50 -10 C 68 -10 80 0 83 17 C 76 16 70 13 65 8 C 57 15 47 18 38 16 C 31 15 25 16 16 21 Z" stroke-width="1.3"/>
|
|
<path class="av-hair-part" d="M 63 -5 C 61 1 61 5 65 8" fill="none" stroke="${avatarShade(c,0.34)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
<path class="av-sideburn" d="M 18 19 C 17 25 19 29 23 32 C 22 26 25 22 28 18 Z" stroke-width="1.05"/>
|
|
</g>`,
|
|
},
|
|
braids: {
|
|
back: (c) => `<g class="av-part av-hair-back av-hair-braids" fill="${c}" stroke="${avatarShade(c,-0.42)}" stroke-width="1.05">
|
|
<path d="M 22 17 C 15 24 15 31 20 36 C 14 40 14 47 19 51 C 14 56 16 64 21 68 C 27 65 28 58 23 53 C 29 49 28 42 23 38 C 29 32 28 23 22 17 Z"/>
|
|
<path d="M 78 17 C 85 24 85 31 80 36 C 86 40 86 47 81 51 C 86 56 84 64 79 68 C 73 65 72 58 77 53 C 71 49 72 42 77 38 C 71 32 72 23 78 17 Z"/>
|
|
<path class="av-hair-texture" d="M 18 31 L 25 35 M 17 45 L 25 49 M 18 58 L 24 62 M 82 31 L 75 35 M 83 45 L 75 49 M 82 58 L 76 62" fill="none" stroke="${avatarShade(c,0.26)}" stroke-width="1.55" stroke-linecap="round"/>
|
|
<path class="av-hair-tie" d="M 17 66 Q 21 70 25 66 M 75 66 Q 79 70 83 66" fill="none" stroke="#d84f78" stroke-width="2.3" stroke-linecap="round"/>
|
|
</g>`,
|
|
front: (c) => `<g class="av-part av-hair av-hair-braids-front" fill="${c}" stroke="${avatarShade(c,-0.38)}" stroke-linejoin="round">
|
|
<path d="M 16 21 C 17 1 31 -10 50 -10 C 69 -10 82 1 84 21 C 76 19 70 14 65 8 C 60 14 54 17 48 18 C 39 19 31 16 23 12 C 22 17 20 19 16 21 Z" stroke-width="1.3"/>
|
|
<path class="av-hair-part" d="M 63 -5 C 61 1 61 5 65 8" fill="none" stroke="${avatarShade(c,0.34)}" stroke-width="1.15" stroke-linecap="round"/>
|
|
<path class="av-sideburn" d="M 18 18 C 17 25 19 29 23 32 C 22 26 25 22 27 18 Z M 82 18 C 83 25 81 29 77 32 C 78 26 75 22 73 18 Z" stroke-width="1.05"/>
|
|
</g>`,
|
|
},
|
|
mohawk: {
|
|
back: () => "",
|
|
front: (c) => `<g class="av-part av-hair av-hair-mohawk" fill="${c}" stroke="${avatarShade(c,-0.42)}" stroke-linejoin="round">
|
|
<path d="M 41 14 C 40 8 40 2 42 -3 L 45 -13 L 50 -6 L 55 -16 L 59 -6 L 62 -11 L 60 2 C 60 7 59 11 58 15 C 53 12 47 12 41 14 Z" stroke-width="1.35"/>
|
|
<path class="av-hair-texture" d="M 47 -9 L 48 8 M 55 -10 L 53 8" fill="none" stroke="${avatarShade(c,0.28)}" stroke-width="1.1" stroke-linecap="round"/>
|
|
</g>`,
|
|
},
|
|
};
|
|
|
|
/* Iedere kop gebruikt dezelfde haarankers, met een minimale correctie voor
|
|
het asymmetrische dinosaurusprofiel. De wrapper maakt de pasvorm expliciet
|
|
en houdt alle lokken als een laag bij elkaar. */
|
|
function avatarHairLayer(markup, species, layer){
|
|
if(!markup) return "";
|
|
const transform = species === "dino" ? "translate(1 0)" : species === "monster" ? "translate(0 -1)" : "translate(0 0)";
|
|
return `<g class="av-hair-layer av-hair-${layer}-layer av-hair-fit-${species}" transform="${transform}">${markup}</g>`;
|
|
}
|
|
const AVATAR_HEAD_ACC_MARKUP = {
|
|
none: () => "",
|
|
cap: () => `
|
|
<path class="av-part av-headwear av-cap" d="M 19 17 C 21 0 34 -8 51 -7 C 68 -6 79 3 81 18 C 63 11 39 11 19 17 Z" fill="#3d7ddb" stroke="#234f91" stroke-width="1.4"/>
|
|
<path d="M 20 16 C 37 11 59 12 75 17 C 61 22 39 22 15 22 C 13 20 15 18 20 16 Z" fill="#2f63b3" stroke="#234f91" stroke-width="1.3"/>
|
|
<path d="M 51 -7 L 51 11" stroke="#72a3e8" stroke-width="1.2" opacity=".8"/>`,
|
|
beanie: () => `
|
|
<path class="av-part av-headwear av-beanie" d="M 19 16 C 19 -2 31 -11 50 -11 C 69 -11 81 -2 81 16 Z" fill="#e56a55" stroke="#9f3e31" stroke-width="1.4"/>
|
|
<path d="M 18 13 C 34 10 66 10 82 13 L 81 22 C 64 18 36 18 19 22 Z" fill="#c94f3d" stroke="#9f3e31" stroke-width="1.3"/>
|
|
<circle cx="50" cy="-12" r="5" fill="#f1a15f" stroke="#9f3e31" stroke-width="1"/>`,
|
|
crown: () => `
|
|
<path class="av-part av-headwear av-crown" d="M 22 17 L 25 -2 L 37 10 L 50 -12 L 63 10 L 75 -2 L 78 17 C 61 12 39 12 22 17 Z" fill="#f0b73a" stroke="#b77b0b" stroke-width="1.3" stroke-linejoin="round"/>
|
|
<path d="M 23 15 C 40 11 60 11 77 15 L 77 20 C 60 16 40 16 23 20 Z" fill="#d99a22"/>
|
|
<circle cx="25" cy="-1" r="2.8" fill="#e0483c"/><circle cx="50" cy="-11" r="3" fill="#3a8ee0"/><circle cx="75" cy="-1" r="2.8" fill="#e0483c"/>`,
|
|
wizardhat: () => `
|
|
<path class="av-part av-headwear av-wizardhat" d="M 49 -16 C 58 -7 64 2 67 15 C 54 12 42 12 30 16 C 39 5 44 -5 49 -16 Z" fill="#6850bd" stroke="#3e2d84" stroke-width="1.4" stroke-linejoin="round"/>
|
|
<path d="M 51 -9 C 57 -3 59 4 61 10" stroke="#9b88df" stroke-width="2" fill="none" stroke-linecap="round"/>
|
|
<path d="M 16 17 C 35 12 66 12 84 17 C 87 20 82 23 75 22 C 57 19 39 19 23 22 C 16 23 12 20 16 17 Z" fill="#513b9d" stroke="#3e2d84" stroke-width="1.4"/>
|
|
<path d="M 45 1 L 47 5 L 52 5 L 48 8 L 50 13 L 45 10 L 41 13 L 42 8 L 38 5 L 43 5 Z" fill="#f1c84c"/>`,
|
|
glasses: () => `
|
|
<circle class="av-part av-eyewear av-glasses" cx="35" cy="27" r="10.5" fill="rgba(255,255,255,.28)" stroke="#232733" stroke-width="2.5"/>
|
|
<circle class="av-part av-eyewear av-glasses" cx="65" cy="27" r="10.5" fill="rgba(255,255,255,.28)" stroke="#232733" stroke-width="2.5"/>
|
|
<path d="M 45.5 27 L 54.5 27 M 24.5 24 L 16 20 M 75.5 24 L 84 20" stroke="#232733" stroke-width="2.4" stroke-linecap="round"/>`,
|
|
sunglasses: () => `
|
|
<path class="av-part av-eyewear av-sunglasses" d="M 23 21 C 23 35 29 39 37 39 C 46 39 50 33 48 23 C 44 19 29 18 23 21 Z" fill="#1a1d24"/>
|
|
<path d="M 52 23 C 50 33 54 39 63 39 C 71 39 77 35 77 21 C 71 18 56 19 52 23 Z" fill="#1a1d24"/>
|
|
<path d="M 48 24 Q 50 21 52 24 M 23 22 L 15 19 M 77 22 L 85 19" stroke="#1a1d24" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
|
<path d="M 28 23 Q 32 20 36 22 M 57 23 Q 61 20 65 22" stroke="#fff" stroke-width="1.7" fill="none" opacity=".55" stroke-linecap="round"/>`,
|
|
eyepatch: () => `
|
|
<path class="av-part av-eyewear av-eyepatch" d="M 16 17 C 38 22 61 25 84 17" stroke="#2a2630" stroke-width="2.4" fill="none"/>
|
|
<path d="M 54 23 C 58 18 70 18 75 24 C 75 34 70 40 64 40 C 58 40 54 34 54 23 Z" fill="#34303b" stroke="#17151b" stroke-width="1.3"/>
|
|
<path d="M 60 24 Q 65 21 70 24" stroke="#666170" stroke-width="1.4" fill="none" stroke-linecap="round"/>`,
|
|
bow: () => `
|
|
<path class="av-part av-headwear av-bow" d="M 76 13 C 68 3 60 6 61 14 C 62 22 69 22 76 17 C 83 23 91 22 92 14 C 93 6 84 3 76 13 Z" fill="#e05a8a" stroke="#a93660" stroke-width="1.3"/>
|
|
<circle cx="76" cy="15" r="4" fill="#c23f6e"/>`,
|
|
flower: () => `
|
|
<g class="av-part av-headwear av-flower" fill="#f08bb0" stroke="#b74470" stroke-width="1">
|
|
<ellipse cx="78" cy="7" rx="4" ry="7"/><ellipse cx="78" cy="21" rx="4" ry="7"/>
|
|
<ellipse cx="71" cy="14" rx="7" ry="4"/><ellipse cx="85" cy="14" rx="7" ry="4"/>
|
|
<ellipse cx="73" cy="9" rx="4" ry="6" transform="rotate(-45 73 9)"/><ellipse cx="83" cy="19" rx="4" ry="6" transform="rotate(-45 83 19)"/>
|
|
</g><circle cx="78" cy="14" r="4" fill="#f0c84a" stroke="#b77b0b" stroke-width="1"/>`,
|
|
headphones: () => `
|
|
<path class="av-part av-headwear av-headphones" d="M 12 29 C 12 3 27 -11 50 -11 C 73 -11 88 3 88 29" stroke="#333" stroke-width="5" fill="none" stroke-linecap="round"/>
|
|
<rect x="8" y="22" width="12" height="23" rx="6" fill="#333" stroke="#17181c" stroke-width="1.3"/>
|
|
<rect x="80" y="22" width="12" height="23" rx="6" fill="#333" stroke="#17181c" stroke-width="1.3"/>
|
|
<rect x="12" y="27" width="4" height="13" rx="2" fill="#667"/><rect x="84" y="27" width="4" height="13" rx="2" fill="#667"/>`,
|
|
partyhat: () => `
|
|
<path class="av-part av-headwear av-partyhat" d="M 50 -15 L 64 18 C 55 22 45 22 36 18 Z" fill="#e05a8a" stroke="#a93660" stroke-width="1.3"/>
|
|
<path d="M 36.5 17 C 45 21 55 21 63.5 17" stroke="#f4c34e" stroke-width="2.2" fill="none"/>
|
|
<circle cx="45" cy="3" r="2" fill="#f0b73a"/><circle cx="56" cy="8" r="2" fill="#3a8ee0"/><circle cx="47" cy="14" r="2" fill="#f0b73a"/>
|
|
<circle cx="50" cy="-15" r="4.2" fill="#f0b73a"/>`,
|
|
};
|
|
|
|
const AVATAR_BODY_ACC_MARKUP = {
|
|
none: { back: () => "", front: () => "" },
|
|
scarf: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-neckwear av-scarf" d="M 34 58 C 42 63 58 63 66 58 L 67 67 C 58 72 42 72 33 67 Z" fill="#d94f4f" stroke="#9d3030" stroke-width="1.3"/>
|
|
<path d="M 57 68 L 63 87 L 54 83 L 50 69 Z" fill="#c23f3f" stroke="#9d3030" stroke-width="1.2" stroke-linejoin="round"/>
|
|
<path d="M 56 79 L 62 82" stroke="#f08080" stroke-width="1.2"/>`,
|
|
},
|
|
cape: {
|
|
back: () => `
|
|
<path class="av-part av-backwear av-cape" d="M 35 59 C 22 67 17 82 17 108 C 29 103 40 99 50 92 C 60 99 71 103 83 108 C 83 82 78 67 65 59 C 57 64 43 64 35 59 Z" fill="#7a4bc7" stroke="#54308f" stroke-width="1.5" stroke-linejoin="round"/>
|
|
<path d="M 28 67 C 24 80 25 94 28 102" stroke="#a67de0" stroke-width="2" fill="none" opacity=".65"/>`,
|
|
front: () => `
|
|
<path class="av-part av-cape-clasp" d="M 36 61 C 43 66 57 66 64 61" stroke="#f0b73a" stroke-width="2.4" fill="none" stroke-linecap="round"/>
|
|
<circle cx="36" cy="61" r="2.5" fill="#f0b73a"/><circle cx="64" cy="61" r="2.5" fill="#f0b73a"/>`,
|
|
},
|
|
badge: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-chestwear av-badge" d="M 55 70 L 61 69 L 65 74 L 63 80 L 57 81 L 53 76 Z" fill="#f0b73a" stroke="#fff" stroke-width="1.4"/>
|
|
<path d="M 59 72 L 60 75 L 63 75 L 60.5 77 L 61.5 80 L 59 78 L 56.5 80 L 57.5 77 L 55 75 L 58 75 Z" fill="#b8750b"/>`,
|
|
},
|
|
necklace: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-neckwear av-necklace" d="M 37 62 C 39 74 45 82 50 83 C 55 82 61 74 63 62" stroke="#f0b73a" stroke-width="2" fill="none" stroke-linecap="round"/>
|
|
<path d="M 50 80 L 55 85 L 50 91 L 45 85 Z" fill="#3a8ee0" stroke="#f0b73a" stroke-width="1.4"/>`,
|
|
},
|
|
bowtie: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-neckwear av-bowtie" d="M 49 67 C 42 61 36 63 37 70 C 38 76 44 75 49 71 C 54 75 62 76 63 70 C 64 63 57 61 51 67 Z" fill="#3a8ee0" stroke="#205d9c" stroke-width="1.2"/>
|
|
<circle cx="50" cy="69" r="3.5" fill="#f0b73a" stroke="#b77b0b" stroke-width="1"/>`,
|
|
},
|
|
belt: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-bodywear av-belt" d="M 35 84 C 43 87 57 87 65 84 L 65 91 C 56 94 44 94 35 91 Z" fill="#5a4029" stroke="#392818" stroke-width="1.2"/>
|
|
<rect x="46" y="85" width="9" height="8" rx="1.5" fill="#f0b73a" stroke="#b77b0b" stroke-width="1.2"/>
|
|
<rect x="49" y="87" width="3" height="4" fill="#5a4029"/>`,
|
|
},
|
|
backpack: {
|
|
back: () => `
|
|
<path class="av-part av-backwear av-backpack" d="M 30 65 C 31 56 39 53 50 53 C 61 53 69 56 70 65 L 75 91 C 68 97 32 97 25 91 Z" fill="#8a6a45" stroke="#5a4029" stroke-width="1.6"/>
|
|
<path d="M 40 55 C 41 49 59 49 60 55" stroke="#5a4029" stroke-width="3" fill="none"/>`,
|
|
front: () => `
|
|
<path class="av-part av-backpack-strap" d="M 39 62 C 33 69 33 80 36 88 M 61 62 C 67 69 67 80 64 88" stroke="#5a4632" stroke-width="3.5" fill="none" stroke-linecap="round"/>
|
|
<path d="M 35 87 L 40 90 M 65 87 L 60 90" stroke="#d0a36d" stroke-width="1.4"/>`,
|
|
},
|
|
satchel: {
|
|
back: () => `
|
|
<path class="av-part av-backwear av-satchel" d="M 61 75 C 68 72 79 74 83 80 L 82 95 C 76 99 65 98 60 93 Z" fill="#b87545" stroke="#704329" stroke-width="1.5"/>
|
|
<path d="M 63 81 C 69 84 76 84 81 81" stroke="#e3ad72" stroke-width="1.4" fill="none"/>`,
|
|
front: () => `
|
|
<path class="av-part av-satchel-strap" d="M 36 62 C 49 71 58 80 68 94" stroke="#704329" stroke-width="2.8" fill="none" stroke-linecap="round"/>
|
|
<circle cx="67" cy="93" r="2" fill="#f0b73a"/>`,
|
|
},
|
|
wings: {
|
|
back: () => `
|
|
<path class="av-part av-backwear av-wings" d="M 37 65 C 25 51 11 48 5 55 C 1 64 12 72 24 74 C 10 77 6 86 13 91 C 23 96 33 85 41 75 Z" fill="#b8e8ef" stroke="#5799ad" stroke-width="1.5" stroke-linejoin="round" opacity=".95"/>
|
|
<path d="M 63 65 C 75 51 89 48 95 55 C 99 64 88 72 76 74 C 90 77 94 86 87 91 C 77 96 67 85 59 75 Z" fill="#b8e8ef" stroke="#5799ad" stroke-width="1.5" stroke-linejoin="round" opacity=".95"/>
|
|
<path d="M 11 58 C 23 61 30 67 37 74 M 89 58 C 77 61 70 67 63 74" stroke="#fff" stroke-width="2" fill="none" opacity=".7"/>`,
|
|
front: () => `<circle class="av-part av-wing-clasp" cx="37" cy="65" r="2.4" fill="#5799ad"/><circle cx="63" cy="65" r="2.4" fill="#5799ad"/>`,
|
|
},
|
|
};
|
|
|
|
/* Pet en muts bedekken de voorste haarlaag. Lange stijlen houden hun
|
|
achterlaag, zodat staarten, vlechten en lokken naast de kop zichtbaar zijn. */
|
|
function avatarHairSuppressed(c){
|
|
return c.accessoryHead === "cap" || c.accessoryHead === "beanie";
|
|
}
|
|
|
|
/* bouwt de SVG-weergave van een personage; config==null → neutrale
|
|
standaardweergave (nooit een lege/kapotte weergave). Tekenvolgorde bepaalt
|
|
de laagvolgorde (SVG kent geen z-index) - vandaar cape/staart/poten eerst,
|
|
accessoires als laatste. */
|
|
function renderAvatarFace(config, size){
|
|
const c = avatarSanitize(config);
|
|
const svg = document.createElementNS(AVATAR_SVG_NS, "svg");
|
|
/* Extra ruimte voorkomt afsnijden van hoofddeksels, haar en voetzolen. */
|
|
svg.setAttribute("viewBox", "-8 -21 116 132");
|
|
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species);
|
|
svg.setAttribute("role", "img");
|
|
svg.setAttribute("aria-label", T("avatarAlt"));
|
|
const bodyAccessory = AVATAR_BODY_ACC_MARKUP[c.accessoryBody];
|
|
const hair = AVATAR_HAIR_MARKUP[c.hairStyle];
|
|
let markup = bodyAccessory.back();
|
|
markup += avatarHairLayer(hair.back(c.hairColor), c.species, "back");
|
|
markup += avatarBodyMarkup(c.species, c.bodyColor);
|
|
if(!avatarHairSuppressed(c)) markup += avatarHairLayer(hair.front(c.hairColor), c.species, "front");
|
|
markup += AVATAR_HEAD_ACC_MARKUP[c.accessoryHead]();
|
|
markup += bodyAccessory.front();
|
|
svg.innerHTML = markup;
|
|
return svg;
|
|
}
|
|
|
|
/* bouwt de interactieve kiezer + live voorbeeld; roept onChange(config) aan
|
|
bij elke wijziging. Hergebruikt door zowel de instellingen-sectie als het
|
|
widget - één UI, geen duplicatie. */
|
|
function buildAvatarEditor(container, initialConfig, onChange){
|
|
let config = avatarSanitize(initialConfig);
|
|
container.innerHTML = "";
|
|
const editor = document.createElement("div");
|
|
editor.className = "avatar-editor";
|
|
const preview = document.createElement("div");
|
|
preview.className = "avatar-preview";
|
|
const controls = document.createElement("div");
|
|
controls.className = "avatar-controls";
|
|
editor.append(preview, controls);
|
|
container.appendChild(editor);
|
|
|
|
const speciesRow = document.createElement("div");
|
|
speciesRow.className = "avatar-species-row";
|
|
AVATAR_SPECIES.forEach(id=>{
|
|
const b = document.createElement("button");
|
|
b.type = "button";
|
|
b.dataset.species = id;
|
|
b.addEventListener("click", ()=>{ config.species = id; update(); });
|
|
speciesRow.appendChild(b);
|
|
});
|
|
controls.appendChild(speciesRow);
|
|
|
|
const row1 = document.createElement("div"); row1.className = "avatar-row";
|
|
const bodyColorField = document.createElement("label");
|
|
const bodyColorSpan = document.createElement("span");
|
|
const bodyColorInput = document.createElement("input"); bodyColorInput.type = "color";
|
|
bodyColorInput.addEventListener("input", ()=>{ config.bodyColor = bodyColorInput.value; update(); });
|
|
bodyColorField.append(bodyColorSpan, bodyColorInput);
|
|
const hairColorField = document.createElement("label");
|
|
const hairColorSpan = document.createElement("span");
|
|
const hairColorInput = document.createElement("input"); hairColorInput.type = "color";
|
|
hairColorInput.addEventListener("input", ()=>{ config.hairColor = hairColorInput.value; update(); });
|
|
hairColorField.append(hairColorSpan, hairColorInput);
|
|
row1.append(bodyColorField, hairColorField);
|
|
controls.appendChild(row1);
|
|
|
|
const row2 = document.createElement("div"); row2.className = "avatar-row";
|
|
const hairField = document.createElement("label");
|
|
const hairSpan = document.createElement("span");
|
|
const hairSel = document.createElement("select");
|
|
AVATAR_HAIR.forEach(id=>hairSel.appendChild(new Option("", id)));
|
|
hairSel.addEventListener("change", ()=>{ config.hairStyle = hairSel.value; update(); });
|
|
hairField.append(hairSpan, hairSel);
|
|
row2.append(hairField);
|
|
controls.appendChild(row2);
|
|
|
|
const row3 = document.createElement("div"); row3.className = "avatar-row";
|
|
const headAccField = document.createElement("label");
|
|
const headAccSpan = document.createElement("span");
|
|
const headAccSel = document.createElement("select");
|
|
AVATAR_HEAD_ACC.forEach(id=>headAccSel.appendChild(new Option("", id)));
|
|
headAccSel.addEventListener("change", ()=>{ config.accessoryHead = headAccSel.value; update(); });
|
|
headAccField.append(headAccSpan, headAccSel);
|
|
const bodyAccField = document.createElement("label");
|
|
const bodyAccSpan = document.createElement("span");
|
|
const bodyAccSel = document.createElement("select");
|
|
AVATAR_BODY_ACC.forEach(id=>bodyAccSel.appendChild(new Option("", id)));
|
|
bodyAccSel.addEventListener("change", ()=>{ config.accessoryBody = bodyAccSel.value; update(); });
|
|
bodyAccField.append(bodyAccSpan, bodyAccSel);
|
|
row3.append(headAccField, bodyAccField);
|
|
controls.appendChild(row3);
|
|
|
|
function syncLabels(){
|
|
speciesRow.querySelectorAll("button").forEach(b=>{
|
|
b.textContent = T("avatarSpecies_" + b.dataset.species);
|
|
b.classList.toggle("active", b.dataset.species === config.species);
|
|
});
|
|
bodyColorSpan.textContent = T("avatarBodyColor");
|
|
hairColorSpan.textContent = T("avatarHairColor");
|
|
hairSpan.textContent = T("avatarHairStyle");
|
|
[...hairSel.options].forEach(o=>o.textContent = T("avatarHair_" + o.value));
|
|
headAccSpan.textContent = T("avatarAccHead");
|
|
[...headAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value));
|
|
bodyAccSpan.textContent = T("avatarAccBody");
|
|
[...bodyAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value));
|
|
}
|
|
function update(){
|
|
bodyColorInput.value = config.bodyColor;
|
|
hairColorInput.value = config.hairColor;
|
|
hairSel.value = config.hairStyle;
|
|
headAccSel.value = config.accessoryHead;
|
|
bodyAccSel.value = config.accessoryBody;
|
|
syncLabels();
|
|
preview.innerHTML = "";
|
|
preview.appendChild(renderAvatarFace(config, "lg"));
|
|
if(onChange) onChange({ ...config });
|
|
}
|
|
document.addEventListener("langchange", syncLabels);
|
|
update();
|
|
return { getConfig: ()=>({ ...config }), setConfig(c){ config = avatarSanitize(c); update(); } };
|
|
}
|
|
|
|
async function saveAvatar(config){
|
|
const clean = avatarSanitize(config);
|
|
const res = await api("/me/avatar", { method: "PUT", body: clean });
|
|
currentUser.avatar = res.avatar;
|
|
document.dispatchEvent(new CustomEvent("avatarchange"));
|
|
return res.avatar;
|
|
}
|
|
|
|
/* ---- Instellingen-sectie "Mijn personage" (cms.js-patroon: eigen .st-section
|
|
zelf aanmaken, niet in index.html) ---- */
|
|
(function(){
|
|
const host = document.createElement("div");
|
|
host.className = "st-section";
|
|
host.dataset.section = "avatar";
|
|
host.style.display = "none";
|
|
document.getElementById("stContent").appendChild(host);
|
|
const msg = document.createElement("p");
|
|
msg.className = "avatar-save-msg";
|
|
let editorApi = null;
|
|
function build(){
|
|
host.innerHTML = "";
|
|
editorApi = buildAvatarEditor(host, currentUser && currentUser.avatar, ()=>{ msg.textContent = ""; });
|
|
const saveBtn = document.createElement("button");
|
|
saveBtn.type = "button";
|
|
saveBtn.className = "tbtn";
|
|
saveBtn.textContent = T("avatarSave");
|
|
saveBtn.addEventListener("click", async ()=>{
|
|
try{ await saveAvatar(editorApi.getConfig()); msg.textContent = T("avatarSaved"); }
|
|
catch(e){ msg.textContent = e.message; }
|
|
});
|
|
host.appendChild(saveBtn);
|
|
host.appendChild(msg);
|
|
}
|
|
registerSettingsSection({
|
|
id: "avatar", icon: "🦖", group: "personal", order: 3,
|
|
label: ()=>T("stAvatar"), sub: ()=>T("stAvatarSub"),
|
|
visible: ()=>!!currentUser,
|
|
activate(){ showSettingsDom("avatar"); build(); },
|
|
keywords: ()=>[T("avatarSpecies_dino"), T("avatarSpecies_monster"), T("avatarSpecies_animal"),
|
|
"personage","avatar","dinosaurus","monster","dier","haar","hair","kleur","color"],
|
|
});
|
|
})();
|
|
|
|
/* ---- widget: dunne wrapper rond dezelfde editor ---- */
|
|
function mountAvatarWidget(root){
|
|
root.innerHTML = `<p class="play-mission"></p>`;
|
|
root.querySelector(".play-mission").textContent = T("wg_avatar_d");
|
|
const host = document.createElement("div");
|
|
host.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column;";
|
|
root.appendChild(host);
|
|
const msg = document.createElement("p");
|
|
msg.className = "avatar-save-msg";
|
|
let editorApi = null;
|
|
function build(){
|
|
host.innerHTML = "";
|
|
editorApi = buildAvatarEditor(host, currentUser && currentUser.avatar, ()=>{ msg.textContent = ""; });
|
|
const saveBtn = document.createElement("button");
|
|
saveBtn.type = "button";
|
|
saveBtn.className = "tbtn";
|
|
saveBtn.textContent = T("avatarSave");
|
|
saveBtn.addEventListener("click", async ()=>{
|
|
try{ await saveAvatar(editorApi.getConfig()); msg.textContent = T("avatarSaved"); }
|
|
catch(e){ msg.textContent = e.message; }
|
|
});
|
|
host.appendChild(saveBtn);
|
|
host.appendChild(msg);
|
|
}
|
|
build();
|
|
document.addEventListener("langchange", ()=>{ root.querySelector(".play-mission").textContent = T("wg_avatar_d"); });
|
|
/* currentUser.avatar is de bron van waarheid (server), niet het widget-eigen
|
|
initState - geen dubbele opslag van dezelfde data op twee plekken. */
|
|
return { getState: ()=>({}) };
|
|
}
|