Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1

Open
bes-r wants to merge 192 commits from bes-r/avatar-realism into main AGit
5 changed files with 6 additions and 4 deletions
Showing only changes of commit 54782781bf - Show all commits

View file

@ -1 +1 @@
0.3.42-beta 0.3.43-beta

View file

@ -1464,7 +1464,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.clock-target{width:100%;min-height:88px;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;padding:10px;border-radius:15px;background:var(--surface-2)}.clock-target>span:first-child{flex-basis:100%;color:var(--muted);font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.5px}.clock-target-words{font-size:20px;color:var(--purple)}.clock-target .clock-reference{--clock-size:112px;border-width:5px;box-shadow:inset 0 0 0 2px var(--line),0 4px 10px rgba(20,35,60,.12)}.clock-reference b{font-size:9px}.clock-reference .clock-pin{left:calc(50% - 6px);top:calc(50% - 6px);width:12px;height:12px;border-width:2px} .clock-target{width:100%;min-height:88px;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;padding:10px;border-radius:15px;background:var(--surface-2)}.clock-target>span:first-child{flex-basis:100%;color:var(--muted);font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.5px}.clock-target-words{font-size:20px;color:var(--purple)}.clock-target .clock-reference{--clock-size:112px;border-width:5px;box-shadow:inset 0 0 0 2px var(--line),0 4px 10px rgba(20,35,60,.12)}.clock-reference b{font-size:9px}.clock-reference .clock-pin{left:calc(50% - 6px);top:calc(50% - 6px);width:12px;height:12px;border-width:2px}
.clock-work{display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}.clock{--clock-size:min(230px,62vw);width:var(--clock-size);height:var(--clock-size);flex:none;position:relative;overflow:hidden;border:9px solid var(--ink);border-radius:50%;background:radial-gradient(circle at 50% 46%,var(--surface) 0 62%,var(--surface-2) 100%);box-shadow:inset 0 0 0 3px var(--line),0 8px 20px rgba(20,35,60,.14)} .clock-work{display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}.clock{--clock-size:min(230px,62vw);width:var(--clock-size);height:var(--clock-size);flex:none;position:relative;overflow:hidden;border:9px solid var(--ink);border-radius:50%;background:radial-gradient(circle at 50% 46%,var(--surface) 0 62%,var(--surface-2) 100%);box-shadow:inset 0 0 0 3px var(--line),0 8px 20px rgba(20,35,60,.14)}
.clock-tick{position:absolute;z-index:1;left:calc(50% - 1px);top:3%;width:2px;height:8%;border-radius:2px;background:var(--ink);transform-origin:50% 587.5%}.clock-tick.minor{top:4%;height:4%;opacity:.28;transform-origin:50% 1150%} .clock-tick{position:absolute;z-index:1;left:calc(50% - 1px);top:3%;width:2px;height:8%;border-radius:2px;background:var(--ink);transform-origin:50% 587.5%}.clock-tick.minor{top:4%;height:4%;opacity:.28;transform-origin:50% 1150%}
.clock b{position:absolute;z-index:2;left:calc(50% - 12px);top:calc(50% - 12px);width:24px;height:24px;display:grid;place-items:center;font-size:14px;transform:rotate(var(--clock-angle)) translateY(calc(var(--clock-size)*-.40)) rotate(calc(-1 * var(--clock-angle)))} .clock b{position:absolute;z-index:2;left:calc(50% - 12px);top:calc(50% - 12px);width:24px;height:24px;display:grid;place-items:center;font-size:14px;transform:rotate(var(--clock-angle)) translateY(calc(var(--clock-size)*-.34)) rotate(calc(-1 * var(--clock-angle)))}
.clock-hand{position:absolute;z-index:4;left:calc(50% - 4px);bottom:50%;width:8px;border-radius:9px;transform-origin:50% 100%;background:var(--ink)}.clock-hour{height:28%}.clock-min{height:39%;background:var(--pink)}.clock-pin{position:absolute;z-index:5;left:calc(50% - 8px);top:calc(50% - 8px);width:16px;height:16px;border:3px solid var(--surface);border-radius:50%;background:var(--pink);box-shadow:0 1px 4px rgba(0,0,0,.28)} .clock-hand{position:absolute;z-index:4;left:calc(50% - 4px);bottom:50%;width:8px;border-radius:9px;transform-origin:50% 100%;background:var(--ink)}.clock-hour{height:28%}.clock-min{height:39%;background:var(--pink)}.clock-pin{position:absolute;z-index:5;left:calc(50% - 8px);top:calc(50% - 8px);width:16px;height:16px;border:3px solid var(--surface);border-radius:50%;background:var(--pink);box-shadow:0 1px 4px rgba(0,0,0,.28)}
.clock.interactive{cursor:grab;touch-action:none;user-select:none;outline:4px solid transparent;transition:outline-color .15s,transform .15s}.clock.interactive:hover,.clock.interactive:focus-within{outline-color:var(--accent-soft)}.clock.interactive:active{cursor:grabbing;transform:scale(.99)} .clock.interactive{cursor:grab;touch-action:none;user-select:none;outline:4px solid transparent;transition:outline-color .15s,transform .15s}.clock.interactive:hover,.clock.interactive:focus-within{outline-color:var(--accent-soft)}.clock.interactive:active{cursor:grabbing;transform:scale(.99)}
.clock-time-blocks{display:flex;align-items:center;justify-content:center;gap:5px;margin:0;padding:6px;border:0;border-radius:14px;background:transparent;color:var(--ink);font:inherit}.clock-time-blocks:is(button){cursor:pointer}.clock-time-blocks:is(button):hover,.clock-time-blocks:is(button):focus-visible{background:var(--accent-soft);outline:2px solid var(--accent)}.clock-time-blocks>strong{font-size:30px} .clock-time-blocks{display:flex;align-items:center;justify-content:center;gap:5px;margin:0;padding:6px;border:0;border-radius:14px;background:transparent;color:var(--ink);font:inherit}.clock-time-blocks:is(button){cursor:pointer}.clock-time-blocks:is(button):hover,.clock-time-blocks:is(button):focus-visible{background:var(--accent-soft);outline:2px solid var(--accent)}.clock-time-blocks>strong{font-size:30px}

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* 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. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.42-beta"; const VERSION = "0.3.43-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;

View file

@ -28,7 +28,7 @@ function mountClock(root,init){
const randomTime=()=>{const values=minuteValues();return{h:1+Math.floor(Math.random()*12),m:values[Math.floor(Math.random()*values.length)]}}; const randomTime=()=>{const values=minuteValues();return{h: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 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 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?'clock-tick minor':'clock-tick';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){hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}else paint();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 S.h=(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))}};face.onpointerdown=e=>{if(S.solved)return;e.preventDefault();const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;face.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';face.setPointerCapture(e.pointerId);move(e)};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++){if(i%5===0)continue;const tick=document.createElement('i');tick.className='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){hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}else paint();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 S.h=(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))}};face.onpointerdown=e=>{if(S.solved)return;e.preventDefault();const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;face.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';face.setPointerCapture(e.pointerId);move(e)};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 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');placed.dataset.placedAt=Date.now();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 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');placed.dataset.placedAt=Date.now();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 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.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])}setFeedback(S.h===S.targetH&&S.m===S.targetM)}; const check=()=>{if(!S.task)return;if(S.game==='read'){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])}setFeedback(S.h===S.targetH&&S.m===S.targetM)};

View file

@ -77,4 +77,6 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge
for(const feature of ['answerDigits','clock-answer-slots','clock-block-tray','dragClone','droptarget',"'0123456789'.split('')"])assert.ok(clockSource.includes(feature),feature); for(const feature of ['answerDigits','clock-answer-slots','clock-block-tray','dragClone','droptarget',"'0123456789'.split('')"])assert.ok(clockSource.includes(feature),feature);
assert.match(clockSource,/String\(hour\)\.padStart\(2,'0'\)\+String\(minute\)\.padStart\(2,'0'\)/); assert.match(clockSource,/String\(hour\)\.padStart\(2,'0'\)\+String\(minute\)\.padStart\(2,'0'\)/);
assert.equal((core.match(/clockIncomplete:/g)||[]).length,2); assert.equal((core.match(/clockIncomplete:/g)||[]).length,2);
assert.ok(clockSource.includes('if(i%5===0)continue'), 'uurstrepen bij cijfers worden overgeslagen');
assert.match(css,/translateY\(calc\(var\(--clock-size\)\*-.34\)\)/);
}); });