/* teach - personage (avatar): eigen figuur samenstellen (soort, kleuren, haar,
accessoires), gedeeld tussen de Instellingen-sectie "Mijn personage" en het
avatar-widget. Getekend als één SVG rond vaste anatomische ankers; de tekenvolgorde =
laagvolgorde - geen z-index-gepuzzel zoals bij de eerdere CSS-laagjesversie)
zodat elk onderdeel z'n vaste plek op het silhouet overlapt in plaats van
los ernaast te zweven. */
"use strict";
const AVATAR_SPECIES = ["dino", "monster", "animal"];
const AVATAR_VARIANTS = Object.freeze({
dino: ["rex", "triceratops", "stegosaurus", "raptor"],
monster: ["horned", "cyclops", "slime", "alien"],
animal: ["cat", "dog", "rabbit", "bear"],
});
const AVATAR_HAIR = ["none", "tuft", "spiky", "curly", "wavy", "bob", "ponytail", "braids", "mohawk"];
const AVATAR_HEAD_ACC = ["none", "cap", "beanie", "crown", "wizardhat", "glasses", "sunglasses", "eyepatch", "bow", "flower", "headphones", "partyhat"];
const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge", "necklace", "bowtie", "belt", "backpack", "satchel", "wings"];
const AVATAR_DANCES = Object.freeze(["bounce", "twist", "spin", "shuffle", "wiggle"]);
const AVATAR_DEFAULT = Object.freeze({
species: "dino", speciesVariant: "rex", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29",
accessoryHead: "none", accessoryBody: "none",
});
const AVATAR_SVG_NS = "http://www.w3.org/2000/svg";
function avatarSanitize(config){
const c = config || {};
const species = AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species;
const variants = AVATAR_VARIANTS[species];
return {
species,
speciesVariant: variants.includes(c.speciesVariant) ? c.speciesVariant : variants[0],
bodyColor: /^#[0-9a-fA-F]{6}$/.test(c.bodyColor||"") ? c.bodyColor : AVATAR_DEFAULT.bodyColor,
hairStyle: AVATAR_HAIR.includes(c.hairStyle) ? c.hairStyle : AVATAR_DEFAULT.hairStyle,
hairColor: /^#[0-9a-fA-F]{6}$/.test(c.hairColor||"") ? c.hairColor : AVATAR_DEFAULT.hairColor,
accessoryHead: AVATAR_HEAD_ACC.includes(c.accessoryHead) ? c.accessoryHead : AVATAR_DEFAULT.accessoryHead,
accessoryBody: AVATAR_BODY_ACC.includes(c.accessoryBody) ? c.accessoryBody : AVATAR_DEFAULT.accessoryBody,
};
}
/* mengt een hex-kleur amt (-1..1) richting zwart (negatief) of wit (positief) -
gebruikt voor buik/snuit-highlights en pootjes/staart-schaduw, zodat die
altijd bij de gekozen lichaamskleur passen zonder een aparte kleurkiezer. */
function avatarShade(hex, amt){
const n = parseInt(hex.slice(1), 16);
let r = (n>>16)&255, g = (n>>8)&255, b = n&255;
if(amt < 0){ r *= 1+amt; g *= 1+amt; b *= 1+amt; }
else{ r += (255-r)*amt; g += (255-g)*amt; b += (255-b)*amt; }
const clamp = v => Math.max(0, Math.min(255, Math.round(v)));
return "#" + [r,g,b].map(v=>clamp(v).toString(16).padStart(2,"0")).join("");
}
/* Groot hoofd en compact, soortspecifiek lijf. De kop beslaat circa twee
derde van de figuurbreedte; romp en ledematen blijven kort en sluiten onder
de wangen aan. Alle soorten delen dezelfde accessoire-ankers rond hals y=61,
borst y=72 en middel y=87. */
function avatarBodyMarkup(species, bodyColor, speciesVariant){
const variants = AVATAR_VARIANTS[species] || AVATAR_VARIANTS[AVATAR_DEFAULT.species];
const variant = variants.includes(speciesVariant) ? speciesVariant : variants[0];
const outline = avatarShade(bodyColor, -0.48);
const dark = avatarShade(bodyColor, -0.24);
const light = avatarShade(bodyColor, 0.4);
const highlight = avatarShade(bodyColor, 0.72);
let m = ``;
/* Silhouetdetails achter kop en lijf: iedere variant is direct herkenbaar. */
if(species === "dino"){
if(variant === "rex"){
m += ``;
}else if(variant === "triceratops"){
m += ``;
m += ``;
}else if(variant === "stegosaurus"){
m += ``;
m += ``;
m += ``;
}else{
m += ``;
m += ``;
}
}else if(species === "monster"){
if(variant === "horned"){
m += ``;
m += ``;
}else if(variant === "cyclops"){
m += ``;
}else if(variant === "slime"){
m += ``;
}else{
m += ``;
m += ``;
}
}else{
if(variant === "cat"){
m += ``;
m += ``;
m += ``;
}else if(variant === "dog"){
m += ``;
m += ``;
m += ``;
}else if(variant === "rabbit"){
m += ``;
m += ``;
m += ``;
}else{
m += ``;
m += ``;
m += ``;
}
}
/* Korte ledematen blijven op gedeelde ankers voor alle accessoires. */
if(species === "dino"){
m += ``;
m += ``;
if(variant === "triceratops"){
m += ``;
m += ``;
}else if(variant === "raptor"){
m += ``;
m += ``;
}else{
m += ``;
m += ``;
}
}else if(species === "monster"){
m += ``;
m += ``;
m += ``;
m += ``;
}else{
m += ``;
m += ``;
m += ``;
m += ``;
}
m += ``;
const torsoPath = species === "dino"
? "M 39 61 C 45 58 55 58 61 61 C 67 68 67 83 62 91 C 56 96 44 96 38 91 C 33 83 33 68 39 61 Z"
: species === "monster"
? "M 35 62 C 42 57 58 57 65 62 C 71 70 70 85 64 92 C 56 97 44 97 36 92 C 30 85 29 70 35 62 Z"
: "M 38 61 C 44 58 56 58 62 61 C 67 69 66 84 61 93 C 54 97 46 97 39 93 C 34 84 33 69 38 61 Z";
m += ``;
if(species === "dino"){
if(variant === "raptor"){
m += ``;
}else{
m += ``;
if(variant === "stegosaurus") m += ``;
}
}else if(species === "monster"){
m += ``;
if(variant === "horned") m += ``;
if(variant === "cyclops") m += ``;
if(variant === "slime") m += ``;
if(variant === "alien") m += ``;
}else{
if(variant === "cat") m += ``;
if(variant === "dog") m += ``;
if(variant === "rabbit") m += ``;
if(variant === "bear") m += ``;
}
const headPaths = {
dino: {
rex: "M 47 -3 C 28 -3 15 10 16 31 C 16 49 28 60 45 63 C 64 67 83 55 85 35 C 87 15 73 -1 55 -2 Z",
triceratops: "M 50 -1 C 29 -2 16 11 17 31 C 17 51 30 63 50 64 C 70 64 83 51 83 31 C 84 11 71 -1 50 -1 Z",
stegosaurus: "M 48 -2 C 29 -2 16 10 16 31 C 16 50 29 62 48 64 C 68 66 84 53 84 32 C 84 11 69 -2 48 -2 Z",
raptor: "M 43 -2 C 26 1 16 14 18 33 C 19 50 30 60 46 62 C 64 65 80 55 87 40 C 91 30 83 24 76 21 C 74 7 61 -4 43 -2 Z",
},
monster: {
horned: "M 50 -3 C 29 -5 16 8 16 30 C 15 51 29 63 48 64 C 69 66 85 53 84 30 C 84 8 71 -2 50 -3 Z",
cyclops: "M 50 -2 C 29 -3 16 10 16 31 C 16 53 30 65 50 65 C 70 65 84 53 84 31 C 84 10 71 -2 50 -2 Z",
slime: "M 49 0 C 30 -4 17 9 18 29 C 12 39 18 48 23 51 C 24 62 34 65 43 61 C 49 68 59 67 63 61 C 75 64 85 55 81 45 C 89 36 84 25 80 21 C 78 6 66 -2 49 0 Z",
alien: "M 50 -2 C 32 -2 20 9 18 28 C 16 48 28 62 50 65 C 72 62 84 48 82 28 C 80 9 68 -2 50 -2 Z",
},
animal: {
cat: "M 50 -3 C 30 -3 17 10 17 31 C 17 52 30 64 50 64 C 70 64 83 52 83 31 C 83 10 70 -3 50 -3 Z",
dog: "M 50 0 C 29 -1 16 12 17 33 C 18 53 31 64 50 64 C 69 64 82 53 83 33 C 84 12 71 -1 50 0 Z",
rabbit: "M 50 -1 C 32 -1 20 11 19 31 C 18 51 30 64 50 66 C 70 64 82 51 81 31 C 80 11 68 -1 50 -1 Z",
bear: "M 50 -3 C 28 -3 15 10 16 32 C 17 54 30 65 50 65 C 70 65 83 54 84 32 C 85 10 72 -3 50 -3 Z",
},
};
m += ``;
m += ``;
if(species === "dino"){
if(variant === "triceratops"){
m += ``;
m += ``;
m += ``;
m += ``;
m += ``;
}else if(variant === "raptor"){
m += ``;
m += ``;
}else{
m += ``;
m += ``;
if(variant === "stegosaurus") m += ``;
}
m += ``;
m += ``;
m += ``;
if(variant === "raptor") m += ``;
}else if(species === "monster"){
if(variant === "cyclops"){
m += ``;
m += ``;
}else if(variant === "alien"){
for(const x of [34,50,66]) m += ``;
m += ``;
}else{
const leftSize = variant === "slime" ? 8.5 : 10;
const rightSize = variant === "slime" ? 8.5 : 8.2;
m += ``;
m += ``;
if(variant === "slime"){
m += ``;
}else{
m += ``;
}
}
}else{
m += ``;
m += ``;
if(variant === "cat"){
m += ``;
m += ``;
}else if(variant === "dog"){
m += ``;
m += ``;
}else if(variant === "rabbit"){
m += ``;
m += ``;
}else{
m += ``;
m += ``;
}
}
m += ``;
return m;
}
const AVATAR_HAIR_MARKUP = {
none: { back: () => "", front: () => "" },
tuft: {
back: () => "",
front: (c) => `
`,
},
spiky: {
back: () => "",
front: (c) => `
`,
},
curly: {
back: (c) => ``,
front: (c) => `
`,
},
wavy: {
back: (c) => ``,
front: (c) => `
`,
},
bob: {
back: (c) => ``,
front: (c) => `
`,
},
ponytail: {
back: (c) => `
`,
front: (c) => `
`,
},
braids: {
back: (c) => `
`,
front: (c) => `
`,
},
mohawk: {
back: () => "",
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: () => `
`,
beanie: () => `
`,
crown: () => `
`,
wizardhat: () => `
`,
glasses: () => `
`,
sunglasses: () => `
`,
eyepatch: () => `
`,
bow: () => `
`,
flower: () => `
`,
headphones: () => `
`,
partyhat: () => `
`,
};
/* Oogaccessoires volgen afwijkende oogindelingen van cycloop en alien. */
function avatarHeadAccessoryMarkup(c){
if(c.species === "monster" && c.speciesVariant === "cyclops"){
if(c.accessoryHead === "glasses") return `
`;
if(c.accessoryHead === "sunglasses") return `
`;
if(c.accessoryHead === "eyepatch") return `
`;
}
if(c.species === "monster" && c.speciesVariant === "alien"){
if(c.accessoryHead === "glasses") return `
`;
if(c.accessoryHead === "sunglasses") return `
`;
}
return AVATAR_HEAD_ACC_MARKUP[c.accessoryHead]();
}
const AVATAR_BODY_ACC_MARKUP = {
none: { back: () => "", front: () => "" },
scarf: {
back: () => "",
front: () => `
`,
},
cape: {
back: () => `
`,
front: () => `
`,
},
badge: {
back: () => "",
front: () => `
`,
},
necklace: {
back: () => "",
front: () => `
`,
},
bowtie: {
back: () => "",
front: () => `
`,
},
belt: {
back: () => "",
front: () => `
`,
},
backpack: {
back: () => `
`,
front: () => `
`,
},
satchel: {
back: () => `
`,
front: () => `
`,
},
wings: {
back: () => `
`,
front: () => ``,
},
};
/* Pet en muts bedekken de voorste haarlaag. Lange stijlen houden hun
achterlaag, zodat staarten, vlechten en lokken naast de kop zichtbaar zijn. */
function avatarHairSuppressed(c){
return c.accessoryHead === "cap" || c.accessoryHead === "beanie";
}
/* bouwt de SVG-weergave van een personage; config==null → neutrale
standaardweergave (nooit een lege/kapotte weergave). Tekenvolgorde bepaalt
de laagvolgorde (SVG kent geen z-index) - vandaar cape/staart/poten eerst,
accessoires als laatste. */
function renderAvatarFace(config, size){
const c = avatarSanitize(config);
const svg = document.createElementNS(AVATAR_SVG_NS, "svg");
/* Extra ruimte voorkomt afsnijden van hoofddeksels, haar en voetzolen. */
svg.setAttribute("viewBox", "-8 -21 116 132");
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
svg.setAttribute("class", "avatar-face " + (size || "md") + " av-species-" + c.species + " av-variant-" + c.speciesVariant);
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", T("avatarAlt"));
const bodyAccessory = AVATAR_BODY_ACC_MARKUP[c.accessoryBody];
const hair = AVATAR_HAIR_MARKUP[c.hairStyle];
let markup = bodyAccessory.back();
markup += avatarHairLayer(hair.back(c.hairColor), c.species, "back");
markup += avatarBodyMarkup(c.species, c.bodyColor, c.speciesVariant);
if(!avatarHairSuppressed(c)) markup += avatarHairLayer(hair.front(c.hairColor), c.species, "front");
markup += avatarHeadAccessoryMarkup(c);
markup += bodyAccessory.front();
svg.innerHTML = markup;
return svg;
}
/* Voltooide opdrachten krijgen een korte feestlaag met het eigen personage.
De hele SVG beweegt als één figuur, zodat haar en accessoires altijd op hun
anatomische ankers blijven. Zonder expliciete keuze rouleren de dansen. */
let avatarDanceCursor = -1;
function avatarDanceChoice(preferred){
if(AVATAR_DANCES.includes(preferred)) return preferred;
avatarDanceCursor = (avatarDanceCursor + 1) % AVATAR_DANCES.length;
return AVATAR_DANCES[avatarDanceCursor];
}
function celebrateAvatar(host, options){
if(!host || typeof currentUser === "undefined" || !currentUser || !currentUser.avatar) return null;
host.querySelectorAll(".avatar-dance-stage").forEach(stage=>stage.remove());
host.classList.add("avatar-dance-host");
const dance = avatarDanceChoice(options && options.dance);
const reduced = typeof window !== "undefined" && window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const stage = document.createElement("div");
stage.className = "avatar-dance-stage av-dance-" + dance + (reduced ? " av-dance-reduced" : "");
stage.dataset.dance = dance;
stage.setAttribute("aria-hidden", "true");
const face = renderAvatarFace(currentUser.avatar, "lg");
face.classList.add("av-dancer");
stage.appendChild(face);
host.appendChild(stage);
setTimeout(()=>{
stage.remove();
if(!host.querySelector(".avatar-dance-stage")) host.classList.remove("avatar-dance-host");
}, reduced ? 1400 : 3400);
return stage;
}
/* bouwt de interactieve kiezer + live voorbeeld; roept onChange(config) aan
bij elke wijziging. Hergebruikt door zowel de instellingen-sectie als het
widget - één UI, geen duplicatie. */
function buildAvatarEditor(container, initialConfig, onChange){
let config = avatarSanitize(initialConfig);
container.innerHTML = "";
const editor = document.createElement("div");
editor.className = "avatar-editor";
const preview = document.createElement("div");
preview.className = "avatar-preview";
const controls = document.createElement("div");
controls.className = "avatar-controls";
editor.append(preview, controls);
container.appendChild(editor);
const speciesRow = document.createElement("div");
speciesRow.className = "avatar-species-row";
AVATAR_SPECIES.forEach(id=>{
const b = document.createElement("button");
b.type = "button";
b.dataset.species = id;
b.addEventListener("click", ()=>{ config.species = id; config.speciesVariant = AVATAR_VARIANTS[id][0]; update(); });
speciesRow.appendChild(b);
});
controls.appendChild(speciesRow);
const variantRow = document.createElement("div"); variantRow.className = "avatar-row";
const variantField = document.createElement("label");
const variantSpan = document.createElement("span");
const variantSel = document.createElement("select");
variantSel.addEventListener("change", ()=>{ config.speciesVariant = variantSel.value; update(); });
variantField.append(variantSpan, variantSel);
variantRow.appendChild(variantField);
controls.appendChild(variantRow);
const row1 = document.createElement("div"); row1.className = "avatar-row";
const bodyColorField = document.createElement("label");
const bodyColorSpan = document.createElement("span");
const bodyColorInput = document.createElement("input"); bodyColorInput.type = "color";
bodyColorInput.addEventListener("input", ()=>{ config.bodyColor = bodyColorInput.value; update(); });
bodyColorField.append(bodyColorSpan, bodyColorInput);
const hairColorField = document.createElement("label");
const hairColorSpan = document.createElement("span");
const hairColorInput = document.createElement("input"); hairColorInput.type = "color";
hairColorInput.addEventListener("input", ()=>{ config.hairColor = hairColorInput.value; update(); });
hairColorField.append(hairColorSpan, hairColorInput);
row1.append(bodyColorField, hairColorField);
controls.appendChild(row1);
const row2 = document.createElement("div"); row2.className = "avatar-row";
const hairField = document.createElement("label");
const hairSpan = document.createElement("span");
const hairSel = document.createElement("select");
AVATAR_HAIR.forEach(id=>hairSel.appendChild(new Option("", id)));
hairSel.addEventListener("change", ()=>{ config.hairStyle = hairSel.value; update(); });
hairField.append(hairSpan, hairSel);
row2.append(hairField);
controls.appendChild(row2);
const row3 = document.createElement("div"); row3.className = "avatar-row";
const headAccField = document.createElement("label");
const headAccSpan = document.createElement("span");
const headAccSel = document.createElement("select");
AVATAR_HEAD_ACC.forEach(id=>headAccSel.appendChild(new Option("", id)));
headAccSel.addEventListener("change", ()=>{ config.accessoryHead = headAccSel.value; update(); });
headAccField.append(headAccSpan, headAccSel);
const bodyAccField = document.createElement("label");
const bodyAccSpan = document.createElement("span");
const bodyAccSel = document.createElement("select");
AVATAR_BODY_ACC.forEach(id=>bodyAccSel.appendChild(new Option("", id)));
bodyAccSel.addEventListener("change", ()=>{ config.accessoryBody = bodyAccSel.value; update(); });
bodyAccField.append(bodyAccSpan, bodyAccSel);
row3.append(headAccField, bodyAccField);
controls.appendChild(row3);
function syncLabels(){
speciesRow.querySelectorAll("button").forEach(b=>{
b.textContent = T("avatarSpecies_" + b.dataset.species);
b.classList.toggle("active", b.dataset.species === config.species);
});
const variantIds = AVATAR_VARIANTS[config.species];
if([...variantSel.options].map(o=>o.value).join(",") !== variantIds.join(",")){
variantSel.innerHTML = "";
variantIds.forEach(id=>variantSel.appendChild(new Option("", id)));
}
variantSpan.textContent = T("avatarVariant");
[...variantSel.options].forEach(o=>o.textContent = T("avatarVariant_" + o.value));
bodyColorSpan.textContent = T("avatarBodyColor");
hairColorSpan.textContent = T("avatarHairColor");
hairSpan.textContent = T("avatarHairStyle");
[...hairSel.options].forEach(o=>o.textContent = T("avatarHair_" + o.value));
headAccSpan.textContent = T("avatarAccHead");
[...headAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value));
bodyAccSpan.textContent = T("avatarAccBody");
[...bodyAccSel.options].forEach(o=>o.textContent = T("avatarAcc_" + o.value));
}
function update(){
const variants = AVATAR_VARIANTS[config.species];
if(!variants.includes(config.speciesVariant)) config.speciesVariant = variants[0];
syncLabels();
bodyColorInput.value = config.bodyColor;
hairColorInput.value = config.hairColor;
variantSel.value = config.speciesVariant;
hairSel.value = config.hairStyle;
headAccSel.value = config.accessoryHead;
bodyAccSel.value = config.accessoryBody;
preview.innerHTML = "";
preview.appendChild(renderAvatarFace(config, "lg"));
if(onChange) onChange({ ...config });
}
document.addEventListener("langchange", syncLabels);
update();
return { getConfig: ()=>({ ...config }), setConfig(c){ config = avatarSanitize(c); update(); } };
}
async function saveAvatar(config){
const clean = avatarSanitize(config);
const res = await api("/me/avatar", { method: "PUT", body: clean });
currentUser.avatar = res.avatar;
document.dispatchEvent(new CustomEvent("avatarchange"));
return res.avatar;
}
/* ---- Instellingen-sectie "Mijn personage" (cms.js-patroon: eigen .st-section
zelf aanmaken, niet in index.html) ---- */
(function(){
const host = document.createElement("div");
host.className = "st-section";
host.dataset.section = "avatar";
host.style.display = "none";
document.getElementById("stContent").appendChild(host);
const msg = document.createElement("p");
msg.className = "avatar-save-msg";
let editorApi = null;
function build(){
host.innerHTML = "";
editorApi = buildAvatarEditor(host, currentUser && currentUser.avatar, ()=>{ msg.textContent = ""; });
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "tbtn";
saveBtn.textContent = T("avatarSave");
saveBtn.addEventListener("click", async ()=>{
try{ await saveAvatar(editorApi.getConfig()); msg.textContent = T("avatarSaved"); }
catch(e){ msg.textContent = e.message; }
});
host.appendChild(saveBtn);
host.appendChild(msg);
}
registerSettingsSection({
id: "avatar", icon: "🦖", group: "personal", order: 3,
label: ()=>T("stAvatar"), sub: ()=>T("stAvatarSub"),
visible: ()=>!!currentUser,
activate(){ showSettingsDom("avatar"); build(); },
keywords: ()=>[T("avatarSpecies_dino"), T("avatarSpecies_monster"), T("avatarSpecies_animal"),
...Object.values(AVATAR_VARIANTS).flat().map(id=>T("avatarVariant_" + id)),
"personage","avatar","dinosaurus","monster","dier","haar","hair","kleur","color"],
});
})();
/* ---- widget: dunne wrapper rond dezelfde editor ---- */
function mountAvatarWidget(root){
root.innerHTML = `
`;
root.querySelector(".play-mission").textContent = T("wg_avatar_d");
const host = document.createElement("div");
host.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column;";
root.appendChild(host);
const msg = document.createElement("p");
msg.className = "avatar-save-msg";
let editorApi = null;
function build(){
host.innerHTML = "";
editorApi = buildAvatarEditor(host, currentUser && currentUser.avatar, ()=>{ msg.textContent = ""; });
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "tbtn";
saveBtn.textContent = T("avatarSave");
saveBtn.addEventListener("click", async ()=>{
try{ await saveAvatar(editorApi.getConfig()); msg.textContent = T("avatarSaved"); }
catch(e){ msg.textContent = e.message; }
});
host.appendChild(saveBtn);
host.appendChild(msg);
}
build();
document.addEventListener("langchange", ()=>{ root.querySelector(".play-mission").textContent = T("wg_avatar_d"); });
/* currentUser.avatar is de bron van waarheid (server), niet het widget-eigen
initState - geen dubbele opslag van dezelfde data op twee plekken. */
return { getState: ()=>({}) };
}