All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m23s
756 lines
57 KiB
JavaScript
756 lines
57 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_VARIANTS = Object.freeze({
|
|
dino: ["rex", "triceratops", "stegosaurus", "raptor"],
|
|
monster: ["horned", "cyclops", "slime", "alien"],
|
|
animal: ["cat", "dog", "rabbit", "bear"],
|
|
});
|
|
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", "star"];
|
|
const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge", "necklace", "bowtie", "belt", "backpack", "satchel", "wings", "medal"];
|
|
const AVATAR_DANCES = Object.freeze(["bounce", "twist", "spin", "shuffle", "wiggle", "robot"]);
|
|
const AVATAR_REWARD_ITEMS = Object.freeze({star:"head:star", medal:"body:medal", robot:"dance:robot"});
|
|
let avatarRewardKeys = new Set();
|
|
let avatarRewardPoints = 0;
|
|
async function loadAvatarRewards(){
|
|
const data = await api("/me/avatar-rewards");
|
|
avatarRewardKeys = new Set(data.rewards || []);
|
|
avatarRewardPoints = Number(data.points || 0);
|
|
return data;
|
|
}
|
|
function avatarAcceptRewards(keys){ (keys || []).forEach(key=>avatarRewardKeys.add(key)); }
|
|
const AVATAR_DEFAULT = Object.freeze({
|
|
species: "dino", speciesVariant: "rex", 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 || {};
|
|
const species = AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species;
|
|
const variants = AVATAR_VARIANTS[species];
|
|
return {
|
|
species,
|
|
speciesVariant: variants.includes(c.speciesVariant) ? c.speciesVariant : variants[0],
|
|
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, speciesVariant){
|
|
const variants = AVATAR_VARIANTS[species] || AVATAR_VARIANTS[AVATAR_DEFAULT.species];
|
|
const variant = variants.includes(speciesVariant) ? speciesVariant : variants[0];
|
|
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 = `<g class="av-creature av-creature-${species} av-creature-${variant}">`;
|
|
|
|
/* Silhouetdetails achter kop en lijf: iedere variant is direct herkenbaar. */
|
|
if(species === "dino"){
|
|
if(variant === "rex"){
|
|
m += `<path class="av-part av-tail av-tail-rex" 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"/>`;
|
|
}else if(variant === "triceratops"){
|
|
m += `<path class="av-part av-tail av-tail-triceratops" d="M 38 82 C 29 83 22 88 16 85 C 22 92 31 94 42 89 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-frill av-frill-triceratops" d="M 18 45 C 8 41 7 32 13 25 C 7 18 12 8 22 9 C 25 0 36 -4 45 2 C 53 -5 65 -2 69 7 C 80 4 89 12 85 22 C 93 29 91 39 82 45 Z" fill="${dark}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
|
}else if(variant === "stegosaurus"){
|
|
m += `<path class="av-part av-tail av-tail-stegosaurus" d="M 40 79 C 25 81 13 76 2 68 C 11 85 25 91 43 87 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-dino-plates" d="M 17 31 L 7 22 L 20 20 L 13 8 L 28 12 L 28 -1 L 40 7 L 46 -5 L 54 6 L 66 -3 L 67 10 L 82 4 L 78 17 L 93 17 L 83 29 L 94 36 L 81 40 Z" fill="${light}" stroke="${outline}" stroke-width="1.3" stroke-linejoin="round"/>`;
|
|
m += `<path d="M 13 73 L 7 66 L 18 67 M 23 80 L 17 71 L 29 74" fill="${light}" stroke="${outline}" stroke-width="1.2" stroke-linejoin="round"/>`;
|
|
}else{
|
|
m += `<path class="av-part av-tail av-tail-raptor" d="M 40 80 C 25 78 12 69 -2 57 C 8 76 22 89 43 88 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-feather-crest av-feather-crest-raptor" d="M 71 13 C 78 3 87 -1 94 1 C 88 7 85 13 82 19 C 90 14 97 15 101 19 C 92 22 86 27 81 33 Z" fill="${light}" stroke="${outline}" stroke-width="1.3" stroke-linejoin="round"/>`;
|
|
}
|
|
}else if(species === "monster"){
|
|
if(variant === "horned"){
|
|
m += `<path class="av-part av-horn av-horn-left" 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 av-horn-right" d="M 72 15 C 81 8 79 -3 72 -10 C 72 2 66 7 60 13 Z" fill="${outline}"/>`;
|
|
}else if(variant === "cyclops"){
|
|
m += `<path class="av-part av-cyclops-crest" d="M 41 4 L 50 -12 L 59 4 C 53 1 47 1 41 4 Z" fill="${dark}" stroke="${outline}" stroke-width="1.3"/>`;
|
|
}else if(variant === "slime"){
|
|
m += `<circle class="av-part av-slime-drop" cx="24" cy="83" r="7" fill="${dark}" stroke="${outline}" stroke-width="1.2"/><circle class="av-part av-slime-drop" cx="76" cy="86" r="6" fill="${dark}" stroke="${outline}" stroke-width="1.2"/>`;
|
|
}else{
|
|
m += `<path class="av-part av-antenna" d="M 34 12 Q 29 -2 23 -8 M 66 12 Q 71 -2 77 -8" fill="none" stroke="${outline}" stroke-width="2.4" stroke-linecap="round"/>`;
|
|
m += `<circle class="av-part av-antenna-tip" cx="22" cy="-9" r="4" fill="${light}" stroke="${outline}" stroke-width="1.2"/><circle class="av-part av-antenna-tip" cx="78" cy="-9" r="4" fill="${light}" stroke="${outline}" stroke-width="1.2"/>`;
|
|
}
|
|
}else{
|
|
if(variant === "cat"){
|
|
m += `<path class="av-part av-tail av-tail-cat" d="M 65 82 C 83 86 91 76 84 67 C 81 63 76 67 79 71 C 82 75 76 79 68 76 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linecap="round"/>`;
|
|
m += `<path class="av-part av-ear av-ear-left av-ear-cat" d="M 30 15 L 18 -6 L 39 5 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/><path d="M 29 9 L 21 -2 L 35 5 Z" fill="${light}"/>`;
|
|
m += `<path class="av-part av-ear av-ear-right av-ear-cat" d="M 70 15 L 82 -6 L 61 5 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/><path d="M 71 9 L 79 -2 L 65 5 Z" fill="${light}"/>`;
|
|
}else if(variant === "dog"){
|
|
m += `<path class="av-part av-tail av-tail-dog" d="M 64 80 C 78 82 86 74 86 65 C 92 72 87 89 67 88 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-ear av-ear-left av-ear-dog" d="M 31 13 C 18 2 8 8 12 23 C 14 35 22 39 29 30 L 38 14 Z" fill="${dark}" stroke="${outline}" stroke-width="1.5"/>`;
|
|
m += `<path class="av-part av-ear av-ear-right av-ear-dog" d="M 69 13 C 82 2 92 8 88 23 C 86 35 78 39 71 30 L 62 14 Z" fill="${dark}" stroke="${outline}" stroke-width="1.5"/>`;
|
|
}else if(variant === "rabbit"){
|
|
m += `<circle class="av-part av-tail av-tail-rabbit" cx="72" cy="83" r="8" fill="${highlight}" stroke="${outline}" stroke-width="1.3"/>`;
|
|
m += `<path class="av-part av-ear av-ear-left av-ear-rabbit" d="M 30 15 C 21 5 18 -13 26 -19 C 36 -18 39 2 38 16 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5"/><path d="M 29 10 C 25 1 24 -11 27 -14 C 32 -10 34 2 34 12 Z" fill="${light}"/>`;
|
|
m += `<path class="av-part av-ear av-ear-right av-ear-rabbit" d="M 70 15 C 79 5 82 -13 74 -19 C 64 -18 61 2 62 16 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5"/><path d="M 71 10 C 75 1 76 -11 73 -14 C 68 -10 66 2 66 12 Z" fill="${light}"/>`;
|
|
}else{
|
|
m += `<circle class="av-part av-tail av-tail-bear" cx="70" cy="84" r="6" fill="${dark}" stroke="${outline}" stroke-width="1.2"/>`;
|
|
m += `<circle class="av-part av-ear av-ear-left av-ear-bear" cx="24" cy="9" r="11" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5"/><circle cx="24" cy="9" r="5.5" fill="${light}"/>`;
|
|
m += `<circle class="av-part av-ear av-ear-right av-ear-bear" cx="76" cy="9" r="11" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5"/><circle cx="76" cy="9" r="5.5" fill="${light}"/>`;
|
|
}
|
|
}
|
|
|
|
/* Korte ledematen blijven op gedeelde ankers voor alle accessoires. */
|
|
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"/>`;
|
|
if(variant === "triceratops"){
|
|
m += `<path class="av-part av-arm av-arm-left av-foreleg" d="M 39 64 C 31 65 29 74 29 82 C 26 85 29 88 34 86 C 37 82 35 73 40 72 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
m += `<path class="av-part av-arm av-arm-right av-foreleg" d="M 61 64 C 69 65 71 74 71 82 C 74 85 71 88 66 86 C 63 82 65 73 60 72 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
|
}else if(variant === "raptor"){
|
|
m += `<path class="av-part av-arm av-arm-left av-raptor-arm" d="M 39 65 C 31 63 28 68 25 74 L 20 76 L 26 79 L 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 av-raptor-arm" d="M 61 65 C 69 63 72 68 75 74 L 80 76 L 74 79 L 69 75 C 66 73 64 75 61 77 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
|
}else{
|
|
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"/>`;
|
|
}
|
|
}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"/>`;
|
|
}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 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} av-torso-${variant}" d="${torsoPath}" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
|
|
|
if(species === "dino"){
|
|
if(variant === "raptor"){
|
|
m += `<path class="av-part av-feather-chest" d="M 39 64 L 45 68 L 50 64 L 55 68 L 61 64 C 59 78 56 87 50 92 C 44 87 41 78 39 64 Z" fill="${light}"/>`;
|
|
}else{
|
|
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"/>`;
|
|
if(variant === "stegosaurus") m += `<path class="av-part av-side-plates" d="M 38 72 L 32 68 L 35 77 L 31 82 L 39 83" fill="${dark}" stroke="${outline}" stroke-width="1"/>`;
|
|
}
|
|
}else if(species === "monster"){
|
|
m += `<ellipse class="av-part av-belly" cx="50" cy="77" rx="12" ry="14" fill="${light}" opacity=".72"/>`;
|
|
if(variant === "horned") m += `<circle cx="43" cy="71" r="2" fill="${dark}"/><circle cx="58" cy="82" r="2.5" fill="${dark}"/>`;
|
|
if(variant === "cyclops") m += `<path class="av-part av-belly-spiral" d="M 44 77 Q 50 69 56 76 Q 58 83 50 85 Q 44 85 45 80" fill="none" stroke="${dark}" stroke-width="1.6" stroke-linecap="round"/>`;
|
|
if(variant === "slime") m += `<path class="av-part av-slime-highlight" d="M 40 66 C 36 75 38 86 42 91" fill="none" stroke="${highlight}" stroke-width="2.2" stroke-linecap="round" opacity=".7"/>`;
|
|
if(variant === "alien") m += `<path class="av-part av-alien-emblem" d="M 43 78 Q 50 70 57 78 Q 50 86 43 78 Z" fill="${dark}"/><circle cx="50" cy="78" r="2" fill="${highlight}"/>`;
|
|
}else{
|
|
if(variant === "cat") 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"/>`;
|
|
if(variant === "dog") m += `<path class="av-part av-dog-chest" d="M 39 65 C 45 61 54 63 58 69 C 55 79 57 87 50 92 C 43 86 45 76 39 65 Z" fill="${light}" opacity=".85"/>`;
|
|
if(variant === "rabbit") m += `<ellipse class="av-part av-rabbit-belly" cx="50" cy="78" rx="10" ry="14" fill="${highlight}" opacity=".72"/>`;
|
|
if(variant === "bear") m += `<path class="av-part av-bear-chest" d="M 39 68 Q 50 61 61 68 Q 59 83 50 90 Q 41 83 39 68 Z" fill="${light}" opacity=".78"/>`;
|
|
}
|
|
|
|
const headPaths = {
|
|
dino: {
|
|
rex: "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",
|
|
triceratops: "M 50 -1 C 29 -2 16 11 17 31 C 17 51 30 63 50 64 C 70 64 83 51 83 31 C 84 11 71 -1 50 -1 Z",
|
|
stegosaurus: "M 48 -2 C 29 -2 16 10 16 31 C 16 50 29 62 48 64 C 68 66 84 53 84 32 C 84 11 69 -2 48 -2 Z",
|
|
raptor: "M 43 -2 C 26 1 16 14 18 33 C 19 50 30 60 46 62 C 64 65 80 55 87 40 C 91 30 83 24 76 21 C 74 7 61 -4 43 -2 Z",
|
|
},
|
|
monster: {
|
|
horned: "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",
|
|
cyclops: "M 50 -2 C 29 -3 16 10 16 31 C 16 53 30 65 50 65 C 70 65 84 53 84 31 C 84 10 71 -2 50 -2 Z",
|
|
slime: "M 49 0 C 30 -4 17 9 18 29 C 12 39 18 48 23 51 C 24 62 34 65 43 61 C 49 68 59 67 63 61 C 75 64 85 55 81 45 C 89 36 84 25 80 21 C 78 6 66 -2 49 0 Z",
|
|
alien: "M 50 -2 C 32 -2 20 9 18 28 C 16 48 28 62 50 65 C 72 62 84 48 82 28 C 80 9 68 -2 50 -2 Z",
|
|
},
|
|
animal: {
|
|
cat: "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",
|
|
dog: "M 50 0 C 29 -1 16 12 17 33 C 18 53 31 64 50 64 C 69 64 82 53 83 33 C 84 12 71 -1 50 0 Z",
|
|
rabbit: "M 50 -1 C 32 -1 20 11 19 31 C 18 51 30 64 50 66 C 70 64 82 51 81 31 C 80 11 68 -1 50 -1 Z",
|
|
bear: "M 50 -3 C 28 -3 15 10 16 32 C 17 54 30 65 50 65 C 70 65 83 54 84 32 C 85 10 72 -3 50 -3 Z",
|
|
},
|
|
};
|
|
m += `<path class="av-part av-head av-head-large av-head-${variant}" d="${headPaths[species][variant]}" 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"){
|
|
if(variant === "triceratops"){
|
|
m += `<path class="av-part av-dino-horn av-dino-horn-left" d="M 27 18 C 24 10 24 3 29 0 C 30 8 34 14 39 18 Z" fill="${highlight}" stroke="${outline}" stroke-width="1.1"/>`;
|
|
m += `<path class="av-part av-dino-horn av-dino-horn-right" d="M 73 18 C 76 10 76 3 71 0 C 70 8 66 14 61 18 Z" fill="${highlight}" stroke="${outline}" stroke-width="1.1"/>`;
|
|
m += `<path class="av-part av-muzzle" d="M 34 39 C 42 34 59 34 67 40 C 70 50 62 58 50 59 C 38 58 30 50 34 39 Z" fill="${light}"/>`;
|
|
m += `<path class="av-part av-dino-horn av-nose-horn" d="M 47 42 L 51 27 L 56 42 Z" fill="${highlight}" stroke="${outline}" stroke-width="1"/>`;
|
|
m += `<ellipse cx="42" cy="47" rx="2" ry="2.6" fill="${outline}"/><ellipse cx="59" cy="47" rx="2" ry="2.6" fill="${outline}"/>`;
|
|
}else if(variant === "raptor"){
|
|
m += `<path class="av-part av-muzzle" d="M 35 39 C 46 34 71 34 80 40 C 82 49 70 56 53 57 C 40 57 32 50 35 39 Z" fill="${light}"/>`;
|
|
m += `<ellipse cx="48" cy="44" rx="1.8" ry="2.5" fill="${outline}"/><ellipse cx="68" cy="43" rx="1.8" ry="2.5" fill="${outline}"/>`;
|
|
}else{
|
|
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}"/>`;
|
|
if(variant === "stegosaurus") m += `<circle class="av-part av-cheek-plate" cx="25" cy="39" r="3" fill="${dark}"/><circle class="av-part av-cheek-plate" cx="78" cy="39" r="3" fill="${dark}"/>`;
|
|
}
|
|
m += `<ellipse class="av-part av-eye" cx="35" cy="27" rx="8.5" ry="9.5" fill="#fff" stroke="${outline}" stroke-width="1"/><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"/><circle cx="38.2" cy="27.5" r="1.2" fill="#fff"/><circle cx="64.2" cy="27.5" r="1.2" fill="#fff"/>`;
|
|
m += `<path d="M 42 54 Q 51 59 60 54" stroke="${outline}" stroke-width="1.7" fill="none" stroke-linecap="round"/>`;
|
|
if(variant === "raptor") m += `<path d="M 56 55 L 59 59 L 62 54" fill="#fff" stroke="${outline}" stroke-width=".7"/>`;
|
|
}else if(species === "monster"){
|
|
if(variant === "cyclops"){
|
|
m += `<ellipse class="av-part av-eye av-cyclops-eye" cx="50" cy="27" rx="13" ry="14" fill="#fff" stroke="${outline}" stroke-width="1.2"/><circle cx="51" cy="29" r="5.2" fill="#232733"/><circle cx="53" cy="26" r="1.7" fill="#fff"/>`;
|
|
m += `<path class="av-part av-mouth" d="M 34 47 Q 50 58 66 47" fill="none" stroke="${outline}" stroke-width="2" stroke-linecap="round"/>`;
|
|
}else if(variant === "alien"){
|
|
for(const x of [34,50,66]) m += `<ellipse class="av-part av-eye av-alien-eye" cx="${x}" cy="28" rx="7" ry="8.5" fill="#fff" stroke="${outline}" stroke-width="1"/><circle cx="${x}" cy="29" r="3" fill="#232733"/><circle cx="${x+1}" cy="27.5" r="1" fill="#fff"/>`;
|
|
m += `<ellipse class="av-part av-mouth av-alien-mouth" cx="50" cy="50" rx="8" ry="4" fill="${dark}"/>`;
|
|
}else{
|
|
const leftSize = variant === "slime" ? 8.5 : 10;
|
|
const rightSize = variant === "slime" ? 8.5 : 8.2;
|
|
m += `<ellipse class="av-part av-eye" cx="35" cy="27" rx="${leftSize}" ry="${leftSize+0.8}" fill="#fff" stroke="${outline}" stroke-width="1"/><ellipse class="av-part av-eye" cx="66" cy="27" rx="${rightSize}" ry="${rightSize+0.8}" 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"/><circle cx="38.2" cy="27.5" r="1.2" fill="#fff"/><circle cx="68" cy="27.5" r="1.1" fill="#fff"/>`;
|
|
if(variant === "slime"){
|
|
m += `<path class="av-part av-mouth" d="M 36 47 Q 50 57 64 47" fill="none" stroke="${outline}" stroke-width="2" stroke-linecap="round"/><path d="M 48 52 L 52 52 L 50 57 Z" fill="#fff" stroke="${outline}" stroke-width=".6"/>`;
|
|
}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"/><path d="M 39 46 L 43 54 L 47 46 Z M 53 46 L 57 54 L 61 46 Z" fill="#fff"/>`;
|
|
}
|
|
}
|
|
}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"/><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"/><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(variant === "cat"){
|
|
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}"/><path d="M 45 40 Q 50 36 55 40 Q 54 46 50 47 Q 46 46 45 40 Z" fill="#3a2a18"/>`;
|
|
m += `<path class="av-part av-whiskers" d="M 39 48 L 20 44 M 39 52 L 19 53 M 61 48 L 80 44 M 61 52 L 81 53" stroke="${outline}" stroke-width="1.1" stroke-linecap="round"/><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.5" fill="none" stroke-linecap="round"/>`;
|
|
}else if(variant === "dog"){
|
|
m += `<ellipse class="av-part av-muzzle av-muzzle-dog" cx="50" cy="47" rx="19" ry="14" fill="${light}"/><ellipse cx="50" cy="40" rx="7" ry="5" fill="#34271f"/>`;
|
|
m += `<path d="M 50 45 Q 50 52 43 53 M 50 45 Q 50 52 57 53" fill="none" stroke="#34271f" stroke-width="1.6" stroke-linecap="round"/><path class="av-part av-tongue" d="M 46 54 Q 50 62 54 54 Z" fill="#e98391" stroke="${outline}" stroke-width=".8"/>`;
|
|
}else if(variant === "rabbit"){
|
|
m += `<path class="av-part av-muzzle" d="M 34 42 C 39 36 47 38 50 43 C 53 38 61 36 66 42 C 70 51 62 57 50 57 C 38 57 30 51 34 42 Z" fill="${light}"/><path d="M 46 41 Q 50 38 54 41 Q 53 45 50 46 Q 47 45 46 41 Z" fill="#b95f72"/>`;
|
|
m += `<path class="av-part av-rabbit-teeth" d="M 45 50 L 50 49 L 55 50 L 54 58 L 46 58 Z" fill="#fff" stroke="${outline}" stroke-width=".8"/><path d="M 50 50 L 50 57" stroke="${outline}" stroke-width=".7"/>`;
|
|
}else{
|
|
m += `<ellipse class="av-part av-muzzle av-muzzle-bear" cx="50" cy="47" rx="18" ry="14" fill="${light}"/><path d="M 43 40 Q 50 35 57 40 Q 56 47 50 48 Q 44 47 43 40 Z" fill="#34271f"/>`;
|
|
m += `<path d="M 50 48 L 50 52 M 40 52 Q 45 58 50 52 Q 55 58 60 52" stroke="#34271f" stroke-width="1.6" fill="none" stroke-linecap="round"/>`;
|
|
}
|
|
}
|
|
m += `</g>`;
|
|
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"/>`,
|
|
star: () => `
|
|
<path class="av-part av-headwear av-reward-star" d="M 50 -18 L 55 -7 L 67 -6 L 58 2 L 61 14 L 50 8 L 39 14 L 42 2 L 33 -6 L 45 -7 Z" fill="#ffd84d" stroke="#b8750b" stroke-width="1.5" stroke-linejoin="round"/>
|
|
<path d="M 45 -5 L 49 -11 M 43 1 L 50 5" stroke="#fff4a8" stroke-width="1.6" stroke-linecap="round"/>`,
|
|
};
|
|
|
|
/* Oogaccessoires volgen afwijkende oogindelingen van cycloop en alien. */
|
|
function avatarHeadAccessoryMarkup(c){
|
|
if(c.species === "monster" && c.speciesVariant === "cyclops"){
|
|
if(c.accessoryHead === "glasses") return `
|
|
<circle class="av-part av-eyewear av-glasses av-glasses-cyclops" cx="50" cy="27" r="15" fill="rgba(255,255,255,.24)" stroke="#232733" stroke-width="2.5"/>
|
|
<path d="M 35 24 L 16 19 M 65 24 L 84 19" stroke="#232733" stroke-width="2.4" stroke-linecap="round"/>`;
|
|
if(c.accessoryHead === "sunglasses") return `
|
|
<path class="av-part av-eyewear av-sunglasses av-sunglasses-cyclops" d="M 34 19 C 31 33 38 41 50 41 C 62 41 69 33 66 19 C 57 16 43 16 34 19 Z" fill="#1a1d24"/>
|
|
<path d="M 34 21 L 16 18 M 66 21 L 84 18 M 40 22 Q 46 18 52 21" stroke="#1a1d24" stroke-width="2.5" fill="none" stroke-linecap="round"/><path d="M 41 22 Q 47 19 53 21" stroke="#fff" stroke-width="1.7" fill="none" opacity=".55"/>`;
|
|
if(c.accessoryHead === "eyepatch") return `
|
|
<path class="av-part av-eyewear av-eyepatch av-eyepatch-cyclops" d="M 16 17 C 38 23 62 23 84 17" stroke="#2a2630" stroke-width="2.4" fill="none"/>
|
|
<path d="M 36 21 C 42 16 58 16 64 21 C 65 35 59 43 50 43 C 41 43 35 35 36 21 Z" fill="#34303b" stroke="#17151b" stroke-width="1.3"/>`;
|
|
}
|
|
if(c.species === "monster" && c.speciesVariant === "alien"){
|
|
if(c.accessoryHead === "glasses") return `
|
|
<g class="av-part av-eyewear av-glasses av-glasses-alien" fill="rgba(255,255,255,.24)" stroke="#232733" stroke-width="2">
|
|
<ellipse cx="34" cy="28" rx="8.5" ry="10"/><ellipse cx="50" cy="28" rx="8.5" ry="10"/><ellipse cx="66" cy="28" rx="8.5" ry="10"/>
|
|
</g><path d="M 25 25 L 17 20 M 75 25 L 83 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>`;
|
|
if(c.accessoryHead === "sunglasses") return `
|
|
<path class="av-part av-eyewear av-sunglasses av-sunglasses-alien" d="M 22 20 C 24 36 33 40 50 39 C 67 40 76 36 78 20 C 64 16 36 16 22 20 Z" fill="#1a1d24"/>
|
|
<path d="M 22 21 L 15 18 M 78 21 L 85 18" stroke="#1a1d24" stroke-width="2.5" stroke-linecap="round"/><path d="M 29 22 Q 39 18 48 21" stroke="#fff" stroke-width="1.7" fill="none" opacity=".55"/>`;
|
|
}
|
|
return AVATAR_HEAD_ACC_MARKUP[c.accessoryHead]();
|
|
}
|
|
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"/>`,
|
|
},
|
|
medal: {
|
|
back: () => "",
|
|
front: () => `
|
|
<path class="av-part av-reward-medal" d="M 43 61 L 48 76 L 52 76 L 57 61" fill="none" stroke="#2879c9" stroke-width="4" stroke-linejoin="round"/>
|
|
<circle cx="50" cy="79" r="9" fill="#ffd84d" stroke="#b8750b" stroke-width="1.6"/>
|
|
<path d="M 50 73 L 52 77 L 57 78 L 53 81 L 54 86 L 50 83 L 46 86 L 47 81 L 43 78 L 48 77 Z" fill="#fff2a3"/>`,
|
|
},
|
|
};
|
|
|
|
/* 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 + " av-variant-" + c.speciesVariant);
|
|
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, c.speciesVariant);
|
|
if(!avatarHairSuppressed(c)) markup += avatarHairLayer(hair.front(c.hairColor), c.species, "front");
|
|
markup += avatarHeadAccessoryMarkup(c);
|
|
markup += bodyAccessory.front();
|
|
svg.innerHTML = markup;
|
|
return svg;
|
|
}
|
|
|
|
/* Voltooide opdrachten krijgen een korte feestlaag met het eigen personage.
|
|
De hele SVG beweegt als één figuur, zodat haar en accessoires altijd op hun
|
|
anatomische ankers blijven. Zonder expliciete keuze rouleren de dansen. */
|
|
let avatarDanceCursor = -1;
|
|
function avatarDanceChoice(preferred){
|
|
const available = AVATAR_DANCES.filter(dance=>dance !== "robot" || avatarRewardKeys.has("dance:robot"));
|
|
if(available.includes(preferred)) return preferred;
|
|
avatarDanceCursor = (avatarDanceCursor + 1) % available.length;
|
|
return available[avatarDanceCursor];
|
|
}
|
|
function celebrateAvatar(host, options){
|
|
if(!host || typeof currentUser === "undefined" || !currentUser || !currentUser.avatar) return null;
|
|
host.querySelectorAll(".avatar-dance-stage").forEach(stage=>stage.remove());
|
|
host.classList.add("avatar-dance-host");
|
|
const dance = avatarDanceChoice(options && options.dance);
|
|
const reduced = typeof window !== "undefined" && window.matchMedia &&
|
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
const stage = document.createElement("div");
|
|
stage.className = "avatar-dance-stage av-dance-" + dance + (reduced ? " av-dance-reduced" : "");
|
|
stage.dataset.dance = dance;
|
|
stage.setAttribute("aria-hidden", "true");
|
|
const face = renderAvatarFace(currentUser.avatar, "lg");
|
|
face.classList.add("av-dancer");
|
|
stage.appendChild(face);
|
|
host.appendChild(stage);
|
|
setTimeout(()=>{
|
|
stage.remove();
|
|
if(!host.querySelector(".avatar-dance-stage")) host.classList.remove("avatar-dance-host");
|
|
}, reduced ? 1400 : 3400);
|
|
return stage;
|
|
}
|
|
|
|
/* 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; config.speciesVariant = AVATAR_VARIANTS[id][0]; update(); });
|
|
speciesRow.appendChild(b);
|
|
});
|
|
controls.appendChild(speciesRow);
|
|
|
|
const variantRow = document.createElement("div"); variantRow.className = "avatar-row";
|
|
const variantField = document.createElement("label");
|
|
const variantSpan = document.createElement("span");
|
|
const variantSel = document.createElement("select");
|
|
variantSel.addEventListener("change", ()=>{ config.speciesVariant = variantSel.value; update(); });
|
|
variantField.append(variantSpan, variantSel);
|
|
variantRow.appendChild(variantField);
|
|
controls.appendChild(variantRow);
|
|
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);
|
|
});
|
|
const variantIds = AVATAR_VARIANTS[config.species];
|
|
if([...variantSel.options].map(o=>o.value).join(",") !== variantIds.join(",")){
|
|
variantSel.innerHTML = "";
|
|
variantIds.forEach(id=>variantSel.appendChild(new Option("", id)));
|
|
}
|
|
variantSpan.textContent = T("avatarVariant");
|
|
[...variantSel.options].forEach(o=>o.textContent = T("avatarVariant_" + o.value));
|
|
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=>{
|
|
const key = AVATAR_REWARD_ITEMS[o.value];
|
|
o.disabled = !!key && !avatarRewardKeys.has(key);
|
|
o.textContent = T("avatarAcc_" + o.value) + (o.disabled ? " 🔒" : "");
|
|
});
|
|
bodyAccSpan.textContent = T("avatarAccBody");
|
|
[...bodyAccSel.options].forEach(o=>{
|
|
const key = AVATAR_REWARD_ITEMS[o.value];
|
|
o.disabled = !!key && !avatarRewardKeys.has(key);
|
|
o.textContent = T("avatarAcc_" + o.value) + (o.disabled ? " 🔒" : "");
|
|
});
|
|
}
|
|
function update(){
|
|
const variants = AVATAR_VARIANTS[config.species];
|
|
if(!variants.includes(config.speciesVariant)) config.speciesVariant = variants[0];
|
|
syncLabels();
|
|
bodyColorInput.value = config.bodyColor;
|
|
hairColorInput.value = config.hairColor;
|
|
variantSel.value = config.speciesVariant;
|
|
hairSel.value = config.hairStyle;
|
|
headAccSel.value = config.accessoryHead;
|
|
bodyAccSel.value = config.accessoryBody;
|
|
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"); loadAvatarRewards().then(build).catch(build); },
|
|
keywords: ()=>[T("avatarSpecies_dino"), T("avatarSpecies_monster"), T("avatarSpecies_animal"),
|
|
...Object.values(AVATAR_VARIANTS).flat().map(id=>T("avatarVariant_" + id)),
|
|
"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);
|
|
}
|
|
loadAvatarRewards().then(build).catch(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: ()=>({}) };
|
|
}
|