diff --git a/VERSION b/VERSION
index 3e9b3af..196f403 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.4.25-beta
\ No newline at end of file
+0.4.26-beta
\ No newline at end of file
diff --git a/public/css/teach.css b/public/css/teach.css
index f4ec74e..f381a60 100644
--- a/public/css/teach.css
+++ b/public/css/teach.css
@@ -1674,69 +1674,10 @@ body.hc #penbar .pen-current,body.hc #penbar .pen-popover{border:2px solid #000;
.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,
zelfde aanpak als de taart hierboven (--cake/--icing), geen SVG/afbeeldingen. ==== */
-.avatar-face{position:relative; display:inline-block; flex:none;}
+.avatar-face{display:inline-block; flex:none; overflow:visible;}
.avatar-face.sm{width:24px; height:24px;}
.avatar-face.md{width:44px; height:44px;}
.avatar-face.lg{width:180px; height:180px;}
-.av-body{position:absolute; left:20%; right:20%; bottom:2%; top:52%; border-radius:45% 45% 40% 40%; background:var(--av-body); z-index:1;}
-.av-head{position:absolute; left:8%; right:8%; top:4%; height:56%; border-radius:50%; background:var(--av-body); z-index:3; box-shadow:inset 0 -6px 10px rgba(0,0,0,.12);}
-.av-part-head{position:absolute; z-index:6; display:none;}
-.av-species-monster .av-part-head{display:block; left:28%; right:28%; top:0; height:14%;}
-.av-species-monster .av-part-head::before,.av-species-monster .av-part-head::after{content:""; position:absolute; width:16%; height:100%; background:var(--av-body); filter:brightness(.85); clip-path:polygon(50% 0,0% 100%,100% 100%);}
-.av-species-monster .av-part-head::before{left:6%;}
-.av-species-monster .av-part-head::after{right:6%;}
-.av-species-animal .av-part-head{display:block; left:0; right:0; top:-2%; height:22%;}
-.av-species-animal .av-part-head::before,.av-species-animal .av-part-head::after{content:""; position:absolute; width:22%; height:100%; border-radius:50%; background:var(--av-body);}
-.av-species-animal .av-part-head::before{left:2%;}
-.av-species-animal .av-part-head::after{right:2%;}
-.av-part-body{position:absolute; z-index:6; display:none;}
-.av-species-dino .av-part-body{display:block; left:22%; right:22%; top:36%; height:26%;}
-.av-species-dino .av-part-body::before,.av-species-dino .av-part-body::after{content:""; position:absolute; width:22%; height:82%; bottom:0; background:var(--av-body); filter:brightness(.68); clip-path:polygon(50% 0%,100% 65%,100% 100%,0% 100%,0% 65%); border-radius:3px 3px 0 0;}
-.av-species-dino .av-part-body::before{left:4%;}
-.av-species-dino .av-part-body::after{right:4%;}
-.av-spike-mid{display:none; position:absolute;}
-.av-species-dino .av-spike-mid{display:block; left:39%; width:22%; height:100%; bottom:0; background:var(--av-body); filter:brightness(.68); clip-path:polygon(50% 0%,100% 65%,100% 100%,0% 100%,0% 65%); border-radius:3px 3px 0 0;}
-.av-species-animal .av-part-body{display:block; right:2%; bottom:8%; width:16%; height:30%; border-radius:50% 50% 50% 10%; background:var(--av-body); transform:rotate(20deg); filter:brightness(.9);}
-.av-tail{position:absolute; z-index:0; display:none;}
-.av-species-dino .av-tail{display:block; left:-14%; bottom:10%; width:30%; height:16%; background:var(--av-body); filter:brightness(.93); clip-path:polygon(100% 15%,100% 85%,35% 100%,0% 55%,30% 0%); transform:rotate(-8deg);}
-.av-leg{position:absolute; z-index:0; display:none; border-radius:45% 45% 55% 55%;}
-.av-species-dino .av-leg{display:block; top:82%; height:16%; width:17%; background:var(--av-body); filter:brightness(.78);}
-.av-leg-l{left:16%;}
-.av-leg-r{right:16%;}
-.av-arm{position:absolute; z-index:2; display:none; border-radius:50%;}
-.av-species-dino .av-arm{display:block; top:66%; height:15%; width:13%; background:var(--av-body); filter:brightness(.88);}
-.av-arm-l{left:9%; transform:rotate(20deg);}
-.av-arm-r{right:9%; transform:rotate(-20deg);}
-.av-belly{position:absolute; z-index:2; display:none; border-radius:50%;}
-.av-species-dino .av-belly{display:block; left:35%; right:35%; top:66%; height:28%; background:var(--av-body); filter:brightness(1.4);}
-.av-eye{position:absolute; z-index:4; top:30%; width:16%; height:16%; border-radius:50%; background:#fff; box-shadow:inset 0 0 0 1.5px rgba(20,20,30,.15);}
-.av-eye::after{content:""; position:absolute; width:46%; height:46%; left:27%; top:30%; border-radius:50%; background:#232733;}
-.av-eye-l{left:25%;}
-.av-eye-r{right:25%;}
-.av-snout{position:absolute; z-index:7; display:none;}
-.av-species-dino .av-snout{display:block; left:30%; right:30%; top:46%; height:20%; border-radius:45%; background:var(--av-body); filter:brightness(1.22);}
-.av-species-dino .av-snout::before,.av-species-dino .av-snout::after{content:""; position:absolute; width:14%; height:22%; top:28%; border-radius:50%; background:#2b2b2b; opacity:.6;}
-.av-species-dino .av-snout::before{left:26%;}
-.av-species-dino .av-snout::after{right:26%;}
-.av-hair{position:absolute; z-index:4; display:none; background:var(--av-hair);}
-.av-hair-spiky{display:block; left:14%; right:14%; top:-2%; height:22%; clip-path:polygon(0% 100%,10% 20%,25% 90%,35% 10%,50% 90%,65% 10%,75% 90%,90% 20%,100% 100%);}
-.av-hair-curly{display:block; left:6%; right:6%; top:-4%; height:26%; border-radius:50% 50% 40% 40%;}
-.av-hair-ponytail{display:block; left:10%; right:10%; top:-2%; height:20%; border-radius:50% 50% 30% 30%;}
-.av-hair-ponytail::after{content:""; position:absolute; right:-6%; top:40%; width:20%; height:45%; border-radius:0 50% 50% 50%; background:var(--av-hair);}
-.av-hair-mohawk{display:block; left:42%; right:42%; top:-8%; height:30%; border-radius:40% 40% 0 0;}
-.av-hair-none{display:none;}
-.av-acc-head{position:absolute; z-index:5; display:none;}
-.av-acc-head-cap{display:block; left:6%; right:6%; top:2%; height:20%; border-radius:50% 50% 0 0; background:var(--blue);}
-.av-acc-head-crown{display:block; left:14%; right:14%; top:-4%; height:18%; background:var(--yellow); clip-path:polygon(0% 100%,0% 40%,20% 70%,35% 10%,50% 60%,65% 10%,80% 70%,100% 40%,100% 100%);}
-.av-acc-head-glasses{display:block; left:14%; right:14%; top:32%; height:16%;}
-.av-acc-head-glasses::before,.av-acc-head-glasses::after{content:""; position:absolute; width:38%; height:100%; border-radius:50%; border:3px solid var(--ink); background:rgba(255,255,255,.35);}
-.av-acc-head-glasses::before{left:0;}
-.av-acc-head-glasses::after{right:0;}
-.av-acc-head-bow{display:block; right:8%; top:8%; width:18%; height:14%; background:var(--pink); clip-path:polygon(0% 0%,50% 40%,100% 0%,100% 100%,50% 60%,0% 100%);}
-.av-acc-body{position:absolute; z-index:4; display:none;}
-.av-acc-body-scarf{display:block; left:18%; right:18%; top:48%; height:10%; border-radius:8px; background:var(--red);}
-.av-acc-body-cape{display:block; left:10%; right:10%; top:54%; bottom:0; z-index:0; border-radius:0 0 40% 40%; background:var(--purple); opacity:.9;}
-.av-acc-body-badge{display:block; left:32%; width:16%; top:62%; height:12%; border-radius:50%; background:var(--yellow); border:2px solid #fff;}
.avatar-editor{display:flex; flex-direction:column; gap:12px; padding:14px; height:100%; min-height:0; overflow:auto;}
.avatar-preview{display:flex; justify-content:center; padding:10px; background:var(--surface-2); border-radius:16px; flex:none;}
.avatar-controls{display:flex; flex-direction:column; gap:10px;}
diff --git a/public/js/avatar.js b/public/js/avatar.js
index 9ae2185..4483955 100644
--- a/public/js/avatar.js
+++ b/public/js/avatar.js
@@ -1,7 +1,9 @@
/* teach - personage (avatar): eigen figuur samenstellen (soort, kleuren, haar,
accessoires), gedeeld tussen de Instellingen-sectie "Mijn personage" en het
- avatar-widget. Gelaagde div's + CSS custom properties, zelfde aanpak als de
- verjaardagstaart-widget (--cake/--icing) - geen SVG/afbeeldingen. */
+ avatar-widget. Getekend als één SVG (viewBox 0 0 100 100, tekenvolgorde =
+ laagvolgorde - geen z-index-gepuzzel zoals bij de eerdere CSS-laagjesversie)
+ zodat elk onderdeel z'n vaste plek op het silhouet overlapt in plaats van
+ los ernaast te zweven. */
"use strict";
const AVATAR_SPECIES = ["dino", "monster", "animal"];
const AVATAR_HAIR = ["none", "spiky", "curly", "ponytail", "mohawk"];
@@ -11,6 +13,7 @@ const AVATAR_DEFAULT = Object.freeze({
species: "dino", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29",
accessoryHead: "none", accessoryBody: "none",
});
+const AVATAR_SVG_NS = "http://www.w3.org/2000/svg";
function avatarSanitize(config){
const c = config || {};
@@ -24,36 +27,166 @@ function avatarSanitize(config){
};
}
-/* bouwt de gelaagde weergave van een personage; config==null → neutrale
- standaardweergave (nooit een lege/kapotte weergave). */
+/* mengt een hex-kleur amt (-1..1) richting zwart (negatief) of wit (positief) -
+ gebruikt voor buik/snuit-highlights en pootjes/staart-schaduw, zodat die
+ altijd bij de gekozen lichaamskleur passen zonder een aparte kleurkiezer. */
+function avatarShade(hex, amt){
+ const n = parseInt(hex.slice(1), 16);
+ let r = (n>>16)&255, g = (n>>8)&255, b = n&255;
+ if(amt < 0){ r *= 1+amt; g *= 1+amt; b *= 1+amt; }
+ else{ r += (255-r)*amt; g += (255-g)*amt; b += (255-b)*amt; }
+ const clamp = v => Math.max(0, Math.min(255, Math.round(v)));
+ return "#" + [r,g,b].map(v=>clamp(v).toString(16).padStart(2,"0")).join("");
+}
+
+/* per-soort silhouet: elk onderdeel overlapt het vorige ruim (poten/armen/
+ staart tot ver ín het lijf, rugstekels/hoorns pas ná de kop getekend) zodat
+ niets los oogt. Volgorde = tekenvolgorde = zichtbare laagvolgorde. */
+function avatarBodyMarkup(species, bodyColor){
+ const dark = avatarShade(bodyColor, -0.35);
+ const light = avatarShade(bodyColor, 0.4);
+ let m = "";
+ if(species === "dino"){
+ m += ``;
+ }
+ if(species === "animal"){
+ m += ``;
+ }
+ m += ``;
+ m += ``;
+ m += ``;
+ m += ``;
+ m += ``;
+ m += ``;
+ if(species === "animal"){
+ m += ``;
+ m += ``;
+ m += ``;
+ m += ``;
+ }
+ if(species === "monster"){
+ const darker = avatarShade(bodyColor, -0.5);
+ m += ``;
+ m += ``;
+ }
+ m += ``;
+ if(species === "dino"){
+ m += ``;
+ m += ``;
+ m += ``;
+ }
+ if(species === "animal"){
+ m += ``;
+ m += ``;
+ }
+ if(species === "monster"){
+ m += ``;
+ m += ``;
+ m += ``;
+ m += ``;
+ }else{
+ m += ``;
+ m += ``;
+ m += ``;
+ m += ``;
+ }
+ if(species === "dino"){
+ m += ``;
+ const plate = avatarShade(bodyColor, -0.45);
+ m += ``;
+ m += ``;
+ m += ``;
+ }
+ if(species === "animal"){
+ m += ``;
+ }
+ if(species === "monster"){
+ m += ``;
+ m += ``;
+ m += ``;
+ }
+ return m;
+}
+
+const AVATAR_HAIR_MARKUP = {
+ none: () => "",
+ spiky: (c) => `
+
+
+
+
+ `,
+ curly: (c) => `
+
+
+
+
+ `,
+ ponytail: (c) => `
+
+
+ `,
+ mohawk: (c) => ``,
+};
+
+const AVATAR_HEAD_ACC_MARKUP = {
+ none: () => "",
+ cap: () => `
+
+ `,
+ crown: () => `
+
+
+
+ `,
+ glasses: () => `
+
+
+
+
+ `,
+ bow: () => `
+
+ `,
+};
+
+const AVATAR_BODY_ACC_MARKUP = {
+ none: () => "",
+ scarf: () => `
+
+ `,
+ cape: () => `
+
+ `,
+ badge: () => ``,
+};
+
+/* een pet/kroon dekt het hele bovenhoofd af - loshangend haar eronder tonen
+ gaf rommelige, half-verstopte plukjes rond de rand (zie roadmap-memory).
+ Een paardenstaart mag wel blijven: die hangt naar opzij, niet over de kop. */
+function avatarHairSuppressed(c){
+ return (c.accessoryHead === "cap" || c.accessoryHead === "crown") && c.hairStyle !== "ponytail";
+}
+
+/* bouwt de SVG-weergave van een personage; config==null → neutrale
+ standaardweergave (nooit een lege/kapotte weergave). Tekenvolgorde bepaalt
+ de laagvolgorde (SVG kent geen z-index) - vandaar cape/staart/poten eerst,
+ accessoires als laatste. */
function renderAvatarFace(config, size){
const c = avatarSanitize(config);
- const face = document.createElement("div");
- face.className = "avatar-face " + (size || "md") + " av-species-" + c.species;
- face.style.setProperty("--av-body", c.bodyColor);
- face.style.setProperty("--av-hair", c.hairColor);
- face.setAttribute("role", "img");
- face.setAttribute("aria-label", T("avatarAlt"));
- const partHead = document.createElement("div"); partHead.className = "av-part-head";
- const hair = document.createElement("div"); hair.className = "av-hair av-hair-" + c.hairStyle;
- const head = document.createElement("div"); head.className = "av-head";
- const eyeL = document.createElement("div"); eyeL.className = "av-eye av-eye-l";
- const eyeR = document.createElement("div"); eyeR.className = "av-eye av-eye-r";
- const snout = document.createElement("div"); snout.className = "av-snout";
- const accHead = document.createElement("div"); accHead.className = "av-acc-head av-acc-head-" + c.accessoryHead;
- const tail = document.createElement("div"); tail.className = "av-tail";
- const legL = document.createElement("div"); legL.className = "av-leg av-leg-l";
- const legR = document.createElement("div"); legR.className = "av-leg av-leg-r";
- const partBody = document.createElement("div"); partBody.className = "av-part-body";
- const spikeMid = document.createElement("div"); spikeMid.className = "av-spike-mid";
- partBody.appendChild(spikeMid);
- const body = document.createElement("div"); body.className = "av-body";
- const belly = document.createElement("div"); belly.className = "av-belly";
- const armL = document.createElement("div"); armL.className = "av-arm av-arm-l";
- const armR = document.createElement("div"); armR.className = "av-arm av-arm-r";
- const accBody = document.createElement("div"); accBody.className = "av-acc-body av-acc-body-" + c.accessoryBody;
- face.append(tail, legL, legR, partBody, body, belly, armL, armR, accBody, partHead, head, eyeL, eyeR, snout, accHead, hair);
- return face;
+ const svg = document.createElementNS(AVATAR_SVG_NS, "svg");
+ svg.setAttribute("viewBox", "0 0 100 100");
+ svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species);
+ svg.setAttribute("role", "img");
+ svg.setAttribute("aria-label", T("avatarAlt"));
+ let markup = "";
+ if(c.accessoryBody === "cape") markup += AVATAR_BODY_ACC_MARKUP.cape();
+ markup += avatarBodyMarkup(c.species, c.bodyColor);
+ if(!avatarHairSuppressed(c)) markup += AVATAR_HAIR_MARKUP[c.hairStyle](c.hairColor);
+ markup += AVATAR_HEAD_ACC_MARKUP[c.accessoryHead]();
+ if(c.accessoryBody !== "cape") markup += AVATAR_BODY_ACC_MARKUP[c.accessoryBody]();
+ svg.innerHTML = markup;
+ return svg;
}
/* bouwt de interactieve kiezer + live voorbeeld; roept onChange(config) aan
diff --git a/public/js/core.js b/public/js/core.js
index 1ec1b4f..533ba80 100644
--- a/public/js/core.js
+++ b/public/js/core.js
@@ -2,7 +2,7 @@
"use strict";
/* 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. */
-const VERSION = "0.4.25-beta";
+const VERSION = "0.4.26-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;