Spaces:
Running
Running
Download sweep.js from HuggingFaceBio/carbon-a-release: direct link, hf CLI and curl.
- Browser
- Download file 4.61 kB
-
https://huggingface.co/spaces/HuggingFaceBio/carbon-a-release/resolve/main/sweep.js
- Command line
-
hf download hf://spaces/HuggingFaceBio/carbon-a-release/sweep.js
-
curl -L -o sweep.js https://huggingface.co/spaces/HuggingFaceBio/carbon-a-release/resolve/main/sweep.js
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(); | |
| } | |