All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- Dino: rugstekels stonden als een rij spitse vormen voor de hals/borst
("Ga als designer kijken" - gebruiker vond dit "wat vreemd"), wat als
een rare kraag oogde. Verplaatst naar een klein randje bovenop de kop
(zoals kleine hoorntjes) - dichter bij de anatomische plek (rug/kruin)
en rustiger op klein formaat.
- Dier (beer): had een los staartje opzij op dezelfde hoogte en met
vrijwel dezelfde vorm/grootte als de armen/benen, waardoor het als een
extra ledemaat oogde. Verwijderd - een beer laat van de voorkant
sowieso nauwelijks iets van zijn staart zien, dus dit is ook
anatomisch kloppender dan een zichtbaar zij-staartje.
- Overwogen: SVG vs. PNG-lagen als basis. SVG blijft de juiste keuze
hier - lichaams-/haarkleur zijn vrije kleurkiezers (elke hex-waarde),
wat via fill-attributen triviaal is maar bij PNG een vast palet of
canvas-hertintplugin zou vereisen; SVG schaalt bovendien scherp op
alle drie weergavegroottes zonder aparte assets.
- Geverifieerd: alle vijf bestaande scenario's, volledige testsuite,
en een School-navigator met een echte klas (leerling met dier-
personage toont nu geen vreemd zij-staartje meer).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
359 lines
17 KiB
JavaScript
359 lines
17 KiB
JavaScript
/* 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 (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"];
|
|
const AVATAR_HEAD_ACC = ["none", "cap", "crown", "glasses", "bow"];
|
|
const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge"];
|
|
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 || {};
|
|
return {
|
|
species: AVATAR_SPECIES.includes(c.species) ? c.species : AVATAR_DEFAULT.species,
|
|
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("");
|
|
}
|
|
|
|
/* 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 += `<path d="M 38 88 C 18 92, 2 82, 1 64 C 3 78, 18 86, 35 76 Z" fill="${dark}"/>`;
|
|
}
|
|
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"){
|
|
m += `<ellipse cx="50" cy="42" rx="14" ry="10" fill="${light}"/>`;
|
|
m += `<ellipse cx="50" cy="38" rx="4.4" ry="3.4" fill="#3a2a18"/>`;
|
|
}
|
|
if(species === "monster"){
|
|
m += `<circle cx="37" cy="25" r="8.4" fill="#fff"/>`;
|
|
m += `<circle cx="63" cy="25" r="6.8" fill="#fff"/>`;
|
|
m += `<circle cx="38.6" cy="27" r="3.6" fill="#232733"/>`;
|
|
m += `<circle cx="64.2" cy="27" r="2.9" fill="#232733"/>`;
|
|
}else{
|
|
m += `<circle cx="38" cy="24" r="7.6" fill="#fff"/>`;
|
|
m += `<circle cx="62" cy="24" r="7.6" fill="#fff"/>`;
|
|
m += `<circle cx="39.4" cy="26" r="3.2" fill="#232733"/>`;
|
|
m += `<circle cx="60.4" cy="26" r="3.2" fill="#232733"/>`;
|
|
}
|
|
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"/>`;
|
|
/* 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"){
|
|
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"/>`;
|
|
}
|
|
if(species === "monster"){
|
|
m += `<path d="M 34 42 Q 50 54 66 42 Q 50 50 34 42 Z" fill="#3c2158"/>`;
|
|
m += `<path d="M 42 43 L 44 49 L 46 43 Z" fill="#fff"/>`;
|
|
m += `<path d="M 54 43 L 56 49 L 58 43 Z" fill="#fff"/>`;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
const AVATAR_HAIR_MARKUP = {
|
|
none: () => "",
|
|
spiky: (c) => `
|
|
<path d="M 27 18 L 24 4 L 32 15 Z" fill="${c}"/>
|
|
<path d="M 37 12 L 36 -1 L 44 10 Z" fill="${c}"/>
|
|
<path d="M 48 10 L 50 -3 L 55 10 Z" fill="${c}"/>
|
|
<path d="M 60 10 L 64 -1 L 63 12 Z" fill="${c}"/>
|
|
<path d="M 68 15 L 76 4 L 73 18 Z" fill="${c}"/>`,
|
|
curly: (c) => `
|
|
<circle cx="26" cy="14" r="9" fill="${c}"/>
|
|
<circle cx="37" cy="6" r="10" fill="${c}"/>
|
|
<circle cx="50" cy="3" r="10.5" fill="${c}"/>
|
|
<circle cx="63" cy="6" r="10" fill="${c}"/>
|
|
<circle cx="74" cy="14" r="9" fill="${c}"/>`,
|
|
ponytail: (c) => `
|
|
<path d="M 22 20 Q 24 -6 50 -6 Q 76 -6 78 20 Q 62 2 50 2 Q 38 2 22 20 Z" fill="${c}"/>
|
|
<path d="M 71 9 Q 89 12 87 28 Q 85 42 72 47 Q 81 34 77 22 Q 75 13 67 11 Z" fill="${c}"/>
|
|
<ellipse cx="72" cy="13" rx="4.2" ry="3.2" fill="${avatarShade(c,-0.25)}"/>`,
|
|
mohawk: (c) => `<path d="M 44 20 Q 43 4 50 1 Q 57 4 56 20 Q 50 15 44 20 Z" fill="${c}"/>`,
|
|
};
|
|
|
|
const AVATAR_HEAD_ACC_MARKUP = {
|
|
none: () => "",
|
|
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 d="M 22 14 Q 34 8 50 8 L 50 14 Q 34 15 21 20 Z" fill="#2f63b3"/>`,
|
|
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"/>
|
|
<circle cx="30" cy="1" r="2.6" fill="#e0483c"/>
|
|
<circle cx="50" cy="-3" r="2.6" fill="#3a8ee0"/>
|
|
<circle cx="70" cy="1" r="2.6" fill="#e0483c"/>`,
|
|
glasses: () => `
|
|
<circle 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"/>
|
|
<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 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"/>`,
|
|
};
|
|
|
|
const AVATAR_BODY_ACC_MARKUP = {
|
|
none: () => "",
|
|
scarf: () => `
|
|
<path d="M 33 58 Q 50 68 67 58 L 65 66 Q 50 74 35 66 Z" fill="#d94f4f"/>
|
|
<path d="M 46 65 L 43 80 L 50 76 Z" fill="#c23f3f"/>`,
|
|
cape: () => `
|
|
<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 70 55 Q 90 75 84 100 L 70 90 Q 74 70 60 58 Z" fill="#7a4bc7" opacity=".92"/>`,
|
|
badge: () => `<circle cx="50" cy="78" r="6" fill="#f0b73a" stroke="#fff" stroke-width="1.6"/>`,
|
|
};
|
|
|
|
/* 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 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
|
|
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; update(); });
|
|
speciesRow.appendChild(b);
|
|
});
|
|
controls.appendChild(speciesRow);
|
|
|
|
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);
|
|
});
|
|
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(){
|
|
bodyColorInput.value = config.bodyColor;
|
|
hairColorInput.value = config.hairColor;
|
|
hairSel.value = config.hairStyle;
|
|
headAccSel.value = config.accessoryHead;
|
|
bodyAccSel.value = config.accessoryBody;
|
|
syncLabels();
|
|
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"),
|
|
"personage","avatar","dinosaurus","monster","dier","haar","hair","kleur","color"],
|
|
});
|
|
})();
|
|
|
|
/* ---- widget: dunne wrapper rond dezelfde editor ---- */
|
|
function mountAvatarWidget(root){
|
|
root.innerHTML = `<p class="play-mission"></p>`;
|
|
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: ()=>({}) };
|
|
}
|