All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
292 lines
10 KiB
JavaScript
292 lines
10 KiB
JavaScript
/* widgets: Rekenen (+ - x : %) */
|
||
/* =========================================================
|
||
Math practice engine — add / sub / mul / div / percentages
|
||
==========================================================*/
|
||
function mountMath(root, initState, mode){
|
||
const ROUND = 10;
|
||
const S = { level:1, snd:true, amode:"blocks", q:null, tries:0, done:0, good:0, input:"", busy:false };
|
||
if(initState){
|
||
S.level = initState.level||1;
|
||
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">
|
||
<button class="lg-snd"></button>
|
||
<button class="lg-mode"></button>
|
||
<div class="lg-levels">
|
||
<button data-l="1"></button><button data-l="2"></button><button data-l="3"></button>
|
||
</div>
|
||
<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);
|
||
const mtEl = el(".mt"), qEl = el(".mt-q"), aEl = el(".mt-a"), fbEl = el(".mt-fb"),
|
||
scoreEl = el(".mt-score"), doneEl = el(".mt-done"),
|
||
sndBtn = el(".lg-snd"), modeBtn = el(".lg-mode"),
|
||
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++;
|
||
fbEl.textContent = T("mGood");
|
||
play(sndWin);
|
||
confetti(el(".mt-card"));
|
||
setTimeout(next, 1000);
|
||
}
|
||
|
||
function gen(){
|
||
const L = S.level;
|
||
let a, b, ans, txt;
|
||
if(mode==="add"){
|
||
const max = 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 = 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 = L===1 ? 5 : L===2 ? 10 : 12;
|
||
a = 1+rnd(tmax); b = 1+rnd(L===3 ? 12 : 10); ans = a*b;
|
||
txt = `${a} × ${b} =`;
|
||
}else if(mode==="div"){
|
||
const tmax = L===1 ? 5 : L===2 ? 10 : 12;
|
||
const t = 1+rnd(tmax), q = 1+rnd(L===3 ? 12 : 10);
|
||
a = t*q; ans = q;
|
||
txt = `${a} ${S.lang==="nl" || LANG==="nl" ? ":" : "÷"} ${t} =`;
|
||
}else{ /* pct */
|
||
const sets = 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,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);
|
||
}
|
||
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");
|
||
fbEl.textContent = T("mGood");
|
||
play(sndGood);
|
||
confetti(el(".mt-card"));
|
||
setTimeout(next, 900);
|
||
}else{
|
||
S.tries++;
|
||
aEl.classList.add("shake");
|
||
setTimeout(()=>aEl.classList.remove("shake"), 400);
|
||
play(sndBad);
|
||
S.input = ""; aEl.textContent = "";
|
||
if(S.tries >= 3){
|
||
S.busy = true;
|
||
fbEl.textContent = `${T("mAnsWas")} ${S.q.ans}`;
|
||
fbEl.classList.add("ans");
|
||
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<4){ 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.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();
|
||
return {
|
||
getState: ()=>({ level:S.level, snd:S.snd, amode:S.amode }),
|
||
setState: st=>{ if(st){ S.level=st.level||1; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; labels(); newQuestion(); } }
|
||
};
|
||
}
|
||
|