Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions public/focus.html
Original file line number Diff line number Diff line change
Expand Up @@ -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/height<GENERATED_RANGES.ratioBounds[0]||width/height>GENERATED_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.'};}
Expand Down Expand Up @@ -946,19 +946,26 @@
function typographyRound(q){shell(q,`<div class="type-specimen" id="type-preview"><span class="memory-prompt">Two seconds to remember it.</span></div>`,`<button class="primary" id="show-type">Show type</button>`);$('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,`<div class="type-specimen" id="type-preview">${typeHTML(q.word,settings)}</div>`,`<div class="type-sliders"><label for="type-size">Size</label><input class="type-slider" id="type-size" type="range" min="28" max="72" value="52"><label for="type-weight">Weight</label><input class="type-slider" id="type-weight" type="range" min="400" max="700" step="1" value="400"><label for="type-spacing">Spacing</label><input class="type-slider" id="type-spacing" type="range" min="-2" max="6" step="0.1" value="0"></div><button class="primary" id="type-lock">Lock it in</button>`);['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,`<div class="angle-wrap"><canvas width="400" height="400" id="angle-dial" tabindex="0" role="application" aria-label="Rotate the line to ${q.answer} degrees. Left and right arrows adjust it."></canvas></div>`,`<p class="small-instruction">Drag the line or use arrow keys.</p><button class="primary" id="angle-lock" disabled>Choose your angle</button>`);
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,`<div class="angle-wrap"><canvas width="400" height="400" id="angle-dial" tabindex="0" role="application" aria-label="Rotate the line to ${q.answer} degrees. Zero degrees points right; angles increase counterclockwise. Left arrow increases the angle; right arrow decreases it."></canvas></div>`,`<p class="small-instruction">0° points right · Measure counterclockwise ↺<br>Drag the line or use arrow keys.</p><button class="primary" id="angle-lock" disabled>Choose your angle</button>`);
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();
}
Expand Down
19 changes: 13 additions & 6 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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/height<GENERATED_RANGES.ratioBounds[0]||width/height>GENERATED_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.'};}
Expand Down Expand Up @@ -946,19 +946,26 @@
function typographyRound(q){shell(q,`<div class="type-specimen" id="type-preview"><span class="memory-prompt">Two seconds to remember it.</span></div>`,`<button class="primary" id="show-type">Show type</button>`);$('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,`<div class="type-specimen" id="type-preview">${typeHTML(q.word,settings)}</div>`,`<div class="type-sliders"><label for="type-size">Size</label><input class="type-slider" id="type-size" type="range" min="28" max="72" value="52"><label for="type-weight">Weight</label><input class="type-slider" id="type-weight" type="range" min="400" max="700" step="1" value="400"><label for="type-spacing">Spacing</label><input class="type-slider" id="type-spacing" type="range" min="-2" max="6" step="0.1" value="0"></div><button class="primary" id="type-lock">Lock it in</button>`);['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,`<div class="angle-wrap"><canvas width="400" height="400" id="angle-dial" tabindex="0" role="application" aria-label="Rotate the line to ${q.answer} degrees. Left and right arrows adjust it."></canvas></div>`,`<p class="small-instruction">Drag the line or use arrow keys.</p><button class="primary" id="angle-lock" disabled>Choose your angle</button>`);
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,`<div class="angle-wrap"><canvas width="400" height="400" id="angle-dial" tabindex="0" role="application" aria-label="Rotate the line to ${q.answer} degrees. Zero degrees points right; angles increase counterclockwise. Left arrow increases the angle; right arrow decreases it."></canvas></div>`,`<p class="small-instruction">0° points right · Measure counterclockwise ↺<br>Drag the line or use arrow keys.</p><button class="primary" id="angle-lock" disabled>Choose your angle</button>`);
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();
}
Expand Down
Loading
Loading