From 52a43ec651cc92c219ef6d2b320b2e77d5b13235 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Jul 2026 01:54:23 -0500 Subject: feat(gallery): add R31 radar sweep The rotating scan after a radar PPI scope (reference filed in the catalogue's working directory): a beam circles inside a numbered bearing ring, an afterglow wedge trails it, contacts bloom as the beam passes and fade over the next half turn, and range rings sit under it all. Clicking marks the current bearing. The kit's round CRT draws traces; nothing rotated a scan until this. I verified it in headless Chrome: the sweep rotates, the mark click captures the live bearing, no console exceptions. --- docs/prototypes/panel-widget-gallery.html | 50 +++++++++++++++++++++ .../references/2026-07-12-radar-ppi.png | Bin 0 -> 169803 bytes 2 files changed, 50 insertions(+) create mode 100644 working/retro-stereo-widgets/references/2026-07-12-radar-ppi.png diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index ab69855..f8330bb 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -894,6 +894,9 @@ card(I,'R26','Response graph', card(I,'R30','Telegraph indicator', ``, 'state on a pie of sectors. The pointer and its little flag name the active state, engine-telegraph style. Click to step the state. After an electric indicator dial.'); +card(I,'R31','Radar sweep', + ``, + 'the rotating scan. A sweep beam circles the bearing ring, contacts bloom as it passes and fade after. Click to mark the current bearing. After a radar PPI scope.'); /* ============ CONTROL WIRING ============ */ /* 01 toggle */ @@ -2006,6 +2009,53 @@ function setTelind(i){telI=i;const N=TELIND_STATES.length; setRd('R30',TELIND_STATES[i]);} setTelind(0); +/* R31 radar sweep: rotating beam, blooming contacts, click to mark */ +let radarA=210,radarSweep=null,radarBlips=[],radarMark=null; +const RADAR_CONTACTS=[[60,.55],[205,.7],[318,.4]]; +(function(){const s=$('radar'),cx=65,cy=65; + const defs=svgEl(s,'defs',{}); + const clip=svgEl(defs,'clipPath',{id:'radarClip'}); + svgEl(clip,'circle',{cx,cy,r:46}); + svgEl(s,'circle',{cx,cy,r:62,fill:'#1a1714',stroke:'#060505','stroke-width':2}); + svgEl(s,'circle',{cx,cy,r:52,fill:'#120b05',stroke:'#3a2a12','stroke-width':1}); + for(let d=0;d<360;d+=30){const a=d; + const [x1,y1]=polar(cx,cy,51,a),[x2,y2]=polar(cx,cy,47,a); + svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--gold)','stroke-width':1,opacity:.8}); + const g=svgEl(s,'g',{transform:`rotate(${a},${cx},${cy})`}); + svgEl(g,'text',{x:cx,y:cy-47.5,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)', + fill:'var(--gold)',opacity:.9}).textContent=String(d).padStart(3,'0');} + for(let d=0;d<360;d+=10){if(d%30===0)continue; + const [x1,y1]=polar(cx,cy,51,d),[x2,y2]=polar(cx,cy,49,d); + svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--gold)','stroke-width':.6,opacity:.5});} + const g=svgEl(s,'g',{'clip-path':'url(#radarClip)'}); + svgEl(g,'circle',{cx,cy,r:46,fill:'#0d0803'}); + for(const r of [15,30,45]) + svgEl(g,'circle',{cx,cy,r,fill:'none',stroke:'rgba(226,160,56,.18)','stroke-width':.7}); + radarSweep=svgEl(g,'g',{}); + for(let t=0;t<5;t++){const a0=-14+t*2.8,a1=a0+2.8; + const [x1,y1]=polar(cx,cy,46,a0),[x2,y2]=polar(cx,cy,46,a1); + svgEl(radarSweep,'path',{d:`M ${cx} ${cy} L ${x1} ${y1} A 46 46 0 0 1 ${x2} ${y2} Z`, + fill:`rgba(226,160,56,${(0.04+t*0.045).toFixed(3)})`});} + svgEl(radarSweep,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'var(--gold-hi)','stroke-width':1.4, + transform:`rotate(0,${cx},${cy})`,opacity:.95}); + RADAR_CONTACTS.forEach(([bd,rf])=>{const [x,y]=polar(cx,cy,46*rf,bd); + radarBlips.push(svgEl(g,'circle',{cx:x,cy:y,r:2.2,fill:'var(--gold-hi)',opacity:0}));}); + svgEl(s,'circle',{cx,cy,r:3,fill:'var(--gold)',opacity:.8}); + radarMark=svgEl(s,'text',{x:cx,y:124,'text-anchor':'middle','font-size':6,'letter-spacing':'.12em', + 'font-family':'var(--mono)',fill:'var(--steel)'}); + radarMark.textContent='BEARING —'; + $('radar').onclick=()=>{radarMark.textContent='MARK '+String(Math.round(radarA)%360).padStart(3,'0'); + setRd('R31','mark '+String(Math.round(radarA)%360).padStart(3,'0'));};})(); +function tickRadar(){radarA=(radarA+2.6)%360; + radarSweep.setAttribute('transform',`rotate(${radarA},65,65)`); + RADAR_CONTACTS.forEach(([bd],i)=>{ + let d=(radarA-bd+360)%360; // degrees since beam passed + radarBlips[i].setAttribute('opacity',d<200?Math.max(0,.95-d/200):0);}); + radarMark.textContent=radarMark.textContent.startsWith('MARK')?radarMark.textContent: + 'BEARING '+String(Math.round(radarA)%360).padStart(3,'0');} +tickRadar();setRd('R31','sweeping'); +if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tickRadar,50); + /* R25 fourteen-segment display: the starburst that spells */ const SEG14_WORDS=['ZOOM','ECHO','TAPE','MOOD','HALL','COMP']; const SEG14_MAP={A:['a','b','c','e','f','g1','g2'],C:['a','d','e','f'], diff --git a/working/retro-stereo-widgets/references/2026-07-12-radar-ppi.png b/working/retro-stereo-widgets/references/2026-07-12-radar-ppi.png new file mode 100644 index 0000000..7d22420 Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-12-radar-ppi.png differ -- cgit v1.2.3