teach/public/js/widgets/math.js
Ramon 84fd02d135
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
fix: libelle en dierenvormen herstellen, instellingen omdraaien (v0.4.81-beta)
- Libelle in de vijverplaat kreeg weer vier symmetrische vleugels
- Zeepaardje in het koraalrif herbouwd van een onherkenbare buis naar een
  duidelijk zeepaardje, verplaatst zodat het niet meer over de clownvis valt
- Nieuwe coloringLegPath()-vorm: giraffe en zebra hebben nu getapte,
  licht geknikte poten in plaats van rechte kokerpoten
- Instellingenindeling omgedraaid: de kleurplaat (thema/plaat) is weer
  direct zichtbaar, kleurhulp/reset gaan achter het tandwiel
- Letterblokken en rekenwidgets: niveauknoppen naar het tandwiel, het
  woord/thema blijft direct zichtbaar
- Klok, geldrekenen, plaatswaardetabel, boom&blad/vogels/planten en
  topografie: spel/activiteit (en bij geld/plaatswaarde het bijpassende
  veld) staan weer in de vaste bovenbalk, niveau staat achter het tandwiel
- Plaatswaardetabel toont nu ook in verkennen-stand een vaste bovenbalk
  (spel, getal, dobbelsteen) in plaats van alleen in bouwen-stand
- Bugfix onderweg: niveauwissel in verkennen-stand van de plaatswaardetabel
  sprong per ongeluk naar bouwen-stand - nu blijft de stand behouden

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 21:39:30 +02:00

331 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* widgets: Rekenen (+ - x : %) */
/* =========================================================
Math practice engine — add / sub / mul / div / percentages
==========================================================*/
function mountMath(root, initState, mode, opts){
const ROUND = 10;
const onProgress = opts && opts.onProgress;
const S = { level:1, yearGroup:null, managedLearningLevel:false, snd:true, amode:"blocks", q:null, tries:0, done:0, good:0, input:"", busy:false };
if(initState){
S.level = initState.level||1;
/* yearGroup komt bij leerlingen van klassenmanagement (pupil.js). Op een
auteursbord bewaart board.js de aparte leerjaarkeuze rondom getState(). */
S.yearGroup = initState.yearGroup || null;
S.managedLearningLevel = !!initState.managedLearningLevel;
S.snd = initState.snd!==false;
S.amode = initState.amode||"blocks";
}
const play = f=>{ if(S.snd) f(); };
const rnd = n => Math.floor(Math.random()*n);
root.innerHTML = `
<div class="mt blocks">
<div class="mt-top">
<div class="mt-score"></div>
</div>
<div class="mt-card">
<div class="mt-q"></div>
<div class="mt-a"></div>
<div class="mt-slots"></div>
<div class="mt-fb"></div>
<div class="mt-done">
<div class="dg">🌟</div><div class="dt"></div><div class="ds"></div>
<button class="mt-again"></button>
</div>
</div>
<div class="lg-tray mt-tray"></div>
<div class="mt-pad"></div>
</div>`;
const el = s => root.querySelector(s);
/* pilot voor "personage moedigt aan" (roadmap-gerelateerd punt): naast het
bestaande "Goed!"-bericht verschijnt kort het eigen personage, als de
leerling er één heeft ingesteld. Puur visueel, geen nieuw feedbacktype -
zonder personage blijft dit exact het oude gedrag. */
function showGoodFeedback(){
fbEl.innerHTML = "";
fbEl.appendChild(document.createTextNode(T("mGood")));
if(currentUser && currentUser.avatar){
const cheer = renderAvatarFace(currentUser.avatar, "md");
cheer.classList.add("av-cheer");
fbEl.appendChild(cheer);
}
}
/* niveau staat meestal vast (en is bij beheerd niveau uitgeschakeld), dus
dat gaat net als geluid/invoerwijze achter het tandwiel in plaats van
permanent ruimte te vragen naast de opgave zelf */
const levels=document.createElement("div");levels.className="lg-levels";
levels.innerHTML=`<button data-l="1"></button><button data-l="2"></button><button data-l="3"></button>`;
const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd";
const modeBtn=document.createElement("button");modeBtn.type="button";modeBtn.className="lg-mode";
el(".mt-top").insertAdjacentElement("beforebegin",widgetGear([
gearField(T("levelWidget"),levels),gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn)
]));
const mtEl = el(".mt"), qEl = el(".mt-q"), aEl = el(".mt-a"), fbEl = el(".mt-fb"),
scoreEl = el(".mt-score"), doneEl = el(".mt-done"),
padEl = el(".mt-pad"), slotsEl = el(".mt-slots"), trayEl = el(".mt-tray");
/* ---- digit blocks with dot patterns (like the letter blocks) ---- */
function makeDigitBlock(d){
const b = document.createElement("div");
b.className = "blok sm";
b.dataset.digit = d;
b.innerHTML = `<div class="bl">${d}</div>`;
const dg = dotGrid(d); dg.classList.add("blok-dots");
b.appendChild(dg);
attachDigitDrag(b);
return b;
}
"0123456789".split("").forEach(d=>trayEl.appendChild(makeDigitBlock(d)));
function makeDigitSlot(d){
const s = document.createElement("div");
s.className = "slot sm";
s.dataset.digit = d;
s.innerHTML = `<div class="ghost"></div>`;
const dg = dotGrid(d); dg.classList.add("slot-dots");
s.appendChild(dg);
s.addEventListener("click", ()=>{
const placed = s.querySelector(".blok");
if(placed && Date.now() - (+placed.dataset.placedAt||0) > 400){
placed.remove();
s.classList.remove("filled");
}
});
return s;
}
function attachDigitDrag(block){
block.addEventListener("pointerdown", e=>{
if(e.pointerType==="mouse" && e.button!==0) return;
if(S.busy || doneEl.classList.contains("show")) return;
e.preventDefault();
try{ block.releasePointerCapture(e.pointerId); }catch(_){}
const clone = block.cloneNode(true);
clone.id = "dragClone";
document.body.appendChild(clone);
const moveTo = ev=>{ const zf = VZ(); clone.style.left = (ev.clientX/zf)+"px"; clone.style.top = (ev.clientY/zf)+"px"; };
moveTo(e);
let hovered = null;
const hit = ev=>{
const pad = 8;
let found = null;
slotsEl.querySelectorAll(".slot:not(.filled)").forEach(s=>{
const r = s.getBoundingClientRect();
if(ev.clientX>=r.left-pad && ev.clientX<=r.right+pad &&
ev.clientY>=r.top-pad && ev.clientY<=r.bottom+pad) found = s;
});
return found;
};
const move = ev=>{
moveTo(ev);
const s = hit(ev);
if(hovered && hovered!==s) hovered.classList.remove("droptarget");
hovered = s;
if(hovered) hovered.classList.add("droptarget");
};
const up = ev=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
if(hovered) hovered.classList.remove("droptarget");
const target = hit(ev);
if(target){
if(target.dataset.digit === block.dataset.digit){
const copy = block.cloneNode(true);
copy.classList.add("in-slot");
copy.dataset.placedAt = Date.now();
target.appendChild(copy);
target.classList.add("filled");
play(sndGood);
if(!slotsEl.querySelector(".slot:not(.filled)")) blocksSolved();
}else{
target.classList.add("shake");
setTimeout(()=>target.classList.remove("shake"), 400);
play(sndBad);
}
}
};
const cancel = ()=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
if(hovered) hovered.classList.remove("droptarget");
};
document.addEventListener("pointermove",move);
document.addEventListener("pointerup",up);
document.addEventListener("pointercancel",cancel);
});
}
function blocksSolved(){
S.busy = true;
S.good++;
showGoodFeedback();
play(sndWin);
confetti(el(".mt-card"));
setTimeout(next, 1000);
}
function gen(){
const L = S.level;
/* klassenmanagement geeft een groep (leerjaar) mee via yearGroup - dan
sturen de SLO-gebaseerde getalgrenzen (data.js: mathRangeFor) de
inhoud, niet meer de vaste 10/20/100-klem. Zonder groep (geen
klassenmanagement ingesteld voor deze leerling/dit bord) blijft het
oude gedrag exact hetzelfde. */
const slo = mathRangeFor(S.yearGroup, L);
const rules = mathTaskRules(S.yearGroup, L);
let a, b, ans, txt;
if(mode==="add"){
const max = rules ? rules.addSub : (slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100));
a = rnd(max+1); b = rnd(max-a+1); ans = a+b;
txt = `${a} + ${b} =`;
}else if(mode==="sub"){
const max = rules ? rules.addSub : (slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100));
a = rnd(max+1); b = rnd(a+1); ans = a-b;
txt = `${a} ${b} =`;
}else if(mode==="mul"){
const tmax = rules ? rules.timesTable : (slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12));
a = 1+rnd(tmax); b = 1+rnd(rules ? rules.factorMax : (L===3 ? 12 : 10)); ans = a*b;
txt = `${a} × ${b} =`;
}else if(mode==="div"){
const tmax = rules ? rules.timesTable : (slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12));
const t = 1+rnd(tmax), q = 1+rnd(rules ? rules.factorMax : (L===3 ? 12 : 10));
a = t*q; ans = q;
txt = `${a} ${S.lang==="nl" || LANG==="nl" ? ":" : "÷"} ${t} =`;
}else{ /* pct */
const sets = rules ? rules.percentSets : (L===1 ? [10,50,100] : L===2 ? [10,20,25,50,75,100] : [5,10,20,25,30,40,50,60,70,75,80,90]);
const p = sets[rnd(sets.length)];
const step = {5:20,10:10,15:20,20:5,25:4,30:10,40:5,50:2,60:5,70:10,75:4,80:5,90:10,100:1}[p];
const base = step*(1+rnd(12));
ans = p*base/100;
txt = `${p}% ${T("mOf")} ${base} =`;
}
return { txt, ans };
}
function newQuestion(){
let q = gen();
if(S.q && q.txt===S.q.txt) q = gen(); /* avoid the same sum twice in a row */
S.q = q; S.tries = 0; S.input = ""; S.busy = false;
qEl.textContent = q.txt;
aEl.textContent = "";
aEl.classList.remove("ok");
fbEl.textContent = ""; fbEl.classList.remove("ans");
slotsEl.innerHTML = "";
if(S.amode==="blocks"){
String(q.ans).split("").forEach(d=>slotsEl.appendChild(makeDigitSlot(d)));
}
updateScore();
}
function updateScore(){
scoreEl.textContent = `${S.good} ${T("mGoed")} · ${Math.min(S.done+1,ROUND)}/${ROUND}`;
}
function endRound(){
doneEl.querySelector(".dt").textContent = T("mDoneT");
doneEl.querySelector(".ds").textContent = `${S.good}/${ROUND} ${T("mGoed")}`;
doneEl.querySelector(".dg").textContent = S.good>=8 ? "🌟" : S.good>=5 ? "👍" : "💪";
doneEl.classList.add("show");
if(S.good>=8) play(sndWin);
celebrateAvatar(root);
}
function next(){
S.done++;
if(S.done >= ROUND){ endRound(); }
else newQuestion();
}
function check(){
if(S.busy || S.input==="") return;
if(+S.input === S.q.ans){
S.busy = true;
S.good++;
aEl.classList.add("ok");
showGoodFeedback();
play(sndGood);
confetti(el(".mt-card"));
/* per som loggen (niet meer één keer per blok van 10 in endRound()) -
zo levert een sessie ook echt een groep aparte momenten op in de
voortgang, en weet de leerkracht welke som het was. */
if(onProgress) onProgress({ attempts: S.tries+1, correct: 1, stars: 0,
detail: { question: S.q.txt, answer: +S.input, expected: S.q.ans, correct: true } });
setTimeout(next, 900);
}else{
S.tries++;
aEl.classList.add("shake");
setTimeout(()=>aEl.classList.remove("shake"), 400);
play(sndBad);
const wrongAnswer = S.input;
S.input = ""; aEl.textContent = "";
if(S.tries >= 3){
S.busy = true;
fbEl.textContent = `${T("mAnsWas")} ${S.q.ans}`;
fbEl.classList.add("ans");
if(onProgress) onProgress({ attempts: S.tries, correct: 0, stars: 0,
detail: { question: S.q.txt, answer: +wrongAnswer, expected: S.q.ans, correct: false } });
setTimeout(next, 1800);
}
}
}
["7","8","9","⌫","4","5","6","0","1","2","3","✓"].forEach(k=>{
const b = document.createElement("button");
b.textContent = k;
if(k==="✓") b.className = "ok";
b.addEventListener("click", ()=>{
if(doneEl.classList.contains("show")) return;
if(k==="⌫"){ if(!S.busy){ S.input = S.input.slice(0,-1); aEl.textContent = S.input; } }
else if(k==="✓") check();
else if(!S.busy && S.input.length<8){ S.input += k; aEl.textContent = S.input; }
});
padEl.appendChild(b);
});
el(".mt-again").addEventListener("click", ()=>{
S.done = 0; S.good = 0;
doneEl.classList.remove("show");
newQuestion();
});
root.querySelectorAll(".lg-levels button").forEach(b=>{
b.disabled = S.managedLearningLevel;
b.addEventListener("click", ()=>{
S.level = +b.dataset.l;
S.done = 0; S.good = 0;
doneEl.classList.remove("show");
labels(); newQuestion();
});
});
sndBtn.addEventListener("click", ()=>{
S.snd = !S.snd;
sndBtn.textContent = S.snd ? "🔔" : "🔕";
sndBtn.classList.toggle("off", !S.snd);
});
modeBtn.addEventListener("click", ()=>{
S.amode = S.amode==="blocks" ? "type" : "blocks";
labels(); newQuestion();
});
function labels(){
root.querySelectorAll(".lg-levels button").forEach(b=>{
b.textContent = T("lvl"+b.dataset.l);
b.classList.toggle("active", +b.dataset.l===S.level);
});
sndBtn.textContent = S.snd ? "🔔" : "🔕";
sndBtn.classList.toggle("off", !S.snd);
sndBtn.title = T("sndWidget");
modeBtn.textContent = S.amode==="blocks" ? "🧱" : "⌨️";
modeBtn.title = T("amode");
mtEl.classList.toggle("blocks", S.amode==="blocks");
el(".mt-again").textContent = T("mAgain");
updateScore();
}
document.addEventListener("langchange", ()=>{ labels(); newQuestion(); });
labels();
newQuestion();
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
return {
getState: ()=>({ level:S.level, snd:S.snd, amode:S.amode }),
setState: st=>{ if(st){ S.level=st.level||1;S.yearGroup=st.yearGroup||null;S.managedLearningLevel=!!st.managedLearningLevel; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; labels(); newQuestion(); } }
};
}