Voeg thematische kleurplaten en creatieve niveaus toe (v0.4.65-beta) #3
3 changed files with 37 additions and 5 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.66-beta
|
||||
0.3.67-beta
|
||||
|
|
|
|||
|
|
@ -653,6 +653,9 @@ function mountFractions(root,init){
|
|||
const draw=()=>{S.den=Math.max(2,Math.min(12,+den.value||4));S.show=show.checked;stage.className='fraction-wall';stage.innerHTML='';for(let d=1;d<=S.den;d++){const row=document.createElement('div');for(let n=0;n<d;n++){const c=document.createElement('button');c.type='button';c.style.setProperty('--fraction-hue',d*31);c.textContent=S.show?`1/${d}`:'';c.className='fraction-piece';c.onclick=()=>c.classList.toggle('selected');row.append(c)}stage.append(row)}};
|
||||
den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);draw();return{getState:()=>({...S})};
|
||||
}
|
||||
const clockTurnDelta=(next,previous)=>((next-previous+540)%360)-180;
|
||||
const clock24Angle=(angle,delta)=>(angle+delta+720)%720;
|
||||
const clock24Hour=(angle,minute)=>((Math.round((angle-minute/2)/30)%24)+24)%24;
|
||||
function mountClock(root,init){
|
||||
const S=Object.assign({h:12,m:0,level:1,style:'both',game:'set',amode:'blocks',input:'',linkHands:true,use24:true,task:true,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[],answerDigits:[null,null,null,null]},init||{});
|
||||
if(typeof S.level==='string')S.level={hour:1,quarter:2,five:2,minute:3}[S.level]||1;S.level=Math.max(1,Math.min(3,+S.level||1));S.amode=S.amode==='type'?'type':'blocks';S.input=String(S.input||'').replace(/\D/g,'').slice(0,4);
|
||||
|
|
@ -661,14 +664,34 @@ function mountClock(root,init){
|
|||
const minuteValues=()=>S.level===1?[0,30]:S.level===2?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i),time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
|
||||
const uses24=()=>S.level===3&&S.use24;
|
||||
const daypartKey=hour=>hour<6?'clockNight':hour<12?'clockMorning':hour<18?'clockAfternoon':'clockEvening',clockHour=hour=>hour%12||12;
|
||||
const inputHour=dialHour=>uses24()?(dialHour%12)+(S.h>=12?12:0):dialHour;
|
||||
const words=(hour,minute)=>{const shown=clockHour(hour),next=shown===12?1:shown+1;let result;if(minute===0)result=T('clockWordsHour').replace('{hour}',shown);else if(minute===15)result=T('clockWordsQuarterPast').replace('{hour}',shown);else if(minute===30)result=T('clockWordsHalf').replace('{hour}',next);else if(minute===45)result=T('clockWordsQuarterTo').replace('{hour}',next);else result=T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?shown:next);return uses24()?result+' '+T(daypartKey(hour)):result};
|
||||
const randomTime=()=>{const values=minuteValues();return{h:uses24()?Math.floor(Math.random()*24):1+Math.floor(Math.random()*12),m:values[Math.floor(Math.random()*values.length)]}};
|
||||
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm clock-digit-block';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 timeBlocks=(hour,minute,click)=>{const el=document.createElement(click?'button':'div');if(click)el.type='button';el.className='clock-time-blocks';el.setAttribute('aria-label',time(hour,minute));const digits=String(hour).padStart(2,'0')+String(minute).padStart(2,'0');el.append(woodDigit(digits[0]),woodDigit(digits[1]));const colon=document.createElement('strong');colon.textContent=':';el.append(colon,woodDigit(digits[2]),woodDigit(digits[3]));if(click)el.onclick=click;return el};
|
||||
const makeClock=(hour,minute,interactive)=>{const face=document.createElement('div');face.className='clock'+(interactive?' interactive':'');face.setAttribute('role','img');face.setAttribute('aria-label',time(hour,minute));for(let i=0;i<60;i++){const tick=document.createElement('i');tick.className=i%5===0?'clock-tick hour':'clock-tick minor';tick.style.transform=`rotate(${i*6}deg)`;face.append(tick)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const mark=document.createElement('b');mark.textContent=n;mark.style.setProperty('--clock-angle',`${i*30}deg`);face.append(mark)});const hourHand=document.createElement('i'),minuteHand=document.createElement('i'),pin=document.createElement('span');hourHand.className='clock-hand clock-hour';minuteHand.className='clock-hand clock-min';pin.className='clock-pin';const paint=()=>{hourHand.style.transform=`rotate(${S.h*30+S.m/2}deg)`;minuteHand.style.transform=`rotate(${S.m*6}deg)`};if(interactive)paint();else{hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}face.append(hourHand,minuteHand,pin);if(interactive){const move=e=>{const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2,deg=(Math.atan2(dx,-dy)*180/Math.PI+360)%360;if(face.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=minuteValues(),distance=v=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted));S.m=allowed.reduce((best,v)=>distance(v)<distance(best)?v:best)}else if(S.linkHands){let bestH=12,bestM=0,bestDistance=360;for(let hour=1;hour<=12;hour++)minuteValues().forEach(minute=>{const angle=(hour%12)*30+minute/2,difference=Math.min(Math.abs(angle-deg),360-Math.abs(angle-deg));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=inputHour(bestH);S.m=bestM}else S.h=inputHour((Math.round((deg-S.m/2)/30)+11)%12+1);paint();const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}};const start=(e,hand)=>{if(S.solved)return;e.preventDefault();e.stopPropagation();face.dataset.hand=hand;face.setPointerCapture(e.pointerId);move(e)};hourHand.onpointerdown=e=>start(e,'hour');minuteHand.onpointerdown=e=>start(e,'minute');face.onpointermove=e=>{if(face.hasPointerCapture(e.pointerId))move(e)};face.onpointerup=e=>{move(e);face.releasePointerCapture(e.pointerId);delete face.dataset.hand};face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);delete face.dataset.hand}}return face};
|
||||
const makeClock=(hour,minute,interactive)=>{
|
||||
const face=document.createElement('div');face.className='clock'+(interactive?' interactive':'');face.setAttribute('role','img');face.setAttribute('aria-label',time(hour,minute));
|
||||
for(let i=0;i<60;i++){const tick=document.createElement('i');tick.className=i%5===0?'clock-tick hour':'clock-tick minor';tick.style.transform=`rotate(${i*6}deg)`;face.append(tick)}
|
||||
[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const mark=document.createElement('b');mark.textContent=n;mark.style.setProperty('--clock-angle',`${i*30}deg`);face.append(mark)});
|
||||
const hourHand=document.createElement('i'),minuteHand=document.createElement('i'),pin=document.createElement('span');hourHand.className='clock-hand clock-hour';minuteHand.className='clock-hand clock-min';pin.className='clock-pin';
|
||||
const paint=()=>{hourHand.style.transform=`rotate(${S.h*30+S.m/2}deg)`;minuteHand.style.transform=`rotate(${S.m*6}deg)`};if(interactive)paint();else{hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}face.append(hourHand,minuteHand,pin);
|
||||
if(interactive){
|
||||
const updateAnswer=()=>{face.setAttribute('aria-label',time(S.h,S.m));const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}stage.querySelectorAll('.clock-period .tbtn').forEach(button=>{const active=+button.dataset.period===(S.h>=12?12:0);button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active))})};
|
||||
const move=e=>{const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2,deg=(Math.atan2(dx,-dy)*180/Math.PI+360)%360;
|
||||
if(face.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=minuteValues(),distance=v=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted));S.m=allowed.reduce((best,v)=>distance(v)<distance(best)?v:best)}
|
||||
else if(uses24()){const previous=face._lastPointerAngle;if(Number.isFinite(previous)){face._dragAngle=clock24Angle(face._dragAngle,clockTurnDelta(deg,previous));if(S.linkHands){let bestH=0,bestM=0,bestDistance=720;for(let hour=0;hour<24;hour++)minuteValues().forEach(minute=>{const angle=hour*30+minute/2,difference=Math.min(Math.abs(angle-face._dragAngle),720-Math.abs(angle-face._dragAngle));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=bestH;S.m=bestM}else S.h=clock24Hour(face._dragAngle,S.m)}face._lastPointerAngle=deg}
|
||||
else if(S.linkHands){let bestH=12,bestM=0,bestDistance=360;for(let hour=1;hour<=12;hour++)minuteValues().forEach(minute=>{const angle=(hour%12)*30+minute/2,difference=Math.min(Math.abs(angle-deg),360-Math.abs(angle-deg));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=bestH;S.m=bestM}
|
||||
else S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;paint();updateAnswer()};
|
||||
const clearDrag=()=>{delete face.dataset.hand;delete face._dragAngle;delete face._lastPointerAngle};
|
||||
const start=(e,hand)=>{if(S.solved)return;e.preventDefault();e.stopPropagation();face.dataset.hand=hand;if(hand==='hour'&&uses24()){face._dragAngle=S.h*30+S.m/2;face._lastPointerAngle=null}face.setPointerCapture(e.pointerId);move(e)};
|
||||
hourHand.onpointerdown=e=>start(e,'hour');minuteHand.onpointerdown=e=>start(e,'minute');
|
||||
face.onpointermove=e=>{if(face.hasPointerCapture(e.pointerId))move(e)};
|
||||
face.onpointerup=e=>{move(e);face.releasePointerCapture(e.pointerId);clearDrag()};
|
||||
face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);clearDrag()}
|
||||
}
|
||||
return face
|
||||
};
|
||||
const assignment=(hour,minute,compact=false)=>{const wrap=document.createElement('div');wrap.className='clock-assignment'+(compact?' compact':'');if(['analogue','both'].includes(S.style)){const clock=makeClock(hour,minute,false);if(compact)clock.classList.add('clock-reference');wrap.append(clock)}if(['digital','both'].includes(S.style)){const digital=document.createElement('output');digital.className='clock-task-digital';digital.textContent=time(hour,minute);wrap.append(digital)}if(['words','both'].includes(S.style)){const written=document.createElement('p');written.className='clock-task-words';written.textContent=words(hour,minute);wrap.append(written)}if(uses24()&&S.style==='analogue'){const period=document.createElement('span');period.className='clock-daypart';period.textContent=T(daypartKey(hour));wrap.append(period)}return wrap};
|
||||
const periodControl=()=>{const control=document.createElement('div');control.className='clock-period';control.setAttribute('aria-label',T('clock24Hour'));[0,12].forEach(period=>{const button=btn(period?'12–23':'00–11',()=>{if(S.solved)return;const dial=clockHour(S.h);S.h=(dial%12)+period;S.feedback='';draw()},'ghost');button.title=period?'12:00–23:59':'00:00–11:59';const active=(S.h>=12?12:0)===period;button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active));control.append(button)});return control};
|
||||
const periodControl=()=>{const control=document.createElement('div');control.className='clock-period';control.setAttribute('aria-label',T('clock24Hour'));[0,12].forEach(period=>{const button=btn(period?'12–23':'00–11',()=>{if(S.solved)return;const dial=clockHour(S.h);S.h=(dial%12)+period;S.feedback='';draw()},'ghost');button.dataset.period=period;button.title=period?'12:00–23:59':'00:00–11:59';const active=(S.h>=12?12:0)===period;button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active));control.append(button)});return control};
|
||||
const blockAnswer=()=>{const answer=document.createElement('div');answer.className='clock-block-answer';const instruction=document.createElement('p');instruction.textContent=T('clockBuildHint');const slots=document.createElement('div');slots.className='clock-answer-slots';const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm clock-digit-slot'+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent=T('clockDigitGhost');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','clock-drag-block');slot.append(placed);slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};slots.append(makeSlot(0),makeSlot(1));const colon=document.createElement('strong');colon.className='clock-slot-colon';colon.textContent=':';slots.append(colon,makeSlot(2),makeSlot(3));const tray=document.createElement('div');tray.className='clock-block-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();try{block.releasePointerCapture(e.pointerId)}catch(_){}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('.clock-digit-slot').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(d=>{const block=woodDigit(+d);block.classList.add('clock-drag-block');block.dataset.digit=d;block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',d));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.indexOf(null);if(index>=0){S.answerDigits[index]=+d;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer};
|
||||
const setFeedback=ok=>{if(S.solved)return;if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()};
|
||||
const check=()=>{if(!S.task)return;if(S.game==='read'){if(S.amode==='blocks'){if(S.answerDigits.some(x=>x===null)){S.feedback='incomplete';sndBad();draw();return}S.h=Number(''+S.answerDigits[0]+S.answerDigits[1]);S.m=Number(''+S.answerDigits[2]+S.answerDigits[3])}else{if(S.input.length<4){S.feedback='incomplete';sndBad();draw();return}S.h=+S.input.slice(0,2);S.m=+S.input.slice(2,4)}}const expectedHour=S.game==='set'?(uses24()?S.targetH:clockHour(S.targetH)):S.targetH;setFeedback(S.h===expectedHour&&S.m===S.targetM)};
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge
|
|||
assert.ok(clockSource.includes("hour24Setting.hidden=S.level!==3"));
|
||||
assert.ok(clockSource.includes("use24.onchange=()=>{S.use24=use24.checked;newTask()}"));
|
||||
assert.ok(clockSource.includes("period?'12–23':'00–11'"), '24-uursmodus laat de juiste daghelft kiezen');
|
||||
assert.ok(clockSource.includes('S.h=inputHour(bestH)'), 'de urenwijzer bewaart de gekozen daghelft');
|
||||
assert.ok(clockSource.includes('face._dragAngle=S.h*30+S.m/2'), 'de urenwijzer start vanaf de actuele positie binnen 24 uur');
|
||||
assert.ok(clockSource.includes('for(let hour=0;hour<24;hour++)'), 'de urenwijzer kan alle 24 uren aanwijzen');
|
||||
assert.ok(clockSource.includes('button.dataset.period=period'), 'de daghelft-indicator kan tijdens het draaien live wisselen');
|
||||
assert.ok(clockSource.includes("const expectedHour=S.game==='set'?(uses24()?S.targetH:clockHour(S.targetH)):S.targetH"), '24-uursantwoorden worden exact gecontroleerd');
|
||||
assert.ok(clockSource.includes('S.h=uses24()?0:12'), 'een 24-uursopdracht begint met een geldige invoerwaarde');
|
||||
assert.ok(clockSource.includes("hourHand.onpointerdown=e=>start(e,'hour')"));
|
||||
|
|
@ -107,6 +109,13 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge
|
|||
assert.match(css,/clock-app\{[^}]*justify-content:flex-start/);
|
||||
assert.match(css,/clock-set-answer\{[^}]*align-items:center[^}]*justify-content:center/);
|
||||
assert.match(css,/clock-period \.tbtn\.active\{[^}]*background:var\(--purple\)/);
|
||||
const context={};
|
||||
runInNewContext(source+';globalThis.__clock24={delta:clockTurnDelta,angle:clock24Angle,hour:clock24Hour}',context);
|
||||
assert.equal(context.__clock24.delta(2,358),4,'vooruit over twaalf uur blijft vooruit draaien');
|
||||
assert.equal(context.__clock24.delta(358,2),-4,'achteruit over twaalf uur blijft achteruit draaien');
|
||||
assert.equal(context.__clock24.angle(719,2),1,'na 23 uur draait de klok door naar 0 uur');
|
||||
assert.equal(context.__clock24.angle(1,-2),719,'achteruit vanaf 0 uur komt de klok bij 23 uur');
|
||||
assert.deepEqual([0,11,12,23].map(hour=>context.__clock24.hour(hour*30,0)),[0,11,12,23]);
|
||||
});
|
||||
|
||||
test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', async()=>{
|
||||
|
|
|
|||
Loading…
Reference in a new issue