From 68320434708c5e6c71144ff3c514768a4373a5eb Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 22 Jul 2026 23:13:20 +0200 Subject: [PATCH] feat: maak avatarhaar natuurlijker (v0.4.48-beta) --- VERSION | 2 +- public/js/avatar.js | 86 ++++++++++++++++++++++++++++++++++----------- public/js/core.js | 2 +- test/avatar.test.js | 73 ++++++++++++++++++++++++++------------ 4 files changed, 118 insertions(+), 45 deletions(-) diff --git a/VERSION b/VERSION index 341341e..241f716 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.47-beta +0.4.48-beta diff --git a/public/js/avatar.js b/public/js/avatar.js index 2e61b8c..ef0824c 100644 --- a/public/js/avatar.js +++ b/public/js/avatar.js @@ -147,45 +147,91 @@ const AVATAR_HAIR_MARKUP = { none: { back: () => "", front: () => "" }, tuft: { back: () => "", - front: (c) => ``, + front: (c) => ` + + + `, }, spiky: { back: () => "", - front: (c) => ``, + front: (c) => ` + + + + `, }, curly: { - back: () => "", - front: (c) => ` - - + back: (c) => ``, + front: (c) => ` + + + + + `, }, wavy: { - back: (c) => ``, - front: (c) => ``, + back: (c) => ``, + front: (c) => ` + + + + + + `, }, bob: { - back: (c) => ``, - front: (c) => ``, + back: (c) => ``, + front: (c) => ` + + + + + + `, }, ponytail: { - back: (c) => ``, - front: (c) => ``, + back: (c) => ` + + + + `, + front: (c) => ` + + + + `, }, braids: { - back: (c) => ` - - - + back: (c) => ` + + + + + `, + front: (c) => ` + + + `, - front: (c) => ``, }, mohawk: { back: () => "", - front: (c) => ``, + front: (c) => ` + + + `, }, }; +/* Iedere kop gebruikt dezelfde haarankers, met een minimale correctie voor + het asymmetrische dinosaurusprofiel. De wrapper maakt de pasvorm expliciet + en houdt alle lokken als een laag bij elkaar. */ +function avatarHairLayer(markup, species, layer){ + if(!markup) return ""; + const transform = species === "dino" ? "translate(1 0)" : species === "monster" ? "translate(0 -1)" : "translate(0 0)"; + return `${markup}`; +} const AVATAR_HEAD_ACC_MARKUP = { none: () => "", cap: () => ` @@ -328,9 +374,9 @@ function renderAvatarFace(config, size){ const bodyAccessory = AVATAR_BODY_ACC_MARKUP[c.accessoryBody]; const hair = AVATAR_HAIR_MARKUP[c.hairStyle]; let markup = bodyAccessory.back(); - markup += hair.back(c.hairColor); + markup += avatarHairLayer(hair.back(c.hairColor), c.species, "back"); markup += avatarBodyMarkup(c.species, c.bodyColor); - if(!avatarHairSuppressed(c)) markup += hair.front(c.hairColor); + if(!avatarHairSuppressed(c)) markup += avatarHairLayer(hair.front(c.hairColor), c.species, "front"); markup += AVATAR_HEAD_ACC_MARKUP[c.accessoryHead](); markup += bodyAccessory.front(); svg.innerHTML = markup; diff --git a/public/js/core.js b/public/js/core.js index 0ded29b..b7b7ca9 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.47-beta"; +const VERSION = "0.4.48-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/test/avatar.test.js b/test/avatar.test.js index 8f3dd6f..4400b4c 100644 --- a/test/avatar.test.js +++ b/test/avatar.test.js @@ -58,34 +58,61 @@ test('avatars combineren een grote kop met een compact soortspecifiek lijf',asyn assert.notEqual(bodies.monster,bodies.animal); }); -test('haar heeft natuurlijke voor- en achterlagen en vier nieuwe stijlen',async()=>{ +test("haar volgt de kop met natuurlijke details en correcte lagen",async()=>{ 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"]); for(const id of api.hairIds){ - assert.equal(typeof api.hair[id].back,'function',id+' mist haar-achterlaag'); - assert.equal(typeof api.hair[id].front,'function',id+' mist haar-voorlaag'); - } - for(const id of ['tuft','wavy','bob','braids']){ - const markup=api.renderAvatarFace({...base,hairStyle:id,hairColor:'#123456'},'lg').innerHTML; - assert.ok(markup.includes('av-hair-'+id),id+' wordt niet getekend'); - assert.ok(markup.includes('#123456'),id+' gebruikt de gekozen haarkleur niet'); + assert.equal(typeof api.hair[id].back,"function",id+" mist haar-achterlaag"); + assert.equal(typeof api.hair[id].front,"function",id+" mist haar-voorlaag"); } - const bob=api.renderAvatarFace({...base,hairStyle:'bob'},'lg').innerHTML; - assert.ok(bob.indexOf('av-hair-bob')bob.indexOf('av-head-large')); - const braids=api.renderAvatarFace({...base,hairStyle:'braids'},'lg').innerHTML; - assert.ok(braids.indexOf('av-hair-braids')braids.indexOf('av-head-large')); + for(const id of api.hairIds.filter(id=>id!=="none")){ + const front=api.hair[id].front("#123456"); + const back=api.hair[id].back("#123456"); + const combined=back+front; + assert.ok(front.includes("av-hair-"+id),id+" mist een herkenbare voorlaag"); + assert.ok(combined.includes("av-hair-texture"),id+" mist natuurlijke haartextuur"); + assert.doesNotMatch(combined,/undefined|NaN/,id+" bevat ongeldige SVG-waarden"); + } + for(const id of ["curly","wavy","bob","ponytail","braids"]){ + assert.notEqual(api.hair[id].back("#123456"),"",id+" mist volume achter de kop"); + } + for(const id of ["tuft","spiky","mohawk"]){ + assert.equal(api.hair[id].back("#123456"),"",id+" hoort geen onzichtbare achterlaag te hebben"); + } + for(const id of ["wavy","bob","ponytail","braids"]){ + assert.ok(api.hair[id].front("#123456").includes("av-hair-part"),id+" mist een natuurlijke scheiding"); + } + assert.ok(api.hair.wavy.front("#123456").includes("av-side-lock")); + assert.ok(api.hair.bob.front("#123456").includes("av-side-lock")); + assert.ok(api.hair.ponytail.back("#123456").includes("av-hair-tie")); + assert.ok(api.hair.braids.back("#123456").includes("av-hair-tie")); - const cap=api.renderAvatarFace({...base,hairStyle:'spiky',hairColor:'#123456',accessoryHead:'cap'},'lg').innerHTML; - const beanie=api.renderAvatarFace({...base,hairStyle:'spiky',hairColor:'#123456',accessoryHead:'beanie'},'lg').innerHTML; - const crown=api.renderAvatarFace({...base,hairStyle:'spiky',hairColor:'#123456',accessoryHead:'crown'},'lg').innerHTML; - const cappedBraids=api.renderAvatarFace({...base,hairStyle:'braids',hairColor:'#123456',accessoryHead:'cap'},'lg').innerHTML; - assert.ok(!cap.includes('#123456'),'pet hoort de voorste haarlaag af te dekken'); - assert.ok(!beanie.includes('#123456'),'muts hoort de voorste haarlaag af te dekken'); - assert.ok(crown.includes('#123456'),'kroon hoort op het haar te rusten'); - assert.ok(cappedBraids.includes('#123456'),'vlechten horen naast een pet zichtbaar te blijven'); + const transforms={dino:"translate(1 0)",monster:"translate(0 -1)",animal:"translate(0 0)"}; + for(const [species,transform] of Object.entries(transforms)){ + const markup=api.renderAvatarFace({...base,species,hairStyle:"wavy",hairColor:"#123456"},"lg").innerHTML; + assert.equal((markup.match(new RegExp("av-hair-fit-"+species,"g"))||[]).length,2,species+" mist voor- of achterpasvorm"); + assert.ok(markup.includes(`transform="${transform}"`),species+" gebruikt een verkeerde kruincorrectie"); + assert.ok(markup.indexOf("av-hair-back-layer")markup.indexOf("av-head-large")); + } + + const bob=api.renderAvatarFace({...base,hairStyle:"bob"},"lg").innerHTML; + assert.ok(bob.indexOf("av-hair-bob")bob.indexOf("av-head-large")); + const braids=api.renderAvatarFace({...base,hairStyle:"braids"},"lg").innerHTML; + assert.ok(braids.indexOf("av-hair-braids")braids.indexOf("av-head-large")); + + const cap=api.renderAvatarFace({...base,hairStyle:"spiky",hairColor:"#123456",accessoryHead:"cap"},"lg").innerHTML; + const beanie=api.renderAvatarFace({...base,hairStyle:"spiky",hairColor:"#123456",accessoryHead:"beanie"},"lg").innerHTML; + const crown=api.renderAvatarFace({...base,hairStyle:"spiky",hairColor:"#123456",accessoryHead:"crown"},"lg").innerHTML; + const cappedBraids=api.renderAvatarFace({...base,hairStyle:"braids",hairColor:"#123456",accessoryHead:"cap"},"lg").innerHTML; + assert.ok(!cap.includes("av-hair-front-layer"),"pet hoort kort haar af te dekken"); + assert.ok(!beanie.includes("av-hair-front-layer"),"muts hoort kort haar af te dekken"); + assert.ok(crown.includes("av-hair-front-layer"),"kroon hoort op het haar te rusten"); + assert.ok(cappedBraids.includes("av-hair-back-layer"),"vlechten horen naast een pet zichtbaar te blijven"); + assert.ok(!cappedBraids.includes("av-hair-front-layer"),"pet hoort de bovenkant van vlechten af te dekken"); }); test('accessoires gebruiken vaste ankers en correcte voor- en achterlagen',async()=>{