feat: voeg avatarsoortvarianten toe (v0.4.50-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m8s

This commit is contained in:
Ramon 2026-07-22 23:40:36 +02:00
parent 2b40488208
commit 4544306ccd
6 changed files with 359 additions and 119 deletions

View file

@ -1 +1 @@
0.4.49-beta 0.4.50-beta

View file

@ -6,19 +6,27 @@
los ernaast te zweven. */ los ernaast te zweven. */
"use strict"; "use strict";
const AVATAR_SPECIES = ["dino", "monster", "animal"]; 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_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_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_BODY_ACC = ["none", "scarf", "cape", "badge", "necklace", "bowtie", "belt", "backpack", "satchel", "wings"];
const AVATAR_DEFAULT = Object.freeze({ const AVATAR_DEFAULT = Object.freeze({
species: "dino", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29", species: "dino", speciesVariant: "rex", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29",
accessoryHead: "none", accessoryBody: "none", accessoryHead: "none", accessoryBody: "none",
}); });
const AVATAR_SVG_NS = "http://www.w3.org/2000/svg"; const AVATAR_SVG_NS = "http://www.w3.org/2000/svg";
function avatarSanitize(config){ function avatarSanitize(config){
const c = config || {}; const c = config || {};
const species = AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species;
const variants = AVATAR_VARIANTS[species];
return { return {
species: AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species, species,
speciesVariant: variants.includes(c.speciesVariant) ? c.speciesVariant : variants[0],
bodyColor: /^#[0-9a-fA-F]{6}$/.test(c.bodyColor||"") ? c.bodyColor : AVATAR_DEFAULT.bodyColor, 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, 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, hairColor: /^#[0-9a-fA-F]{6}$/.test(c.hairColor||"") ? c.hairColor : AVATAR_DEFAULT.hairColor,
@ -43,48 +51,86 @@ function avatarShade(hex, amt){
derde van de figuurbreedte; romp en ledematen blijven kort en sluiten onder 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, de wangen aan. Alle soorten delen dezelfde accessoire-ankers rond hals y=61,
borst y=72 en middel y=87. */ borst y=72 en middel y=87. */
function avatarBodyMarkup(species, bodyColor){ 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 outline = avatarShade(bodyColor, -0.48);
const dark = avatarShade(bodyColor, -0.24); const dark = avatarShade(bodyColor, -0.24);
const light = avatarShade(bodyColor, 0.4); const light = avatarShade(bodyColor, 0.4);
const highlight = avatarShade(bodyColor, 0.72); const highlight = avatarShade(bodyColor, 0.72);
let m = ""; let m = `<g class="av-creature av-creature-${species} av-creature-${variant}">`;
/* Soortdetails achter kop en lijf. */ /* Silhouetdetails achter kop en lijf: iedere variant is direct herkenbaar. */
if(species === "dino"){ 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"/>`; if(variant === "rex"){
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"/>`; 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"/>`;
} }
if(species === "animal"){ }else if(species === "monster"){
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"/>`; if(variant === "horned"){
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-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-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 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}"/>`;
m += `<path d="M 73 11 C 81 11 83 4 79 1 C 73 -1 68 6 68 12 Z" fill="${light}"/>`; }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}"/>`;
} }
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. */ /* Korte ledematen blijven op gedeelde ankers voor alle accessoires. */
if(species === "dino"){ 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-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-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-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 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"){ }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-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-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-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 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{ }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-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-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-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-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"/>`; 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"/>`;
@ -93,56 +139,108 @@ function avatarBodyMarkup(species, bodyColor){
: species === "monster" : 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 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 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"/>`; 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(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"/>`; 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}"/>`; 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"){ }else if(species === "monster"){
m += `<ellipse class="av-part av-belly" cx="50" cy="77" rx="12" ry="14" fill="${light}" opacity=".72"/>`; 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}"/>`; 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{ }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"/>`; 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"/>`;
} }
/* De forse kop overlapt de hals en de bovenkant van het kleine lijf. */ const headPaths = {
const headPath = species === "monster" dino: {
? "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" 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",
: species === "dino" 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",
? "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" 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",
: "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"; 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",
m += `<path class="av-part av-head av-head-large" d="${headPath}" fill="${bodyColor}" stroke="${outline}" stroke-width="1.6" stroke-linejoin="round"/>`; },
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"/>`; 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(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 += `<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}"/>`; 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"){ 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 += `<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"/>`;
} }
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"/>`;
if(species === "monster"){ 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 += `<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"/>`; 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"){ if(variant === "raptor") m += `<path d="M 56 55 L 59 59 L 62 54" fill="#fff" stroke="${outline}" stroke-width=".7"/>`;
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 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{ }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"/>`; const leftSize = variant === "slime" ? 8.5 : 10;
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"/>`; 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; return m;
} }
const AVATAR_HAIR_MARKUP = { const AVATAR_HAIR_MARKUP = {
none: { back: () => "", front: () => "" }, none: { back: () => "", front: () => "" },
tuft: { tuft: {
@ -285,6 +383,30 @@ const AVATAR_HEAD_ACC_MARKUP = {
<circle cx="50" cy="-15" r="4.2" fill="#f0b73a"/>`, <circle cx="50" cy="-15" r="4.2" fill="#f0b73a"/>`,
}; };
/* 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 = { const AVATAR_BODY_ACC_MARKUP = {
none: { back: () => "", front: () => "" }, none: { back: () => "", front: () => "" },
scarf: { scarf: {
@ -368,16 +490,16 @@ function renderAvatarFace(config, size){
/* Extra ruimte voorkomt afsnijden van hoofddeksels, haar en voetzolen. */ /* Extra ruimte voorkomt afsnijden van hoofddeksels, haar en voetzolen. */
svg.setAttribute("viewBox", "-8 -21 116 132"); svg.setAttribute("viewBox", "-8 -21 116 132");
svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species); svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species + " av-variant-" + c.speciesVariant);
svg.setAttribute("role", "img"); svg.setAttribute("role", "img");
svg.setAttribute("aria-label", T("avatarAlt")); svg.setAttribute("aria-label", T("avatarAlt"));
const bodyAccessory = AVATAR_BODY_ACC_MARKUP[c.accessoryBody]; const bodyAccessory = AVATAR_BODY_ACC_MARKUP[c.accessoryBody];
const hair = AVATAR_HAIR_MARKUP[c.hairStyle]; const hair = AVATAR_HAIR_MARKUP[c.hairStyle];
let markup = bodyAccessory.back(); let markup = bodyAccessory.back();
markup += avatarHairLayer(hair.back(c.hairColor), c.species, "back"); markup += avatarHairLayer(hair.back(c.hairColor), c.species, "back");
markup += avatarBodyMarkup(c.species, c.bodyColor); markup += avatarBodyMarkup(c.species, c.bodyColor, c.speciesVariant);
if(!avatarHairSuppressed(c)) markup += avatarHairLayer(hair.front(c.hairColor), c.species, "front"); if(!avatarHairSuppressed(c)) markup += avatarHairLayer(hair.front(c.hairColor), c.species, "front");
markup += AVATAR_HEAD_ACC_MARKUP[c.accessoryHead](); markup += avatarHeadAccessoryMarkup(c);
markup += bodyAccessory.front(); markup += bodyAccessory.front();
svg.innerHTML = markup; svg.innerHTML = markup;
return svg; return svg;
@ -404,11 +526,19 @@ function buildAvatarEditor(container, initialConfig, onChange){
const b = document.createElement("button"); const b = document.createElement("button");
b.type = "button"; b.type = "button";
b.dataset.species = id; b.dataset.species = id;
b.addEventListener("click", ()=>{ config.species = id; update(); }); b.addEventListener("click", ()=>{ config.species = id; config.speciesVariant = AVATAR_VARIANTS[id][0]; update(); });
speciesRow.appendChild(b); speciesRow.appendChild(b);
}); });
controls.appendChild(speciesRow); 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 row1 = document.createElement("div"); row1.className = "avatar-row";
const bodyColorField = document.createElement("label"); const bodyColorField = document.createElement("label");
const bodyColorSpan = document.createElement("span"); const bodyColorSpan = document.createElement("span");
@ -454,6 +584,13 @@ function buildAvatarEditor(container, initialConfig, onChange){
b.textContent = T("avatarSpecies_" + b.dataset.species); b.textContent = T("avatarSpecies_" + b.dataset.species);
b.classList.toggle("active", b.dataset.species === config.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"); bodyColorSpan.textContent = T("avatarBodyColor");
hairColorSpan.textContent = T("avatarHairColor"); hairColorSpan.textContent = T("avatarHairColor");
hairSpan.textContent = T("avatarHairStyle"); hairSpan.textContent = T("avatarHairStyle");
@ -464,12 +601,15 @@ function buildAvatarEditor(container, initialConfig, onChange){
[...bodyAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value)); [...bodyAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value));
} }
function update(){ function update(){
const variants = AVATAR_VARIANTS[config.species];
if(!variants.includes(config.speciesVariant)) config.speciesVariant = variants[0];
syncLabels();
bodyColorInput.value = config.bodyColor; bodyColorInput.value = config.bodyColor;
hairColorInput.value = config.hairColor; hairColorInput.value = config.hairColor;
variantSel.value = config.speciesVariant;
hairSel.value = config.hairStyle; hairSel.value = config.hairStyle;
headAccSel.value = config.accessoryHead; headAccSel.value = config.accessoryHead;
bodyAccSel.value = config.accessoryBody; bodyAccSel.value = config.accessoryBody;
syncLabels();
preview.innerHTML = ""; preview.innerHTML = "";
preview.appendChild(renderAvatarFace(config, "lg")); preview.appendChild(renderAvatarFace(config, "lg"));
if(onChange) onChange({ ...config }); if(onChange) onChange({ ...config });
@ -518,6 +658,7 @@ async function saveAvatar(config){
visible: ()=>!!currentUser, visible: ()=>!!currentUser,
activate(){ showSettingsDom("avatar"); build(); }, activate(){ showSettingsDom("avatar"); build(); },
keywords: ()=>[T("avatarSpecies_dino"), T("avatarSpecies_monster"), T("avatarSpecies_animal"), 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"], "personage","avatar","dinosaurus","monster","dier","haar","hair","kleur","color"],
}); });
})(); })();

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.4.49-beta"; const VERSION = "0.4.50-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;
@ -162,9 +162,12 @@ const I18N = {
"brePattern_4-4":"4 tellen in, 4 uit", "brePattern_4-7-8":"4 in, 7 vast, 8 uit", "brePattern_4-4-4-4":"4 in, 4 vast, 4 uit, 4 vast", "brePattern_4-4":"4 tellen in, 4 uit", "brePattern_4-7-8":"4 in, 7 vast, 8 uit", "brePattern_4-4-4-4":"4 in, 4 vast, 4 uit, 4 vast",
wg_compliments:"Complimentenmachine", wg_compliments_d:"Trek een willekeurig compliment om iemand blij te maken.", wg_compliments:"Complimentenmachine", wg_compliments_d:"Trek een willekeurig compliment om iemand blij te maken.",
complimentsDraw:"Trek een compliment", complimentsDraw:"Trek een compliment",
wg_avatar:"Mijn personage", wg_avatar_d:"Stel je eigen personage samen: soort, kleuren, haar en accessoires.", wg_avatar:"Mijn personage", wg_avatar_d:"Kies een dinosaurus, monster of dier en daarna de variant, kleuren, haar en accessoires.",
stAvatar:"Mijn personage", stAvatarSub:"Stel je eigen personage samen.", stAvatar:"Mijn personage", stAvatarSub:"Stel je eigen personage samen.",
avatarAlt:"Mijn personage", avatarSpecies_dino:"Dino", avatarSpecies_monster:"Monster", avatarSpecies_animal:"Dier", avatarAlt:"Mijn personage", avatarSpecies_dino:"Dino", avatarSpecies_monster:"Monster", avatarSpecies_animal:"Dier",
avatarVariant:"Soortvariant", avatarVariant_rex:"T-rex", avatarVariant_triceratops:"Triceratops", avatarVariant_stegosaurus:"Stegosaurus", avatarVariant_raptor:"Velociraptor",
avatarVariant_horned:"Gehoornd monster", avatarVariant_cyclops:"Cycloop", avatarVariant_slime:"Slijmmonster", avatarVariant_alien:"Ruimtemonster",
avatarVariant_cat:"Kat", avatarVariant_dog:"Hond", avatarVariant_rabbit:"Konijn", avatarVariant_bear:"Beer",
avatarBodyColor:"Lichaamskleur", avatarHairColor:"Haarkleur", avatarHairStyle:"Haarstijl", avatarBodyColor:"Lichaamskleur", avatarHairColor:"Haarkleur", avatarHairStyle:"Haarstijl",
avatarHair_none:"Geen", avatarHair_tuft:"Kuifje", avatarHair_spiky:"Piekerig", avatarHair_curly:"Krullen", avatarHair_wavy:"Golvend", avatarHair_bob:"Boblijn", avatarHair_ponytail:"Staartje", avatarHair_braids:"Vlechten", avatarHair_mohawk:"Hanenkam", avatarHair_none:"Geen", avatarHair_tuft:"Kuifje", avatarHair_spiky:"Piekerig", avatarHair_curly:"Krullen", avatarHair_wavy:"Golvend", avatarHair_bob:"Boblijn", avatarHair_ponytail:"Staartje", avatarHair_braids:"Vlechten", avatarHair_mohawk:"Hanenkam",
avatarAccHead:"Hoofdaccessoire", avatarAccBody:"Lichaamsaccessoire", avatarAccHead:"Hoofdaccessoire", avatarAccBody:"Lichaamsaccessoire",
@ -549,9 +552,12 @@ const I18N = {
"brePattern_4-4":"4 counts in, 4 out", "brePattern_4-7-8":"4 in, 7 hold, 8 out", "brePattern_4-4-4-4":"4 in, 4 hold, 4 out, 4 hold", "brePattern_4-4":"4 counts in, 4 out", "brePattern_4-7-8":"4 in, 7 hold, 8 out", "brePattern_4-4-4-4":"4 in, 4 hold, 4 out, 4 hold",
wg_compliments:"Compliment machine", wg_compliments_d:"Draw a random compliment to make someone's day.", wg_compliments:"Compliment machine", wg_compliments_d:"Draw a random compliment to make someone's day.",
complimentsDraw:"Draw a compliment", complimentsDraw:"Draw a compliment",
wg_avatar:"My character", wg_avatar_d:"Build your own character: species, colours, hair and accessories.", wg_avatar:"My character", wg_avatar_d:"Choose a dinosaur, monster or animal, then its variant, colours, hair and accessories.",
stAvatar:"My character", stAvatarSub:"Build your own character.", stAvatar:"My character", stAvatarSub:"Build your own character.",
avatarAlt:"My character", avatarSpecies_dino:"Dino", avatarSpecies_monster:"Monster", avatarSpecies_animal:"Animal", avatarAlt:"My character", avatarSpecies_dino:"Dino", avatarSpecies_monster:"Monster", avatarSpecies_animal:"Animal",
avatarVariant:"Creature variant", avatarVariant_rex:"T. rex", avatarVariant_triceratops:"Triceratops", avatarVariant_stegosaurus:"Stegosaurus", avatarVariant_raptor:"Velociraptor",
avatarVariant_horned:"Horned monster", avatarVariant_cyclops:"Cyclops", avatarVariant_slime:"Slime monster", avatarVariant_alien:"Alien monster",
avatarVariant_cat:"Cat", avatarVariant_dog:"Dog", avatarVariant_rabbit:"Rabbit", avatarVariant_bear:"Bear",
avatarBodyColor:"Body colour", avatarHairColor:"Hair colour", avatarHairStyle:"Hair style", avatarBodyColor:"Body colour", avatarHairColor:"Hair colour", avatarHairStyle:"Hair style",
avatarHair_none:"None", avatarHair_tuft:"Tuft", avatarHair_spiky:"Spiky", avatarHair_curly:"Curly", avatarHair_wavy:"Wavy", avatarHair_bob:"Bob", avatarHair_ponytail:"Ponytail", avatarHair_braids:"Braids", avatarHair_mohawk:"Mohawk", avatarHair_none:"None", avatarHair_tuft:"Tuft", avatarHair_spiky:"Spiky", avatarHair_curly:"Curly", avatarHair_wavy:"Wavy", avatarHair_bob:"Bob", avatarHair_ponytail:"Ponytail", avatarHair_braids:"Braids", avatarHair_mohawk:"Mohawk",
avatarAccHead:"Head accessory", avatarAccBody:"Body accessory", avatarAccHead:"Head accessory", avatarAccBody:"Body accessory",

View file

@ -195,6 +195,11 @@ export default async function api(app) {
// Open voor iedere ingelogde rol (ook leerling/ouder) - net als /me/data hierboven, // Open voor iedere ingelogde rol (ook leerling/ouder) - net als /me/data hierboven,
// geen rol-beperkte PERMISSIONS-sleutel nodig, altijd de eigen rij. // geen rol-beperkte PERMISSIONS-sleutel nodig, altijd de eigen rij.
const AVATAR_SPECIES = ['dino', 'monster', 'animal']; const AVATAR_SPECIES = ['dino', 'monster', 'animal'];
const AVATAR_VARIANTS = {
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_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_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_BODY_ACC = ['none', 'scarf', 'cape', 'badge', 'necklace', 'bowtie', 'belt', 'backpack', 'satchel', 'wings'];
@ -203,13 +208,16 @@ export default async function api(app) {
need(req, reply); need(req, reply);
const b = req.body ?? {}; const b = req.body ?? {};
if (!AVATAR_SPECIES.includes(b.species)) return fail(reply, 400, 'ongeldige soort'); if (!AVATAR_SPECIES.includes(b.species)) return fail(reply, 400, 'ongeldige soort');
const variantOptions = AVATAR_VARIANTS[b.species];
const speciesVariant = b.speciesVariant ?? variantOptions[0];
if (!variantOptions.includes(speciesVariant)) return fail(reply, 400, 'ongeldige soortvariant');
if (!isHexColor(b.bodyColor)) return fail(reply, 400, 'ongeldige lichaamskleur'); if (!isHexColor(b.bodyColor)) return fail(reply, 400, 'ongeldige lichaamskleur');
if (!AVATAR_HAIR.includes(b.hairStyle)) return fail(reply, 400, 'ongeldige haarstijl'); if (!AVATAR_HAIR.includes(b.hairStyle)) return fail(reply, 400, 'ongeldige haarstijl');
if (!isHexColor(b.hairColor)) return fail(reply, 400, 'ongeldige haarkleur'); if (!isHexColor(b.hairColor)) return fail(reply, 400, 'ongeldige haarkleur');
if (!AVATAR_HEAD_ACC.includes(b.accessoryHead)) return fail(reply, 400, 'ongeldig hoofdaccessoire'); if (!AVATAR_HEAD_ACC.includes(b.accessoryHead)) return fail(reply, 400, 'ongeldig hoofdaccessoire');
if (!AVATAR_BODY_ACC.includes(b.accessoryBody)) return fail(reply, 400, 'ongeldig lichaamsaccessoire'); if (!AVATAR_BODY_ACC.includes(b.accessoryBody)) return fail(reply, 400, 'ongeldig lichaamsaccessoire');
const avatar = { const avatar = {
species: b.species, bodyColor: b.bodyColor, hairStyle: b.hairStyle, hairColor: b.hairColor, species: b.species, speciesVariant, bodyColor: b.bodyColor, hairStyle: b.hairStyle, hairColor: b.hairColor,
accessoryHead: b.accessoryHead, accessoryBody: b.accessoryBody, accessoryHead: b.accessoryHead, accessoryBody: b.accessoryBody,
}; };
await pool.query('UPDATE users SET avatar_data = $1 WHERE id = $2', [avatar, req.user.id]); await pool.query('UPDATE users SET avatar_data = $1 WHERE id = $2', [avatar, req.user.id]);

View file

@ -20,13 +20,14 @@ async function loadAvatarRenderer(){
}, },
}; };
runInNewContext(source.slice(0,cutoff)+ runInNewContext(source.slice(0,cutoff)+
';globalThis.__avatar={renderAvatarFace,avatarBodyMarkup,hair:AVATAR_HAIR_MARKUP,bodyAccessories:AVATAR_BODY_ACC_MARKUP,headAccessories:AVATAR_HEAD_ACC_MARKUP,hairIds:AVATAR_HAIR,headIds:AVATAR_HEAD_ACC,bodyIds:AVATAR_BODY_ACC};', ';globalThis.__avatar={renderAvatarFace,avatarBodyMarkup,sanitize:avatarSanitize,variants:AVATAR_VARIANTS,hair:AVATAR_HAIR_MARKUP,bodyAccessories:AVATAR_BODY_ACC_MARKUP,headAccessories:AVATAR_HEAD_ACC_MARKUP,hairIds:AVATAR_HAIR,headIds:AVATAR_HEAD_ACC,bodyIds:AVATAR_BODY_ACC};',
context); context);
return context.__avatar; return context.__avatar;
} }
const base={ const base={
species:'dino', species:'dino',
speciesVariant:'rex',
bodyColor:'#3fae6a', bodyColor:'#3fae6a',
hairStyle:'none', hairStyle:'none',
hairColor:'#5b3a29', hairColor:'#5b3a29',
@ -34,30 +35,63 @@ const base={
accessoryBody:'none', accessoryBody:'none',
}; };
test('avatars combineren een grote kop met een compact soortspecifiek lijf',async()=>{ test("iedere categorie heeft vier anatomisch herkenbare avatarvarianten",async()=>{
const api=await loadAvatarRenderer(); const api=await loadAvatarRenderer();
const bodies={}; const variants={
for(const species of ['dino','monster','animal']){ dino:["rex","triceratops","stegosaurus","raptor"],
const markup=api.avatarBodyMarkup(species,base.bodyColor); monster:["horned","cyclops","slime","alien"],
bodies[species]=markup; animal:["cat","dog","rabbit","bear"],
assert.equal((markup.match(/av-arm av-arm-/g)||[]).length,2,species+' mist twee armen'); };
assert.equal((markup.match(/av-leg av-leg-/g)||[]).length,2,species+' mist twee benen'); for(const [species,ids] of Object.entries(variants)){
for(const part of ['av-neck','av-torso-'+species,'av-head-large','av-eye']){ assert.deepEqual(Array.from(api.variants[species]),ids);
assert.ok(markup.includes(part),species+' mist '+part); const bodies=[];
for(const id of ids){
const markup=api.avatarBodyMarkup(species,base.bodyColor,id);
bodies.push(markup);
assert.ok(markup.includes("av-creature-"+id),species+"/"+id+" mist variantklasse");
assert.equal((markup.match(/av-arm av-arm-/g)||[]).length,2,species+"/"+id+" mist twee armen");
assert.equal((markup.match(/av-leg av-leg-/g)||[]).length,2,species+"/"+id+" mist twee benen");
for(const part of ["av-neck","av-torso-"+species,"av-head-large","av-eye"]){
assert.ok(markup.includes(part),species+"/"+id+" mist "+part);
} }
assert.ok(markup.indexOf('av-arm')<markup.indexOf('av-torso'),species+'-armen moeten achter de compacte romp aansluiten'); assert.ok(markup.indexOf("av-arm")<markup.indexOf("av-torso"),id+"-armen moeten achter de romp aansluiten");
assert.ok(markup.indexOf('av-leg')<markup.indexOf('av-torso'),species+'-benen moeten onder de compacte romp aansluiten'); assert.ok(markup.indexOf("av-leg")<markup.indexOf("av-torso"),id+"-benen moeten onder de romp aansluiten");
assert.ok(markup.indexOf('av-torso')<markup.indexOf('av-head-large'),species+'-kop moet de bovenkant van het lijf overlappen'); assert.ok(markup.indexOf("av-torso")<markup.indexOf("av-head-large"),id+"-kop moet de romp overlappen");
const svg=api.renderAvatarFace({...base,species,speciesVariant:id},"lg");
assert.ok(svg.attributes.class.includes("av-variant-"+id),id+" ontbreekt op SVG-klasse");
}
assert.equal(new Set(bodies).size,4,species+"-varianten moeten een uniek lichaam hebben");
} }
assert.ok(bodies.dino.indexOf('av-tail')<bodies.dino.indexOf('av-head-large'));
assert.ok(bodies.dino.indexOf('av-crest')<bodies.dino.indexOf('av-head-large'));
assert.equal((bodies.monster.match(/av-horn/g)||[]).length,2);
assert.equal((bodies.animal.match(/av-ear av-ear-/g)||[]).length,2);
assert.ok(bodies.animal.includes('av-chest-fluff'));
assert.notEqual(bodies.dino,bodies.monster);
assert.notEqual(bodies.monster,bodies.animal);
});
const markup=(species,id)=>api.avatarBodyMarkup(species,base.bodyColor,id);
assert.ok(markup("dino","rex").includes("av-tail-rex"));
assert.ok(markup("dino","triceratops").includes("av-frill-triceratops"));
assert.ok(markup("dino","triceratops").includes("av-nose-horn"));
assert.ok(markup("dino","stegosaurus").includes("av-dino-plates"));
assert.ok(markup("dino","raptor").includes("av-feather-crest-raptor"));
assert.ok(markup("dino","raptor").includes("av-feather-chest"));
assert.ok(markup("monster","horned").includes("av-horn-left"));
assert.equal((markup("monster","cyclops").match(/class="av-part av-eye/g)||[]).length,1);
assert.ok(markup("monster","cyclops").includes("av-cyclops-eye"));
assert.ok(markup("monster","slime").includes("av-slime-drop"));
assert.equal((markup("monster","alien").match(/av-alien-eye/g)||[]).length,3);
assert.ok(markup("monster","alien").includes("av-antenna-tip"));
assert.ok(markup("animal","cat").includes("av-whiskers"));
assert.ok(markup("animal","dog").includes("av-ear-dog"));
assert.ok(markup("animal","dog").includes("av-tongue"));
assert.ok(markup("animal","rabbit").includes("av-ear-rabbit"));
assert.ok(markup("animal","rabbit").includes("av-rabbit-teeth"));
assert.ok(markup("animal","bear").includes("av-ear-bear"));
assert.ok(markup("animal","bear").includes("av-muzzle-bear"));
assert.equal(api.sanitize({species:"dino"}).speciesVariant,"rex");
assert.equal(api.sanitize({species:"monster"}).speciesVariant,"horned");
assert.equal(api.sanitize({species:"animal"}).speciesVariant,"cat");
assert.equal(api.sanitize({species:"animal",speciesVariant:"rabbit"}).speciesVariant,"rabbit");
assert.equal(api.sanitize({species:"monster",speciesVariant:"rabbit"}).speciesVariant,"horned");
});
test("haar volgt de kop met natuurlijke details en correcte lagen",async()=>{ test("haar volgt de kop met natuurlijke details en correcte lagen",async()=>{
const api=await loadAvatarRenderer(); const api=await loadAvatarRenderer();
assert.deepEqual(Array.from(api.hairIds),["none","tuft","spiky","curly","wavy","bob","ponytail","braids","mohawk"]); assert.deepEqual(Array.from(api.hairIds),["none","tuft","spiky","curly","wavy","bob","ponytail","braids","mohawk"]);
@ -128,6 +162,14 @@ test('accessoires gebruiken vaste ankers en correcte voor- en achterlagen',async
const markup=api.renderAvatarFace({...base,accessoryHead:id},'lg').innerHTML; const markup=api.renderAvatarFace({...base,accessoryHead:id},'lg').innerHTML;
assert.ok(markup.indexOf(part)>markup.indexOf('av-head-large'),id+' hoort op de kop'); assert.ok(markup.indexOf(part)>markup.indexOf('av-head-large'),id+' hoort op de kop');
} }
const cyclopsGlasses=api.renderAvatarFace({...base,species:"monster",speciesVariant:"cyclops",accessoryHead:"glasses"},"lg").innerHTML;
const cyclopsPatch=api.renderAvatarFace({...base,species:"monster",speciesVariant:"cyclops",accessoryHead:"eyepatch"},"lg").innerHTML;
const alienGlasses=api.renderAvatarFace({...base,species:"monster",speciesVariant:"alien",accessoryHead:"glasses"},"lg").innerHTML;
const alienShades=api.renderAvatarFace({...base,species:"monster",speciesVariant:"alien",accessoryHead:"sunglasses"},"lg").innerHTML;
assert.ok(cyclopsGlasses.includes("av-glasses-cyclops"),"cycloopbril hoort rond het ene oog");
assert.ok(cyclopsPatch.includes("av-eyepatch-cyclops"),"cycloopooglap hoort voor het ene oog");
assert.ok(alienGlasses.includes("av-glasses-alien"),"alienbril hoort rond drie ogen");
assert.ok(alienShades.includes("av-sunglasses-alien"),"alienzonnebril hoort alle drie ogen af te dekken");
const cape=api.renderAvatarFace({...base,accessoryBody:'cape'},'lg').innerHTML; const cape=api.renderAvatarFace({...base,accessoryBody:'cape'},'lg').innerHTML;
assert.ok(cape.indexOf('av-cape"')<cape.indexOf('av-torso')); assert.ok(cape.indexOf('av-cape"')<cape.indexOf('av-torso'));
@ -151,45 +193,55 @@ test('accessoires gebruiken vaste ankers en correcte voor- en achterlagen',async
} }
}); });
test('alle 3240 avatarcombinaties renderen volledig binnen de SVG',async()=>{ test("alle 12960 variant-, haar- en accessoirecombinaties renderen volledig",async()=>{
const api=await loadAvatarRenderer(); const api=await loadAvatarRenderer();
let combinations=0; let combinations=0;
for(const speciesId of ['dino','monster','animal']){ for(const speciesId of ["dino","monster","animal"]){
for(const variantId of api.variants[speciesId]){
for(const hairId of api.hairIds){ for(const hairId of api.hairIds){
for(const headId of api.headIds){ for(const headId of api.headIds){
for(const bodyId of api.bodyIds){ for(const bodyId of api.bodyIds){
const svg=api.renderAvatarFace({ const svg=api.renderAvatarFace({
...base, ...base,
species:speciesId, species:speciesId,
speciesVariant:variantId,
hairStyle:hairId, hairStyle:hairId,
accessoryHead:headId, accessoryHead:headId,
accessoryBody:bodyId, accessoryBody:bodyId,
},'sm'); },"sm");
assert.equal(svg.attributes.viewBox,'-8 -21 116 132'); assert.equal(svg.attributes.viewBox,"-8 -21 116 132");
assert.equal(svg.attributes.preserveAspectRatio,'xMidYMid meet'); assert.equal(svg.attributes.preserveAspectRatio,"xMidYMid meet");
assert.doesNotMatch(svg.innerHTML,/undefined|NaN/); assert.doesNotMatch(svg.innerHTML,/undefined|NaN/);
assert.ok(svg.innerHTML.includes('av-head-large')); assert.ok(svg.innerHTML.includes("av-head-large"));
assert.ok(svg.innerHTML.includes("av-creature-"+variantId));
combinations++; combinations++;
} }
} }
} }
} }
assert.equal(combinations,3240); }
assert.equal(combinations,12960);
}); });
test("client, API en beide talen bevatten dezelfde avataropties",async()=>{
test('client, API en beide talen bevatten dezelfde nieuwe avataropties',async()=>{ const [apiSource,core]=await Promise.all([
const [api,core]=await Promise.all([ readFile("src/api.js","utf8"),
readFile('src/api.js','utf8'), readFile("public/js/core.js","utf8"),
readFile('public/js/core.js','utf8'),
]); ]);
const hair=['tuft','wavy','bob','braids']; const hair=["tuft","wavy","bob","braids"];
const accessories=['beanie','wizardhat','eyepatch','flower','bowtie','belt','satchel','wings']; const accessories=["beanie","wizardhat","eyepatch","flower","bowtie","belt","satchel","wings"];
const variants=["rex","triceratops","stegosaurus","raptor","horned","cyclops","slime","alien","cat","dog","rabbit","bear"];
for(const id of hair){ for(const id of hair){
assert.ok(api.includes("'"+id+"'"),id+' ontbreekt in API-validatie'); assert.ok(apiSource.includes("'"+id+"'"),id+" ontbreekt in API-validatie");
assert.equal(core.split('avatarHair_'+id+':').length-1,2,id+' mist een vertaling'); assert.equal(core.split("avatarHair_"+id+":").length-1,2,id+" mist een vertaling");
} }
for(const id of accessories){ for(const id of accessories){
assert.ok(api.includes("'"+id+"'"),id+' ontbreekt in API-validatie'); assert.ok(apiSource.includes("'"+id+"'"),id+" ontbreekt in API-validatie");
assert.equal(core.split('avatarAcc_'+id+':').length-1,2,id+' mist een vertaling'); assert.equal(core.split("avatarAcc_"+id+":").length-1,2,id+" mist een vertaling");
} }
for(const id of variants){
assert.ok(apiSource.includes("'"+id+"'"),id+" ontbreekt in API-variantvalidatie");
assert.equal(core.split("avatarVariant_"+id+":").length-1,2,id+" mist een variantvertaling");
}
assert.ok(apiSource.includes("speciesVariant"),"API slaat de soortvariant niet op");
assert.equal(core.split("avatarVariant:").length-1,2,"variantveld mist een label");
}); });

View file

@ -38,6 +38,39 @@ function makeApp({ dataRev = 3, updateSucceeds = true } = {}) {
const sessionCookie = { teach_session: 'x'.repeat(64) }; const sessionCookie = { teach_session: 'x'.repeat(64) };
const avatarPayload = {
species: "animal", speciesVariant: "rabbit", bodyColor: "#aabbcc",
hairStyle: "wavy", hairColor: "#553311", accessoryHead: "glasses", accessoryBody: "scarf",
};
test("avatar-API bewaart een geldige variant binnen de gekozen categorie", async () => {
const { app, calls } = await makeApp();
const res = await app.inject({ method: "PUT", url: "/api/me/avatar", cookies: sessionCookie, payload: avatarPayload });
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.json().avatar.speciesVariant, "rabbit");
const update = calls.find((c) => c.sql.startsWith("UPDATE users SET avatar_data"));
assert.equal(update.params[0].species, "animal");
assert.equal(update.params[0].speciesVariant, "rabbit");
await app.close();
});
test("avatar-API geeft oudere clients een standaardvariant", async () => {
const { app } = await makeApp();
const { speciesVariant, ...legacyPayload } = avatarPayload;
const res = await app.inject({ method: "PUT", url: "/api/me/avatar", cookies: sessionCookie, payload: legacyPayload });
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.json().avatar.speciesVariant, "cat");
await app.close();
});
test("avatar-API weigert een variant uit een andere categorie", async () => {
const { app, calls } = await makeApp();
const res = await app.inject({ method: "PUT", url: "/api/me/avatar", cookies: sessionCookie, payload: { ...avatarPayload, speciesVariant: "rex" } });
assert.equal(res.statusCode, 400, res.body);
assert.equal(res.json().error, "ongeldige soortvariant");
assert.equal(calls.some((c) => c.sql.startsWith("UPDATE users SET avatar_data")),false);
await app.close();
});
test('GET /me/data geeft data én rev terug', async () => { test('GET /me/data geeft data én rev terug', async () => {
const { app } = await makeApp({ dataRev: 3 }); const { app } = await makeApp({ dataRev: 3 });
const res = await app.inject({ method: 'GET', url: '/api/me/data', cookies: sessionCookie }); const res = await app.inject({ method: 'GET', url: '/api/me/data', cookies: sessionCookie });