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,``,`
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,``,`
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,``,`
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,``,`
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.'};}