Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
6 changed files with 94 additions and 8 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.49-beta
|
||||
0.3.50-beta
|
||||
|
|
|
|||
|
|
@ -1452,7 +1452,19 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
|
|||
.edu{height:100%;display:flex;flex-direction:column;background:var(--surface);color:var(--ink)}.edu-settings{flex:none;background:var(--surface-2);border-bottom:1px solid var(--line)}.edu-settings summary{cursor:pointer;padding:9px 12px;font-weight:800;font-size:13px}.edu-controls{display:flex;align-items:end;gap:8px;flex-wrap:wrap;padding:0 12px 10px}.edu-controls label{display:flex;flex-direction:column;gap:3px;font-size:11px;font-weight:800;color:var(--muted)}.edu-controls input,.edu-controls select,.edu-controls textarea{min-height:34px;border:1px solid var(--line);border-radius:9px;padding:5px 8px;background:var(--surface);color:var(--ink);font:inherit}.edu-controls textarea{height:65px;resize:vertical}.edu-stage{flex:1;min-height:0;overflow:auto;padding:16px;display:flex;align-items:center;justify-content:center}
|
||||
.fraction-wall{width:100%;display:flex;flex-direction:column;gap:5px}.fraction-wall>div{display:flex;gap:4px}.fraction-piece{flex:1;min-height:34px;border:2px solid hsl(var(--fraction-hue) 55% 43%);border-radius:5px;background:hsl(var(--fraction-hue) 75% 90%);font-weight:800;color:#20304a}.fraction-piece.selected{background:hsl(var(--fraction-hue) 70% 55%);color:#fff}
|
||||
.place-grid{display:grid;grid-template-columns:repeat(6,1fr);width:100%;border:1px solid var(--line);border-radius:14px;overflow:hidden}.place-grid div{text-align:center;border-right:1px solid var(--line)}.place-grid b{display:block;padding:8px;background:var(--surface-2);font-size:12px}.place-grid output{display:block;padding:18px 4px;font-size:clamp(25px,5vw,48px);font-weight:900;color:var(--blue)}
|
||||
.money{text-align:center;display:flex;align-items:center;justify-content:center;gap:9px;flex-wrap:wrap}.money h3{flex:1 0 100%;font-size:25px}.money .tbtn{border-radius:50%;min-width:62px}.money output{flex:1 0 100%;font-size:32px;font-weight:900;color:var(--green)}
|
||||
.money-app{width:100%;min-height:100%;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)}
|
||||
.money-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.money-level-control{display:flex;align-items:center;gap:6px}.money-level-control>span{color:var(--muted);font-size:12px;font-weight:900}.money-levels button{min-width:31px;padding:6px 9px;white-space:nowrap}.money-mode{width:36px;padding:0;font-size:18px}.money-score{margin-left:auto;padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900}
|
||||
.money-task-card{width:min(100%,650px);margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px solid var(--line);border-radius:20px;background:var(--surface);box-shadow:var(--shadow-1)}.money-task-card h3{margin:0;text-align:center;font-size:clamp(19px,3vw,25px)}
|
||||
.money-target{width:100%;min-height:98px;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;padding:12px;border-radius:16px;background:var(--surface-2)}.money-target>span:first-child{flex-basis:100%;color:var(--muted);font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.5px}.money-assignment{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;text-align:center}.money-task-amount{font-size:clamp(45px,10vw,72px);line-height:1;font-weight:950;color:var(--green)}.money-task-words{flex-basis:100%;margin:0;font-size:clamp(21px,4vw,31px);line-height:1.15;font-weight:950;color:var(--purple)}.money-assignment.compact .money-task-amount{font-size:clamp(38px,8vw,58px)}.money-assignment.compact .money-task-words{font-size:clamp(19px,4vw,27px)}
|
||||
.money-wallet{width:100%;min-height:112px;padding:10px;border:3px dashed var(--line);border-radius:18px;background:linear-gradient(145deg,var(--surface-2),var(--surface));transition:border-color .15s,background .15s,transform .15s}.money-wallet>div:first-child{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.money-wallet output{font-size:24px;font-weight:950;color:var(--green)}.money-wallet>p{margin:22px auto;color:var(--muted);font-weight:800;text-align:center}.money-wallet.droptarget{border-color:var(--green);background:#e4f6eb;transform:scale(1.01)}
|
||||
.money-pile{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;min-height:54px}.money-piece{display:grid;place-items:center;flex:none;border:2px solid rgba(35,45,60,.3);font:900 13px/1 system-ui;color:#26354a;box-shadow:0 3px 5px rgba(20,35,60,.2);user-select:none}.money-piece:is(button){cursor:grab;touch-action:none}.money-piece:is(button):active{cursor:grabbing}.money-piece.coin{width:48px;height:48px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#fff3a9,#d5a72e 68%,#a87812)}.money-piece.coin[data-value="100"]{background:radial-gradient(circle,#e9edf0 0 46%,#d7ad37 48% 70%,#ffec92)}.money-piece.coin[data-value="200"]{background:radial-gradient(circle,#e6bd4b 0 45%,#dfe4e8 48% 71%,#fff)}.money-piece.coin[data-value="1"],.money-piece.coin[data-value="2"],.money-piece.coin[data-value="5"]{background:radial-gradient(circle at 35% 30%,#ffd1b1,#b9683d 72%,#824125)}.money-piece.note{width:76px;height:43px;border-radius:7px;background:linear-gradient(135deg,#c9c9c2,#f0eee2);font-size:15px}.money-piece.note[data-value="500"]{background:linear-gradient(135deg,#a6b7a3,#e7ddd0)}.money-piece.note[data-value="1000"]{background:linear-gradient(135deg,#d09a91,#f4d2b5)}.money-piece.note[data-value="2000"]{background:linear-gradient(135deg,#7eb2c4,#d2e9d8)}.money-piece.note[data-value="5000"]{background:linear-gradient(135deg,#d99f58,#f1d6a4)}.money-drag-clone{opacity:.95}
|
||||
.money-tray-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:8px}.money-tray-wrap>b{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.4px}.money-cash-tray{width:100%;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;padding:11px;border:1px solid var(--line);border-radius:16px;background:var(--surface-2)}
|
||||
.money-count-task{width:100%;min-height:128px;display:grid;place-items:center;padding:14px;border-radius:17px;background:linear-gradient(145deg,#eef7e9,var(--surface-2));border:1px solid var(--line)}.money-count-task .money-piece{transform:rotate(var(--money-tilt,0deg))}
|
||||
.money-shop-task{width:100%;display:grid;grid-template-columns:minmax(0,1fr) minmax(150px,.8fr);gap:12px}.money-product,.money-paid{min-height:150px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:12px;border:1px solid var(--line);border-radius:17px;background:var(--surface-2)}.money-product>span{font-size:48px}.money-product>b,.money-paid>b{color:var(--muted);font-size:12px;text-transform:uppercase}.money-product .money-task-amount{font-size:37px}.money-product .money-task-words{font-size:19px}.money-paid>output{font-size:31px;font-weight:950;color:var(--blue)}
|
||||
.money-digit-answer,.money-type-answer{width:min(100%,300px);display:flex;flex-direction:column;align-items:center;gap:10px}.money-digit-answer>p,.money-type-answer>p{margin:0;color:var(--muted);font-weight:800;text-align:center}.money-answer-slots{display:flex;align-items:center;justify-content:center;gap:6px}.money-answer-slots>strong{font-size:29px}.money-digit-slot.slot.sm{width:43px;height:64px;padding-top:1px}.money-digit-slot.slot.sm .ghost{height:24px;font-size:20px}.money-digit-slot.slot.sm .slot-dots{margin-bottom:3px}.money-digit-slot.filled{padding:0}.money-digit-slot.fixed{border-style:solid;background:var(--surface-2);opacity:.72}.money-digit-block.blok{width:39px;height:58px;min-height:58px;padding-top:2px;cursor:default}.money-digit-block.blok .bl{height:28px;font-size:25px;color:var(--purple)}.money-digit-block.blok .blok-dots{margin-bottom:4px}.money-digit-tray.lg-tray{display:grid;grid-template-columns:repeat(5,39px);gap:7px;min-height:0;padding:11px 14px}.money-digit-tray .money-drag-digit{cursor:grab}.money-digit-tray .money-drag-digit:active{cursor:grabbing}.money-digit-slot.droptarget{border-color:var(--green);background:#e3f6ea;transform:scale(1.06)}
|
||||
.money-type-display{min-width:190px;padding:9px 14px;border:3px dashed var(--line);border-radius:14px;background:var(--surface-2);font-size:34px;font-weight:950;letter-spacing:2px;color:var(--green)}.money-keypad{width:100%;display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.money-keypad .tbtn{min-width:0;padding:0;font-size:20px}
|
||||
.money-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.money-feedback.ok{color:var(--green)}.money-feedback.bad{color:var(--red)}.money-actions{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}.money-actions .tbtn:not(.ghost){background:var(--green);color:#fff}
|
||||
@media(max-width:520px){.money-task-card{padding:12px}.money-level-control>span{display:none}.money-top{gap:5px}.money-shop-task{grid-template-columns:1fr}.money-product,.money-paid{min-height:120px}.money-cash-tray{gap:6px;padding:9px}.money-piece.coin{width:44px;height:44px}.money-piece.note{width:69px;height:40px}}
|
||||
.wordtypes{display:flex;gap:9px;flex-wrap:wrap;justify-content:center}.wordtypes button{border:0;border-radius:10px;padding:10px 13px;font:inherit;font-weight:900;background:var(--surface-2)}.wordtypes button[data-type=noun]{background:#dce9ff;color:#174c9b}.wordtypes button[data-type=verb]{background:#dcf7e7;color:#176b3a}.wordtypes button[data-type=adjective]{background:#fff0cf;color:#855b00}.wordtypes p{flex-basis:100%;text-align:center;color:var(--muted);font-size:12px}
|
||||
.schedule{width:min(100%,450px);display:flex;flex-direction:column;gap:7px}.schedule>div{display:flex;align-items:center;gap:12px;padding:9px;background:var(--surface-2);border-radius:10px}.schedule time{font-weight:900;color:var(--blue)}.schedule span{flex:1;font-weight:800}.schedule .tbtn{height:30px;min-height:30px;padding:0 9px}.mood{display:flex;gap:8px;flex-wrap:wrap}.mood .tbtn{font-size:36px;height:auto;padding:12px}.mood span{display:block;font-size:14px}.poll{width:min(100%,440px);display:flex;flex-direction:column;gap:8px}.poll h3{text-align:center}.poll .tbtn{position:relative;overflow:hidden;text-align:left}.poll .tbtn i{position:absolute;inset:0 auto 0 0;background:var(--accent-soft);z-index:0}.poll .tbtn span{position:relative;float:right;font-weight:900}.poll .tbtn{isolation:isolate}
|
||||
@media(max-width:600px){.gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.gcard{min-height:120px;padding:13px 8px}.gcard .gd{display:none}.widget .wdup,.widget .wpreview{display:none}.edu-stage{padding:10px}.place-grid b{font-size:9px}.place-grid output{padding:12px 2px}}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const REGISTRY = [
|
|||
{ id:"fractions", cat:"rekenen", icon:"🍫", w:600, h:480, minW:380, minH:320, mount:mountFractions },
|
||||
{ id:"clock", cat:"rekenen", icon:"🕙", w:500, h:500, minW:340, minH:340, mount:mountClock },
|
||||
{ id:"placevalue", cat:"rekenen", icon:"🔢", w:610, h:390, minW:390, minH:280, mount:mountPlacevalue },
|
||||
{ id:"money", cat:"rekenen", icon:"🪙", w:520, h:430, minW:360, minH:300, mount:mountMoney },
|
||||
{ id:"money", cat:"rekenen", icon:"🪙", w:580, h:560, minW:380, minH:360, mount:mountMoney },
|
||||
{ id:"wordtypes", cat:"taal", icon:"🎨", w:600, h:410, minW:380, minH:290, mount:mountWordtypes },
|
||||
{ id:"write", cat:"tools", icon:"✏️", w:560, h:440, minW:360, minH:280, mount:mountWrite },
|
||||
{ id:"mind", cat:"tools", icon:"🕸️", w:640, h:480, minW:420, minH:320, mount:mountMind },
|
||||
|
|
|
|||
|
|
@ -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.3.49-beta";
|
||||
const VERSION = "0.3.50-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -118,7 +118,15 @@ const I18N = {
|
|||
wg_fractions:"Breukenmuur", wg_fractions_d:"Vergelijk breuken visueel en markeer gelijke delen.",
|
||||
wg_clock:"Oefenklok", wg_clock_d:"Leer kloktijden op vier niveaus met meerdere leerwijzen en spellen.",
|
||||
wg_placevalue:"Plaatswaardetabel", wg_placevalue_d:"Ontdek de waarde van cijfers tot honderdduizend.",
|
||||
wg_money:"Geldrekenen", wg_money_d:"Stel bedragen samen met euromunten.",
|
||||
wg_money:"Geldrekenen", wg_money_d:"Oefen bedragen maken, geld tellen en wisselgeld op drie niveaus.",
|
||||
moneyLearningStyle:"Opdracht tonen", moneyStyleAmount:"Als bedrag", moneyStyleWords:"In woorden", moneyStyleBoth:"Bedrag en woorden",
|
||||
moneyGame:"Spel", moneyGameMake:"Bedrag maken", moneyGameCount:"Geld tellen", moneyGameChange:"Wisselgeld",
|
||||
moneyLevel:"Niveau", moneyLevel1Hint:"Niveau 1: hele euro’s tot € 9", moneyLevel2Hint:"Niveau 2: tot € 10 in stappen van 5 cent", moneyLevel3Hint:"Niveau 3: tot € 50 met alle centen, munten en biljetten",
|
||||
moneyScore:"Score: {score} · reeks: {streak}", moneyMakeTitle:"Maak het gevraagde bedrag", moneyCountTitle:"Hoeveel geld zie je?", moneyChangeTitle:"Bereken het wisselgeld",
|
||||
moneyYourTask:"Jouw opdracht", moneyPrice:"Prijs", moneyPaid:"Betaald", moneyWallet:"Jouw geld", moneyWalletHint:"Sleep geld hierheen of tik op een munt of biljet.", moneyTray:"Munten en biljetten",
|
||||
moneyBuildHint:"Sleep de cijfers naar de vakken en maak het bedrag.", moneyTypeHint:"Voer het bedrag in met een komma.", moneyIncomplete:"Maak eerst het volledige bedrag af.",
|
||||
moneyCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", moneyTryAgain:"Bijna! Reken het bedrag nog eens na.", moneyNeed:"Nog {amount} nodig.", moneyTooMuch:"{amount} te veel.",
|
||||
moneyClear:"Wissen", moneyNextTask:"Volgende opdracht",
|
||||
wg_wordtypes:"Woordsoorten", wg_wordtypes_d:"Geef woorden kleur per woordsoort.",
|
||||
wg_schedule:"Dagplanning", wg_schedule_d:"Maak een duidelijke visuele planning voor de klas.",
|
||||
wg_mood:"Gevoelsmeter", wg_mood_d:"Doe een snelle, anonieme check-in met de klas.",
|
||||
|
|
@ -317,7 +325,15 @@ const I18N = {
|
|||
wg_fractions:"Fraction wall", wg_fractions_d:"Compare fractions visually and mark equal parts.",
|
||||
wg_clock:"Practice clock", wg_clock_d:"Learn time at four levels with multiple learning styles and games.",
|
||||
wg_placevalue:"Place value", wg_placevalue_d:"Explore digit values up to hundred thousands.",
|
||||
wg_money:"Money", wg_money_d:"Build amounts using euro coins.",
|
||||
wg_money:"Money", wg_money_d:"Practise making amounts, counting money and change at three levels.",
|
||||
moneyLearningStyle:"Show task as", moneyStyleAmount:"As an amount", moneyStyleWords:"In words", moneyStyleBoth:"Amount and words",
|
||||
moneyGame:"Game", moneyGameMake:"Make an amount", moneyGameCount:"Count money", moneyGameChange:"Change",
|
||||
moneyLevel:"Level", moneyLevel1Hint:"Level 1: whole euros up to €9", moneyLevel2Hint:"Level 2: up to €10 in five-cent steps", moneyLevel3Hint:"Level 3: up to €50 with all cents, coins and notes",
|
||||
moneyScore:"Score: {score} · streak: {streak}", moneyMakeTitle:"Make the requested amount", moneyCountTitle:"How much money do you see?", moneyChangeTitle:"Work out the change",
|
||||
moneyYourTask:"Your task", moneyPrice:"Price", moneyPaid:"Paid", moneyWallet:"Your money", moneyWalletHint:"Drag money here or tap a coin or note.", moneyTray:"Coins and notes",
|
||||
moneyBuildHint:"Drag the digits into the slots to make the amount.", moneyTypeHint:"Enter the amount with a decimal point.", moneyIncomplete:"Complete the full amount first.",
|
||||
moneyCorrect:"Well done! Choose the next task. 🌟", moneyTryAgain:"Almost! Check the amount again.", moneyNeed:"You still need {amount}.", moneyTooMuch:"{amount} too much.",
|
||||
moneyClear:"Clear", moneyNextTask:"Next task",
|
||||
wg_wordtypes:"Word classes", wg_wordtypes_d:"Colour words by their word class.",
|
||||
wg_schedule:"Day schedule", wg_schedule_d:"Create a clear visual class schedule.",
|
||||
wg_mood:"Mood check-in", wg_mood_d:"Run a quick anonymous class check-in.",
|
||||
|
|
|
|||
|
|
@ -46,9 +46,49 @@ function mountPlacevalue(root,init){
|
|||
const draw=()=>{S.number=Math.max(0,Math.min(999999,+input.value||0));const digits=String(S.number).padStart(6,'0').split('');stage.className='place-grid';stage.innerHTML='';['HD','TD','D','H','T','E'].forEach((x,i)=>{const d=document.createElement('div');const b=document.createElement('b');b.textContent=x;const o=document.createElement('output');o.textContent=digits[i];d.append(b,o);stage.append(d)})};input.oninput=draw;learningShell(root,[field(T('placeNumber'),input),btn('🎲',()=>{input.value=Math.floor(Math.random()*999999);draw()})],stage);draw();return{getState:()=>({...S})};
|
||||
}
|
||||
function mountMoney(root,init){
|
||||
const S=Object.assign({target:3.75,total:0},init||{}),stage=document.createElement('div'),out=document.createElement('output');
|
||||
const draw=()=>{stage.className='money';stage.innerHTML='';const task=document.createElement('h3');task.textContent=T('moneyMake').replace('{amount}',S.target.toFixed(2));stage.append(task);[2,1,.5,.2,.1,.05].forEach(v=>stage.append(btn('€ '+v.toFixed(2),()=>{S.total=Math.round((S.total+v)*100)/100;renderTotal()})));stage.append(out,btn(T('sentenceClear'),()=>{S.total=0;renderTotal()}),btn(T('numberlineCheck'),()=>{S.total===S.target?sndGood():sndBad()},''));renderTotal()};const renderTotal=()=>out.textContent='€ '+S.total.toFixed(2);learningShell(root,[btn(T('numberlineNew'),()=>{S.target=(50+Math.floor(Math.random()*950))/100;S.total=0;draw()})],stage);draw();return{getState:()=>({...S})};
|
||||
const legacy=init||{},S=Object.assign({level:1,style:'both',game:'make',amode:'blocks',targetCents:375,paidCents:500,selected:[],given:[],input:'',answerDigits:[0,null,0,0],score:0,streak:0,solved:false,feedback:'',product:'🧃'},legacy);
|
||||
if(legacy.targetCents==null&&Number.isFinite(legacy.target))S.targetCents=Math.round(legacy.target*100);
|
||||
S.level=Math.max(1,Math.min(3,+S.level||1));S.style=['amount','words','both'].includes(S.style)?S.style:'both';S.game=['make','count','change'].includes(S.game)?S.game:'make';S.amode=S.amode==='type'?'type':'blocks';S.selected=Array.isArray(S.selected)?S.selected.map(Number).filter(Number.isFinite):[];S.given=Array.isArray(S.given)?S.given.map(Number).filter(Number.isFinite):[];
|
||||
const style=document.createElement('select'),game=document.createElement('select'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
|
||||
add(style,[['amount','moneyStyleAmount'],['words','moneyStyleWords'],['both','moneyStyleBoth']]);add(game,[['make','moneyGameMake'],['count','moneyGameCount'],['change','moneyGameChange']]);style.value=S.style;game.value=S.game;
|
||||
const styleSetting=field(T('moneyLearningStyle'),style),gameSetting=field(T('moneyGame'),game),syncSettings=()=>{styleSetting.hidden=S.game==='count'};
|
||||
const rnd=max=>Math.floor(Math.random()*max),format=cents=>{const value=(Math.max(0,cents)/100).toFixed(2);return '€ '+(LANG==='nl'?value.replace('.',','):value)};
|
||||
const nlSmall=['nul','een','twee','drie','vier','vijf','zes','zeven','acht','negen','tien','elf','twaalf','dertien','veertien','vijftien','zestien','zeventien','achttien','negentien'],nlTens=['','','twintig','dertig','veertig','vijftig','zestig','zeventig','tachtig','negentig'];
|
||||
const enSmall=['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen'],enTens=['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety'];
|
||||
const numberWords=value=>{const n=Math.max(0,Math.min(99,value));if(LANG==='nl'){if(n<20)return nlSmall[n];const ones=n%10;return ones?nlSmall[ones]+nlTens[Math.floor(n/10)]:nlTens[Math.floor(n/10)]}if(n<20)return enSmall[n];const ones=n%10;return enTens[Math.floor(n/10)]+(ones?'-'+enSmall[ones]:'')};
|
||||
const amountWords=cents=>{const euros=Math.floor(cents/100),rest=cents%100;if(LANG==='nl')return numberWords(euros)+' euro'+(rest?' en '+numberWords(rest)+' cent':'');return numberWords(euros)+' euro'+(euros===1?'':'s')+(rest?' and '+numberWords(rest)+' cent'+(rest===1?'':'s'):'')};
|
||||
const denominations=()=>S.level===1?[200,100]:S.level===2?[500,200,100,50,20,10,5]:[2000,1000,500,200,100,50,20,10,5,2,1];
|
||||
const randomTarget=()=>S.level===1?(1+rnd(9))*100:S.level===2?(10+rnd(190))*5:1+rnd(4999);
|
||||
const paymentFor=price=>{const options=S.level===1?[500,1000]:S.level===2?[500,1000,2000]:[100,200,500,1000,2000,5000];return options.find(value=>value>price)||Math.ceil((price+1)/5000)*5000};
|
||||
const breakdown=amount=>{const result=[];let rest=amount;denominations().forEach(value=>{while(rest>=value&&result.length<24){result.push(value);rest-=value}});return result.sort(()=>Math.random()-.5)};
|
||||
const fixedIndices=()=>S.level===1?[0,2,3]:S.level===2?[0]:[],emptyDigits=()=>S.level===1?[0,null,0,0]:S.level===2?[0,null,null,null]:[null,null,null,null];
|
||||
const piece=(value,interactive=false)=>{const el=document.createElement(interactive?'button':'span');if(interactive)el.type='button';el.className='money-piece '+(value>=500?'note':'coin');el.dataset.value=value;el.textContent=value>=100?'€'+value/100:value+'c';el.setAttribute('aria-label',format(value));return el};
|
||||
const pile=(values,interactive=false)=>{const wrap=document.createElement('div');wrap.className='money-pile'+(interactive?' interactive':' readonly');values.forEach((value,index)=>{const el=piece(value,interactive);if(interactive)el.onclick=()=>{if(!S.solved){S.selected.splice(index,1);S.feedback='';draw()}};wrap.append(el)});return wrap};
|
||||
const amountDisplay=(cents,compact=false)=>{const wrap=document.createElement('div');wrap.className='money-assignment'+(compact?' compact':'');if(['amount','both'].includes(S.style)){const output=document.createElement('output');output.className='money-task-amount';output.textContent=format(cents);wrap.append(output)}if(['words','both'].includes(S.style)){const words=document.createElement('p');words.className='money-task-words';words.textContent=amountWords(cents);wrap.append(words)}return wrap};
|
||||
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm money-digit-block';block.dataset.digit=value;const label=document.createElement('span');label.className='bl';label.textContent=String(value);const dots=dotGrid(Number(value));dots.classList.add('blok-dots');block.append(label,dots);return block};
|
||||
const answerValue=()=>{if(S.amode==='blocks'){if(S.answerDigits.some(value=>value===null))return null;return Number(S.answerDigits.join(''))}if(!S.input)return null;const parts=S.input.replace('.',',').split(',');if(parts.length>2||parts[1]?.length>2)return null;return (+parts[0]||0)*100+Number((parts[1]||'').padEnd(2,'0')||0)};
|
||||
const digitAnswer=()=>{const answer=document.createElement('div');answer.className='money-digit-answer';const instruction=document.createElement('p');instruction.textContent=T('moneyBuildHint');const slots=document.createElement('div');slots.className='money-answer-slots';const fixed=new Set(fixedIndices());
|
||||
const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm money-digit-slot'+(fixed.has(index)?' fixed':'')+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent='?';const dots=dotGrid(S.answerDigits[index]===null?0:S.answerDigits[index]);dots.classList.add('slot-dots');slot.append(ghost,dots);if(S.answerDigits[index]!==null){const placed=woodDigit(S.answerDigits[index]);placed.classList.add('in-slot','money-drag-digit');slot.append(placed);if(!fixed.has(index))slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};
|
||||
const euro=document.createElement('strong');euro.textContent='€';slots.append(euro,makeSlot(0),makeSlot(1));const comma=document.createElement('strong');comma.textContent=LANG==='nl'?',':'.';slots.append(comma,makeSlot(2),makeSlot(3));
|
||||
const tray=document.createElement('div');tray.className='money-digit-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const clone=block.cloneNode(true);clone.id='dragClone';document.body.append(clone);block.classList.add('dragging');const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},hit=ev=>{let found=null;slots.querySelectorAll('.money-digit-slot:not(.fixed)').forEach(slot=>{const r=slot.getBoundingClientRect(),pad=8;if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot});return found};moveTo(e);let hovered=null;const move=ev=>{moveTo(ev);const target=hit(ev);if(hovered&&hovered!==target)hovered.classList.remove('droptarget');hovered=target;if(hovered)hovered.classList.add('droptarget')},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);clone.remove();block.classList.remove('dragging');if(hovered)hovered.classList.remove('droptarget')},up=ev=>{const target=hit(ev);clean();if(target){S.answerDigits[+target.dataset.index]=+block.dataset.digit;S.feedback='';draw()}},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)}};'0123456789'.split('').forEach(digit=>{const block=woodDigit(+digit);block.classList.add('money-drag-digit');block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',digit));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.findIndex((value,i)=>value===null&&!fixed.has(i));if(index>=0){S.answerDigits[index]=+digit;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer};
|
||||
const typeAnswer=()=>{const wrap=document.createElement('div');wrap.className='money-type-answer';const hint=document.createElement('p');hint.textContent=T('moneyTypeHint');const display=document.createElement('output');display.className='money-type-display';display.textContent='€ '+(S.input||'…');const pad=document.createElement('div');pad.className='money-keypad';['7','8','9','⌫','4','5','6',LANG==='nl'?',':'.','1','2','3','0'].forEach(key=>{const button=btn(key,()=>{if(S.solved)return;if(key==='⌫')S.input=S.input.slice(0,-1);else if(key===','||key==='.'){if(!/[,.]/.test(S.input))S.input=(S.input||'0')+key}else{const parts=S.input.split(/[,.]/);if((parts.length===1&&parts[0].length<2)||(parts.length===2&&parts[1].length<2))S.input+=key}S.feedback='';draw()});pad.append(button)});wrap.append(hint,display,pad);return wrap};
|
||||
const addMoney=value=>{if(!S.solved){S.selected.push(value);S.feedback='';draw()}};
|
||||
const moneyTray=wallet=>{const section=document.createElement('section');section.className='money-tray-wrap';const label=document.createElement('b');label.textContent=T('moneyTray');const tray=document.createElement('div');tray.className='money-cash-tray';denominations().forEach(value=>{const item=piece(value,true);item.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const startX=e.clientX,startY=e.clientY,clone=item.cloneNode(true);clone.id='dragClone';clone.classList.add('money-drag-clone');document.body.append(clone);const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},over=ev=>{const r=wallet.getBoundingClientRect(),pad=12;return ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad};moveTo(e);const move=ev=>{moveTo(ev);wallet.classList.toggle('droptarget',over(ev))},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);wallet.classList.remove('droptarget');clone.remove()},up=ev=>{const tapped=Math.hypot(ev.clientX-startX,ev.clientY-startY)<6,target=over(ev);clean();if(tapped||target)addMoney(value)},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)};item.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();addMoney(value)}};tray.append(item)});section.append(label,tray);return section};
|
||||
const expected=()=>S.game==='change'?S.paidCents-S.targetCents:S.targetCents,setFeedback=ok=>{if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()};
|
||||
const check=()=>{const value=S.game==='make'?S.selected.reduce((sum,item)=>sum+item,0):answerValue();if(value===null){S.feedback='incomplete';sndBad();draw();return}setFeedback(value===expected())};
|
||||
const feedbackText=()=>{if(S.feedback==='good')return T('moneyCorrect');if(S.feedback==='incomplete')return T('moneyIncomplete');if(S.feedback==='bad'&&S.game==='make'){const total=S.selected.reduce((sum,item)=>sum+item,0),difference=S.targetCents-total;return T(difference>0?'moneyNeed':'moneyTooMuch').replace('{amount}',format(Math.abs(difference)))}return T('moneyTryAgain')};
|
||||
const resetAnswer=()=>{S.selected=[];S.input='';S.answerDigits=emptyDigits();S.feedback='';S.solved=false;draw()};
|
||||
const newTask=()=>{let target=randomTarget();if(target===S.targetCents)target=randomTarget();S.targetCents=target;S.paidCents=paymentFor(target);S.selected=[];S.given=breakdown(target);S.input='';S.answerDigits=emptyDigits();S.solved=false;S.feedback='';S.product=['🍎','🧃','📘','🧸','⚽','✏️'][rnd(6)];draw()};
|
||||
const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;newTask()},toggleMode=()=>{S.amode=S.amode==='blocks'?'type':'blocks';S.input='';S.answerDigits=emptyDigits();S.feedback='';draw()};
|
||||
const draw=()=>{S.style=style.value;S.game=game.value;stage.className='money-app';stage.innerHTML='';const top=document.createElement('div');top.className='money-top';const levelControl=document.createElement('div');levelControl.className='money-level-control';const levelLabel=document.createElement('span');levelLabel.textContent=T('moneyLevel');const levels=document.createElement('div');levels.className='lg-levels money-levels';[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('moneyLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});levelControl.append(levelLabel,levels);top.append(levelControl);if(S.game!=='make'){const mode=btn(S.amode==='blocks'?'🧱':'⌨️',toggleMode);mode.classList.add('money-mode');mode.title=T('amode');top.append(mode)}const score=document.createElement('span');score.className='money-score';score.textContent=T('moneyScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);
|
||||
const card=document.createElement('section');card.className='money-task-card';const title=document.createElement('h3');title.textContent=T(S.game==='make'?'moneyMakeTitle':S.game==='count'?'moneyCountTitle':'moneyChangeTitle');card.append(title);
|
||||
if(S.game==='make'){const target=document.createElement('div');target.className='money-target';const label=document.createElement('span');label.textContent=T('moneyYourTask');target.append(label,amountDisplay(S.targetCents,true));card.append(target);const wallet=document.createElement('div');wallet.className='money-wallet';const walletHead=document.createElement('div');const walletLabel=document.createElement('b');walletLabel.textContent=T('moneyWallet');const total=document.createElement('output');total.textContent=format(S.selected.reduce((sum,item)=>sum+item,0));walletHead.append(walletLabel,total);wallet.append(walletHead);if(S.selected.length)wallet.append(pile(S.selected,true));else{const hint=document.createElement('p');hint.textContent=T('moneyWalletHint');wallet.append(hint)}card.append(wallet,moneyTray(wallet))}
|
||||
if(S.game==='count'){const target=document.createElement('div');target.className='money-count-task';target.append(pile(S.given));card.append(target,S.amode==='blocks'?digitAnswer():typeAnswer())}
|
||||
if(S.game==='change'){const shop=document.createElement('div');shop.className='money-shop-task';const product=document.createElement('div');product.className='money-product';const emoji=document.createElement('span');emoji.textContent=S.product;const priceLabel=document.createElement('b');priceLabel.textContent=T('moneyPrice');product.append(emoji,priceLabel,amountDisplay(S.targetCents,true));const paid=document.createElement('div');paid.className='money-paid';const paidLabel=document.createElement('b');paidLabel.textContent=T('moneyPaid');const paidAmount=document.createElement('output');paidAmount.textContent=format(S.paidCents);paid.append(paidLabel,paidAmount,pile([S.paidCents]));shop.append(product,paid);card.append(shop,S.amode==='blocks'?digitAnswer():typeAnswer())}
|
||||
const feedback=document.createElement('p');feedback.className='money-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');feedback.textContent=S.feedback?feedbackText():'';card.append(feedback);const actions=document.createElement('div');actions.className='money-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));if(!S.solved)actions.append(btn(T('moneyClear'),resetAnswer,'ghost'));actions.append(btn(T('moneyNextTask'),newTask,'ghost'));card.append(actions);stage.append(card)};
|
||||
style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};learningShell(root,[styleSetting,gameSetting],stage);syncSettings();newTask();return{getState:()=>({...S,selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})};
|
||||
}
|
||||
|
||||
function mountWordtypes(root,init){
|
||||
const S=Object.assign({sentence:'De vrolijke hond rent snel',answers:{}},init||{}),input=document.createElement('input'),stage=document.createElement('div');input.value=S.sentence;
|
||||
const types=['noun','verb','adjective','other'],draw=()=>{S.sentence=input.value.slice(0,120);stage.className='wordtypes';stage.innerHTML='';S.sentence.split(/\s+/).filter(Boolean).forEach((word,i)=>{const w=document.createElement('button');w.type='button';w.textContent=word;const type=S.answers[i]||'other';w.dataset.type=type;w.onclick=()=>{S.answers[i]=types[(types.indexOf(w.dataset.type)+1)%types.length];draw()};stage.append(w)});const legend=document.createElement('p');legend.textContent=T('wordtypeHint');stage.append(legend)};input.oninput=draw;learningShell(root,[field(T('wordtypeSentence'),input)],stage);draw();return{getState:()=>({...S})};
|
||||
|
|
|
|||
|
|
@ -101,3 +101,21 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge
|
|||
assert.match(css,/clock-top\{[^}]*position:sticky[^}]*flex-wrap:wrap/);
|
||||
assert.match(css,/clock-app\{[^}]*justify-content:flex-start/);
|
||||
});
|
||||
|
||||
test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', async()=>{
|
||||
const [source,css,core,board]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/css/teach.css','utf8'),readFile('public/js/core.js','utf8'),readFile('public/js/board.js','utf8')]);
|
||||
const moneySource=source.slice(source.indexOf('function mountMoney'),source.indexOf('function mountWordtypes'));
|
||||
for(const value of ["['make','moneyGameMake']","['count','moneyGameCount']","['change','moneyGameChange']","['amount','moneyStyleAmount']","['words','moneyStyleWords']","['both','moneyStyleBoth']","S.amode==='blocks'?'🧱':'⌨️'"])assert.ok(moneySource.includes(value),value);
|
||||
assert.ok(moneySource.includes("S.level===1?(1+rnd(9))*100:S.level===2?(10+rnd(190))*5:1+rnd(4999)"));
|
||||
assert.ok(moneySource.includes("S.level===1?[200,100]:S.level===2?[500,200,100,50,20,10,5]"));
|
||||
for(const feature of ['money-wallet','money-cash-tray','money-answer-slots','money-digit-tray','money-keypad','dragClone','droptarget','getBoundingClientRect','amountWords','paymentFor'])assert.ok(moneySource.includes(feature),feature);
|
||||
assert.ok(moneySource.includes("learningShell(root,[styleSetting,gameSetting],stage)"),'alleen voorkeuren staan onder instellingen');
|
||||
assert.ok(moneySource.includes("style.onchange=draw"));
|
||||
assert.ok(moneySource.includes("game.onchange=()=>{S.game=game.value;syncSettings();newTask()}"));
|
||||
assert.doesNotMatch(moneySource,/learningShell\(root,\[[^\]]*(?:numberlineCheck|numberlineNew|moneyClear|moneyNextTask)/);
|
||||
for(const key of ['moneyLearningStyle','moneyStyleAmount','moneyStyleWords','moneyStyleBoth','moneyGame','moneyGameMake','moneyGameCount','moneyGameChange','moneyLevel','moneyLevel1Hint','moneyLevel2Hint','moneyLevel3Hint','moneyScore','moneyMakeTitle','moneyCountTitle','moneyChangeTitle','moneyWallet','moneyTray','moneyBuildHint','moneyTypeHint','moneyCorrect','moneyNextTask'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||||
assert.match(css,/money-app\{[^}]*justify-content:flex-start/);
|
||||
assert.match(css,/money-wallet\.droptarget/);
|
||||
assert.match(css,/money-piece\.coin\[data-value="100"\]/);
|
||||
assert.match(board,/id:"money"[^\n]*w:580, h:560, minW:380, minH:360/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue