diff --git a/public/focus.html b/public/focus.html index ace9d35..64d5285 100644 --- a/public/focus.html +++ b/public/focus.html @@ -680,7 +680,7 @@ function generatedQuestion(type,random,round){ const q={id:type+'-'+round,type}; if(type==='view3d'){const answer={yaw:random()*360-180,pitch:random()*120-60,roll:random()*180-90};return {...q,skill:'3D view',title:'Match the view.',answer,shape:makeBlockShape(random),tip:'The lime outline marks the target. The solid shape moves from your view to the target.',explain:'Each puzzle has a generated, connected shape of three to five coloured pieces. Drag the shape to turn it; use the green handle to tilt the whole view. The score measures the smallest rotation between your view and the target.'};} - if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} + if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'0° points right. Measure counterclockwise from that baseline. The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} if(type==='time'){q.answer=integerBetween(random,...GENERATED_RANGES.time);return {...q,skill:'Time awareness',title:'Feel '+q.answer+' seconds.',unit:' s',tip:'Notice whether you stop early or late.',explain:'Any whole duration from 2 to 15 seconds. The clock measures Start to Stop; switching away resets a running round.'};} if(type==='rhythm'){q.answer=integerBetween(random,...GENERATED_RANGES.rhythm);return {...q,skill:'Rhythm',title:'Copy the pulse.',tip:'Watch five flashes, then tap five beats at the same pace.',explain:'The pulse can be any whole tempo from 60 to 144 BPM. Your tempo is measured from the intervals between five taps. No sound is played.'};} if(type==='proportion'){let width,height;do{width=integerBetween(random,...GENERATED_RANGES.ratioTerms);height=integerBetween(random,...GENERATED_RANGES.ratioTerms);}while(width/heightGENERATED_RANGES.ratioBounds[1]);const gcd=(a,b)=>b?gcd(b,a%b):a,divisor=gcd(width,height);width/=divisor;height/=divisor;q.answer=width/height;q.label=width+' : '+height;return {...q,skill:'Proportions',title:'Make '+q.label+'.',tip:'The lime outline shows the target proportion.',explain:'The ratio compares width with height using whole numbers, reduced to their simplest form. Any size works. Scores measure ratio error independently of size.'};} @@ -946,19 +946,26 @@ function typographyRound(q){shell(q,`
Two seconds to remember it.
`,``);$('show-type').addEventListener('click',()=>{$('show-type').disabled=true;$('show-type').textContent='Remember it…';$('type-preview').innerHTML=typeHTML(q.word,q.answer);afterEffect(()=>typographyPicker(q),2000);});} function typographyPicker(q){let settings={size:52,weight:400,spacing:0};shell(q,`
${typeHTML(q.word,settings)}
`,`
`);['size','weight','spacing'].forEach(key=>$('type-'+key).addEventListener('input',()=>{settings[key]=Number($('type-'+key).value);$('type-preview').innerHTML=typeHTML(q.word,settings);}));$('type-lock').addEventListener('click',()=>reveal({...settings}));} function angleRound(q){let angle=0,chosen=false; - shell(q,`
`,`

Drag the line or use arrow keys.

`); - const dial=$('angle-dial'); - const choose=v=>{if(locked)return;angle=(v%360+360)%360;chosen=true;drawAngle(angle,undefined,false);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; + shell(q,`
`,`

0° points right · Measure counterclockwise ↺
Drag the line or use arrow keys.

`); + const dial=$('angle-dial');let moved=false; + const choose=v=>{if(locked)return;const next=(v%360+360)%360;if(Math.abs(next-angle)>1e-6)moved=true;angle=next;chosen=true;drawAngle(angle,undefined,!moved);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; const point=e=>{const p=canvasPointerPoint(dial,e),x=p.x-200,y=200-p.y;if(Math.hypot(x,y)>8)choose(Math.atan2(y,x)*180/Math.PI);}; let drag=false;dial.addEventListener('pointerdown',e=>{e.preventDefault();drag=true;dial.setPointerCapture(e.pointerId);point(e);});dial.addEventListener('pointermove',e=>{if(drag)point(e);});dial.addEventListener('pointerup',()=>drag=false);dial.addEventListener('pointercancel',()=>drag=false); dial.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();choose(angle+(e.key==='ArrowLeft'?1:-1));}}); $('angle-lock').addEventListener('click',()=>{if(chosen)reveal(angle);});drawAngle(angle,undefined,true); } -function drawAngle(angle,target,showZero=false){ +function drawAngle(angle,target,showGuides=false){ const canvas=$('angle-dial');if(!canvas)return;const ctx=canvas.getContext('2d'),style=getComputedStyle(document.documentElement),colour=name=>style.getPropertyValue('--'+name).trim(); ctx.clearRect(0,0,400,400);ctx.strokeStyle=colour('line');ctx.lineWidth=2;ctx.beginPath();ctx.arc(200,200,170,0,Math.PI*2);ctx.stroke(); const line=(a,c,width,dashed=false)=>{const t=a*Math.PI/180;ctx.beginPath();ctx.setLineDash(dashed?[5,6]:[]);ctx.moveTo(200,200);ctx.lineTo(200+Math.cos(t)*160,200-Math.sin(t)*160);ctx.strokeStyle=c;ctx.lineWidth=width;ctx.lineCap='round';ctx.stroke();ctx.setLineDash([]);}; - if(showZero)line(0,colour('muted'),2,true); + if(showGuides){ + line(0,colour('muted'),2,true); + // Explain the reference and direction before the player moves the line. + ctx.fillStyle=colour('ink');ctx.font='600 20px sans-serif';ctx.textAlign='center';ctx.fillText('0°',378,185); + const end=-Math.PI/3,radius=106,x=200+Math.cos(end)*radius,y=200+Math.sin(end)*radius,direction=end-Math.PI/2; + ctx.strokeStyle=colour('muted');ctx.lineWidth=2;ctx.lineCap='round';ctx.beginPath();ctx.arc(200,200,radius,-.12,end,true);ctx.stroke(); + ctx.beginPath();ctx.moveTo(x-12*Math.cos(direction-.45),y-12*Math.sin(direction-.45));ctx.lineTo(x,y);ctx.lineTo(x-12*Math.cos(direction+.45),y-12*Math.sin(direction+.45));ctx.stroke(); + } if(target!==undefined)line(target,colour('target'),5,true); line(angle,colour('draw'),5);ctx.fillStyle=colour('ink');ctx.beginPath();ctx.arc(200,200,6,0,Math.PI*2);ctx.fill(); } diff --git a/public/index.html b/public/index.html index ace9d35..64d5285 100644 --- a/public/index.html +++ b/public/index.html @@ -680,7 +680,7 @@ function generatedQuestion(type,random,round){ const q={id:type+'-'+round,type}; if(type==='view3d'){const answer={yaw:random()*360-180,pitch:random()*120-60,roll:random()*180-90};return {...q,skill:'3D view',title:'Match the view.',answer,shape:makeBlockShape(random),tip:'The lime outline marks the target. The solid shape moves from your view to the target.',explain:'Each puzzle has a generated, connected shape of three to five coloured pieces. Drag the shape to turn it; use the green handle to tilt the whole view. The score measures the smallest rotation between your view and the target.'};} - if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} + if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'0° points right. Measure counterclockwise from that baseline. The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} if(type==='time'){q.answer=integerBetween(random,...GENERATED_RANGES.time);return {...q,skill:'Time awareness',title:'Feel '+q.answer+' seconds.',unit:' s',tip:'Notice whether you stop early or late.',explain:'Any whole duration from 2 to 15 seconds. The clock measures Start to Stop; switching away resets a running round.'};} if(type==='rhythm'){q.answer=integerBetween(random,...GENERATED_RANGES.rhythm);return {...q,skill:'Rhythm',title:'Copy the pulse.',tip:'Watch five flashes, then tap five beats at the same pace.',explain:'The pulse can be any whole tempo from 60 to 144 BPM. Your tempo is measured from the intervals between five taps. No sound is played.'};} if(type==='proportion'){let width,height;do{width=integerBetween(random,...GENERATED_RANGES.ratioTerms);height=integerBetween(random,...GENERATED_RANGES.ratioTerms);}while(width/heightGENERATED_RANGES.ratioBounds[1]);const gcd=(a,b)=>b?gcd(b,a%b):a,divisor=gcd(width,height);width/=divisor;height/=divisor;q.answer=width/height;q.label=width+' : '+height;return {...q,skill:'Proportions',title:'Make '+q.label+'.',tip:'The lime outline shows the target proportion.',explain:'The ratio compares width with height using whole numbers, reduced to their simplest form. Any size works. Scores measure ratio error independently of size.'};} @@ -946,19 +946,26 @@ function typographyRound(q){shell(q,`
Two seconds to remember it.
`,``);$('show-type').addEventListener('click',()=>{$('show-type').disabled=true;$('show-type').textContent='Remember it…';$('type-preview').innerHTML=typeHTML(q.word,q.answer);afterEffect(()=>typographyPicker(q),2000);});} function typographyPicker(q){let settings={size:52,weight:400,spacing:0};shell(q,`
${typeHTML(q.word,settings)}
`,`
`);['size','weight','spacing'].forEach(key=>$('type-'+key).addEventListener('input',()=>{settings[key]=Number($('type-'+key).value);$('type-preview').innerHTML=typeHTML(q.word,settings);}));$('type-lock').addEventListener('click',()=>reveal({...settings}));} function angleRound(q){let angle=0,chosen=false; - shell(q,`
`,`

Drag the line or use arrow keys.

`); - const dial=$('angle-dial'); - const choose=v=>{if(locked)return;angle=(v%360+360)%360;chosen=true;drawAngle(angle,undefined,false);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; + shell(q,`
`,`

0° points right · Measure counterclockwise ↺
Drag the line or use arrow keys.

`); + const dial=$('angle-dial');let moved=false; + const choose=v=>{if(locked)return;const next=(v%360+360)%360;if(Math.abs(next-angle)>1e-6)moved=true;angle=next;chosen=true;drawAngle(angle,undefined,!moved);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; const point=e=>{const p=canvasPointerPoint(dial,e),x=p.x-200,y=200-p.y;if(Math.hypot(x,y)>8)choose(Math.atan2(y,x)*180/Math.PI);}; let drag=false;dial.addEventListener('pointerdown',e=>{e.preventDefault();drag=true;dial.setPointerCapture(e.pointerId);point(e);});dial.addEventListener('pointermove',e=>{if(drag)point(e);});dial.addEventListener('pointerup',()=>drag=false);dial.addEventListener('pointercancel',()=>drag=false); dial.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();choose(angle+(e.key==='ArrowLeft'?1:-1));}}); $('angle-lock').addEventListener('click',()=>{if(chosen)reveal(angle);});drawAngle(angle,undefined,true); } -function drawAngle(angle,target,showZero=false){ +function drawAngle(angle,target,showGuides=false){ const canvas=$('angle-dial');if(!canvas)return;const ctx=canvas.getContext('2d'),style=getComputedStyle(document.documentElement),colour=name=>style.getPropertyValue('--'+name).trim(); ctx.clearRect(0,0,400,400);ctx.strokeStyle=colour('line');ctx.lineWidth=2;ctx.beginPath();ctx.arc(200,200,170,0,Math.PI*2);ctx.stroke(); const line=(a,c,width,dashed=false)=>{const t=a*Math.PI/180;ctx.beginPath();ctx.setLineDash(dashed?[5,6]:[]);ctx.moveTo(200,200);ctx.lineTo(200+Math.cos(t)*160,200-Math.sin(t)*160);ctx.strokeStyle=c;ctx.lineWidth=width;ctx.lineCap='round';ctx.stroke();ctx.setLineDash([]);}; - if(showZero)line(0,colour('muted'),2,true); + if(showGuides){ + line(0,colour('muted'),2,true); + // Explain the reference and direction before the player moves the line. + ctx.fillStyle=colour('ink');ctx.font='600 20px sans-serif';ctx.textAlign='center';ctx.fillText('0°',378,185); + const end=-Math.PI/3,radius=106,x=200+Math.cos(end)*radius,y=200+Math.sin(end)*radius,direction=end-Math.PI/2; + ctx.strokeStyle=colour('muted');ctx.lineWidth=2;ctx.lineCap='round';ctx.beginPath();ctx.arc(200,200,radius,-.12,end,true);ctx.stroke(); + ctx.beginPath();ctx.moveTo(x-12*Math.cos(direction-.45),y-12*Math.sin(direction-.45));ctx.lineTo(x,y);ctx.lineTo(x-12*Math.cos(direction+.45),y-12*Math.sin(direction+.45));ctx.stroke(); + } if(target!==undefined)line(target,colour('target'),5,true); line(angle,colour('draw'),5);ctx.fillStyle=colour('ink');ctx.beginPath();ctx.arc(200,200,6,0,Math.PI*2);ctx.fill(); } diff --git a/public/night-lab.html b/public/night-lab.html index ace9d35..64d5285 100644 --- a/public/night-lab.html +++ b/public/night-lab.html @@ -680,7 +680,7 @@ function generatedQuestion(type,random,round){ const q={id:type+'-'+round,type}; if(type==='view3d'){const answer={yaw:random()*360-180,pitch:random()*120-60,roll:random()*180-90};return {...q,skill:'3D view',title:'Match the view.',answer,shape:makeBlockShape(random),tip:'The lime outline marks the target. The solid shape moves from your view to the target.',explain:'Each puzzle has a generated, connected shape of three to five coloured pieces. Drag the shape to turn it; use the green handle to tilt the whole view. The score measures the smallest rotation between your view and the target.'};} - if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} + if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'0° points right. Measure counterclockwise from that baseline. The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} if(type==='time'){q.answer=integerBetween(random,...GENERATED_RANGES.time);return {...q,skill:'Time awareness',title:'Feel '+q.answer+' seconds.',unit:' s',tip:'Notice whether you stop early or late.',explain:'Any whole duration from 2 to 15 seconds. The clock measures Start to Stop; switching away resets a running round.'};} if(type==='rhythm'){q.answer=integerBetween(random,...GENERATED_RANGES.rhythm);return {...q,skill:'Rhythm',title:'Copy the pulse.',tip:'Watch five flashes, then tap five beats at the same pace.',explain:'The pulse can be any whole tempo from 60 to 144 BPM. Your tempo is measured from the intervals between five taps. No sound is played.'};} if(type==='proportion'){let width,height;do{width=integerBetween(random,...GENERATED_RANGES.ratioTerms);height=integerBetween(random,...GENERATED_RANGES.ratioTerms);}while(width/heightGENERATED_RANGES.ratioBounds[1]);const gcd=(a,b)=>b?gcd(b,a%b):a,divisor=gcd(width,height);width/=divisor;height/=divisor;q.answer=width/height;q.label=width+' : '+height;return {...q,skill:'Proportions',title:'Make '+q.label+'.',tip:'The lime outline shows the target proportion.',explain:'The ratio compares width with height using whole numbers, reduced to their simplest form. Any size works. Scores measure ratio error independently of size.'};} @@ -946,19 +946,26 @@ function typographyRound(q){shell(q,`
Two seconds to remember it.
`,``);$('show-type').addEventListener('click',()=>{$('show-type').disabled=true;$('show-type').textContent='Remember it…';$('type-preview').innerHTML=typeHTML(q.word,q.answer);afterEffect(()=>typographyPicker(q),2000);});} function typographyPicker(q){let settings={size:52,weight:400,spacing:0};shell(q,`
${typeHTML(q.word,settings)}
`,`
`);['size','weight','spacing'].forEach(key=>$('type-'+key).addEventListener('input',()=>{settings[key]=Number($('type-'+key).value);$('type-preview').innerHTML=typeHTML(q.word,settings);}));$('type-lock').addEventListener('click',()=>reveal({...settings}));} function angleRound(q){let angle=0,chosen=false; - shell(q,`
`,`

Drag the line or use arrow keys.

`); - const dial=$('angle-dial'); - const choose=v=>{if(locked)return;angle=(v%360+360)%360;chosen=true;drawAngle(angle,undefined,false);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; + shell(q,`
`,`

0° points right · Measure counterclockwise ↺
Drag the line or use arrow keys.

`); + const dial=$('angle-dial');let moved=false; + const choose=v=>{if(locked)return;const next=(v%360+360)%360;if(Math.abs(next-angle)>1e-6)moved=true;angle=next;chosen=true;drawAngle(angle,undefined,!moved);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; const point=e=>{const p=canvasPointerPoint(dial,e),x=p.x-200,y=200-p.y;if(Math.hypot(x,y)>8)choose(Math.atan2(y,x)*180/Math.PI);}; let drag=false;dial.addEventListener('pointerdown',e=>{e.preventDefault();drag=true;dial.setPointerCapture(e.pointerId);point(e);});dial.addEventListener('pointermove',e=>{if(drag)point(e);});dial.addEventListener('pointerup',()=>drag=false);dial.addEventListener('pointercancel',()=>drag=false); dial.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();choose(angle+(e.key==='ArrowLeft'?1:-1));}}); $('angle-lock').addEventListener('click',()=>{if(chosen)reveal(angle);});drawAngle(angle,undefined,true); } -function drawAngle(angle,target,showZero=false){ +function drawAngle(angle,target,showGuides=false){ const canvas=$('angle-dial');if(!canvas)return;const ctx=canvas.getContext('2d'),style=getComputedStyle(document.documentElement),colour=name=>style.getPropertyValue('--'+name).trim(); ctx.clearRect(0,0,400,400);ctx.strokeStyle=colour('line');ctx.lineWidth=2;ctx.beginPath();ctx.arc(200,200,170,0,Math.PI*2);ctx.stroke(); const line=(a,c,width,dashed=false)=>{const t=a*Math.PI/180;ctx.beginPath();ctx.setLineDash(dashed?[5,6]:[]);ctx.moveTo(200,200);ctx.lineTo(200+Math.cos(t)*160,200-Math.sin(t)*160);ctx.strokeStyle=c;ctx.lineWidth=width;ctx.lineCap='round';ctx.stroke();ctx.setLineDash([]);}; - if(showZero)line(0,colour('muted'),2,true); + if(showGuides){ + line(0,colour('muted'),2,true); + // Explain the reference and direction before the player moves the line. + ctx.fillStyle=colour('ink');ctx.font='600 20px sans-serif';ctx.textAlign='center';ctx.fillText('0°',378,185); + const end=-Math.PI/3,radius=106,x=200+Math.cos(end)*radius,y=200+Math.sin(end)*radius,direction=end-Math.PI/2; + ctx.strokeStyle=colour('muted');ctx.lineWidth=2;ctx.lineCap='round';ctx.beginPath();ctx.arc(200,200,radius,-.12,end,true);ctx.stroke(); + ctx.beginPath();ctx.moveTo(x-12*Math.cos(direction-.45),y-12*Math.sin(direction-.45));ctx.lineTo(x,y);ctx.lineTo(x-12*Math.cos(direction+.45),y-12*Math.sin(direction+.45));ctx.stroke(); + } if(target!==undefined)line(target,colour('target'),5,true); line(angle,colour('draw'),5);ctx.fillStyle=colour('ink');ctx.beginPath();ctx.arc(200,200,6,0,Math.PI*2);ctx.fill(); } diff --git a/public/studio.html b/public/studio.html index ace9d35..64d5285 100644 --- a/public/studio.html +++ b/public/studio.html @@ -680,7 +680,7 @@ function generatedQuestion(type,random,round){ const q={id:type+'-'+round,type}; if(type==='view3d'){const answer={yaw:random()*360-180,pitch:random()*120-60,roll:random()*180-90};return {...q,skill:'3D view',title:'Match the view.',answer,shape:makeBlockShape(random),tip:'The lime outline marks the target. The solid shape moves from your view to the target.',explain:'Each puzzle has a generated, connected shape of three to five coloured pieces. Drag the shape to turn it; use the green handle to tilt the whole view. The score measures the smallest rotation between your view and the target.'};} - if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} + if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'0° points right. Measure counterclockwise from that baseline. The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} if(type==='time'){q.answer=integerBetween(random,...GENERATED_RANGES.time);return {...q,skill:'Time awareness',title:'Feel '+q.answer+' seconds.',unit:' s',tip:'Notice whether you stop early or late.',explain:'Any whole duration from 2 to 15 seconds. The clock measures Start to Stop; switching away resets a running round.'};} if(type==='rhythm'){q.answer=integerBetween(random,...GENERATED_RANGES.rhythm);return {...q,skill:'Rhythm',title:'Copy the pulse.',tip:'Watch five flashes, then tap five beats at the same pace.',explain:'The pulse can be any whole tempo from 60 to 144 BPM. Your tempo is measured from the intervals between five taps. No sound is played.'};} if(type==='proportion'){let width,height;do{width=integerBetween(random,...GENERATED_RANGES.ratioTerms);height=integerBetween(random,...GENERATED_RANGES.ratioTerms);}while(width/heightGENERATED_RANGES.ratioBounds[1]);const gcd=(a,b)=>b?gcd(b,a%b):a,divisor=gcd(width,height);width/=divisor;height/=divisor;q.answer=width/height;q.label=width+' : '+height;return {...q,skill:'Proportions',title:'Make '+q.label+'.',tip:'The lime outline shows the target proportion.',explain:'The ratio compares width with height using whole numbers, reduced to their simplest form. Any size works. Scores measure ratio error independently of size.'};} @@ -946,19 +946,26 @@ function typographyRound(q){shell(q,`
Two seconds to remember it.
`,``);$('show-type').addEventListener('click',()=>{$('show-type').disabled=true;$('show-type').textContent='Remember it…';$('type-preview').innerHTML=typeHTML(q.word,q.answer);afterEffect(()=>typographyPicker(q),2000);});} function typographyPicker(q){let settings={size:52,weight:400,spacing:0};shell(q,`
${typeHTML(q.word,settings)}
`,`
`);['size','weight','spacing'].forEach(key=>$('type-'+key).addEventListener('input',()=>{settings[key]=Number($('type-'+key).value);$('type-preview').innerHTML=typeHTML(q.word,settings);}));$('type-lock').addEventListener('click',()=>reveal({...settings}));} function angleRound(q){let angle=0,chosen=false; - shell(q,`
`,`

Drag the line or use arrow keys.

`); - const dial=$('angle-dial'); - const choose=v=>{if(locked)return;angle=(v%360+360)%360;chosen=true;drawAngle(angle,undefined,false);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; + shell(q,`
`,`

0° points right · Measure counterclockwise ↺
Drag the line or use arrow keys.

`); + const dial=$('angle-dial');let moved=false; + const choose=v=>{if(locked)return;const next=(v%360+360)%360;if(Math.abs(next-angle)>1e-6)moved=true;angle=next;chosen=true;drawAngle(angle,undefined,!moved);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; const point=e=>{const p=canvasPointerPoint(dial,e),x=p.x-200,y=200-p.y;if(Math.hypot(x,y)>8)choose(Math.atan2(y,x)*180/Math.PI);}; let drag=false;dial.addEventListener('pointerdown',e=>{e.preventDefault();drag=true;dial.setPointerCapture(e.pointerId);point(e);});dial.addEventListener('pointermove',e=>{if(drag)point(e);});dial.addEventListener('pointerup',()=>drag=false);dial.addEventListener('pointercancel',()=>drag=false); dial.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();choose(angle+(e.key==='ArrowLeft'?1:-1));}}); $('angle-lock').addEventListener('click',()=>{if(chosen)reveal(angle);});drawAngle(angle,undefined,true); } -function drawAngle(angle,target,showZero=false){ +function drawAngle(angle,target,showGuides=false){ const canvas=$('angle-dial');if(!canvas)return;const ctx=canvas.getContext('2d'),style=getComputedStyle(document.documentElement),colour=name=>style.getPropertyValue('--'+name).trim(); ctx.clearRect(0,0,400,400);ctx.strokeStyle=colour('line');ctx.lineWidth=2;ctx.beginPath();ctx.arc(200,200,170,0,Math.PI*2);ctx.stroke(); const line=(a,c,width,dashed=false)=>{const t=a*Math.PI/180;ctx.beginPath();ctx.setLineDash(dashed?[5,6]:[]);ctx.moveTo(200,200);ctx.lineTo(200+Math.cos(t)*160,200-Math.sin(t)*160);ctx.strokeStyle=c;ctx.lineWidth=width;ctx.lineCap='round';ctx.stroke();ctx.setLineDash([]);}; - if(showZero)line(0,colour('muted'),2,true); + if(showGuides){ + line(0,colour('muted'),2,true); + // Explain the reference and direction before the player moves the line. + ctx.fillStyle=colour('ink');ctx.font='600 20px sans-serif';ctx.textAlign='center';ctx.fillText('0°',378,185); + const end=-Math.PI/3,radius=106,x=200+Math.cos(end)*radius,y=200+Math.sin(end)*radius,direction=end-Math.PI/2; + ctx.strokeStyle=colour('muted');ctx.lineWidth=2;ctx.lineCap='round';ctx.beginPath();ctx.arc(200,200,radius,-.12,end,true);ctx.stroke(); + ctx.beginPath();ctx.moveTo(x-12*Math.cos(direction-.45),y-12*Math.sin(direction-.45));ctx.lineTo(x,y);ctx.lineTo(x-12*Math.cos(direction+.45),y-12*Math.sin(direction+.45));ctx.stroke(); + } if(target!==undefined)line(target,colour('target'),5,true); line(angle,colour('draw'),5);ctx.fillStyle=colour('ink');ctx.beginPath();ctx.arc(200,200,6,0,Math.PI*2);ctx.fill(); } diff --git a/src/design-options/angle-game.js b/src/design-options/angle-game.js index 9a7e482..ce54437 100644 --- a/src/design-options/angle-game.js +++ b/src/design-options/angle-game.js @@ -1,17 +1,24 @@ function angleRound(q){let angle=0,chosen=false; - shell(q,`
`,`

Drag the line or use arrow keys.

`); - const dial=$('angle-dial'); - const choose=v=>{if(locked)return;angle=(v%360+360)%360;chosen=true;drawAngle(angle,undefined,false);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; + shell(q,`
`,`

0° points right · Measure counterclockwise ↺
Drag the line or use arrow keys.

`); + const dial=$('angle-dial');let moved=false; + const choose=v=>{if(locked)return;const next=(v%360+360)%360;if(Math.abs(next-angle)>1e-6)moved=true;angle=next;chosen=true;drawAngle(angle,undefined,!moved);$('angle-lock').disabled=false;$('angle-lock').textContent='Lock it in';}; const point=e=>{const p=canvasPointerPoint(dial,e),x=p.x-200,y=200-p.y;if(Math.hypot(x,y)>8)choose(Math.atan2(y,x)*180/Math.PI);}; let drag=false;dial.addEventListener('pointerdown',e=>{e.preventDefault();drag=true;dial.setPointerCapture(e.pointerId);point(e);});dial.addEventListener('pointermove',e=>{if(drag)point(e);});dial.addEventListener('pointerup',()=>drag=false);dial.addEventListener('pointercancel',()=>drag=false); dial.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();choose(angle+(e.key==='ArrowLeft'?1:-1));}}); $('angle-lock').addEventListener('click',()=>{if(chosen)reveal(angle);});drawAngle(angle,undefined,true); } -function drawAngle(angle,target,showZero=false){ +function drawAngle(angle,target,showGuides=false){ const canvas=$('angle-dial');if(!canvas)return;const ctx=canvas.getContext('2d'),style=getComputedStyle(document.documentElement),colour=name=>style.getPropertyValue('--'+name).trim(); ctx.clearRect(0,0,400,400);ctx.strokeStyle=colour('line');ctx.lineWidth=2;ctx.beginPath();ctx.arc(200,200,170,0,Math.PI*2);ctx.stroke(); const line=(a,c,width,dashed=false)=>{const t=a*Math.PI/180;ctx.beginPath();ctx.setLineDash(dashed?[5,6]:[]);ctx.moveTo(200,200);ctx.lineTo(200+Math.cos(t)*160,200-Math.sin(t)*160);ctx.strokeStyle=c;ctx.lineWidth=width;ctx.lineCap='round';ctx.stroke();ctx.setLineDash([]);}; - if(showZero)line(0,colour('muted'),2,true); + if(showGuides){ + line(0,colour('muted'),2,true); + // Explain the reference and direction before the player moves the line. + ctx.fillStyle=colour('ink');ctx.font='600 20px sans-serif';ctx.textAlign='center';ctx.fillText('0°',378,185); + const end=-Math.PI/3,radius=106,x=200+Math.cos(end)*radius,y=200+Math.sin(end)*radius,direction=end-Math.PI/2; + ctx.strokeStyle=colour('muted');ctx.lineWidth=2;ctx.lineCap='round';ctx.beginPath();ctx.arc(200,200,radius,-.12,end,true);ctx.stroke(); + ctx.beginPath();ctx.moveTo(x-12*Math.cos(direction-.45),y-12*Math.sin(direction-.45));ctx.lineTo(x,y);ctx.lineTo(x-12*Math.cos(direction+.45),y-12*Math.sin(direction+.45));ctx.stroke(); + } if(target!==undefined)line(target,colour('target'),5,true); line(angle,colour('draw'),5);ctx.fillStyle=colour('ink');ctx.beginPath();ctx.arc(200,200,6,0,Math.PI*2);ctx.fill(); } diff --git a/src/design-options/generated-games.js b/src/design-options/generated-games.js index 2eb7346..671a8be 100644 --- a/src/design-options/generated-games.js +++ b/src/design-options/generated-games.js @@ -52,7 +52,7 @@ function makeDeck(day,game=mini){ function generatedQuestion(type,random,round){ const q={id:type+'-'+round,type}; if(type==='view3d'){const answer={yaw:random()*360-180,pitch:random()*120-60,roll:random()*180-90};return {...q,skill:'3D view',title:'Match the view.',answer,shape:makeBlockShape(random),tip:'Solid is your view. The lime outline is the target.',explain:'Each puzzle has a generated, connected shape of three to five coloured pieces. Drag the shape to turn it; use the green handle to tilt the whole view. The score measures the smallest rotation between your view and the target.'};} - if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} + if(type==='angle'){q.answer=integerBetween(random,...GENERATED_RANGES.angle);return {...q,skill:'Angles',title:q.answer+'°',tip:'The lime line shows the target angle.',explain:'0° points right. Measure counterclockwise from that baseline. The target can be any whole degree from 5° to 175°. Scores measure the angular distance from your guess.'};} if(type==='time'){q.answer=integerBetween(random,...GENERATED_RANGES.time);return {...q,skill:'Time awareness',title:'Feel '+q.answer+' seconds.',unit:' s',tip:'Notice whether you stop early or late.',explain:'Any whole duration from 2 to 15 seconds. The clock measures Start to Stop; switching away resets a running round.'};} if(type==='rhythm'){q.answer=integerBetween(random,...GENERATED_RANGES.rhythm);return {...q,skill:'Rhythm',title:'Copy the pulse.',tip:'Watch five flashes, then tap five beats at the same pace.',explain:'The pulse can be any whole tempo from 60 to 144 BPM. Your tempo is measured from the intervals between five taps. No sound is played.'};} if(type==='proportion'){let width,height;do{width=integerBetween(random,...GENERATED_RANGES.ratioTerms);height=integerBetween(random,...GENERATED_RANGES.ratioTerms);}while(width/heightGENERATED_RANGES.ratioBounds[1]);const gcd=(a,b)=>b?gcd(b,a%b):a,divisor=gcd(width,height);width/=divisor;height/=divisor;q.answer=width/height;q.label=width+' : '+height;return {...q,skill:'Proportions',title:'Make '+q.label+'.',tip:'The lime outline shows the target proportion.',explain:'The ratio compares width with height using whole numbers, reduced to their simplest form. Any size works. Scores measure ratio error independently of size.'};}