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}; }