carbon-a-release / sweep.js
lvwerra's picture
lvwerra HF Staff
Restore strand hover previews and make Space stop playback while retaining assembly details
c3828f6 verified
Raw History Blame Contribute Delete
4.61 kB
// A four-second sweep for video capture, with no counters or on-page controls.
const SWEEP_DURATION=4000,NOON=-Math.PI/2;
const sweep={running:false,status:'ready',progress:0,angle:NOON,frame:0,startedAt:0};
let interactionPending=false;
function prepareSweep(){
Object.assign(POC,{sweep,startSweep,stopSweep});
let handledSpace=false;
document.addEventListener('keydown',e=>{
if(e.key==='Escape'&&sweep.running)stopSweep();
if((e.code!=='Space'&&e.key!==' ')||e.altKey||e.ctrlKey||e.metaKey)return;
// A held key must neither toggle repeatedly nor activate a focused button.
if(e.repeat){if(handledSpace){e.preventDefault();e.stopPropagation()}return}
if(e.target.closest('input,textarea,select,[contenteditable]')||document.querySelector('dialog[open]'))return;
// Stopping playback takes precedence over a button's normal Space action.
if(!sweep.running&&e.target.closest('button,a'))return;
handledSpace=true;e.preventDefault();e.stopPropagation();
sweep.running?stopSweep():startSweep();
},true);
document.addEventListener('keyup',e=>{
if((e.code==='Space'||e.key===' ')&&handledSpace){handledSpace=false;e.preventDefault();e.stopPropagation()}
},true);
window.addEventListener('blur',()=>{handledSpace=false});
document.addEventListener('visibilitychange',()=>{if(document.hidden&&sweep.running)stopSweep()});
}
function scheduleInteraction(){
if(interactionPending)return;
interactionPending=true;requestAnimationFrame(()=>{interactionPending=false;drawInteraction()});
}
function drawInteraction(){
const dpr=interaction.width/W;
fx.setTransform(dpr,0,0,dpr,0,0);fx.clearRect(0,0,W,H);
const angle=sweep.running?sweep.angle:selected?.angle??sweep.angle;
// A fine hand joins the selected strand at its root, with no dial or numerals.
const c=screen([0,0]),tip=screen(polar(92,angle));
fx.strokeStyle=palette.highlight;fx.lineWidth=.85;fx.lineCap='round';
fx.beginPath();fx.moveTo(...c);fx.lineTo(...tip);fx.stroke();
fx.fillStyle=palette.highlight;fx.beginPath();fx.arc(...c,1.5,0,TAU);fx.fill();
if(!selected)return;
const a=selected;fx.lineCap='butt';fx.strokeStyle=palette.highlight;fx.lineWidth=1.8;
fx.beginPath();fx.moveTo(...screen([hitData[a.geometryStart],hitData[a.geometryStart+1]]));
for(let k=a.geometryStart+4;k<a.geometryEnd;k+=8)fx.lineTo(...screen([hitData[k],hitData[k+1]]));
fx.stroke();fx.beginPath();fx.arc(...screen(a.tip),4,0,TAU);fx.fill();
}
function updateSweep(progress){
sweep.progress=Math.max(0,Math.min(1,progress));
// Gentle acceleration and deceleration; a complete turn still takes 4 s.
const t=sweep.progress,eased=t*t*(3-2*t);
sweep.angle=NOON+TAU*eased;
const n=leaves.length,count=Math.max(0,Math.min(n,Math.floor((sweep.angle-START)/SPAN*n+.5)));
const a=leaves[Math.max(0,Math.min(n-1,count-1))];
if(a!==selected||$('#selection').hidden)selectAssembly(a,false,false);
}
function updateSweepState(){
$('.stage-wrap').dataset.sweeping=String(sweep.running);
}
function cacheSweepTree(){
const still=$('#tree-still');still.width=canvas.width;still.height=canvas.height;
const frozen=still.getContext('2d',{willReadFrequently:true});
frozen.drawImage(canvas,0,0);frozen.drawImage(overlay,0,0);
}
function startSweep(){
stopSweep(false);
const redraw=zoom!==1||pan.x!==0||pan.y!==0||hoverBin!==null;
zoom=1;pan={x:0,y:0};clearSelection();hoverBin=null;$('#layer-tooltip').hidden=true;
$('#results').replaceChildren();$('#selection').hidden=true;
sweep.running=true;sweep.status='running';sweep.startedAt=0;
updateSweep(0);updateSweepState();
// Render the stationary tree once. Only the thin interaction canvas moves.
if(redraw)draw();else{cacheSweepTree();drawInteraction()}
sweep.frame=requestAnimationFrame(function step(){
const now=performance.now();
if(!sweep.running)return;
if(!sweep.startedAt)sweep.startedAt=now;
updateSweep((now-sweep.startedAt)/SWEEP_DURATION);drawInteraction();
if(sweep.progress<1){sweep.frame=requestAnimationFrame(step);return}
sweep.running=false;sweep.status='complete';sweep.frame=0;
// Leave the final assembly visible so its details can be read after playback.
if(selected)selectAssembly(selected,true,false);
updateSweepState();
$('#sweep-announcement').textContent='Sweep complete.';
drawInteraction();
});
}
function stopSweep(pin=true){
if(!sweep.running)return;
cancelAnimationFrame(sweep.frame);sweep.frame=0;sweep.running=false;sweep.status='stopped';
updateSweepState();$('#sweep-announcement').textContent='Sweep stopped.';
if(pin&&selected)selectAssembly(selected);else clearSelection();
scheduleInteraction();
}