diff --git a/VERSION b/VERSION index 52b739a..119e317 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.52-beta +0.3.53-beta diff --git a/public/img/money/SOURCES.md b/public/img/money/SOURCES.md index 118234b..9aba54f 100644 --- a/public/img/money/SOURCES.md +++ b/public/img/money/SOURCES.md @@ -3,6 +3,6 @@ Deze afbeeldingen worden lokaal meegeleverd, zodat de geldwidget ook zonder internetverbinding werkt. - **Munten (1, 2, 5, 10, 20 en 50 cent, €1 en €2):** actuele gemeenschappelijke zijden van de Europese Commissie, gedownload van [Common sides of euro coins](https://economy-finance.ec.europa.eu/euro/euro-coins-and-notes/euro-coins/common-sides-euro-coins_en) op 15 juli 2026. De tweedimensionale afbeeldingen zijn eigendom van de Europese Unie en worden hier educatief gebruikt volgens de reproductievoorwaarden van de Europese Commissie. -- **Bankbiljetten (€5, €10, €20 en €50):** voorzijde van de Europa-serie met Lagarde-handtekening uit het officiële [ECB-pakket met 72-dpi SPECIMEN-afbeeldingen](https://www.ecb.europa.eu/euro/shared/img/euro_banknotes_specimen_72dpi.zip), gedownload op 15 juli 2026. De zichtbare SPECIMEN-markering en originele 72-dpi-bestanden worden behouden volgens de [ECB-reproductieregels](https://www.ecb.europa.eu/euro/banknotes/images/html/index.en.html). +- **Oefenbiljetten (€5, €10, €20 en €50):** eigen vectorontwerpen voor Teach. Alleen de herkenbare waardekleuren en onderlinge verhoudingen volgen de [ECB-uitleg over eurobiljetten](https://www.ecb.europa.eu/euro/visually/html/index.en.html). De illustraties, patronen, serienummers en opschriften zijn nieuw ontworpen en stellen geen officiële eurobiljetten voor. -De bestandsnamen gebruiken centen als waarde, bijvoorbeeld coin-200.png voor €2 en note-500.jpg voor €5. +De bestandsnamen gebruiken centen als waarde, bijvoorbeeld coin-200.png voor €2 en note-500.svg voor €5. diff --git a/public/img/money/note-1000.jpg b/public/img/money/note-1000.jpg deleted file mode 100644 index 1ce44c1..0000000 Binary files a/public/img/money/note-1000.jpg and /dev/null differ diff --git a/public/img/money/note-1000.svg b/public/img/money/note-1000.svg new file mode 100644 index 0000000..0a242ca --- /dev/null +++ b/public/img/money/note-1000.svg @@ -0,0 +1,52 @@ + + + + + + + + ×− ÷ + + + + + + + + + + + + + + + + OEFENGELD + TEACH 2026 · B10 + 10 + EURO + €10 + GEEN BETAALMIDDEL + LEREN · TELLEN · REKENEN + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/money/note-2000.jpg b/public/img/money/note-2000.jpg deleted file mode 100644 index 8898bbf..0000000 Binary files a/public/img/money/note-2000.jpg and /dev/null differ diff --git a/public/img/money/note-2000.svg b/public/img/money/note-2000.svg new file mode 100644 index 0000000..80f9e57 --- /dev/null +++ b/public/img/money/note-2000.svg @@ -0,0 +1,52 @@ + + + + + + + + ×− ÷ + + + + + + + + + + + + + + + + OEFENGELD + TEACH 2026 · C20 + 20 + EURO + €20 + GEEN BETAALMIDDEL + LEREN · TELLEN · REKENEN + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/money/note-500.jpg b/public/img/money/note-500.jpg deleted file mode 100644 index ad85f93..0000000 Binary files a/public/img/money/note-500.jpg and /dev/null differ diff --git a/public/img/money/note-500.svg b/public/img/money/note-500.svg new file mode 100644 index 0000000..d8e2cf8 --- /dev/null +++ b/public/img/money/note-500.svg @@ -0,0 +1,52 @@ + + + + + + + + ×− ÷ + + + + + + + + + + + + + + + + OEFENGELD + TEACH 2026 · A05 + 5 + EURO + €5 + GEEN BETAALMIDDEL + LEREN · TELLEN · REKENEN + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/money/note-5000.jpg b/public/img/money/note-5000.jpg deleted file mode 100644 index c42b6d8..0000000 Binary files a/public/img/money/note-5000.jpg and /dev/null differ diff --git a/public/img/money/note-5000.svg b/public/img/money/note-5000.svg new file mode 100644 index 0000000..1543b52 --- /dev/null +++ b/public/img/money/note-5000.svg @@ -0,0 +1,52 @@ + + + + + + + + ×− ÷ + + + + + + + + + + + + + + + + OEFENGELD + TEACH 2026 · D50 + 50 + EURO + €50 + GEEN BETAALMIDDEL + LEREN · TELLEN · REKENEN + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/js/core.js b/public/js/core.js index 8a39ff6..7baa20c 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -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.52-beta"; +const VERSION = "0.3.53-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/public/js/widgets/learning.js b/public/js/widgets/learning.js index eeed669..146a56f 100644 --- a/public/js/widgets/learning.js +++ b/public/js/widgets/learning.js @@ -62,7 +62,7 @@ function mountMoney(root,init){ 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';const type=value>=500?'note':'coin',extension=value>=500?'jpg':'png';el.className='money-piece '+type;el.dataset.value=value;const image=document.createElement('img'),fallback=document.createElement('span');image.className='money-piece-image';image.src=`/img/money/${type}-${value}.${extension}`;image.alt='';image.draggable=false;image.decoding='async';image.setAttribute('aria-hidden','true');image.onerror=()=>el.classList.add('image-missing');fallback.className='money-piece-fallback';fallback.textContent=value>=100?'€'+value/100:value+'c';el.append(image,fallback);el.setAttribute('aria-label',format(value));return el}; + const piece=(value,interactive=false)=>{const el=document.createElement(interactive?'button':'span');if(interactive)el.type='button';const type=value>=500?'note':'coin',extension=value>=500?'svg':'png';el.className='money-piece '+type;el.dataset.value=value;const image=document.createElement('img'),fallback=document.createElement('span');image.className='money-piece-image';image.src=`/img/money/${type}-${value}.${extension}`;image.alt='';image.draggable=false;image.decoding='async';image.setAttribute('aria-hidden','true');image.onerror=()=>el.classList.add('image-missing');fallback.className='money-piece-fallback';fallback.textContent=value>=100?'€'+value/100:value+'c';el.append(image,fallback);el.setAttribute('aria-label',format(value));return el}; const pile=(values,interactive=false,wallet=null)=>{const wrap=document.createElement('div');wrap.className='money-pile'+(interactive?' interactive':' readonly');values.forEach((value,index)=>{const el=piece(value,interactive);if(interactive){el.title=T('moneyRemove');el.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const startX=e.clientX,startY=e.clientY,clone=el.cloneNode(true);clone.id='dragClone';clone.classList.add('money-drag-clone','from-wallet');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();return ev.clientX>=r.left&&ev.clientX<=r.right&&ev.clientY>=r.top&&ev.clientY<=r.bottom},hasMoved=ev=>Math.hypot(ev.clientX-startX,ev.clientY-startY)>=6;moveTo(e);const move=ev=>{moveTo(ev);wallet.classList.toggle('money-remove-target',hasMoved(ev)&&!over(ev))},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);wallet.classList.remove('money-remove-target');clone.remove()},remove=()=>{S.selected.splice(index,1);S.feedback='';draw()},up=ev=>{const moved=hasMoved(ev),outside=!over(ev);clean();if(!moved||(moved&&outside))remove()},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)};el.onkeydown=e=>{if((e.key==='Enter'||e.key===' '||e.key==='Delete'||e.key==='Backspace')&&!S.solved){e.preventDefault();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}; diff --git a/test/widgets.test.js b/test/widgets.test.js index 6440744..5a48fc1 100644 --- a/test/widgets.test.js +++ b/test/widgets.test.js @@ -131,9 +131,17 @@ test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', assert.ok(moneySource.includes("/img/money/")); assert.ok(moneySource.includes("image.draggable=false")); assert.doesNotMatch(moneySource,/money-(?:arch|hologram|serial|stars|map|unit)/); - for(const file of ['coin-1.png','coin-2.png','coin-5.png','coin-10.png','coin-20.png','coin-50.png','coin-100.png','coin-200.png','note-500.jpg','note-1000.jpg','note-2000.jpg','note-5000.jpg']){ + for(const file of ['coin-1.png','coin-2.png','coin-5.png','coin-10.png','coin-20.png','coin-50.png','coin-100.png','coin-200.png']){ const asset=await readFile('public/img/money/'+file); assert.ok(asset.length>5000,file); } + assert.ok(moneySource.includes("extension=value>=500?'svg':'png'")); + for(const file of ['note-500.svg','note-1000.svg','note-2000.svg','note-5000.svg']){ + const asset=await readFile('public/img/money/'+file,'utf8'); + assert.match(asset,/]+viewBox=/,file); + assert.match(asset,/>OEFENGELD<\/text>/,file); + assert.match(asset,/>GEEN BETAALMIDDEL<\/text>/,file); + assert.doesNotMatch(asset,/SPECIMEN/i,file); + } assert.match(board,/id:"money"[^\n]*w:580, h:560, minW:380, minH:360/); });