Download annotations.mjs from Xenova/navier-stokes-vortex-webgpu: direct link, hf CLI and curl.
- Browser
- Download file 6.31 kB
-
https://huggingface.co/spaces/Xenova/navier-stokes-vortex-webgpu/resolve/main/annotations.mjs
- Command line
-
hf download hf://spaces/Xenova/navier-stokes-vortex-webgpu/annotations.mjs
-
curl -L -o annotations.mjs https://huggingface.co/spaces/Xenova/navier-stokes-vortex-webgpu/resolve/main/annotations.mjs
6.31 kB
| import { CORE } from './core-profile.mjs'; | |
| import { DEFAULT_H } from './proof-model.mjs'; | |
| import { VISUAL_RADIAL_GAIN, axialMap } from './presentation.mjs'; | |
| import { storyDiagramCue, storyAnnotationSlot, storyOpeningCaption, storyEditOpacity, storyEnding, storyEquatorialOpacity } from './story.mjs'; | |
| import { projectScenePoint, projectWorldPoint, scalePoint, unprojectAtDepth } from './camera.mjs'; | |
| // Reference coordinates use the renderer's [x,z,y] convention. | |
| function fieldPoint(X,eta,theta) { | |
| const q=1/(1-eta*eta), r=Math.sqrt(2*X*q)*VISUAL_RADIAL_GAIN*CORE.worldScale; | |
| return [r*Math.cos(theta),axialMap(q**(.5-DEFAULT_H)*eta)*CORE.worldScale,r*Math.sin(theta)]; | |
| } | |
| // The two opening mechanisms are shown together until the top view arrives. | |
| function showPairedCallouts(state) { | |
| return state.storyChoice===null ? state.storySeconds<24 : state.storyChoice!==2; | |
| } | |
| // Sampled field anchors and the world-space endpoints their leaders reach. | |
| const OPENING_CALLOUTS=Object.freeze([ | |
| {id:'story-axial-note', X:.00002, eta:.10, end:[5.5,10,2]}, | |
| {id:'story-inward-note', X:.045, eta:.0008, end:[8.2,-1.8,2]}, | |
| ]); | |
| // HTML captions occupy stable reading slots. Their leaders have two actual | |
| // world-space endpoints and share the flow depth buffer. The label endpoint | |
| // is projected onto the caption plane, keeping the words steady as the flow turns. | |
| export function createAnnotations(document) { | |
| const $=id=>document.getElementById(id); | |
| const notes=['story-diagram-note','story-axial-note','story-inward-note'].map($); | |
| function placeNote(label,view,start,zone,opacity,reference=[6,8,2]) { | |
| const box=storyAnnotationSlot(view.rect,zone,view.radialBound/view.frame.worldPerPixel,projectWorldPoint(view,[0,0,0]).y); | |
| label.style.width=`${box.width}px`;label.style.left=`${box.x}px`;label.style.top=`${box.y}px`; | |
| label.style.opacity=String(opacity);label.dataset.zone=zone; | |
| const endpoint=box.attach==='top'?{x:box.x+box.width/2,y:box.y-12}: | |
| {x:box.attach==='right'?box.x+box.width+12:box.x-12,y:box.y+16}; | |
| const end=unprojectAtDepth(view,endpoint,reference); | |
| return [start,end,opacity]; | |
| } | |
| function positionStoryDiagram(state,views){ | |
| const view=views.find(v=>v.featured),layer=$('story-diagram-layer'); | |
| layer.hidden=!view;if(!view)return []; | |
| const cue=storyDiagramCue(state.storySeconds,state.storyChoice); | |
| const paired=showPairedCallouts(state); | |
| notes[0].hidden=paired;notes[1].hidden=notes[2].hidden=!paired; | |
| const reduced=!!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches; | |
| const fading=state.playing&&state.storyChoice===null&&!reduced; | |
| const opacity=fading?storyEditOpacity(state.storySeconds):1; | |
| const ending=storyEnding(state.storyChoice===null?state.storySeconds:0); | |
| layer.style.opacity=String(ending.annotations); | |
| const conclusion=$('story-ending'),rect=view.rect; | |
| conclusion.hidden=ending.conclusion<=0; | |
| conclusion.style.opacity=String(ending.conclusion); | |
| conclusion.style.width=`${Math.min(530,rect.width*(rect.width>=760?.52:.9))}px`; | |
| conclusion.style.left=`${rect.x+rect.width*(rect.width>=760?.64:.5)}px`; | |
| conclusion.style.top=`${rect.y+rect.height*.5}px`; | |
| const lines=[]; | |
| if(paired){ | |
| for(const definition of OPENING_CALLOUTS){ | |
| const axial=definition.id==='story-axial-note',label=$(definition.id); | |
| const caption=storyOpeningCaption(state.storyChoice===null?state.storySeconds:0,axial); | |
| const body=label.querySelector('p');body.textContent=caption.text; | |
| body.style.opacity=String(fading?storyEditOpacity(state.storySeconds,caption.edits):1); | |
| // Titles and leaders remain present while only the sentence changes. | |
| // The pair leaves together once the top-view explanation arrives. | |
| const alpha=fading?storyEditOpacity(state.storySeconds,[0,24]):1; | |
| const theta=axial?0:view.pose.yaw+Math.PI; | |
| const start=scalePoint(view,fieldPoint(definition.X,definition.eta,theta)); | |
| lines.push(placeNote(label,view,start,axial?'upper-right':'inflow',alpha,definition.end)); | |
| } | |
| }else{ | |
| const theta=cue.zone==='rotation'?-Math.PI/2+.45:cue.radiusGuide?view.pose.yaw-.55:view.pose.yaw; | |
| const start=scalePoint(view,fieldPoint(cue.X,cue.eta,theta)); | |
| lines.push(placeNote(notes[0],view,start,cue.zone,opacity)); | |
| } | |
| const path=points=>points.map((p,i)=>`${i?'L':'M'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' '); | |
| const point=(X,eta,theta=0)=>projectScenePoint(view,fieldPoint(X,eta,theta)); | |
| // The rotation arrow sits just outside the visible circle and follows | |
| // increasing physical theta. It is not hidden inside the dense ribbons. | |
| const top=storyEquatorialOpacity(state.storySeconds,state.storyChoice,view.pose.top); | |
| let direction='',width=''; | |
| if(top>.001){ | |
| const radius=view.radialBound*1.10; | |
| direction=path(Array.from({length:37},(_,i)=>{ | |
| const theta=.25+i*.045; | |
| return projectWorldPoint(view,[radius*Math.cos(theta),0,radius*Math.sin(theta)]); | |
| })); | |
| } | |
| if(cue.radiusGuide){ | |
| // A fixed similarity radius, just below the central layer. The leader | |
| // starts at this same endpoint, clear of the coordinate-axis guides. | |
| const a=point(0,cue.eta),b=point(cue.X,cue.eta,view.pose.yaw-.55); | |
| const length=Math.hypot(b.x-a.x,b.y-a.y),nx=-(b.y-a.y)/Math.max(length,1),ny=(b.x-a.x)/Math.max(length,1); | |
| width=path([a,b])+' '+path([{x:a.x-6*nx,y:a.y-6*ny},{x:a.x+6*nx,y:a.y+6*ny}])+' '+path([{x:b.x-6*nx,y:b.y-6*ny},{x:b.x+6*nx,y:b.y+6*ny}]); | |
| } | |
| const arrow=$('story-direction-guide'),radial=$('story-width-guide'); | |
| arrow.setAttribute('d',direction);arrow.style.opacity=String(top); | |
| radial.setAttribute('d',width);radial.style.opacity=String(opacity); | |
| const plane=$('story-plane-label'); | |
| plane.hidden=top<.001;plane.style.opacity=String(top); | |
| const center=projectWorldPoint(view,[0,0,0]); | |
| plane.style.left=`${Math.min(center.x+24,rect.x+rect.width-(plane.offsetWidth||220)-12)}px`; | |
| plane.style.top=`${center.y-1.10*view.radialBound/view.frame.worldPerPixel-58}px`; | |
| // WebGPU leaders do not inherit the DOM layer's opacity. | |
| return lines.map(([a,b,alpha])=>[a,b,alpha*ending.annotations]); | |
| } | |
| return {update:positionStoryDiagram}; | |
| } | |