All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 58s
422 lines
24 KiB
JavaScript
422 lines
24 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", "spiky", "curly", "ponytail", "mohawk"];
|
|
const AVATAR_HEAD_ACC = ["none", "cap", "crown", "glasses", "sunglasses", "bow", "headphones", "partyhat"];
|
|
const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge", "necklace", "backpack"];
|
|
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("");
|
|
}
|
|
|
|
/* Per-soort silhouet met vaste anatomische ankers: kruin y=2, ogen y=25,
|
|
hals y=53, schouders y=60, heupen y=88 en voetzolen y=108. Armen en benen
|
|
lopen bewust onder het lijf door. Daardoor blijven gewrichten verbonden en
|
|
kunnen accessoires steeds aan hetzelfde lichaamsdeel worden vastgemaakt. */
|
|
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.7);
|
|
let m = "";
|
|
|
|
/* Onderdelen achter het lijf: soortdetails, benen en doorlopende armen. */
|
|
if(species === "dino"){
|
|
m += `<path class="av-part av-tail" d="M 36 78 C 24 82 15 83 5 76 C 12 90 25 96 42 87 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-crest" d="M 68 14 L 78 9 L 75 21 L 84 20 L 77 31" fill="${dark}" stroke="${outline}" stroke-width="1.2" stroke-linejoin="round"/>`;
|
|
}
|
|
if(species === "animal"){
|
|
m += `<path class="av-part av-ear av-ear-left" d="M 30 17 C 16 15 16 2 22 0 C 31 -2 37 7 36 17 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path d="M 28 13 C 21 12 21 6 24 4 C 29 3 32 8 32 13 Z" fill="${light}"/>`;
|
|
m += `<path class="av-part av-ear av-ear-right" d="M 70 17 C 84 15 84 2 78 0 C 69 -2 63 7 64 17 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path d="M 72 13 C 79 12 79 6 76 4 C 71 3 68 8 68 13 Z" fill="${light}"/>`;
|
|
}
|
|
if(species === "monster"){
|
|
m += `<path class="av-part av-horn" d="M 31 18 C 24 12 25 3 30 -3 C 31 7 35 11 39 16 Z" fill="${outline}"/>`;
|
|
m += `<path class="av-part av-horn" d="M 69 18 C 76 12 75 3 70 -3 C 69 7 65 11 61 16 Z" fill="${outline}"/>`;
|
|
}
|
|
|
|
m += `<path class="av-part av-leg av-leg-left" d="M 34 80 C 30 87 29 97 30 101 C 23 102 20 106 23 108 C 29 111 39 109 40 104 L 43 84 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-leg av-leg-right" d="M 66 80 C 70 87 71 97 70 101 C 77 102 80 106 77 108 C 71 111 61 109 60 104 L 57 84 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 61 C 29 58 23 62 19 69 L 14 78 C 11 78 8 80 9 83 C 10 87 16 87 19 84 L 26 75 C 29 72 31 73 35 76 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-arm av-arm-right" d="M 64 61 C 71 58 77 62 81 69 L 86 78 C 89 78 92 80 91 83 C 90 87 84 87 81 84 L 74 75 C 71 72 69 73 65 76 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
m += `<path d="M 11 81 L 15 83 M 89 81 L 85 83" stroke="${outline}" stroke-width="1.1" stroke-linecap="round"/>`;
|
|
|
|
/* Hals en romp bedekken de aanzetten van armen en benen. */
|
|
m += `<path class="av-part av-neck" d="M 40 45 C 42 55 39 58 35 61 L 65 61 C 61 58 58 55 60 45 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-torso" d="M 35 57 C 42 53 58 53 65 57 C 72 63 73 79 67 91 C 60 98 40 98 33 91 C 27 79 28 63 35 57 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
|
m += `<path class="av-part av-belly" d="M 41 63 C 46 59 54 59 59 63 C 64 70 63 85 58 91 C 53 94 47 94 42 91 C 37 85 36 70 41 63 Z" fill="${light}" opacity=".88"/>`;
|
|
m += `<path d="M 43 64 C 48 61 53 61 57 63" stroke="${highlight}" stroke-width="2" fill="none" stroke-linecap="round" opacity=".7"/>`;
|
|
|
|
/* Kop en gelaat; oren en hoorns blijven achter de schedel. */
|
|
const headPath = species === "monster"
|
|
? "M 50 1 C 34 -1 23 9 23 26 C 22 42 31 53 48 55 C 65 57 78 47 77 28 C 77 10 67 2 50 1 Z"
|
|
: species === "dino"
|
|
? "M 48 1 C 33 1 23 11 23 27 C 23 41 31 51 43 54 C 58 59 75 51 77 35 C 79 19 69 4 54 2 Z"
|
|
: "M 50 1 C 34 1 23 11 23 28 C 23 45 34 55 50 55 C 66 55 77 45 77 28 C 77 11 66 1 50 1 Z";
|
|
m += `<path class="av-part av-head" d="${headPath}" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
|
m += `<path d="M 31 13 C 35 7 42 5 48 5" stroke="${highlight}" stroke-width="2.2" fill="none" stroke-linecap="round" opacity=".65"/>`;
|
|
|
|
if(species === "dino"){
|
|
m += `<path class="av-part av-muzzle" d="M 34 38 C 40 34 62 34 69 40 C 72 47 63 53 51 53 C 39 53 31 47 34 38 Z" fill="${light}"/>`;
|
|
m += `<ellipse cx="43" cy="42" rx="2" ry="2.8" fill="${outline}"/>`;
|
|
m += `<ellipse cx="60" cy="42" rx="2" ry="2.8" fill="${outline}"/>`;
|
|
}
|
|
if(species === "animal"){
|
|
m += `<path class="av-part av-muzzle" d="M 37 37 C 41 33 47 34 50 38 C 53 34 59 33 63 37 C 68 43 61 52 50 52 C 39 52 32 43 37 37 Z" fill="${light}"/>`;
|
|
m += `<path d="M 46 37 Q 50 34 54 37 Q 53 42 50 43 Q 47 42 46 37 Z" fill="#3a2a18"/>`;
|
|
}
|
|
if(species === "monster"){
|
|
m += `<ellipse class="av-part av-eye" cx="37" cy="25" rx="8.5" ry="9.2" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<ellipse class="av-part av-eye" cx="63" cy="25" rx="7" ry="7.8" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<circle cx="39" cy="27" r="3.6" fill="#232733"/><circle cx="64" cy="27" r="3" fill="#232733"/>`;
|
|
}else{
|
|
m += `<ellipse class="av-part av-eye" cx="38" cy="25" rx="7.5" ry="8.2" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<ellipse class="av-part av-eye" cx="62" cy="25" rx="7.5" ry="8.2" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<circle cx="39.5" cy="27" r="3.2" fill="#232733"/><circle cx="60.5" cy="27" r="3.2" fill="#232733"/>`;
|
|
}
|
|
m += `<circle cx="40.5" cy="25.5" r="1" fill="#fff"/><circle cx="61.5" cy="25.5" r="1" fill="#fff"/>`;
|
|
if(species === "dino"){
|
|
m += `<path d="M 43 48 Q 51 53 59 48" stroke="${outline}" stroke-width="1.6" fill="none" stroke-linecap="round"/>`;
|
|
}
|
|
if(species === "animal"){
|
|
m += `<path d="M 50 43 L 50 46 M 42 47 Q 46 52 50 47 Q 54 52 58 47" stroke="#3a2a18" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
}
|
|
if(species === "monster"){
|
|
m += `<path class="av-part av-mouth" d="M 33 41 Q 50 55 67 41 Q 63 52 50 53 Q 37 52 33 41 Z" fill="#3c2158" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<path d="M 41 44 L 44 50 L 47 44 Z M 53 44 L 56 50 L 59 44 Z" fill="#fff"/>`;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
const AVATAR_HAIR_MARKUP = {
|
|
none: () => "",
|
|
spiky: (c) => `
|
|
<path d="M 27 18 L 24 4 L 32 15 Z" fill="${c}"/>
|
|
<path d="M 37 12 L 36 -1 L 44 10 Z" fill="${c}"/>
|
|
<path d="M 48 10 L 50 -3 L 55 10 Z" fill="${c}"/>
|
|
<path d="M 60 10 L 64 -1 L 63 12 Z" fill="${c}"/>
|
|
<path d="M 68 15 L 76 4 L 73 18 Z" fill="${c}"/>`,
|
|
curly: (c) => `
|
|
<circle cx="26" cy="14" r="9" fill="${c}"/>
|
|
<circle cx="37" cy="6" r="10" fill="${c}"/>
|
|
<circle cx="50" cy="3" r="10.5" fill="${c}"/>
|
|
<circle cx="63" cy="6" r="10" fill="${c}"/>
|
|
<circle cx="74" cy="14" r="9" fill="${c}"/>`,
|
|
ponytail: (c) => `
|
|
<path d="M 22 20 Q 24 -6 50 -6 Q 76 -6 78 20 Q 62 2 50 2 Q 38 2 22 20 Z" fill="${c}"/>
|
|
<path d="M 71 9 Q 89 12 87 28 Q 85 42 72 47 Q 81 34 77 22 Q 75 13 67 11 Z" fill="${c}"/>
|
|
<ellipse cx="72" cy="13" rx="4.2" ry="3.2" fill="${avatarShade(c,-0.25)}"/>`,
|
|
mohawk: (c) => `<path d="M 44 20 Q 43 4 50 1 Q 57 4 56 20 Q 50 15 44 20 Z" fill="${c}"/>`,
|
|
};
|
|
|
|
const AVATAR_HEAD_ACC_MARKUP = {
|
|
none: () => "",
|
|
cap: () => `
|
|
<path class="av-part av-headwear av-cap" d="M 26 15 C 28 1 39 -5 52 -4 C 65 -3 73 4 74 16 C 60 10 41 9 26 15 Z" fill="#3d7ddb" stroke="#234f91" stroke-width="1.3"/>
|
|
<path d="M 26 14 C 38 10 55 10 67 14 C 57 19 40 19 22 20 C 20 19 22 16 26 14 Z" fill="#2f63b3" stroke="#234f91" stroke-width="1.2"/>
|
|
<path d="M 50 -4 L 50 11" stroke="#72a3e8" stroke-width="1.1" opacity=".8"/>`,
|
|
crown: () => `
|
|
<path class="av-part av-headwear av-crown" d="M 27 16 L 30 0 L 39 11 L 50 -5 L 61 11 L 70 0 L 73 16 C 59 12 41 12 27 16 Z" fill="#f0b73a" stroke="#b77b0b" stroke-width="1.2" stroke-linejoin="round"/>
|
|
<path d="M 28 14 C 42 11 58 11 72 14 L 72 18 C 58 15 42 15 28 18 Z" fill="#d99a22"/>
|
|
<circle cx="30" cy="1" r="2.6" fill="#e0483c"/>
|
|
<circle cx="50" cy="-4" r="2.6" fill="#3a8ee0"/>
|
|
<circle cx="70" cy="1" r="2.6" fill="#e0483c"/>`,
|
|
glasses: () => `
|
|
<circle class="av-part av-eyewear" cx="38" cy="25" r="9.5" fill="rgba(255,255,255,.3)" stroke="#232733" stroke-width="2.4"/>
|
|
<circle class="av-part av-eyewear" cx="62" cy="25" r="9.5" fill="rgba(255,255,255,.3)" stroke="#232733" stroke-width="2.4"/>
|
|
<path d="M 47.5 25 L 52.5 25" stroke="#232733" stroke-width="2.4"/>
|
|
<path d="M 28.5 23 L 22 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>
|
|
<path d="M 71.5 23 L 78 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>`,
|
|
sunglasses: () => `
|
|
<path class="av-part av-eyewear av-sunglasses" d="M 27 20 Q 27 32 38 32 Q 49 32 48 22 Q 48 18 38 18 Q 27 18 27 20 Z" fill="#1a1d24"/>
|
|
<path d="M 52 22 Q 51 32 62 32 Q 73 32 73 20 Q 73 18 62 18 Q 52 18 52 22 Z" fill="#1a1d24"/>
|
|
<path d="M 48 22 Q 50 20 52 22" stroke="#1a1d24" stroke-width="2.4" fill="none"/>
|
|
<path d="M 27 21 L 20 19 M 73 21 L 80 19" stroke="#1a1d24" stroke-width="2.4" stroke-linecap="round"/>
|
|
<path d="M 31 22 Q 33 20 36 21 M 56 22 Q 58 20 61 21" stroke="#fff" stroke-width="1.6" fill="none" opacity=".55" stroke-linecap="round"/>`,
|
|
bow: () => `
|
|
<path class="av-part av-headwear av-bow" d="M 69 13 C 62 5 56 7 57 14 C 58 20 63 20 69 16 C 75 20 81 20 82 14 C 83 7 76 5 69 13 Z" fill="#e05a8a" stroke="#a93660" stroke-width="1.2"/>
|
|
<circle cx="69" cy="14" r="3.5" fill="#c23f6e"/>`,
|
|
headphones: () => `
|
|
<path class="av-part av-headwear av-headphones" d="M 20 25 C 20 4 32 -7 50 -7 C 68 -7 80 4 80 25" stroke="#333" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
|
<rect x="15" y="20" width="10" height="19" rx="5" fill="#333" stroke="#17181c" stroke-width="1.2"/>
|
|
<rect x="75" y="20" width="10" height="19" rx="5" fill="#333" stroke="#17181c" stroke-width="1.2"/>
|
|
<rect x="18" y="24" width="4" height="11" rx="2" fill="#667"/>
|
|
<rect x="78" y="24" width="4" height="11" rx="2" fill="#667"/>`,
|
|
partyhat: () => `
|
|
<path class="av-part av-headwear av-partyhat" d="M 50 -8 L 61 19 C 54 22 46 22 39 19 Z" fill="#e05a8a" stroke="#a93660" stroke-width="1.2"/>
|
|
<path d="M 39.5 18 C 46 21 54 21 60.5 18" stroke="#f4c34e" stroke-width="2" fill="none"/>
|
|
<circle cx="45.5" cy="8" r="1.8" fill="#f0b73a"/>
|
|
<circle cx="54" cy="12" r="1.8" fill="#3a8ee0"/>
|
|
<circle cx="47.5" cy="16" r="1.8" fill="#f0b73a"/>
|
|
<circle cx="50" cy="-8" r="3.6" fill="#f0b73a"/>`,
|
|
};
|
|
|
|
const AVATAR_BODY_ACC_MARKUP = {
|
|
none: { back: () => "", front: () => "" },
|
|
scarf: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-neckwear av-scarf" d="M 35 53 C 43 57 57 57 65 53 L 66 62 C 57 67 43 67 34 62 Z" fill="#d94f4f" stroke="#9d3030" stroke-width="1.2"/>
|
|
<path d="M 57 63 L 62 84 L 53 79 L 50 64 Z" fill="#c23f3f" stroke="#9d3030" stroke-width="1.1" stroke-linejoin="round"/>
|
|
<path d="M 55 75 L 62 78" stroke="#f08080" stroke-width="1.2"/>`,
|
|
},
|
|
cape: {
|
|
back: () => `
|
|
<path class="av-part av-backwear av-cape" d="M 33 55 C 19 62 13 79 13 108 C 26 103 38 99 50 91 C 62 99 74 103 87 108 C 87 79 81 62 67 55 C 58 61 42 61 33 55 Z" fill="#7a4bc7" stroke="#54308f" stroke-width="1.5" stroke-linejoin="round"/>
|
|
<path d="M 26 63 C 22 76 23 91 26 101" stroke="#a67de0" stroke-width="2" fill="none" opacity=".65"/>`,
|
|
front: () => `
|
|
<path class="av-part av-cape-clasp" d="M 35 56 C 42 61 58 61 65 56" stroke="#f0b73a" stroke-width="2.4" fill="none" stroke-linecap="round"/>
|
|
<circle cx="35" cy="56" r="2.5" fill="#f0b73a"/>
|
|
<circle cx="65" cy="56" r="2.5" fill="#f0b73a"/>`,
|
|
},
|
|
badge: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-chestwear av-badge" d="M 57 67 L 63 67 L 66 72 L 63 78 L 57 78 L 54 72 Z" fill="#f0b73a" stroke="#fff" stroke-width="1.5"/>
|
|
<path d="M 60 69 L 61 72 L 64 72 L 61.5 74 L 62.5 77 L 60 75 L 57.5 77 L 58.5 74 L 56 72 L 59 72 Z" fill="#b8750b"/>`,
|
|
},
|
|
necklace: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-neckwear av-necklace" d="M 35 57 C 38 69 44 76 50 77 C 56 76 62 69 65 57" stroke="#f0b73a" stroke-width="2" fill="none" stroke-linecap="round"/>
|
|
<path d="M 50 74 L 55 80 L 50 85 L 45 80 Z" fill="#3a8ee0" stroke="#f0b73a" stroke-width="1.4"/>`,
|
|
},
|
|
backpack: {
|
|
back: () => `
|
|
<path class="av-part av-backwear av-backpack" d="M 28 63 C 29 54 39 50 50 50 C 61 50 71 54 72 63 L 77 89 C 70 96 30 96 23 89 Z" fill="#8a6a45" stroke="#5a4029" stroke-width="1.6"/>
|
|
<path d="M 39 53 C 40 46 60 46 61 53" stroke="#5a4029" stroke-width="3" fill="none"/>`,
|
|
front: () => `
|
|
<path class="av-part av-backpack-strap" d="M 37 57 C 29 63 29 76 33 84 M 63 57 C 71 63 71 76 67 84" stroke="#5a4632" stroke-width="4" fill="none" stroke-linecap="round"/>
|
|
<path d="M 33 83 L 38 86 M 67 83 L 62 86" stroke="#d0a36d" stroke-width="1.5"/>`,
|
|
},
|
|
};
|
|
|
|
/* Alleen een pet bedekt het bovenhoofd volledig. Een kroon rust juist op het
|
|
haar; een paardenstaart blijft naast de pet zichtbaar. */
|
|
function avatarHairSuppressed(c){
|
|
return c.accessoryHead === "cap" && c.hairStyle !== "ponytail";
|
|
}
|
|
|
|
/* 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", "-6 -12 112 124");
|
|
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];
|
|
let markup = bodyAccessory.back();
|
|
markup += avatarBodyMarkup(c.species, c.bodyColor);
|
|
if(!avatarHairSuppressed(c)) markup += AVATAR_HAIR_MARKUP[c.hairStyle](c.hairColor);
|
|
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: ()=>({}) };
|
|
}
|