Voeg thematische kleurplaten en creatieve niveaus toe (v0.4.65-beta) #3
5 changed files with 248 additions and 100 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.45-beta
|
0.4.46-beta
|
||||||
|
|
|
||||||
|
|
@ -1779,8 +1779,8 @@ body.hc #penbar .pen-current,body.hc #penbar .pen-popover{border:2px solid #000;
|
||||||
.bd-candle::before{content:""; position:absolute; width:8px; height:12px; left:-1.5px; top:-12px; border-radius:70% 30% 60% 40%; background:linear-gradient(#fff7a0,var(--orange)); box-shadow:0 0 8px var(--orange);}
|
.bd-candle::before{content:""; position:absolute; width:8px; height:12px; left:-1.5px; top:-12px; border-radius:70% 30% 60% 40%; background:linear-gradient(#fff7a0,var(--orange)); box-shadow:0 0 8px var(--orange);}
|
||||||
.bd-age-badge{position:absolute; right:8%; top:20%; z-index:4; min-width:58px; height:58px; padding:0 10px; border-radius:20px; display:grid; place-items:center; color:#fff; font-size:29px; font-weight:900; transform:rotate(7deg); box-shadow:0 5px 12px rgba(0,0,0,.2); border:3px solid #fff;}
|
.bd-age-badge{position:absolute; right:8%; top:20%; z-index:4; min-width:58px; height:58px; padding:0 10px; border-radius:20px; display:grid; place-items:center; color:#fff; font-size:29px; font-weight:900; transform:rotate(7deg); box-shadow:0 5px 12px rgba(0,0,0,.2); border:3px solid #fff;}
|
||||||
.bd-plate{width:min(94%,420px); height:30px; border-radius:50%; margin-top:-12px; box-shadow:0 8px 12px rgba(29,52,97,.22); z-index:1;}
|
.bd-plate{width:min(94%,420px); height:30px; border-radius:50%; margin-top:-12px; box-shadow:0 8px 12px rgba(29,52,97,.22); z-index:1;}
|
||||||
/* =========== Personage (avatar): gelaagde div's + CSS custom properties,
|
/* =========== Personage (avatar): schaalbare SVG met anatomische ankerpunten
|
||||||
zelfde aanpak als de taart hierboven (--cake/--icing), geen SVG/afbeeldingen. ==== */
|
en vaste voor-/achterlagen voor haar en accessoires. ====================== */
|
||||||
.avatar-face{display:inline-block; flex:none; overflow:visible;}
|
.avatar-face{display:inline-block; flex:none; overflow:visible;}
|
||||||
.avatar-face.sm{width:24px; height:24px;}
|
.avatar-face.sm{width:24px; height:24px;}
|
||||||
.avatar-face.md{width:44px; height:44px;}
|
.avatar-face.md{width:44px; height:44px;}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* teach - personage (avatar): eigen figuur samenstellen (soort, kleuren, haar,
|
/* teach - personage (avatar): eigen figuur samenstellen (soort, kleuren, haar,
|
||||||
accessoires), gedeeld tussen de Instellingen-sectie "Mijn personage" en het
|
accessoires), gedeeld tussen de Instellingen-sectie "Mijn personage" en het
|
||||||
avatar-widget. Getekend als één SVG (viewBox 0 0 100 100, tekenvolgorde =
|
avatar-widget. Getekend als één SVG rond vaste anatomische ankers; de tekenvolgorde =
|
||||||
laagvolgorde - geen z-index-gepuzzel zoals bij de eerdere CSS-laagjesversie)
|
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
|
zodat elk onderdeel z'n vaste plek op het silhouet overlapt in plaats van
|
||||||
los ernaast te zweven. */
|
los ernaast te zweven. */
|
||||||
|
|
@ -39,70 +39,82 @@ function avatarShade(hex, amt){
|
||||||
return "#" + [r,g,b].map(v=>clamp(v).toString(16).padStart(2,"0")).join("");
|
return "#" + [r,g,b].map(v=>clamp(v).toString(16).padStart(2,"0")).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* per-soort silhouet: elk onderdeel overlapt het vorige ruim (poten/armen/
|
/* Per-soort silhouet met vaste anatomische ankers: kruin y=2, ogen y=25,
|
||||||
staart tot ver ín het lijf, rugstekels/hoorns pas ná de kop getekend) zodat
|
hals y=53, schouders y=60, heupen y=88 en voetzolen y=108. Armen en benen
|
||||||
niets los oogt. Volgorde = tekenvolgorde = zichtbare laagvolgorde. */
|
lopen bewust onder het lijf door. Daardoor blijven gewrichten verbonden en
|
||||||
|
kunnen accessoires steeds aan hetzelfde lichaamsdeel worden vastgemaakt. */
|
||||||
function avatarBodyMarkup(species, bodyColor){
|
function avatarBodyMarkup(species, bodyColor){
|
||||||
const dark = avatarShade(bodyColor, -0.35);
|
const outline = avatarShade(bodyColor, -0.48);
|
||||||
|
const dark = avatarShade(bodyColor, -0.24);
|
||||||
const light = avatarShade(bodyColor, 0.4);
|
const light = avatarShade(bodyColor, 0.4);
|
||||||
|
const highlight = avatarShade(bodyColor, 0.7);
|
||||||
let m = "";
|
let m = "";
|
||||||
|
|
||||||
|
/* Onderdelen achter het lijf: soortdetails, benen en doorlopende armen. */
|
||||||
if(species === "dino"){
|
if(species === "dino"){
|
||||||
m += `<path d="M 38 88 C 18 92, 2 82, 1 64 C 3 78, 18 86, 35 76 Z" fill="${dark}"/>`;
|
m += `<path class="av-part av-tail" d="M 36 78 C 24 82 15 83 5 76 C 12 90 25 96 42 87 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
||||||
}
|
m += `<path class="av-part av-crest" d="M 68 14 L 78 9 L 75 21 L 84 20 L 77 31" fill="${dark}" stroke="${outline}" stroke-width="1.2" stroke-linejoin="round"/>`;
|
||||||
m += `<ellipse cx="34" cy="92" rx="10" ry="8" fill="${dark}"/>`;
|
|
||||||
m += `<ellipse cx="66" cy="92" rx="10" ry="8" fill="${dark}"/>`;
|
|
||||||
m += `<ellipse cx="50" cy="72" rx="24" ry="19" fill="${bodyColor}"/>`;
|
|
||||||
m += `<ellipse cx="50" cy="75" rx="13" ry="13" fill="${light}"/>`;
|
|
||||||
m += `<ellipse cx="25" cy="68" rx="10" ry="8" fill="${dark}"/>`;
|
|
||||||
m += `<ellipse cx="75" cy="68" rx="10" ry="8" fill="${dark}"/>`;
|
|
||||||
if(species === "animal"){
|
|
||||||
m += `<circle cx="27" cy="10" r="11" fill="${bodyColor}"/>`;
|
|
||||||
m += `<circle cx="73" cy="10" r="11" fill="${bodyColor}"/>`;
|
|
||||||
m += `<circle cx="27" cy="10" r="6" fill="${light}"/>`;
|
|
||||||
m += `<circle cx="73" cy="10" r="6" fill="${light}"/>`;
|
|
||||||
}
|
|
||||||
if(species === "monster"){
|
|
||||||
const darker = avatarShade(bodyColor, -0.5);
|
|
||||||
m += `<path d="M 30 20 C 26 12, 27 4, 32 0 C 33 8, 34 15, 38 20 Z" fill="${darker}"/>`;
|
|
||||||
m += `<path d="M 70 20 C 74 12, 73 4, 68 0 C 67 8, 66 15, 62 20 Z" fill="${darker}"/>`;
|
|
||||||
}
|
|
||||||
m += `<ellipse cx="50" cy="28" rx="26" ry="26" fill="${bodyColor}"/>`;
|
|
||||||
if(species === "dino"){
|
|
||||||
m += `<ellipse cx="50" cy="43" rx="15" ry="10" fill="${light}"/>`;
|
|
||||||
m += `<ellipse cx="44.5" cy="41" rx="2.1" ry="3" fill="#234"/>`;
|
|
||||||
m += `<ellipse cx="55.5" cy="41" rx="2.1" ry="3" fill="#234"/>`;
|
|
||||||
}
|
}
|
||||||
if(species === "animal"){
|
if(species === "animal"){
|
||||||
m += `<ellipse cx="50" cy="42" rx="14" ry="10" fill="${light}"/>`;
|
m += `<path class="av-part av-ear av-ear-left" d="M 30 17 C 16 15 16 2 22 0 C 31 -2 37 7 36 17 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
||||||
m += `<ellipse cx="50" cy="38" rx="4.4" ry="3.4" fill="#3a2a18"/>`;
|
m += `<path d="M 28 13 C 21 12 21 6 24 4 C 29 3 32 8 32 13 Z" fill="${light}"/>`;
|
||||||
|
m += `<path class="av-part av-ear av-ear-right" d="M 70 17 C 84 15 84 2 78 0 C 69 -2 63 7 64 17 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4"/>`;
|
||||||
|
m += `<path d="M 72 13 C 79 12 79 6 76 4 C 71 3 68 8 68 13 Z" fill="${light}"/>`;
|
||||||
}
|
}
|
||||||
if(species === "monster"){
|
if(species === "monster"){
|
||||||
m += `<circle cx="37" cy="25" r="8.4" fill="#fff"/>`;
|
m += `<path class="av-part av-horn" d="M 31 18 C 24 12 25 3 30 -3 C 31 7 35 11 39 16 Z" fill="${outline}"/>`;
|
||||||
m += `<circle cx="63" cy="25" r="6.8" fill="#fff"/>`;
|
m += `<path class="av-part av-horn" d="M 69 18 C 76 12 75 3 70 -3 C 69 7 65 11 61 16 Z" fill="${outline}"/>`;
|
||||||
m += `<circle cx="38.6" cy="27" r="3.6" fill="#232733"/>`;
|
}
|
||||||
m += `<circle cx="64.2" cy="27" r="2.9" fill="#232733"/>`;
|
|
||||||
|
m += `<path class="av-part av-leg av-leg-left" d="M 34 80 C 30 87 29 97 30 101 C 23 102 20 106 23 108 C 29 111 39 109 40 104 L 43 84 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
||||||
|
m += `<path class="av-part av-leg av-leg-right" d="M 66 80 C 70 87 71 97 70 101 C 77 102 80 106 77 108 C 71 111 61 109 60 104 L 57 84 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4" stroke-linejoin="round"/>`;
|
||||||
|
m += `<path class="av-part av-arm av-arm-left" d="M 36 61 C 29 58 23 62 19 69 L 14 78 C 11 78 8 80 9 83 C 10 87 16 87 19 84 L 26 75 C 29 72 31 73 35 76 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>`;
|
||||||
|
m += `<path class="av-part av-arm av-arm-right" d="M 64 61 C 71 58 77 62 81 69 L 86 78 C 89 78 92 80 91 83 C 90 87 84 87 81 84 L 74 75 C 71 72 69 73 65 76 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>`;
|
||||||
|
m += `<path d="M 11 81 L 15 83 M 89 81 L 85 83" stroke="${outline}" stroke-width="1.1" stroke-linecap="round"/>`;
|
||||||
|
|
||||||
|
/* Hals en romp bedekken de aanzetten van armen en benen. */
|
||||||
|
m += `<path class="av-part av-neck" d="M 40 45 C 42 55 39 58 35 61 L 65 61 C 61 58 58 55 60 45 Z" fill="${dark}" stroke="${outline}" stroke-width="1.4"/>`;
|
||||||
|
m += `<path class="av-part av-torso" d="M 35 57 C 42 53 58 53 65 57 C 72 63 73 79 67 91 C 60 98 40 98 33 91 C 27 79 28 63 35 57 Z" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
||||||
|
m += `<path class="av-part av-belly" d="M 41 63 C 46 59 54 59 59 63 C 64 70 63 85 58 91 C 53 94 47 94 42 91 C 37 85 36 70 41 63 Z" fill="${light}" opacity=".88"/>`;
|
||||||
|
m += `<path d="M 43 64 C 48 61 53 61 57 63" stroke="${highlight}" stroke-width="2" fill="none" stroke-linecap="round" opacity=".7"/>`;
|
||||||
|
|
||||||
|
/* Kop en gelaat; oren en hoorns blijven achter de schedel. */
|
||||||
|
const headPath = species === "monster"
|
||||||
|
? "M 50 1 C 34 -1 23 9 23 26 C 22 42 31 53 48 55 C 65 57 78 47 77 28 C 77 10 67 2 50 1 Z"
|
||||||
|
: species === "dino"
|
||||||
|
? "M 48 1 C 33 1 23 11 23 27 C 23 41 31 51 43 54 C 58 59 75 51 77 35 C 79 19 69 4 54 2 Z"
|
||||||
|
: "M 50 1 C 34 1 23 11 23 28 C 23 45 34 55 50 55 C 66 55 77 45 77 28 C 77 11 66 1 50 1 Z";
|
||||||
|
m += `<path class="av-part av-head" d="${headPath}" fill="${bodyColor}" stroke="${outline}" stroke-width="1.5" stroke-linejoin="round"/>`;
|
||||||
|
m += `<path d="M 31 13 C 35 7 42 5 48 5" stroke="${highlight}" stroke-width="2.2" fill="none" stroke-linecap="round" opacity=".65"/>`;
|
||||||
|
|
||||||
|
if(species === "dino"){
|
||||||
|
m += `<path class="av-part av-muzzle" d="M 34 38 C 40 34 62 34 69 40 C 72 47 63 53 51 53 C 39 53 31 47 34 38 Z" fill="${light}"/>`;
|
||||||
|
m += `<ellipse cx="43" cy="42" rx="2" ry="2.8" fill="${outline}"/>`;
|
||||||
|
m += `<ellipse cx="60" cy="42" rx="2" ry="2.8" fill="${outline}"/>`;
|
||||||
|
}
|
||||||
|
if(species === "animal"){
|
||||||
|
m += `<path class="av-part av-muzzle" d="M 37 37 C 41 33 47 34 50 38 C 53 34 59 33 63 37 C 68 43 61 52 50 52 C 39 52 32 43 37 37 Z" fill="${light}"/>`;
|
||||||
|
m += `<path d="M 46 37 Q 50 34 54 37 Q 53 42 50 43 Q 47 42 46 37 Z" fill="#3a2a18"/>`;
|
||||||
|
}
|
||||||
|
if(species === "monster"){
|
||||||
|
m += `<ellipse class="av-part av-eye" cx="37" cy="25" rx="8.5" ry="9.2" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
||||||
|
m += `<ellipse class="av-part av-eye" cx="63" cy="25" rx="7" ry="7.8" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
||||||
|
m += `<circle cx="39" cy="27" r="3.6" fill="#232733"/><circle cx="64" cy="27" r="3" fill="#232733"/>`;
|
||||||
}else{
|
}else{
|
||||||
m += `<circle cx="38" cy="24" r="7.6" fill="#fff"/>`;
|
m += `<ellipse class="av-part av-eye" cx="38" cy="25" rx="7.5" ry="8.2" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
||||||
m += `<circle cx="62" cy="24" r="7.6" fill="#fff"/>`;
|
m += `<ellipse class="av-part av-eye" cx="62" cy="25" rx="7.5" ry="8.2" fill="#fff" stroke="${outline}" stroke-width="1"/>`;
|
||||||
m += `<circle cx="39.4" cy="26" r="3.2" fill="#232733"/>`;
|
m += `<circle cx="39.5" cy="27" r="3.2" fill="#232733"/><circle cx="60.5" cy="27" r="3.2" fill="#232733"/>`;
|
||||||
m += `<circle cx="60.4" cy="26" r="3.2" fill="#232733"/>`;
|
|
||||||
}
|
}
|
||||||
|
m += `<circle cx="40.5" cy="25.5" r="1" fill="#fff"/><circle cx="61.5" cy="25.5" r="1" fill="#fff"/>`;
|
||||||
if(species === "dino"){
|
if(species === "dino"){
|
||||||
m += `<path d="M 43 51 Q 50 55.5 57 51" stroke="${avatarShade(bodyColor,-0.55)}" stroke-width="1.6" fill="none" stroke-linecap="round"/>`;
|
m += `<path d="M 43 48 Q 51 53 59 48" stroke="${outline}" stroke-width="1.6" fill="none" stroke-linecap="round"/>`;
|
||||||
/* rugstekels als klein randje bovenop de kop i.p.v. voor de hals - een
|
|
||||||
rij spitse vormen op de borst/hals oogde als een vreemde kraag; dit
|
|
||||||
zit anatomisch dichter bij de rug en is ook stiller op klein formaat. */
|
|
||||||
const plate = avatarShade(bodyColor, -0.45);
|
|
||||||
m += `<path d="M 39 13 Q 38 2 44 0 Q 46 7 45 15 Z" fill="${plate}"/>`;
|
|
||||||
m += `<path d="M 55 15 Q 54 7 56 0 Q 62 2 61 13 Z" fill="${plate}"/>`;
|
|
||||||
}
|
}
|
||||||
if(species === "animal"){
|
if(species === "animal"){
|
||||||
m += `<path d="M 50 41 L 50 45 M 44 49 Q 50 52 56 49" stroke="#3a2a18" stroke-width="1.6" fill="none" stroke-linecap="round"/>`;
|
m += `<path d="M 50 43 L 50 46 M 42 47 Q 46 52 50 47 Q 54 52 58 47" stroke="#3a2a18" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`;
|
||||||
}
|
}
|
||||||
if(species === "monster"){
|
if(species === "monster"){
|
||||||
m += `<path d="M 34 42 Q 50 54 66 42 Q 50 50 34 42 Z" fill="#3c2158"/>`;
|
m += `<path class="av-part av-mouth" d="M 33 41 Q 50 55 67 41 Q 63 52 50 53 Q 37 52 33 41 Z" fill="#3c2158" stroke="${outline}" stroke-width="1"/>`;
|
||||||
m += `<path d="M 42 43 L 44 49 L 46 43 Z" fill="#fff"/>`;
|
m += `<path d="M 41 44 L 44 50 L 47 44 Z M 53 44 L 56 50 L 59 44 Z" fill="#fff"/>`;
|
||||||
m += `<path d="M 54 43 L 56 49 L 58 43 Z" fill="#fff"/>`;
|
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
@ -131,69 +143,89 @@ const AVATAR_HAIR_MARKUP = {
|
||||||
const AVATAR_HEAD_ACC_MARKUP = {
|
const AVATAR_HEAD_ACC_MARKUP = {
|
||||||
none: () => "",
|
none: () => "",
|
||||||
cap: () => `
|
cap: () => `
|
||||||
<path d="M 22 14 Q 24 -6 50 -6 Q 76 -6 78 14 Q 64 4 50 4 Q 36 4 22 14 Z" fill="#3d7ddb"/>
|
<path class="av-part av-headwear av-cap" d="M 26 15 C 28 1 39 -5 52 -4 C 65 -3 73 4 74 16 C 60 10 41 9 26 15 Z" fill="#3d7ddb" stroke="#234f91" stroke-width="1.3"/>
|
||||||
<path d="M 22 14 Q 34 8 50 8 L 50 14 Q 34 15 21 20 Z" fill="#2f63b3"/>`,
|
<path d="M 26 14 C 38 10 55 10 67 14 C 57 19 40 19 22 20 C 20 19 22 16 26 14 Z" fill="#2f63b3" stroke="#234f91" stroke-width="1.2"/>
|
||||||
|
<path d="M 50 -4 L 50 11" stroke="#72a3e8" stroke-width="1.1" opacity=".8"/>`,
|
||||||
crown: () => `
|
crown: () => `
|
||||||
<path d="M 26 16 L 30 0 L 38 12 L 50 -4 L 62 12 L 70 0 L 74 16 Q 50 8 26 16 Z" fill="#f0b73a"/>
|
<path class="av-part av-headwear av-crown" d="M 27 16 L 30 0 L 39 11 L 50 -5 L 61 11 L 70 0 L 73 16 C 59 12 41 12 27 16 Z" fill="#f0b73a" stroke="#b77b0b" stroke-width="1.2" stroke-linejoin="round"/>
|
||||||
|
<path d="M 28 14 C 42 11 58 11 72 14 L 72 18 C 58 15 42 15 28 18 Z" fill="#d99a22"/>
|
||||||
<circle cx="30" cy="1" r="2.6" fill="#e0483c"/>
|
<circle cx="30" cy="1" r="2.6" fill="#e0483c"/>
|
||||||
<circle cx="50" cy="-3" r="2.6" fill="#3a8ee0"/>
|
<circle cx="50" cy="-4" r="2.6" fill="#3a8ee0"/>
|
||||||
<circle cx="70" cy="1" r="2.6" fill="#e0483c"/>`,
|
<circle cx="70" cy="1" r="2.6" fill="#e0483c"/>`,
|
||||||
glasses: () => `
|
glasses: () => `
|
||||||
<circle cx="38" cy="25" r="9.5" fill="rgba(255,255,255,.3)" stroke="#232733" stroke-width="2.4"/>
|
<circle class="av-part av-eyewear" cx="38" cy="25" r="9.5" fill="rgba(255,255,255,.3)" stroke="#232733" stroke-width="2.4"/>
|
||||||
<circle cx="62" cy="25" r="9.5" fill="rgba(255,255,255,.3)" stroke="#232733" stroke-width="2.4"/>
|
<circle class="av-part av-eyewear" cx="62" cy="25" r="9.5" fill="rgba(255,255,255,.3)" stroke="#232733" stroke-width="2.4"/>
|
||||||
<path d="M 47.5 25 L 52.5 25" stroke="#232733" stroke-width="2.4"/>
|
<path d="M 47.5 25 L 52.5 25" stroke="#232733" stroke-width="2.4"/>
|
||||||
<path d="M 28.5 23 L 22 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>
|
<path d="M 28.5 23 L 22 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
<path d="M 71.5 23 L 78 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>`,
|
<path d="M 71.5 23 L 78 20" stroke="#232733" stroke-width="2.2" stroke-linecap="round"/>`,
|
||||||
bow: () => `
|
|
||||||
<path d="M 68 14 Q 58 8 58 16 Q 58 22 68 18 Q 74 24 78 18 Q 80 12 74 10 Q 78 6 74 2 Q 68 2 68 14 Z" fill="#e05a8a"/>
|
|
||||||
<circle cx="69" cy="14" r="3" fill="#c23f6e"/>`,
|
|
||||||
sunglasses: () => `
|
sunglasses: () => `
|
||||||
<path d="M 27 20 Q 27 32 38 32 Q 49 32 48 22 Q 48 18 38 18 Q 27 18 27 20 Z" fill="#1a1d24"/>
|
<path class="av-part av-eyewear av-sunglasses" d="M 27 20 Q 27 32 38 32 Q 49 32 48 22 Q 48 18 38 18 Q 27 18 27 20 Z" fill="#1a1d24"/>
|
||||||
<path d="M 52 22 Q 51 32 62 32 Q 73 32 73 20 Q 73 18 62 18 Q 52 18 52 22 Z" fill="#1a1d24"/>
|
<path d="M 52 22 Q 51 32 62 32 Q 73 32 73 20 Q 73 18 62 18 Q 52 18 52 22 Z" fill="#1a1d24"/>
|
||||||
<path d="M 48 22 Q 50 20 52 22" stroke="#1a1d24" stroke-width="2.4" fill="none"/>
|
<path d="M 48 22 Q 50 20 52 22" stroke="#1a1d24" stroke-width="2.4" fill="none"/>
|
||||||
<path d="M 27 21 L 20 19" stroke="#1a1d24" stroke-width="2.4" stroke-linecap="round"/>
|
<path d="M 27 21 L 20 19 M 73 21 L 80 19" stroke="#1a1d24" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
<path d="M 73 21 L 80 19" stroke="#1a1d24" stroke-width="2.4" stroke-linecap="round"/>
|
<path d="M 31 22 Q 33 20 36 21 M 56 22 Q 58 20 61 21" stroke="#fff" stroke-width="1.6" fill="none" opacity=".55" stroke-linecap="round"/>`,
|
||||||
<path d="M 31 22 Q 33 20 36 21" stroke="#fff" stroke-width="1.6" fill="none" opacity=".55" stroke-linecap="round"/>
|
bow: () => `
|
||||||
<path d="M 56 22 Q 58 20 61 21" stroke="#fff" stroke-width="1.6" fill="none" opacity=".55" stroke-linecap="round"/>`,
|
<path class="av-part av-headwear av-bow" d="M 69 13 C 62 5 56 7 57 14 C 58 20 63 20 69 16 C 75 20 81 20 82 14 C 83 7 76 5 69 13 Z" fill="#e05a8a" stroke="#a93660" stroke-width="1.2"/>
|
||||||
|
<circle cx="69" cy="14" r="3.5" fill="#c23f6e"/>`,
|
||||||
headphones: () => `
|
headphones: () => `
|
||||||
<path d="M 20 20 Q 24 -6 50 -6 Q 76 -6 80 20" stroke="#333" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
<path class="av-part av-headwear av-headphones" d="M 20 25 C 20 4 32 -7 50 -7 C 68 -7 80 4 80 25" stroke="#333" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
||||||
<ellipse cx="19" cy="26" rx="7" ry="9" fill="#333"/>
|
<rect x="15" y="20" width="10" height="19" rx="5" fill="#333" stroke="#17181c" stroke-width="1.2"/>
|
||||||
<ellipse cx="81" cy="26" rx="7" ry="9" fill="#333"/>
|
<rect x="75" y="20" width="10" height="19" rx="5" fill="#333" stroke="#17181c" stroke-width="1.2"/>
|
||||||
<ellipse cx="19" cy="26" rx="3.6" ry="5.4" fill="#556"/>
|
<rect x="18" y="24" width="4" height="11" rx="2" fill="#667"/>
|
||||||
<ellipse cx="81" cy="26" rx="3.6" ry="5.4" fill="#556"/>`,
|
<rect x="78" y="24" width="4" height="11" rx="2" fill="#667"/>`,
|
||||||
partyhat: () => `
|
partyhat: () => `
|
||||||
<path d="M 50 -6 L 60 21 Q 50 25 40 21 Z" fill="#e05a8a"/>
|
<path class="av-part av-headwear av-partyhat" d="M 50 -8 L 61 19 C 54 22 46 22 39 19 Z" fill="#e05a8a" stroke="#a93660" stroke-width="1.2"/>
|
||||||
<path d="M 40.5 19 Q 50 23 59.5 19" stroke="#c23f6e" stroke-width="1.4" fill="none"/>
|
<path d="M 39.5 18 C 46 21 54 21 60.5 18" stroke="#f4c34e" stroke-width="2" fill="none"/>
|
||||||
<circle cx="45.5" cy="8" r="1.8" fill="#f0b73a"/>
|
<circle cx="45.5" cy="8" r="1.8" fill="#f0b73a"/>
|
||||||
<circle cx="54" cy="12" r="1.8" fill="#3a8ee0"/>
|
<circle cx="54" cy="12" r="1.8" fill="#3a8ee0"/>
|
||||||
<circle cx="47.5" cy="16" r="1.8" fill="#f0b73a"/>
|
<circle cx="47.5" cy="16" r="1.8" fill="#f0b73a"/>
|
||||||
<circle cx="50" cy="-7" r="3.6" fill="#f0b73a"/>`,
|
<circle cx="50" cy="-8" r="3.6" fill="#f0b73a"/>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AVATAR_BODY_ACC_MARKUP = {
|
const AVATAR_BODY_ACC_MARKUP = {
|
||||||
none: () => "",
|
none: { back: () => "", front: () => "" },
|
||||||
scarf: () => `
|
scarf: {
|
||||||
<path d="M 33 58 Q 50 68 67 58 L 65 66 Q 50 74 35 66 Z" fill="#d94f4f"/>
|
back: () => "",
|
||||||
<path d="M 46 65 L 43 80 L 50 76 Z" fill="#c23f3f"/>`,
|
front: () => `
|
||||||
cape: () => `
|
<path class="av-part av-neckwear av-scarf" d="M 35 53 C 43 57 57 57 65 53 L 66 62 C 57 67 43 67 34 62 Z" fill="#d94f4f" stroke="#9d3030" stroke-width="1.2"/>
|
||||||
<path d="M 30 55 Q 10 75 16 100 L 30 90 Q 26 70 40 58 Z" fill="#7a4bc7" opacity=".92"/>
|
<path d="M 57 63 L 62 84 L 53 79 L 50 64 Z" fill="#c23f3f" stroke="#9d3030" stroke-width="1.1" stroke-linejoin="round"/>
|
||||||
<path d="M 70 55 Q 90 75 84 100 L 70 90 Q 74 70 60 58 Z" fill="#7a4bc7" opacity=".92"/>`,
|
<path d="M 55 75 L 62 78" stroke="#f08080" stroke-width="1.2"/>`,
|
||||||
badge: () => `<circle cx="50" cy="78" r="6" fill="#f0b73a" stroke="#fff" stroke-width="1.6"/>`,
|
},
|
||||||
necklace: () => `
|
cape: {
|
||||||
<path d="M 34 58 Q 50 70 66 58" stroke="#f0b73a" stroke-width="2" fill="none" stroke-linecap="round"/>
|
back: () => `
|
||||||
<circle cx="50" cy="70" r="4.4" fill="#3a8ee0" stroke="#f0b73a" stroke-width="1.4"/>`,
|
<path class="av-part av-backwear av-cape" d="M 33 55 C 19 62 13 79 13 108 C 26 103 38 99 50 91 C 62 99 74 103 87 108 C 87 79 81 62 67 55 C 58 61 42 61 33 55 Z" fill="#7a4bc7" stroke="#54308f" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
backpack: () => `
|
<path d="M 26 63 C 22 76 23 91 26 101" stroke="#a67de0" stroke-width="2" fill="none" opacity=".65"/>`,
|
||||||
<path d="M 30 58 Q 26 62 27 70" stroke="#5a4632" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
front: () => `
|
||||||
<path d="M 70 58 Q 74 62 73 70" stroke="#5a4632" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
<path class="av-part av-cape-clasp" d="M 35 56 C 42 61 58 61 65 56" stroke="#f0b73a" stroke-width="2.4" fill="none" stroke-linecap="round"/>
|
||||||
<circle cx="27" cy="59" r="2" fill="#8a6a45"/>
|
<circle cx="35" cy="56" r="2.5" fill="#f0b73a"/>
|
||||||
<circle cx="73" cy="59" r="2" fill="#8a6a45"/>`,
|
<circle cx="65" cy="56" r="2.5" fill="#f0b73a"/>`,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
back: () => "",
|
||||||
|
front: () => `
|
||||||
|
<path class="av-part av-chestwear av-badge" d="M 57 67 L 63 67 L 66 72 L 63 78 L 57 78 L 54 72 Z" fill="#f0b73a" stroke="#fff" stroke-width="1.5"/>
|
||||||
|
<path d="M 60 69 L 61 72 L 64 72 L 61.5 74 L 62.5 77 L 60 75 L 57.5 77 L 58.5 74 L 56 72 L 59 72 Z" fill="#b8750b"/>`,
|
||||||
|
},
|
||||||
|
necklace: {
|
||||||
|
back: () => "",
|
||||||
|
front: () => `
|
||||||
|
<path class="av-part av-neckwear av-necklace" d="M 35 57 C 38 69 44 76 50 77 C 56 76 62 69 65 57" stroke="#f0b73a" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M 50 74 L 55 80 L 50 85 L 45 80 Z" fill="#3a8ee0" stroke="#f0b73a" stroke-width="1.4"/>`,
|
||||||
|
},
|
||||||
|
backpack: {
|
||||||
|
back: () => `
|
||||||
|
<path class="av-part av-backwear av-backpack" d="M 28 63 C 29 54 39 50 50 50 C 61 50 71 54 72 63 L 77 89 C 70 96 30 96 23 89 Z" fill="#8a6a45" stroke="#5a4029" stroke-width="1.6"/>
|
||||||
|
<path d="M 39 53 C 40 46 60 46 61 53" stroke="#5a4029" stroke-width="3" fill="none"/>`,
|
||||||
|
front: () => `
|
||||||
|
<path class="av-part av-backpack-strap" d="M 37 57 C 29 63 29 76 33 84 M 63 57 C 71 63 71 76 67 84" stroke="#5a4632" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M 33 83 L 38 86 M 67 83 L 62 86" stroke="#d0a36d" stroke-width="1.5"/>`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/* een pet/kroon dekt het hele bovenhoofd af - loshangend haar eronder tonen
|
/* Alleen een pet bedekt het bovenhoofd volledig. Een kroon rust juist op het
|
||||||
gaf rommelige, half-verstopte plukjes rond de rand (zie roadmap-memory).
|
haar; een paardenstaart blijft naast de pet zichtbaar. */
|
||||||
Een paardenstaart mag wel blijven: die hangt naar opzij, niet over de kop. */
|
|
||||||
function avatarHairSuppressed(c){
|
function avatarHairSuppressed(c){
|
||||||
return (c.accessoryHead === "cap" || c.accessoryHead === "crown") && c.hairStyle !== "ponytail";
|
return c.accessoryHead === "cap" && c.hairStyle !== "ponytail";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* bouwt de SVG-weergave van een personage; config==null → neutrale
|
/* bouwt de SVG-weergave van een personage; config==null → neutrale
|
||||||
|
|
@ -203,16 +235,18 @@ function avatarHairSuppressed(c){
|
||||||
function renderAvatarFace(config, size){
|
function renderAvatarFace(config, size){
|
||||||
const c = avatarSanitize(config);
|
const c = avatarSanitize(config);
|
||||||
const svg = document.createElementNS(AVATAR_SVG_NS, "svg");
|
const svg = document.createElementNS(AVATAR_SVG_NS, "svg");
|
||||||
svg.setAttribute("viewBox", "0 0 100 100");
|
/* Extra ruimte voorkomt afsnijden van hoofddeksels, haar en voetzolen. */
|
||||||
|
svg.setAttribute("viewBox", "-6 -12 112 124");
|
||||||
|
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species);
|
svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species);
|
||||||
svg.setAttribute("role", "img");
|
svg.setAttribute("role", "img");
|
||||||
svg.setAttribute("aria-label", T("avatarAlt"));
|
svg.setAttribute("aria-label", T("avatarAlt"));
|
||||||
let markup = "";
|
const bodyAccessory = AVATAR_BODY_ACC_MARKUP[c.accessoryBody];
|
||||||
if(c.accessoryBody === "cape") markup += AVATAR_BODY_ACC_MARKUP.cape();
|
let markup = bodyAccessory.back();
|
||||||
markup += avatarBodyMarkup(c.species, c.bodyColor);
|
markup += avatarBodyMarkup(c.species, c.bodyColor);
|
||||||
if(!avatarHairSuppressed(c)) markup += AVATAR_HAIR_MARKUP[c.hairStyle](c.hairColor);
|
if(!avatarHairSuppressed(c)) markup += AVATAR_HAIR_MARKUP[c.hairStyle](c.hairColor);
|
||||||
markup += AVATAR_HEAD_ACC_MARKUP[c.accessoryHead]();
|
markup += AVATAR_HEAD_ACC_MARKUP[c.accessoryHead]();
|
||||||
if(c.accessoryBody !== "cape") markup += AVATAR_BODY_ACC_MARKUP[c.accessoryBody]();
|
markup += bodyAccessory.front();
|
||||||
svg.innerHTML = markup;
|
svg.innerHTML = markup;
|
||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.45-beta";
|
const VERSION = "0.4.46-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
|
||||||
114
test/avatar.test.js
Normal file
114
test/avatar.test.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
import {runInNewContext} from 'node:vm';
|
||||||
|
|
||||||
|
async function loadAvatarRenderer(){
|
||||||
|
const source=await readFile('public/js/avatar.js','utf8');
|
||||||
|
const cutoff=source.indexOf('/* bouwt de interactieve kiezer');
|
||||||
|
assert.ok(cutoff>0,'rendergedeelte van avatar.js ontbreekt');
|
||||||
|
const created=[];
|
||||||
|
const context={
|
||||||
|
T:key=>key,
|
||||||
|
document:{
|
||||||
|
createElementNS(){
|
||||||
|
const element={
|
||||||
|
attributes:{},
|
||||||
|
innerHTML:'',
|
||||||
|
setAttribute(name,value){this.attributes[name]=String(value);},
|
||||||
|
};
|
||||||
|
created.push(element);
|
||||||
|
return element;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
runInNewContext(source.slice(0,cutoff)+
|
||||||
|
';globalThis.__avatar={renderAvatarFace,avatarBodyMarkup,bodyAccessories:AVATAR_BODY_ACC_MARKUP,headAccessories:AVATAR_HEAD_ACC_MARKUP};',
|
||||||
|
context);
|
||||||
|
return {api:context.__avatar,created};
|
||||||
|
}
|
||||||
|
|
||||||
|
const base={
|
||||||
|
species:'dino',
|
||||||
|
bodyColor:'#3fae6a',
|
||||||
|
hairStyle:'none',
|
||||||
|
hairColor:'#5b3a29',
|
||||||
|
accessoryHead:'none',
|
||||||
|
accessoryBody:'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('avatars hebben verbonden en herkenbare lichaamsdelen per soort',async()=>{
|
||||||
|
const {api}=await loadAvatarRenderer();
|
||||||
|
for(const species of ['dino','monster','animal']){
|
||||||
|
const markup=api.avatarBodyMarkup(species,base.bodyColor);
|
||||||
|
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 part of ['av-neck','av-torso','av-head','av-eye']){
|
||||||
|
assert.ok(markup.includes(part),species+' mist '+part);
|
||||||
|
}
|
||||||
|
assert.ok(markup.indexOf('av-arm')<markup.indexOf('av-torso'),species+'-armen moeten achter de romp aansluiten');
|
||||||
|
assert.ok(markup.indexOf('av-leg')<markup.indexOf('av-torso'),species+'-benen moeten onder de romp aansluiten');
|
||||||
|
}
|
||||||
|
const dino=api.avatarBodyMarkup('dino',base.bodyColor);
|
||||||
|
const monster=api.avatarBodyMarkup('monster',base.bodyColor);
|
||||||
|
const animal=api.avatarBodyMarkup('animal',base.bodyColor);
|
||||||
|
assert.ok(dino.indexOf('av-tail')<dino.indexOf('av-head'));
|
||||||
|
assert.ok(dino.indexOf('av-crest')<dino.indexOf('av-head'));
|
||||||
|
assert.equal((monster.match(/av-horn/g)||[]).length,2);
|
||||||
|
assert.equal((animal.match(/av-ear av-ear-/g)||[]).length,2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lichaamsaccessoires gebruiken anatomisch correcte voor- en achterlagen',async()=>{
|
||||||
|
const {api}=await loadAvatarRenderer();
|
||||||
|
for(const id of ['none','scarf','cape','badge','necklace','backpack']){
|
||||||
|
assert.equal(typeof api.bodyAccessories[id].back,'function',id+' mist achterlaag');
|
||||||
|
assert.equal(typeof api.bodyAccessories[id].front,'function',id+' mist voorlaag');
|
||||||
|
}
|
||||||
|
|
||||||
|
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-clasp')>cape.indexOf('av-head\"'));
|
||||||
|
|
||||||
|
const backpack=api.renderAvatarFace({...base,accessoryBody:'backpack'},'lg').innerHTML;
|
||||||
|
assert.ok(backpack.indexOf('av-backpack\"')<backpack.indexOf('av-torso\"'));
|
||||||
|
assert.ok(backpack.indexOf('av-backpack-strap')>backpack.indexOf('av-head\"'));
|
||||||
|
|
||||||
|
for(const [id,part] of [['scarf','av-scarf'],['badge','av-badge'],['necklace','av-necklace']]){
|
||||||
|
const markup=api.renderAvatarFace({...base,accessoryBody:id},'lg').innerHTML;
|
||||||
|
assert.ok(markup.indexOf(part)>markup.indexOf('av-head\"'),id+' hoort vóór op het lichaam');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alle avatarcombinaties renderen volledig binnen de verruimde SVG',async()=>{
|
||||||
|
const {api}=await loadAvatarRenderer();
|
||||||
|
const species=['dino','monster','animal'];
|
||||||
|
const hair=['none','spiky','curly','ponytail','mohawk'];
|
||||||
|
const head=['none','cap','crown','glasses','sunglasses','bow','headphones','partyhat'];
|
||||||
|
const body=['none','scarf','cape','badge','necklace','backpack'];
|
||||||
|
|
||||||
|
for(const speciesId of species){
|
||||||
|
for(const hairId of hair){
|
||||||
|
for(const headId of head){
|
||||||
|
for(const bodyId of body){
|
||||||
|
const svg=api.renderAvatarFace({
|
||||||
|
...base,
|
||||||
|
species:speciesId,
|
||||||
|
hairStyle:hairId,
|
||||||
|
accessoryHead:headId,
|
||||||
|
accessoryBody:bodyId,
|
||||||
|
},'sm');
|
||||||
|
assert.equal(svg.attributes.viewBox,'-6 -12 112 124');
|
||||||
|
assert.equal(svg.attributes.preserveAspectRatio,'xMidYMid meet');
|
||||||
|
assert.doesNotMatch(svg.innerHTML,/undefined|NaN/);
|
||||||
|
assert.ok(svg.innerHTML.includes('av-head'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cap=api.renderAvatarFace({...base,hairStyle:'spiky',hairColor:'#123456',accessoryHead:'cap'},'lg').innerHTML;
|
||||||
|
const crown=api.renderAvatarFace({...base,hairStyle:'spiky',hairColor:'#123456',accessoryHead:'crown'},'lg').innerHTML;
|
||||||
|
assert.ok(!cap.includes('#123456'),'pet hoort het bovenhaar af te dekken');
|
||||||
|
assert.ok(crown.includes('#123456'),'kroon hoort op het haar te rusten');
|
||||||
|
for(const id of head)assert.equal(typeof api.headAccessories[id],'function');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue