diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 22:40:22 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 22:40:22 -0500 |
| commit | 066bdac1c195fa27d4b71f2ba53ee9265ee7c833 (patch) | |
| tree | 451f613874801c291b9fb692c8ed6da3d731dce9 /docs/prototypes | |
| parent | 4064de53a9abfa7620d9d7e3f95e867ba0bf54e3 (diff) | |
| download | archsetup-066bdac1c195fa27d4b71f2ba53ee9265ee7c833.tar.gz archsetup-066bdac1c195fa27d4b71f2ba53ee9265ee7c833.zip | |
refactor(gallery): extract indicators R10, R11, R25, R26, R30, R31 into GW builders
Diffstat (limited to 'docs/prototypes')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 226 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 314 |
2 files changed, 328 insertions, 212 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 2d7bcb9..f81b60b 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -1850,22 +1850,22 @@ card(I,'N28','Patch-bay jack field', (st,rd)=>GW.patchBay(st,{onChange:(v,t)=>rd(t)}), '<b>routing shown as patches.</b> A grid of jacks with cables between connected pairs. Click one jack then another to add/remove a cable.'); card(I,'R10','Data matrix readout', - `<svg id="dmx" class="rsvg press" viewBox="0 0 190 74" width="190" height="74"></svg>`, + (st,rd)=>GW.dataMatrix(st,{onChange:(v,t)=>rd(t)}), '<b>a page of amber fields.</b> The console data block — several labeled values on one lit matrix. Click to cycle pages; readout names the page. After a cockpit data readout.'); card(I,'R11','Warning flag window', - `<svg id="wflag" class="rsvg press" viewBox="0 0 120 64" width="120" height="64"></svg>`, + (st,rd)=>GW.warningFlag(st,{onChange:(v,t)=>rd(t)}), '<b>the mechanical alarm flag.</b> A striped barber-pole slides into a window when the condition trips — not a lamp, a flag. Click to trip and clear it. After the VIB flag on an altimeter.'); card(I,'R25','Fourteen-segment display', - `<svg id="seg14" class="rsvg press" viewBox="0 0 130 54" width="130" height="54"></svg>`, + (st,rd)=>GW.seg14(st,{onChange:(v,t)=>rd(t)}), '<b>segments that spell.</b> The starburst alphanumeric — diagonals and split bars let it write words, not just digits. Click to cycle the word. After a pedal preset display.'); card(I,'R26','Response graph', - `<svg id="rgraph" class="rsvg" viewBox="0 0 190 110" width="190" height="110"></svg>`, + (st,rd)=>GW.responseGraph(st,{onChange:(v,t)=>rd(t)}), '<b>a curve on labeled axes.</b> Log frequency across, dB up the side, and the amber peak is yours to place — drag it anywhere on the plot. After the response display of a mastering processor.'); card(I,'R30','Telegraph indicator', - `<svg id="telind" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.telegraphIndicator(st,{onChange:(v,t)=>rd(t)}), '<b>state on a pie of sectors.</b> 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', - `<svg id="radar" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.radarSweep(st,{onChange:(v,t)=>rd(t)}), '<b>the rotating scan.</b> 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.'); card(I,'R35','Day-date disc calendar', `<svg id="daydate" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, @@ -1916,215 +1916,17 @@ function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12); /* R07-R09 extracted to widgets.js (GW.*) */ -/* R10 data matrix readout */ -const DMX_PAGES=[ - ['SYS',()=>['CPU 42 MEM 61','DSK 58 TMP 47C','ARCH 6.18 LTS']], - ['NET',()=>['WIFI @HYATT 300M','VPN UP DNS OK','BT 2 PAIRED']], - ['TIME',()=>{const d=new Date(),p=n=>String(n).padStart(2,'0'); - return [`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`, - `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`, - 'UP 14D 06:12'];}]]; -let dmxI=0,dmxLines=[]; -(function(){const s=$('dmx'); - const defs=svgEl(s,'defs',{}); - const fl=svgEl(defs,'filter',{id:'dmxGlow',x:'-30%',y:'-30%',width:'160%',height:'160%'}); - svgEl(fl,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:1.1}); - const bg=svgEl(defs,'linearGradient',{id:'dmxBg',x1:0,y1:0,x2:0,y2:1}); - svgEl(bg,'stop',{offset:'0','stop-color':'var(--scr-bg1,#140d06)'}); - svgEl(bg,'stop',{offset:'1','stop-color':'var(--scr-bg2,#0a0705)'}); - svgEl(s,'rect',{x:1,y:1,width:188,height:72,rx:5,fill:'url(#dmxBg)',stroke:'#2c261d'}); - for(let r=0;r<3;r++){ - const glow=svgEl(s,'text',{x:12,y:24+r*18,'font-size':9,'letter-spacing':'.15em', - 'font-family':'var(--mono)',fill:'var(--scr-hi,var(--gold-hi))',opacity:.6,filter:'url(#dmxGlow)'}); - const crisp=svgEl(s,'text',{x:12,y:24+r*18,'font-size':9,'letter-spacing':'.15em', - 'font-family':'var(--mono)',fill:'var(--scr-hi,var(--gold-hi))'}); - dmxLines.push([glow,crisp]);} - $('dmx').onclick=()=>setDmx((dmxI+1)%DMX_PAGES.length);})(); -function setDmx(i){dmxI=i;const [name,rowsFn]=DMX_PAGES[i]; - rowsFn().forEach((txt,r)=>{dmxLines[r][0].textContent=txt;dmxLines[r][1].textContent=txt;}); - setRd('R10','page '+name);} -setDmx(0); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{if(DMX_PAGES[dmxI][0]==='TIME')setDmx(dmxI);},1000); - -/* R11 warning flag window */ -let flagOn=false,flagStripes=null; -(function(){const s=$('wflag'); - const defs=svgEl(s,'defs',{}); - const pat=svgEl(defs,'pattern',{id:'barber',width:10,height:10,patternUnits:'userSpaceOnUse', - patternTransform:'rotate(45)'}); - svgEl(pat,'rect',{x:0,y:0,width:10,height:10,fill:'#17130c'}); - svgEl(pat,'rect',{x:0,y:0,width:5,height:10,fill:'var(--gold)'}); - const clip=svgEl(defs,'clipPath',{id:'flagWin'}); - svgEl(clip,'rect',{x:14,y:20,width:56,height:24}); - svgEl(s,'rect',{x:11,y:17,width:62,height:30,rx:3,fill:'#0a0908',stroke:'#2c261d','stroke-width':2}); - const g=svgEl(s,'g',{'clip-path':'url(#flagWin)'}); - flagStripes=svgEl(g,'rect',{x:14,y:20,width:56,height:24,fill:'url(#barber)'}); - flagStripes.style.transition='transform .18s';flagStripes.style.transform='translateX(-58px)'; - svgEl(s,'text',{x:92,y:35,'text-anchor':'middle','font-size':7,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='VIB'; - $('wflag').onclick=()=>setFlag(!flagOn);})(); -function setFlag(on){flagOn=on;flagStripes.style.transform=on?'translateX(0)':'translateX(-58px)'; - setRd('R11',on?'FLAG':'clear');} -setFlag(false); +/* R10, R11 extracted to widgets.js (GW.*) */ + /* R13, R17 extracted to widgets.js (GW.*) */ -/* R30 telegraph indicator: the pointer names the state on labeled sectors */ -const TELIND_STATES=['OK','TEST','BUSY','STOP','ATTN','FAULT']; -let telI=0,telPtr=null; -(function(){const s=$('telind'),cx=55,cy=55,N=TELIND_STATES.length; - svgEl(s,'circle',{cx,cy,r:50,fill:'#171412',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:43,fill:'var(--cream)',stroke:'#b7b29a','stroke-width':1}); - TELIND_STATES.forEach((st,i)=>{ - const a=i*360/N-90+180/N; - const [lx1,ly1]=polar(cx,cy,9,i*360/N),[lx2,ly2]=polar(cx,cy,43,i*360/N); - svgEl(s,'line',{x1:lx1,y1:ly1,x2:lx2,y2:ly2,stroke:'#2b2418','stroke-width':1}); - const g=svgEl(s,'g',{transform:`rotate(${a+90},${cx},${cy})`}); - svgEl(g,'text',{x:cx,y:cy-28,'text-anchor':'middle','font-size':7,'font-weight':700, - 'font-family':'var(--mono)',fill:'#2b2418'}).textContent=st;}); - telPtr=svgEl(s,'g',{});telPtr.style.transition='transform .18s';telPtr.style.transformOrigin=`${cx}px ${cy}px`; - svgEl(telPtr,'polygon',{points:`${cx-2.5},${cy} ${cx},${cy-38} ${cx+2.5},${cy}`,fill:'var(--fail)',stroke:'#7a2a1a','stroke-width':.5}); - svgEl(telPtr,'rect',{x:cx+1,y:cy-16,width:10,height:7,rx:1,fill:'var(--gold)',stroke:'#7d5c16','stroke-width':.6}); - svgEl(s,'circle',{cx,cy,r:8,fill:'#171412',stroke:'#000'}); - $('telind').onclick=()=>setTelind((telI+1)%TELIND_STATES.length);})(); -function setTelind(i){telI=i;const N=TELIND_STATES.length; - telPtr.style.transform=`rotate(${i*360/N+180/N}deg)`; - 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:'var(--scr-bg1,#120b05)',stroke:'var(--scr-brd,#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(--scr-ink,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(--scr-ink,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(--scr-ink,var(--gold))','stroke-width':.6,opacity:.5});} - const g=svgEl(s,'g',{'clip-path':'url(#radarClip)'}); - svgEl(g,'circle',{cx,cy,r:46,fill:'var(--scr-bg2,#0d0803)'}); - for(const r of [15,30,45]) - svgEl(g,'circle',{cx,cy,r,fill:'none',stroke:'var(--scr-ink,var(--gold))','stroke-opacity':.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:'var(--scr-ink,var(--gold))','fill-opacity':(0.04+t*0.045).toFixed(3)});} - svgEl(radarSweep,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'var(--scr-hi,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(--scr-hi,var(--gold-hi))',opacity:0}));}); - svgEl(s,'circle',{cx,cy,r:3,fill:'var(--scr-ink,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'], - D:['a','b','c','d','i','l'],E:['a','d','e','f','g1','g2'],H:['b','c','e','f','g1','g2'], - L:['d','e','f'],M:['b','c','e','f','h','j'],O:['a','b','c','d','e','f'], - P:['a','b','e','f','g1','g2'],T:['a','i','l'],Z:['a','d','j','k']}; -let seg14I=0,seg14Cells=[]; -(function(){const s=$('seg14'); - svgEl(s,'rect',{x:1,y:1,width:128,height:52,rx:5,fill:'#0a0f08',stroke:'#1c2a16','stroke-width':2}); - // segment shapes in a 22x36 local box (x0,y0 top-left) - const TH=2.6,W=22,H=36,mid=H/2; - const segs={ - a:[[2,0],[W-2,0],[W-4,TH],[4,TH]], - d:[[2,H],[W-2,H],[W-4,H-TH],[4,H-TH]], - f:[[0,2],[TH,4],[TH,mid-2],[0,mid-1]], - e:[[0,H-2],[TH,H-4],[TH,mid+2],[0,mid+1]], - b:[[W,2],[W-TH,4],[W-TH,mid-2],[W,mid-1]], - c:[[W,H-2],[W-TH,H-4],[W-TH,mid+2],[W,mid+1]], - g1:[[2,mid],[4,mid-TH/2],[W/2-1,mid-TH/2],[W/2-1,mid+TH/2],[4,mid+TH/2]], - g2:[[W-2,mid],[W-4,mid-TH/2],[W/2+1,mid-TH/2],[W/2+1,mid+TH/2],[W-4,mid+TH/2]], - i:[[W/2-TH/2,3],[W/2+TH/2,3],[W/2+TH/2,mid-2],[W/2-TH/2,mid-2]], - l:[[W/2-TH/2,H-3],[W/2+TH/2,H-3],[W/2+TH/2,mid+2],[W/2-TH/2,mid+2]], - h:[[3,3],[5.5,3],[W/2-2,mid-3],[W/2-4.5,mid-3]], - j:[[W-3,3],[W-5.5,3],[W/2+2,mid-3],[W/2+4.5,mid-3]], - k:[[3,H-3],[5.5,H-3],[W/2-2,mid+3],[W/2-4.5,mid+3]], - m:[[W-3,H-3],[W-5.5,H-3],[W/2+2,mid+3],[W/2+4.5,mid+3]]}; - for(let d=0;d<4;d++){const x0=13+d*28,y0=9,cell={}; - for(const k in segs){ - cell[k]=svgEl(s,'polygon',{points:segs[k].map(p=>`${x0+p[0]},${y0+p[1]}`).join(' '), - fill:'var(--sevoff)'});} - seg14Cells.push(cell);} - $('seg14').onclick=()=>setSeg14((seg14I+1)%SEG14_WORDS.length);})(); -function setSeg14(i){seg14I=i;const word=SEG14_WORDS[i].padEnd(4,' '); - seg14Cells.forEach((cell,d)=>{const lit=SEG14_MAP[word[d]]||[]; - for(const k in cell){const on=lit.includes(k); - cell[k].setAttribute('fill',on?'var(--sevgrn)':'var(--sevoff)'); - cell[k].setAttribute('style',on?'filter:drop-shadow(0 0 2.5px rgba(87,211,87,.6))':'');}}); - setRd('R25',SEG14_WORDS[i]);} -setSeg14(0); - -/* R26 response graph: log-frequency axes, a draggable amber peak */ -const RG={x0:30,x1:182,y0:8,y1:86,fLo:Math.log10(32),fHi:Math.log10(16000),db:12}; -let rgFc=1200,rgGain=6.5,rgCurve=null,rgGlowLine=null,rgHandle=null; -(function(){const s=$('rgraph'); - svgEl(s,'rect',{x:1,y:1,width:188,height:108,rx:5,fill:'#0d1410',stroke:'#22301f','stroke-width':2}); - const FREQS=[32,64,128,250,500,1000,2000,4000,8000,16000]; - FREQS.forEach(f=>{const x=RG.x0+(Math.log10(f)-RG.fLo)/(RG.fHi-RG.fLo)*(RG.x1-RG.x0); - svgEl(s,'line',{x1:x,y1:RG.y0,x2:x,y2:RG.y1,stroke:'#22301f','stroke-width':.6}); - svgEl(s,'text',{x,y:RG.y1+9,'text-anchor':'middle','font-size':4.8,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=f<1000?f:(f/1000)+'k';}); - for(const db of [12,6,0,-6,-12]){const y=RG.y0+(RG.db-db)/(2*RG.db)*(RG.y1-RG.y0); - svgEl(s,'line',{x1:RG.x0,y1:y,x2:RG.x1,y2:y,stroke:'#22301f','stroke-width':db===0?1:.6}); - svgEl(s,'text',{x:RG.x0-4,y:y+2,'text-anchor':'end','font-size':4.8,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=(db>0?'+':'')+db;} - rgGlowLine=svgEl(s,'polyline',{points:'',fill:'none',stroke:'var(--gold-hi)','stroke-width':3, - opacity:.35,filter:'url(#avGlow)'}); - rgCurve=svgEl(s,'polyline',{points:'',fill:'none',stroke:'var(--gold)','stroke-width':1.6}); - rgHandle=svgEl(s,'circle',{r:4,fill:'var(--gold-hi)',stroke:'#7d5c16','stroke-width':1}); - rgHandle.style.cursor='move'; - // 2D drag: the handle (and the plot) place the peak in both axes at once - s.style.touchAction='none'; - s.addEventListener('pointerdown',e=>{s.setPointerCapture(e.pointerId); - const move=ev=>{const r=s.getBoundingClientRect(),sx=190/r.width,sy=110/r.height; - const px=(ev.clientX-r.left)*sx,py=(ev.clientY-r.top)*sy; - const lf=RG.fLo+Math.max(0,Math.min(1,(px-RG.x0)/(RG.x1-RG.x0)))*(RG.fHi-RG.fLo); - const g=RG.db-Math.max(0,Math.min(1,(py-RG.y0)/(RG.y1-RG.y0)))*2*RG.db; - setRgraph(Math.pow(10,lf),g);}; - move(e); - const up=()=>{s.removeEventListener('pointermove',move);s.removeEventListener('pointerup',up);s.removeEventListener('pointercancel',up);}; - s.addEventListener('pointermove',move);s.addEventListener('pointerup',up);s.addEventListener('pointercancel',up); - e.preventDefault();});})(); -function setRgraph(fc,gain){rgFc=Math.max(32,Math.min(16000,fc));rgGain=Math.max(-12,Math.min(12,gain)); - const xOf=f=>RG.x0+(Math.log10(f)-RG.fLo)/(RG.fHi-RG.fLo)*(RG.x1-RG.x0); - const yOf=db=>RG.y0+(RG.db-db)/(2*RG.db)*(RG.y1-RG.y0); - const sigma=0.28;let pts=''; - for(let i=0;i<=76;i++){const lf=RG.fLo+i/76*(RG.fHi-RG.fLo); - const db=rgGain*Math.exp(-Math.pow(lf-Math.log10(rgFc),2)/(2*sigma*sigma)); - pts+=`${xOf(Math.pow(10,lf)).toFixed(1)},${yOf(db).toFixed(1)} `;} - rgCurve.setAttribute('points',pts.trim());rgGlowLine.setAttribute('points',pts.trim()); - rgHandle.setAttribute('cx',xOf(rgFc));rgHandle.setAttribute('cy',yOf(rgGain)); - const fLbl=rgFc<1000?Math.round(rgFc)+' Hz':(rgFc/1000).toFixed(1)+' kHz'; - setRd('R26',`${fLbl} · ${rgGain>=0?'+':''}${rgGain.toFixed(1)} dB`);} -setRgraph(1200,6.5); +/* R25, R26, R30, R31 extracted to widgets.js (GW.*) */ + + + /* ============ LIVE SIGNAL LOOPS ============ */ /* extracted meters: the page keeps the clock + demo signal, drives card handles */ @@ -2371,10 +2173,10 @@ let dkPos=0,dkCarry=0; /* ---- screen-family chips: first round, the clearly-screen widgets ---- Defaults match each widget's shipped color (var fallbacks), so no family is applied until a chip is clicked. The nixie stays chipless on purpose: neon is only ever orange, so alternates would be historically false. */ -screenChips('R10',$('dmx'),['amber','green','red','blue','vfd'],'amber'); +screenChips('R10',$('card-R10').gw.el,['amber','green','red','blue','vfd'],'amber'); screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'green'); screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white'); -screenChips('R31',$('radar'),['amber','green','red','blue','vfd'],'amber'); +screenChips('R31',$('card-R31').gw.el,['amber','green','red','blue','vfd'],'amber'); screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green'); BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no); if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');}); diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index f3e1127..4cd25eb 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -3207,6 +3207,320 @@ GW.patchBay = function (host, opts = {}) { return { el: patch, get: () => conns.slice(), set: c => { conns = c.slice(); draw(); } }; }; +/* R10 data matrix readout — a page of labeled amber fields; click cycles pages; + a page marked live re-renders on the builder's own 1s clock (reduced-motion gated) */ +GW.dataMatrix = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const pages = opts.pages || [ + ['SYS', () => ['CPU 42 MEM 61', 'DSK 58 TMP 47C', 'ARCH 6.18 LTS']], + ['NET', () => ['WIFI @HYATT 300M', 'VPN UP DNS OK', 'BT 2 PAIRED']], + ['TIME', () => { + const d = new Date(), p = n => String(n).padStart(2, '0'); + return [`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`, + `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`, + 'UP 14D 06:12']; + }, true]]; + const s = stageSvg(host, 'rsvg press', 190, 74); + def('dmxGlow', d => { + const fl = svgEl(d, 'filter', { id: 'dmxGlow', x: '-30%', y: '-30%', width: '160%', height: '160%' }); + svgEl(fl, 'feGaussianBlur', { in: 'SourceGraphic', stdDeviation: 1.1 }); + }); + /* the background gradient reads screen-family vars from this widget's subtree, + so it lives in the local defs, not the shared def sink */ + const bgId = uid('dmxBg'); + const defs = svgEl(s, 'defs', {}); + const bg = svgEl(defs, 'linearGradient', { id: bgId, x1: 0, y1: 0, x2: 0, y2: 1 }); + svgEl(bg, 'stop', { offset: '0', 'stop-color': 'var(--scr-bg1,#140d06)' }); + svgEl(bg, 'stop', { offset: '1', 'stop-color': 'var(--scr-bg2,#0a0705)' }); + svgEl(s, 'rect', { x: 1, y: 1, width: 188, height: 72, rx: 5, fill: `url(#${bgId})`, stroke: '#2c261d' }); + const lines = []; + for (let r = 0; r < 3; r++) { + const glow = svgEl(s, 'text', { + x: 12, y: 24 + r * 18, 'font-size': 9, 'letter-spacing': '.15em', + 'font-family': 'var(--mono)', fill: 'var(--scr-hi,var(--gold-hi))', opacity: .6, filter: 'url(#dmxGlow)' + }); + const crisp = svgEl(s, 'text', { + x: 12, y: 24 + r * 18, 'font-size': 9, 'letter-spacing': '.15em', + 'font-family': 'var(--mono)', fill: 'var(--scr-hi,var(--gold-hi))' + }); + lines.push([glow, crisp]); + } + let pi; + const set = i => { + pi = ((i % pages.length) + pages.length) % pages.length; + const [name, rowsFn] = pages[pi]; + rowsFn().forEach((txt, r) => { lines[r][0].textContent = txt; lines[r][1].textContent = txt; }); + onChange(pi, 'page ' + name); + }; + s.addEventListener('click', () => set(pi + 1)); + set(opts.page || 0); + if (!matchMedia('(prefers-reduced-motion: reduce)').matches) + setInterval(() => { if (pages[pi][2]) set(pi); }, 1000); + return { el: s, get: () => pi, set }; +}; + +/* R11 warning flag window — the striped mechanical flag slides into the window + when the condition trips; click trips and clears it */ +GW.warningFlag = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 120, 64); + def('barber', d => { + const pat = svgEl(d, 'pattern', { id: 'barber', width: 10, height: 10, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' }); + svgEl(pat, 'rect', { x: 0, y: 0, width: 10, height: 10, fill: '#17130c' }); + svgEl(pat, 'rect', { x: 0, y: 0, width: 5, height: 10, fill: 'var(--gold)' }); + }); + const clipId = uid('flagWin'); + const defs = svgEl(s, 'defs', {}); + const clip = svgEl(defs, 'clipPath', { id: clipId }); + svgEl(clip, 'rect', { x: 14, y: 20, width: 56, height: 24 }); + svgEl(s, 'rect', { x: 11, y: 17, width: 62, height: 30, rx: 3, fill: '#0a0908', stroke: '#2c261d', 'stroke-width': 2 }); + const g = svgEl(s, 'g', { 'clip-path': `url(#${clipId})` }); + const stripes = svgEl(g, 'rect', { x: 14, y: 20, width: 56, height: 24, fill: 'url(#barber)' }); + stripes.style.transition = 'transform .18s'; stripes.style.transform = 'translateX(-58px)'; + svgEl(s, 'text', { + x: 92, y: 35, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.14em', + 'font-family': 'var(--mono)', fill: 'var(--steel)' + }).textContent = opts.label || 'VIB'; + let on; + const set = v => { on = !!v; stripes.style.transform = on ? 'translateX(0)' : 'translateX(-58px)'; onChange(on, on ? 'FLAG' : 'clear'); }; + s.addEventListener('click', () => set(!on)); + set(opts.on || false); + return { el: s, get: () => on, set }; +}; + +/* R25 fourteen-segment display — the starburst alphanumeric that spells words; + click cycles the word */ +GW.seg14 = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const words = opts.words || ['ZOOM', 'ECHO', 'TAPE', 'MOOD', 'HALL', 'COMP']; + const MAP = { + A: ['a', 'b', 'c', 'e', 'f', 'g1', 'g2'], C: ['a', 'd', 'e', 'f'], + D: ['a', 'b', 'c', 'd', 'i', 'l'], E: ['a', 'd', 'e', 'f', 'g1', 'g2'], H: ['b', 'c', 'e', 'f', 'g1', 'g2'], + L: ['d', 'e', 'f'], M: ['b', 'c', 'e', 'f', 'h', 'j'], O: ['a', 'b', 'c', 'd', 'e', 'f'], + P: ['a', 'b', 'e', 'f', 'g1', 'g2'], T: ['a', 'i', 'l'], Z: ['a', 'd', 'j', 'k'] + }; + const s = stageSvg(host, 'rsvg press', 130, 54); + svgEl(s, 'rect', { x: 1, y: 1, width: 128, height: 52, rx: 5, fill: '#0a0f08', stroke: '#1c2a16', 'stroke-width': 2 }); + /* segment shapes in a 22x36 local box */ + const TH = 2.6, W = 22, H = 36, mid = H / 2; + const segs = { + a: [[2, 0], [W - 2, 0], [W - 4, TH], [4, TH]], + d: [[2, H], [W - 2, H], [W - 4, H - TH], [4, H - TH]], + f: [[0, 2], [TH, 4], [TH, mid - 2], [0, mid - 1]], + e: [[0, H - 2], [TH, H - 4], [TH, mid + 2], [0, mid + 1]], + b: [[W, 2], [W - TH, 4], [W - TH, mid - 2], [W, mid - 1]], + c: [[W, H - 2], [W - TH, H - 4], [W - TH, mid + 2], [W, mid + 1]], + g1: [[2, mid], [4, mid - TH / 2], [W / 2 - 1, mid - TH / 2], [W / 2 - 1, mid + TH / 2], [4, mid + TH / 2]], + g2: [[W - 2, mid], [W - 4, mid - TH / 2], [W / 2 + 1, mid - TH / 2], [W / 2 + 1, mid + TH / 2], [W - 4, mid + TH / 2]], + i: [[W / 2 - TH / 2, 3], [W / 2 + TH / 2, 3], [W / 2 + TH / 2, mid - 2], [W / 2 - TH / 2, mid - 2]], + l: [[W / 2 - TH / 2, H - 3], [W / 2 + TH / 2, H - 3], [W / 2 + TH / 2, mid + 2], [W / 2 - TH / 2, mid + 2]], + h: [[3, 3], [5.5, 3], [W / 2 - 2, mid - 3], [W / 2 - 4.5, mid - 3]], + j: [[W - 3, 3], [W - 5.5, 3], [W / 2 + 2, mid - 3], [W / 2 + 4.5, mid - 3]], + k: [[3, H - 3], [5.5, H - 3], [W / 2 - 2, mid + 3], [W / 2 - 4.5, mid + 3]], + m: [[W - 3, H - 3], [W - 5.5, H - 3], [W / 2 + 2, mid + 3], [W / 2 + 4.5, mid + 3]] + }; + const cells = []; + for (let d = 0; d < 4; d++) { + const x0 = 13 + d * 28, y0 = 9, cell = {}; + for (const k in segs) { + cell[k] = svgEl(s, 'polygon', { points: segs[k].map(p => `${x0 + p[0]},${y0 + p[1]}`).join(' '), fill: 'var(--sevoff)' }); + } + cells.push(cell); + } + let wi; + const set = i => { + wi = ((i % words.length) + words.length) % words.length; + const word = words[wi].padEnd(4, ' '); + cells.forEach((cell, d) => { + const lit = MAP[word[d]] || []; + for (const k in cell) { + const on = lit.includes(k); + cell[k].setAttribute('fill', on ? 'var(--sevgrn)' : 'var(--sevoff)'); + cell[k].setAttribute('style', on ? 'filter:drop-shadow(0 0 2.5px rgba(87,211,87,.6))' : ''); + } + }); + onChange(wi, words[wi]); + }; + s.addEventListener('click', () => set(wi + 1)); + set(opts.index || 0); + return { el: s, get: () => wi, set }; +}; + +/* R26 response graph — log-frequency axes with a draggable amber peak; 2D drag + places the peak in both axes at once */ +GW.responseGraph = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg', 190, 110); + const RG = { x0: 30, x1: 182, y0: 8, y1: 86, fLo: Math.log10(32), fHi: Math.log10(16000), db: 12 }; + def('avGlow', d => { + const fl = svgEl(d, 'filter', { id: 'avGlow', x: '-60%', y: '-60%', width: '220%', height: '220%' }); + svgEl(fl, 'feGaussianBlur', { in: 'SourceGraphic', stdDeviation: 1.4 }); + }); + svgEl(s, 'rect', { x: 1, y: 1, width: 188, height: 108, rx: 5, fill: '#0d1410', stroke: '#22301f', 'stroke-width': 2 }); + const FREQS = [32, 64, 128, 250, 500, 1000, 2000, 4000, 8000, 16000]; + const xOf = f => RG.x0 + (Math.log10(f) - RG.fLo) / (RG.fHi - RG.fLo) * (RG.x1 - RG.x0); + const yOf = db => RG.y0 + (RG.db - db) / (2 * RG.db) * (RG.y1 - RG.y0); + FREQS.forEach(f => { + const x = xOf(f); + svgEl(s, 'line', { x1: x, y1: RG.y0, x2: x, y2: RG.y1, stroke: '#22301f', 'stroke-width': .6 }); + svgEl(s, 'text', { + x, y: RG.y1 + 9, 'text-anchor': 'middle', 'font-size': 4.8, 'font-family': 'var(--mono)', + fill: 'var(--steel)' + }).textContent = f < 1000 ? f : (f / 1000) + 'k'; + }); + for (const db of [12, 6, 0, -6, -12]) { + const y = yOf(db); + svgEl(s, 'line', { x1: RG.x0, y1: y, x2: RG.x1, y2: y, stroke: '#22301f', 'stroke-width': db === 0 ? 1 : .6 }); + svgEl(s, 'text', { + x: RG.x0 - 4, y: y + 2, 'text-anchor': 'end', 'font-size': 4.8, 'font-family': 'var(--mono)', + fill: 'var(--steel)' + }).textContent = (db > 0 ? '+' : '') + db; + } + const glowLine = svgEl(s, 'polyline', { points: '', fill: 'none', stroke: 'var(--gold-hi)', 'stroke-width': 3, opacity: .35, filter: 'url(#avGlow)' }); + const curve = svgEl(s, 'polyline', { points: '', fill: 'none', stroke: 'var(--gold)', 'stroke-width': 1.6 }); + const handle = svgEl(s, 'circle', { r: 4, fill: 'var(--gold-hi)', stroke: '#7d5c16', 'stroke-width': 1 }); + handle.style.cursor = 'move'; + let fc, gain; + const set = (f, g) => { + fc = Math.max(32, Math.min(16000, f)); gain = Math.max(-12, Math.min(12, g)); + const sigma = 0.28; let pts = ''; + for (let i = 0; i <= 76; i++) { + const lf = RG.fLo + i / 76 * (RG.fHi - RG.fLo); + const db = gain * Math.exp(-Math.pow(lf - Math.log10(fc), 2) / (2 * sigma * sigma)); + pts += `${xOf(Math.pow(10, lf)).toFixed(1)},${yOf(db).toFixed(1)} `; + } + curve.setAttribute('points', pts.trim()); glowLine.setAttribute('points', pts.trim()); + handle.setAttribute('cx', xOf(fc)); handle.setAttribute('cy', yOf(gain)); + const fLbl = fc < 1000 ? Math.round(fc) + ' Hz' : (fc / 1000).toFixed(1) + ' kHz'; + onChange([fc, gain], `${fLbl} · ${gain >= 0 ? '+' : ''}${gain.toFixed(1)} dB`); + }; + /* 2D drag: the plot places the peak in both axes at once */ + s.style.touchAction = 'none'; + s.addEventListener('pointerdown', e => { + s.setPointerCapture(e.pointerId); + const move = ev => { + const r = s.getBoundingClientRect(), sx = 190 / r.width, sy = 110 / r.height; + const px = (ev.clientX - r.left) * sx, py = (ev.clientY - r.top) * sy; + const lf = RG.fLo + Math.max(0, Math.min(1, (px - RG.x0) / (RG.x1 - RG.x0))) * (RG.fHi - RG.fLo); + const g = RG.db - Math.max(0, Math.min(1, (py - RG.y0) / (RG.y1 - RG.y0))) * 2 * RG.db; + set(Math.pow(10, lf), g); + }; + move(e); + const up = () => { s.removeEventListener('pointermove', move); s.removeEventListener('pointerup', up); s.removeEventListener('pointercancel', up); }; + s.addEventListener('pointermove', move); s.addEventListener('pointerup', up); s.addEventListener('pointercancel', up); + e.preventDefault(); + }); + set(opts.fc !== undefined ? opts.fc : 1200, opts.gain !== undefined ? opts.gain : 6.5); + return { el: s, get: () => [fc, gain], set }; +}; + +/* R30 telegraph indicator — the pointer names the active state on labeled + sectors, engine-telegraph style; click steps the state */ +GW.telegraphIndicator = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const states = opts.states || ['OK', 'TEST', 'BUSY', 'STOP', 'ATTN', 'FAULT']; + const s = stageSvg(host, 'rsvg press', 110, 110), cx = 55, cy = 55, N = states.length; + svgEl(s, 'circle', { cx, cy, r: 50, fill: '#171412', stroke: '#060505', 'stroke-width': 2 }); + svgEl(s, 'circle', { cx, cy, r: 43, fill: 'var(--cream)', stroke: '#b7b29a', 'stroke-width': 1 }); + states.forEach((st, i) => { + const a = i * 360 / N - 90 + 180 / N; + const [lx1, ly1] = polar(cx, cy, 9, i * 360 / N), [lx2, ly2] = polar(cx, cy, 43, i * 360 / N); + svgEl(s, 'line', { x1: lx1, y1: ly1, x2: lx2, y2: ly2, stroke: '#2b2418', 'stroke-width': 1 }); + const g = svgEl(s, 'g', { transform: `rotate(${a + 90},${cx},${cy})` }); + svgEl(g, 'text', { + x: cx, y: cy - 28, 'text-anchor': 'middle', 'font-size': 7, 'font-weight': 700, + 'font-family': 'var(--mono)', fill: '#2b2418' + }).textContent = st; + }); + const ptr = svgEl(s, 'g', {}); ptr.style.transition = 'transform .18s'; ptr.style.transformOrigin = `${cx}px ${cy}px`; + svgEl(ptr, 'polygon', { points: `${cx - 2.5},${cy} ${cx},${cy - 38} ${cx + 2.5},${cy}`, fill: 'var(--fail)', stroke: '#7a2a1a', 'stroke-width': .5 }); + svgEl(ptr, 'rect', { x: cx + 1, y: cy - 16, width: 10, height: 7, rx: 1, fill: 'var(--gold)', stroke: '#7d5c16', 'stroke-width': .6 }); + svgEl(s, 'circle', { cx, cy, r: 8, fill: '#171412', stroke: '#000' }); + let si; + const set = i => { + si = ((i % N) + N) % N; + ptr.style.transform = `rotate(${si * 360 / N + 180 / N}deg)`; + onChange(si, states[si]); + }; + s.addEventListener('click', () => set(si + 1)); + set(opts.index || 0); + return { el: s, get: () => si, set }; +}; + +/* R31 radar sweep — the beam circles the bearing ring on the builder's own + clock (reduced-motion gated), contacts bloom as it passes; click marks the bearing */ +GW.radarSweep = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const contacts = opts.contacts || [[60, .55], [205, .7], [318, .4]]; + const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 65; + const clipId = uid('radarClip'); + const defs = svgEl(s, 'defs', {}); + const clip = svgEl(defs, 'clipPath', { id: clipId }); + 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: 'var(--scr-bg1,#120b05)', stroke: 'var(--scr-brd,#3a2a12)', 'stroke-width': 1 }); + for (let d = 0; d < 360; d += 30) { + const [x1, y1] = polar(cx, cy, 51, d), [x2, y2] = polar(cx, cy, 47, d); + svgEl(s, 'line', { x1, y1, x2, y2, stroke: 'var(--scr-ink,var(--gold))', 'stroke-width': 1, opacity: .8 }); + const g = svgEl(s, 'g', { transform: `rotate(${d},${cx},${cy})` }); + svgEl(g, 'text', { + x: cx, y: cy - 47.5, 'text-anchor': 'middle', 'font-size': 4.6, 'font-family': 'var(--mono)', + fill: 'var(--scr-ink,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(--scr-ink,var(--gold))', 'stroke-width': .6, opacity: .5 }); + } + const g = svgEl(s, 'g', { 'clip-path': `url(#${clipId})` }); + svgEl(g, 'circle', { cx, cy, r: 46, fill: 'var(--scr-bg2,#0d0803)' }); + for (const r of [15, 30, 45]) + svgEl(g, 'circle', { cx, cy, r, fill: 'none', stroke: 'var(--scr-ink,var(--gold))', 'stroke-opacity': .18, 'stroke-width': .7 }); + const sweep = 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(sweep, 'path', { + d: `M ${cx} ${cy} L ${x1} ${y1} A 46 46 0 0 1 ${x2} ${y2} Z`, + fill: 'var(--scr-ink,var(--gold))', 'fill-opacity': (0.04 + t * 0.045).toFixed(3) + }); + } + svgEl(sweep, 'line', { + x1: cx, y1: cy, x2: cx, y2: cy - 46, stroke: 'var(--scr-hi,var(--gold-hi))', 'stroke-width': 1.4, + transform: `rotate(0,${cx},${cy})`, opacity: .95 + }); + const blips = contacts.map(([bd, rf]) => { + const [x, y] = polar(cx, cy, 46 * rf, bd); + return svgEl(g, 'circle', { cx: x, cy: y, r: 2.2, fill: 'var(--scr-hi,var(--gold-hi))', opacity: 0 }); + }); + svgEl(s, 'circle', { cx, cy, r: 3, fill: 'var(--scr-ink,var(--gold))', opacity: .8 }); + const mark = svgEl(s, 'text', { + x: cx, y: 124, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em', + 'font-family': 'var(--mono)', fill: 'var(--steel)' + }); + mark.textContent = 'BEARING —'; + let a = opts.bearing !== undefined ? opts.bearing : 210; + const tick = () => { + a = (a + 2.6) % 360; + sweep.setAttribute('transform', `rotate(${a},${cx},${cy})`); + contacts.forEach(([bd], i) => { + const d = (a - bd + 360) % 360; /* degrees since beam passed */ + blips[i].setAttribute('opacity', d < 200 ? Math.max(0, .95 - d / 200) : 0); + }); + mark.textContent = mark.textContent.startsWith('MARK') ? mark.textContent : + 'BEARING ' + String(Math.round(a) % 360).padStart(3, '0'); + }; + s.addEventListener('click', () => { + const b = String(Math.round(a) % 360).padStart(3, '0'); + mark.textContent = 'MARK ' + b; + onChange(a, 'mark ' + b); + }); + tick(); onChange(a, 'sweeping'); + if (!matchMedia('(prefers-reduced-motion: reduce)').matches) setInterval(tick, 50); + return { el: s, get: () => a, tick }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
