diff options
Diffstat (limited to 'docs/prototypes')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 292 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 306 |
2 files changed, 315 insertions, 283 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index b192fb0..709b2e8 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -1638,28 +1638,28 @@ card(C,'R15','Multi-band dial', (st,rd)=>GW.multiBandDial(st,{onChange:(v,t)=>rd(t)}), '<b>four scales, one needle.</b> Nested band arcs on a cream face; the active ring reads bold. Drag the left dial to tune; click the bandspread dial to switch bands. After a shortwave receiver dial pair.'); card(C,'R16','Entry keypad', - `<svg id="keypad" class="rsvg" viewBox="0 0 150 190" width="150" height="190"></svg>`, + (st,rd)=>GW.entryKeypad(st,{onChange:(v,t)=>rd(t)}), '<b>a composed entry instrument.</b> Worn keycaps feed the amber display; the lamp watches the buffer; the amber keys confirm and clear. Click the keys. After an industrial keypad prop.'); card(C,'R18','Thumb-slide attenuator pair', - `<svg id="thumbsl" class="rsvg" viewBox="0 0 130 150" width="130" height="150"></svg>`, + (st,rd)=>GW.thumbSlide(st,{onChange:(v,t)=>rd(t)}), '<b>a lit column of numbers, a thumb tab.</b> The scale glows inside the rail; the cream tab rides beside it and the nearest figure lights. Drag either channel. After a compressor blend/mix pair.'); card(C,'R19','Waveform region editor', - `<svg id="wregion" class="rsvg" viewBox="0 0 190 100" width="190" height="100"></svg>`, + (st,rd)=>GW.waveRegion(st,{start:22,end:76,onChange:(v,t)=>rd(t)}), '<b>pick a region on the wave.</b> The monochrome LCD shows the sample; drag near either flag to move START or END, and the selection draws bright. After a sampler edit screen.'); card(C,'R20','Drum roller selector', - `<svg id="drumr" class="rsvg" viewBox="0 0 130 140" width="130" height="140"></svg>`, + (st,rd)=>GW.drumRoller(st,{onChange:(v,t)=>rd(t)}), '<b>roll to the number.</b> The numbered drum shows through a tall window and the center value sits on an inverted chip. Drag either drum. After an equalizer tone selector.'); card(C,'R21','LED program row', - `<svg id="ledrow" class="rsvg" viewBox="0 0 190 58" width="190" height="58"></svg>`, + (st,rd)=>GW.ledRow(st,{index:5,onChange:(v,t)=>rd(t)}), '<b>identical keys, one lit dot.</b> The LED above the button carries the selection, not the key itself — the classic program-select row. Click a key; readout names the program. After a digital reverb’s program bank.'); card(C,'R22','Three-position slide', - `<svg id="varslide" class="rsvg press" viewBox="0 0 110 64" width="110" height="64"></svg>`, + (st,rd)=>GW.pillSlide(st,{index:1,onChange:(v,t)=>rd(t)}), '<b>a detented pill, three stops.</b> The chrome pill parks at A, AB, or B, and the LED pair below tells the truth — both light on AB. Click a position. After a drum machine variation switch.'); card(C,'R23','Spun-aluminum knob', - `<svg id="spunk" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.spunKnob(st,{value:62,onChange:(v,t)=>rd(t)}), '<b>bright metal, machined rings.</b> A spun face with concentric tooling marks in a knurled grip ring, red index over dot ticks. Drag up/down. After a boutique pedal knob, confirmed by an avionics cluster.'); card(C,'R24','Stomp switch + jewel', - `<svg id="stomp" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.stompSwitch(st,{on:false,onChange:(v,t)=>rd(t)}), '<b>the engage pair.</b> A chrome dome stomp and the faceted jewel that reports it — press to engage, the jewel lights; press again to bypass. Click it. After a tremolo pedal footswitch.'); card(C,'R27','Winged gain selector', `<svg id="wingk" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`, @@ -2270,48 +2270,6 @@ setEdge(.62); -/* R16 entry keypad: worn keys feed the amber display; lamps watch state */ -let kpBuf='',kpDisp=null,kpLampTop=null,kpLampBot=null; -(function(){const s=$('keypad'); - const defs=svgEl(s,'defs',{}); - const kg=svgEl(defs,'linearGradient',{id:'kpKey',x1:0,y1:0,x2:0,y2:1}); - svgEl(kg,'stop',{offset:'0','stop-color':'#dbd8cf'}); - svgEl(kg,'stop',{offset:'1','stop-color':'#a29d92'}); - const ag=svgEl(defs,'linearGradient',{id:'kpAmber',x1:0,y1:0,x2:0,y2:1}); - svgEl(ag,'stop',{offset:'0','stop-color':'var(--amber-grad-top)'}); - svgEl(ag,'stop',{offset:'1','stop-color':'var(--amber-grad-mid)'}); - // lamp pod - const lamp=cy=>{const l=svgEl(s,'circle',{cx:13,cy,r:6.5,fill:'#3a0f0a',stroke:'#14100e','stroke-width':1.5});return l;}; - kpLampTop=lamp(20);kpLampBot=lamp(38); - // display window - svgEl(s,'rect',{x:26,y:8,width:116,height:32,rx:5,fill:'#0a0806',stroke:'#2c261d','stroke-width':2}); - kpDisp=svgEl(s,'text',{x:84,y:31,'text-anchor':'middle','font-size':16,'letter-spacing':'.22em', - 'font-family':'var(--mono)',fill:'var(--gold-hi)'}); - // key plate - svgEl(s,'rect',{x:24,y:48,width:118,height:138,rx:8,fill:'#1a1714',stroke:'#0a0908','stroke-width':1.5}); - const KEYS=[['1','2','3'],['4','5','6'],['7','8','9'],['✓','0','✗']]; - const jitter=[-2,1,-1,2,0,-2,1,-1,2,0,-1,1]; - KEYS.forEach((row,r)=>row.forEach((k,c)=>{ - const x=47+c*36,y=68+r*32,amber=(k==='✓'||k==='✗'); - const g=svgEl(s,'g',{});g.style.cursor='pointer';g.style.transition='transform .07s'; - svgEl(g,'rect',{x:x-14,y:y-11,width:28,height:24,rx:4,fill:amber?'url(#kpAmber)':'url(#kpKey)', - stroke:'#4e4a42','stroke-width':1,'stroke-opacity':.8}); - svgEl(g,'text',{x,y:y+6,'text-anchor':'middle','font-size':13,'font-weight':700, - 'font-family':'var(--mono)',fill:k==='✓'?'#3f5a1f':k==='✗'?'#7a2a1a':'#2b2721', - transform:`rotate(${jitter[r*3+c]},${x},${y})`}).textContent=k; - g.addEventListener('click',()=>{g.style.transform='translateY(1.5px)'; - setTimeout(()=>g.style.transform='',80);kpPress(k);});})); - })(); -function kpRender(){kpDisp.textContent=kpBuf.padEnd(6,'–'); - kpLampTop.setAttribute('fill',kpBuf?'var(--jewel-r)':'#3a0f0a');} -function kpPress(k){ - if(k==='✓'){setRd('R16',kpBuf?'OK · '+kpBuf:'empty');kpBuf='';kpRender();return;} - if(k==='✗'){kpBuf='';kpRender();setRd('R16','cleared'); - kpLampBot.setAttribute('fill','var(--jewel-r)'); - setTimeout(()=>kpLampBot.setAttribute('fill','#3a0f0a'),350);return;} - if(kpBuf.length<6){kpBuf+=k;kpRender();setRd('R16',kpBuf);}} -kpRender();setRd('R16','enter a code'); - /* R17 round CRT scope: pale phosphor face, live trace */ let rcrtPh=0,rcrtTrace=null; (function(){const s=$('rcrt'),cx=55,cy=52; @@ -2341,238 +2299,6 @@ function tickRcrt(){rcrtPh+=0.12;const amp=8+3*Math.sin(rcrtPh*0.35);let pts=''; tickRcrt(); if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tickRcrt,80); -/* R18 thumb-slide attenuator pair: lit numeral strip, side thumb tab */ -const THSL=[{name:'BLEND',v:74},{name:'MIX',v:92}]; -const thslParts=[]; -(function(){const s=$('thumbsl'),y0=22,y1=122; - const defs=svgEl(s,'defs',{}); - const rg=svgEl(defs,'linearGradient',{id:'thRail',x1:0,y1:0,x2:1,y2:0}); - svgEl(rg,'stop',{offset:'0','stop-color':'#3a3733'}); - svgEl(rg,'stop',{offset:'.5','stop-color':'#211f1c'}); - svgEl(rg,'stop',{offset:'1','stop-color':'#161412'}); - const tg=svgEl(defs,'linearGradient',{id:'thTab',x1:0,y1:0,x2:0,y2:1}); - svgEl(tg,'stop',{offset:'0','stop-color':'#f2efe6'}); - svgEl(tg,'stop',{offset:'1','stop-color':'#c6c1b3'}); - THSL.forEach((ch,i)=>{const x=40+i*50; - svgEl(s,'rect',{x:x-9,y:y0-10,width:18,height:y1-y0+20,rx:3,fill:'url(#thRail)',stroke:'#0a0908','stroke-width':1.2}); - for(const sy of [y0-6,y1+6]){svgEl(s,'circle',{cx:x,cy:sy,r:2.4,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5}); - svgEl(s,'line',{x1:x-1.8,y1:sy-1,x2:x+1.8,y2:sy+1,stroke:'#14110e','stroke-width':.8});} - svgEl(s,'rect',{x:x-5,y:y0,width:10,height:y1-y0,fill:'#0d0a08',stroke:'#050404'}); - for(let n=0;n<=10;n++){const val=100-n*10,y=y0+6+n*(y1-y0-12)/10; - svgEl(s,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)', - fill:'var(--gold)',opacity:.55}).textContent=val;} - const tab=svgEl(s,'g',{}); - svgEl(tab,'rect',{x:x+7,y:-6,width:8,height:12,rx:2,fill:'url(#thTab)',stroke:'#7a766a','stroke-width':.6}); - svgEl(tab,'rect',{x:x+8.2,y:-1,width:5.6,height:1.6,fill:'rgba(0,0,0,.25)'}); - svgEl(s,'text',{x,y:145,'text-anchor':'middle','font-size':6,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=ch.name; - const hit=svgEl(s,'rect',{x:x-12,y:y0-10,width:30,height:y1-y0+20,fill:'transparent'}); - hit.style.cursor='ns-resize'; - thslParts.push({tab,nums:[...s.querySelectorAll('text')].filter(t=>t.getAttribute('x')==String(x)&&t.textContent!==ch.name)}); - dragY(hit,pct=>setThsl(i,pct));}); - })(); -function setThsl(i,v){v=Math.max(0,Math.min(100,Math.round(v)));THSL[i].v=v; - const p=thslParts[i]; - p.tab.setAttribute('transform',`translate(0,${28+(100-v)*0.88})`); - p.nums.forEach(t=>{const on=Math.abs(parseInt(t.textContent)-v)<=5; - t.setAttribute('fill',on?'var(--gold-hi)':'var(--gold)');t.setAttribute('opacity',on?'1':'.55');}); - setRd('R18',`${THSL[0].name} ${THSL[0].v} · ${THSL[1].name} ${THSL[1].v}`);} -setThsl(0,74);setThsl(1,92); - -/* R19 waveform region editor: monochrome LCD, draggable S/E flags */ -let wrS=22,wrE=76,wrBars=[],wrFlagS=null,wrFlagE=null; -(function(){const s=$('wregion'),x0=10,x1=180,yMid=48,N=85; - svgEl(s,'rect',{x:1,y:1,width:188,height:98,rx:6,fill:'var(--scr-bg1,#0b0c0b)',stroke:'var(--scr-brd,#2a2c2a)','stroke-width':2}); - svgEl(s,'text',{x:12,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C1:START'; - svgEl(s,'text',{x:78,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C2:OFF'; - svgEl(s,'text',{x:136,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C3:END'; - svgEl(s,'line',{x1:x0,y1:yMid,x2:x1,y2:yMid,stroke:'#3a3c3a','stroke-width':.6,'stroke-dasharray':'1.5 2'}); - for(let i=0;i<N;i++){const x=x0+(i/(N-1))*(x1-x0); - const env=Math.abs(Math.sin(i*0.19))*Math.abs(Math.sin(i*0.045))*(i%17<11?1:.25); - const h=Math.max(1.2,env*26); - wrBars.push(svgEl(s,'rect',{x:x-0.8,y:yMid-h,width:1.6,height:h*2,fill:'var(--scr-dim,#5a5c5a)'}));} - const flag=(lbl)=>{const g=svgEl(s,'g',{}); - svgEl(g,'line',{x1:0,y1:18,x2:0,y2:78,stroke:'var(--scr-hi,#f2f4f2)','stroke-width':1}); - svgEl(g,'rect',{x:lbl==='S'?0:-8,y:70,width:8,height:8,fill:'var(--scr-hi,#f2f4f2)'}); - svgEl(g,'text',{x:lbl==='S'?4:-4,y:76.5,'text-anchor':'middle','font-size':6.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent=lbl;return g;}; - wrFlagS=flag('S');wrFlagE=flag('E'); - svgEl(s,'rect',{x:10,y:84,width:78,height:11,fill:'var(--scr-hi,#e8eae8)'}); - svgEl(s,'text',{x:14,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent='ENC:ZOOM(1x)'; - svgEl(s,'text',{x:96,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='A-3 M:[S]'; - svgEl(s,'rect',{x:148,y:84,width:32,height:11,fill:'var(--scr-hi,#e8eae8)'}); - svgEl(s,'text',{x:164,y:92.5,'text-anchor':'middle','font-size':6.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent='MENU'; - s.style.cursor='ew-resize'; - dragX(s,pct=>{ - // move whichever flag is nearer the pointer - if(Math.abs(pct-wrS)<=Math.abs(pct-wrE))setWregion(Math.min(pct,wrE-4),wrE); - else setWregion(wrS,Math.max(pct,wrS+4));});})(); -function setWregion(sv,ev){wrS=Math.max(0,Math.min(96,sv));wrE=Math.max(4,Math.min(100,ev)); - const x0=10,x1=180,xOf=p=>x0+p/100*(x1-x0); - wrFlagS.setAttribute('transform',`translate(${xOf(wrS)},0)`); - wrFlagE.setAttribute('transform',`translate(${xOf(wrE)},0)`); - wrBars.forEach((b,i)=>{const p=i/(wrBars.length-1)*100; - b.setAttribute('fill',(p>=wrS&&p<=wrE)?'var(--scr-hi,#f2f4f2)':'var(--scr-dim,#5a5c5a)');}); - setRd('R19',`S ${Math.round(wrS)}% · E ${Math.round(wrE)}%`);} -setWregion(22,76); - -/* R20 drum roller selector: numbered drum in a window, center chip reads it */ -const DRUMR=[{name:'HIGH',v:6},{name:'LOW',v:8}]; -const drumrGrps=[]; -(function(){const s=$('drumr'),cy=76,step=17; - const defs=svgEl(s,'defs',{}); - const paper=svgEl(defs,'linearGradient',{id:'drPaper',x1:0,y1:0,x2:1,y2:0}); - svgEl(paper,'stop',{offset:'0','stop-color':'#cfc6a8'}); - svgEl(paper,'stop',{offset:'.5','stop-color':'#ece4c8'}); - svgEl(paper,'stop',{offset:'1','stop-color':'#c6bd9e'}); - const curve=svgEl(defs,'linearGradient',{id:'drCurve',x1:0,y1:0,x2:0,y2:1}); - svgEl(curve,'stop',{offset:'0','stop-color':'rgba(0,0,0,.4)'}); - svgEl(curve,'stop',{offset:'.28','stop-color':'rgba(0,0,0,0)'}); - svgEl(curve,'stop',{offset:'.72','stop-color':'rgba(0,0,0,0)'}); - svgEl(curve,'stop',{offset:'1','stop-color':'rgba(0,0,0,.4)'}); - svgEl(s,'text',{x:65,y:12,'text-anchor':'middle','font-size':7,'letter-spacing':'.18em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='EQUALIZER'; - DRUMR.forEach((ch,i)=>{const x=45+i*50; - svgEl(s,'text',{x:x+18,y:34,'text-anchor':'middle','font-size':8,fill:'var(--steel)'}).textContent='+'; - svgEl(s,'text',{x:x+18,y:124,'text-anchor':'middle','font-size':8,fill:'var(--steel)'}).textContent='−'; - svgEl(s,'rect',{x:x-12,y:24,width:24,height:104,rx:4,fill:'url(#thRail)',stroke:'#0a0908','stroke-width':1.2}); - const clip=svgEl(defs,'clipPath',{id:'drClip'+i}); - svgEl(clip,'rect',{x:x-9,y:28,width:18,height:96}); - svgEl(s,'rect',{x:x-9,y:28,width:18,height:96,fill:'url(#drPaper)'}); - const g=svgEl(s,'g',{'clip-path':`url(#drClip${i})`}); - const nums=svgEl(g,'g',{});nums.style.transition='transform .12s'; - for(let n=1;n<=10;n++) - svgEl(nums,'text',{x,y:cy+3+(ch.v-n)*step,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'#2b2418'}).textContent=n; - svgEl(g,'rect',{x:x-9,y:28,width:18,height:96,fill:'url(#drCurve)','pointer-events':'none'}); - svgEl(s,'rect',{x:x-8,y:cy-6.5,width:16,height:13,rx:1.5,fill:'#1a1613',opacity:.92}); - const chip=svgEl(s,'text',{x,y:cy+3,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}); - svgEl(s,'text',{x,y:136,'text-anchor':'middle','font-size':6,'letter-spacing':'.12em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=ch.name; - const hit=svgEl(s,'rect',{x:x-14,y:24,width:28,height:104,fill:'transparent'}); - hit.style.cursor='ns-resize'; - drumrGrps.push({nums,chip,base:ch.v}); - dragDelta(hit,()=>DRUMR[i].v,v=>setDrumr(i,v),{min:1,max:10,sens:.08});}); - })(); -function setDrumr(i,v){v=Math.max(1,Math.min(10,v));DRUMR[i].v=v; - const p=drumrGrps[i]; - p.nums.style.transform=`translateY(${(v-p.base)*17}px)`; - p.chip.textContent=Math.round(v); - setRd('R20',`${DRUMR[0].name} ${Math.round(DRUMR[0].v)} · ${DRUMR[1].name} ${Math.round(DRUMR[1].v)}`);} -setDrumr(0,6);setDrumr(1,8); - -/* R21 LED program row: exclusive select, the LED carries the state */ -const LEDROW_PROGS=['Sm Hall B','VocPlate','Lg Hall B','Chamber','ParcPlate','Sm Hall A','Room A','Const Plate']; -let ledrowI=5,ledrowLeds=[]; -(function(){const s=$('ledrow'); - const defs=svgEl(s,'defs',{}); - const kb=svgEl(defs,'linearGradient',{id:'lrKey',x1:0,y1:0,x2:0,y2:1}); - svgEl(kb,'stop',{offset:'0','stop-color':'#2b2823'}); - svgEl(kb,'stop',{offset:'1','stop-color':'#171512'}); - LEDROW_PROGS.forEach((name,i)=>{const x=16+i*22.6; - svgEl(s,'text',{x,y:9,'text-anchor':'middle','font-size':6,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=i+1; - ledrowLeds.push(svgEl(s,'circle',{cx:x,cy:17,r:2.4,fill:'#3a0f0a'})); - const g=svgEl(s,'g',{});g.style.cursor='pointer';g.style.transition='transform .07s'; - svgEl(g,'rect',{x:x-8.5,y:24,width:17,height:22,rx:2.5,fill:'url(#lrKey)',stroke:'#0a0908','stroke-width':1}); - svgEl(g,'rect',{x:x-6.5,y:26.5,width:13,height:3,rx:1.5,fill:'rgba(255,255,255,.06)'}); - g.addEventListener('click',()=>{g.style.transform='translateY(1.5px)'; - setTimeout(()=>g.style.transform='',80);setLedrow(i);});}); - svgEl(s,'text',{x:95,y:56,'text-anchor':'middle','font-size':6,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='PROGRAM';})(); -function setLedrow(i){ledrowI=i; - ledrowLeds.forEach((l,k)=>{const on=k===i; - l.setAttribute('fill',on?'var(--jewel-r)':'#3a0f0a'); - l.setAttribute('style',on?'filter:drop-shadow(0 0 3px rgba(255,91,69,.8))':'');}); - setRd('R21',`${ledrowI+1} · ${LEDROW_PROGS[ledrowI]}`);} -setLedrow(5); - -/* R22 three-position slide: chrome pill, detents, honest LED pair */ -const VARSL_POS=['A','AB','B']; -let varslI=1,varslPill=null,varslLbls=[],varslLeds=[]; -(function(){const s=$('varslide');const detX=[34,55,76]; - svgEl(s,'text',{x:55,y:10,'text-anchor':'middle','font-size':5.5,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='BASIC · VARIATION'; - svgEl(s,'rect',{x:20,y:16,width:70,height:14,rx:7,fill:'#0b0a09',stroke:'#000','stroke-width':1}); - svgEl(s,'rect',{x:21,y:17,width:68,height:5,rx:2.5,fill:'rgba(255,255,255,.04)'}); - varslPill=svgEl(s,'rect',{x:-13,y:18.5,width:26,height:9,rx:4.5,fill:'url(#sfChrome)',stroke:'#4e4a42','stroke-width':.6}); - varslPill.style.transition='transform .12s'; - VARSL_POS.forEach((lbl,i)=>{ - varslLbls.push(svgEl(s,'text',{x:detX[i],y:40,'text-anchor':'middle','font-size':6.5, - 'font-family':'var(--mono)',fill:'var(--dim)'})); - varslLbls[i].textContent=lbl;}); - svgEl(s,'rect',{x:32,y:45,width:46,height:11,rx:2,fill:'#141210',stroke:'#060505'}); - [44,66].forEach(x=>varslLeds.push(svgEl(s,'circle',{cx:x,cy:50.5,r:2.4,fill:'#3a0f0a'}))); - s.addEventListener('click',e=>{const r=s.getBoundingClientRect(); - const x=(e.clientX-r.left)/r.width*110; - setVarsl(x<45?0:x<66?1:2);});})(); -function setVarsl(i){varslI=i;const detX=[34,55,76]; - varslPill.setAttribute('transform',`translate(${detX[i]},0)`); - varslLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)')); - const lit=[i===0||i===1,i===2||i===1]; - varslLeds.forEach((l,k)=>l.setAttribute('fill',lit[k]?'var(--jewel-r)':'#3a0f0a')); - setRd('R22',VARSL_POS[i]);} -setVarsl(1); - -/* R23 spun-aluminum knob: machined rings in a knurled grip */ -let spunV=62,spunIdx=null; -(function(){const s=$('spunk'),cx=55,cy=52; - const defs=svgEl(s,'defs',{}); - const al=svgEl(defs,'radialGradient',{id:'spunFace',cx:'42%',cy:'34%',r:'85%'}); - svgEl(al,'stop',{offset:'0','stop-color':'#e8e6e0'}); - svgEl(al,'stop',{offset:'.7','stop-color':'#b3b0a8'}); - svgEl(al,'stop',{offset:'1','stop-color':'#8a877e'}); - for(let k=0;k<8;k++){const [x,y]=polar(cx,cy,45,-135+k*38.6); - svgEl(s,'circle',{cx:x,cy:y,r:1.3,fill:'var(--steel)'});} - for(let k=0;k<22;k++){const [x,y]=polar(cx,cy,36,k*16.36); - svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'#14110e'});} - svgEl(s,'circle',{cx,cy,r:36,fill:'#1a1714',stroke:'#000','stroke-width':.8}); - svgEl(s,'circle',{cx,cy,r:30,fill:'url(#spunFace)',stroke:'#6e6b63','stroke-width':.8}); - for(const r of [24,18,12,6]) - svgEl(s,'circle',{cx,cy,r,fill:'none',stroke:r%12===0?'rgba(0,0,0,.12)':'rgba(255,255,255,.3)','stroke-width':.6}); - spunIdx=svgEl(s,'line',{x1:cx,y1:cy-28,x2:cx,y2:cy-10,stroke:'var(--fail)','stroke-width':2.6,'stroke-linecap':'round'}); - svgEl(s,'ellipse',{cx:cx-9,cy:cy-11,rx:9,ry:5,fill:'rgba(255,255,255,.28)',transform:`rotate(-32,${cx-9},${cy-11})`}); - svgEl(s,'text',{x:cx,y:103,'text-anchor':'middle','font-size':6,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='SPEED';})(); -function setSpun(v){spunV=Math.max(0,Math.min(100,v)); - spunIdx.setAttribute('transform',`rotate(${-135+spunV/100*270},55,52)`); - setRd('R23',Math.round(spunV)+'%');} -dragDelta($('spunk'),()=>spunV,setSpun,{min:0,max:100});setSpun(62); - -/* R24 stomp switch + jewel: press to engage, the jewel reports */ -let stompOn=false,stompDome=null,stompJewel=null,stompGlow=null; -(function(){const s=$('stomp'),cx=55; - const defs=svgEl(s,'defs',{}); - const dg=svgEl(defs,'radialGradient',{id:'stompDome',cx:'40%',cy:'30%',r:'80%'}); - svgEl(dg,'stop',{offset:'0','stop-color':'#f4f2ec'}); - svgEl(dg,'stop',{offset:'.55','stop-color':'#b9b6ae'}); - svgEl(dg,'stop',{offset:'1','stop-color':'#6e6b63'}); - const jg=svgEl(defs,'radialGradient',{id:'stompJewel',cx:'38%',cy:'30%',r:'80%'}); - svgEl(jg,'stop',{offset:'0','stop-color':'#ffe9b0'}); - svgEl(jg,'stop',{offset:'.45','stop-color':'var(--jewel-a)'}); - svgEl(jg,'stop',{offset:'1','stop-color':'#5c3d0c'}); - stompGlow=svgEl(s,'circle',{cx,cy:22,r:14,fill:'rgba(255,180,58,.35)',filter:'url(#avGlow)',opacity:0}); - stompJewel=svgEl(s,'circle',{cx,cy:22,r:9,fill:'#241f1b',stroke:'#0a0908','stroke-width':1.5}); - for(let k=0;k<6;k++){const a1=k*60,a2=k*60+30; - const [x1,y1]=polar(cx,22,9,a1),[x2,y2]=polar(cx,22,9,a2); - svgEl(s,'path',{d:`M ${cx} 22 L ${x1} ${y1} A 9 9 0 0 1 ${x2} ${y2} Z`, - fill:k%2?'rgba(255,255,255,.09)':'rgba(0,0,0,.10)','pointer-events':'none'});} - for(let k=0;k<20;k++){const [x,y]=polar(cx,72,25,k*18); - svgEl(s,'circle',{cx:x,cy:y,r:2.4,fill:'#14110e'});} - svgEl(s,'circle',{cx,cy:72,r:25,fill:'#1a1714',stroke:'#000','stroke-width':.8}); - stompDome=svgEl(s,'g',{});stompDome.style.transition='transform .08s'; - svgEl(stompDome,'circle',{cx,cy:72,r:19,fill:'url(#stompDome)',stroke:'#55524a','stroke-width':.8}); - svgEl(stompDome,'ellipse',{cx:cx-6,cy:64,rx:8,ry:4.5,fill:'rgba(255,255,255,.5)',transform:`rotate(-24,${cx-6},64)`}); - $('stomp').onclick=()=>{stompDome.style.transform='translateY(1.5px)'; - setTimeout(()=>stompDome.style.transform='',90);setStomp(!stompOn);};})(); -function setStomp(on){stompOn=on; - stompJewel.setAttribute('fill',on?'url(#stompJewel)':'#241f1b'); - stompGlow.setAttribute('opacity',on?'1':'0'); - setRd('R24',on?'ENGAGED':'bypass');} -setStomp(false); - /* R27 winged gain selector: red T-bar over a dot ring, stepped detents */ const WINGK_STEPS=[-80,-70,-60,-50,-40,-30,-20,-10,-5,0,5,10]; let wingI=7,wingGrp=null; @@ -3813,7 +3539,7 @@ let dkPos=0,dkCarry=0; 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('R17',$('rcrt'),['green','amber','red','blue','vfd'],'green'); -screenChips('R19',$('wregion'),['white','green','amber','blue','vfd'],'white'); +screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white'); screenChips('R31',$('radar'),['amber','green','red','blue','vfd'],'amber'); screenChips('N11',$('scopep').closest('.scope'),['green','amber','red','blue','vfd'],'green'); BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no); diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 2cc7b51..cd74771 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -549,6 +549,9 @@ function stageSvg(host, cls, vw, vh) { s.setAttribute('width', vw); s.setAttribute('height', vh); host.appendChild(s); return s; } +/* instance-unique ids for defs that can't be shared (e.g. clip paths sized per instance) */ +let uidN = 0; +function uid(prefix) { return prefix + '-' + (++uidN); } /* R02 calibrated vernier dial — the disc turns under a fixed hairline */ GW.vernierDial = function (host, opts = {}) { @@ -833,6 +836,309 @@ GW.multiBandDial = function (host, opts = {}) { return { el: s, get: () => [val, band], set }; }; +/* R16 entry keypad — worn keys feed the amber display; lamps watch state */ +GW.entryKeypad = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg', 150, 190); + gradDef('kpKey', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#dbd8cf'], ['1', '#a29d92']]); + gradDef('kpAmber', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'var(--amber-grad-top)'], ['1', 'var(--amber-grad-mid)']]); + const lamp = cy => svgEl(s, 'circle', { cx: 13, cy, r: 6.5, fill: '#3a0f0a', stroke: '#14100e', 'stroke-width': 1.5 }); + const lampTop = lamp(20), lampBot = lamp(38); + svgEl(s, 'rect', { x: 26, y: 8, width: 116, height: 32, rx: 5, fill: '#0a0806', stroke: '#2c261d', 'stroke-width': 2 }); + const disp = svgEl(s, 'text', { x: 84, y: 31, 'text-anchor': 'middle', 'font-size': 16, 'letter-spacing': '.22em', 'font-family': 'var(--mono)', fill: 'var(--gold-hi)' }); + svgEl(s, 'rect', { x: 24, y: 48, width: 118, height: 138, rx: 8, fill: '#1a1714', stroke: '#0a0908', 'stroke-width': 1.5 }); + let buf = ''; + const render = () => { + disp.textContent = buf.padEnd(6, '–'); + lampTop.setAttribute('fill', buf ? 'var(--jewel-r)' : '#3a0f0a'); + }; + const press = k => { + if (k === '✓') { onChange(buf, buf ? 'OK · ' + buf : 'empty'); buf = ''; render(); return; } + if (k === '✗') { + buf = ''; render(); onChange(buf, 'cleared'); + lampBot.setAttribute('fill', 'var(--jewel-r)'); + setTimeout(() => lampBot.setAttribute('fill', '#3a0f0a'), 350); return; + } + if (buf.length < 6) { buf += k; render(); onChange(buf, buf); } + }; + const KEYS = [['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['✓', '0', '✗']]; + const jitter = [-2, 1, -1, 2, 0, -2, 1, -1, 2, 0, -1, 1]; + KEYS.forEach((row, r) => row.forEach((k, c) => { + const x = 47 + c * 36, y = 68 + r * 32, amber = (k === '✓' || k === '✗'); + const g = svgEl(s, 'g', {}); g.style.cursor = 'pointer'; g.style.transition = 'transform .07s'; + svgEl(g, 'rect', { x: x - 14, y: y - 11, width: 28, height: 24, rx: 4, fill: amber ? 'url(#kpAmber)' : 'url(#kpKey)', stroke: '#4e4a42', 'stroke-width': 1, 'stroke-opacity': .8 }); + svgEl(g, 'text', { x, y: y + 6, 'text-anchor': 'middle', 'font-size': 13, 'font-weight': 700, 'font-family': 'var(--mono)', fill: k === '✓' ? '#3f5a1f' : k === '✗' ? '#7a2a1a' : '#2b2721', transform: `rotate(${jitter[r * 3 + c]},${x},${y})` }).textContent = k; + g.addEventListener('click', () => { g.style.transform = 'translateY(1.5px)'; setTimeout(() => g.style.transform = '', 80); press(k); }); + })); + render(); onChange('', 'enter a code'); + return { el: s, get: () => buf, press }; +}; + +/* R18 thumb-slide attenuator pair — lit numeral strip, cream side tab */ +GW.thumbSlide = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const chans = (opts.channels || [{ name: 'BLEND', v: 74 }, { name: 'MIX', v: 92 }]).map(c => ({ name: c.name, v: c.v })); + const s = stageSvg(host, 'rsvg', 130, 150), y0 = 22, y1 = 122; + gradDef('thRail', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]); + gradDef('thTab', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f2efe6'], ['1', '#c6c1b3']]); + gradDef('sfScrew', 'radialGradient', { cx: '40%', cy: '35%', r: '75%' }, [['0', '#9b968a'], ['1', '#4a463e']]); + const parts = []; + const set = (i, v) => { + v = Math.max(0, Math.min(100, Math.round(v))); chans[i].v = v; + const p = parts[i]; + p.tab.setAttribute('transform', `translate(0,${28 + (100 - v) * 0.88})`); + p.nums.forEach(t => { + const on = Math.abs(parseInt(t.textContent) - v) <= 5; + t.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--gold)'); t.setAttribute('opacity', on ? '1' : '.55'); + }); + onChange(chans.map(c => c.v), `${chans[0].name} ${chans[0].v} · ${chans[1].name} ${chans[1].v}`); + }; + chans.forEach((ch, i) => { + const x = 40 + i * 50; + svgEl(s, 'rect', { x: x - 9, y: y0 - 10, width: 18, height: y1 - y0 + 20, rx: 3, fill: 'url(#thRail)', stroke: '#0a0908', 'stroke-width': 1.2 }); + for (const sy of [y0 - 6, y1 + 6]) { + svgEl(s, 'circle', { cx: x, cy: sy, r: 2.4, fill: 'url(#sfScrew)', stroke: '#0a0908', 'stroke-width': .5 }); + svgEl(s, 'line', { x1: x - 1.8, y1: sy - 1, x2: x + 1.8, y2: sy + 1, stroke: '#14110e', 'stroke-width': .8 }); + } + svgEl(s, 'rect', { x: x - 5, y: y0, width: 10, height: y1 - y0, fill: '#0d0a08', stroke: '#050404' }); + const nums = []; + for (let n = 0; n <= 10; n++) { + const val = 100 - n * 10, y = y0 + 6 + n * (y1 - y0 - 12) / 10; + const t = svgEl(s, 'text', { x, y: y + 2, 'text-anchor': 'middle', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: .55 }); + t.textContent = val; nums.push(t); + } + const tab = svgEl(s, 'g', {}); + svgEl(tab, 'rect', { x: x + 7, y: -6, width: 8, height: 12, rx: 2, fill: 'url(#thTab)', stroke: '#7a766a', 'stroke-width': .6 }); + svgEl(tab, 'rect', { x: x + 8.2, y: -1, width: 5.6, height: 1.6, fill: 'rgba(0,0,0,.25)' }); + svgEl(s, 'text', { x, y: 145, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.14em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = ch.name; + const hit = svgEl(s, 'rect', { x: x - 12, y: y0 - 10, width: 30, height: y1 - y0 + 20, fill: 'transparent' }); + hit.style.cursor = 'ns-resize'; + parts.push({ tab, nums }); + dragY(hit, pct => set(i, pct)); + }); + set(0, chans[0].v); set(1, chans[1].v); + return { el: s, get: () => chans.map(c => c.v), set }; +}; + +/* R19 waveform region editor — monochrome LCD, draggable S/E flags */ +GW.waveRegion = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg', 190, 100), x0 = 10, x1 = 180, yMid = 48, N = 85; + svgEl(s, 'rect', { x: 1, y: 1, width: 188, height: 98, rx: 6, fill: 'var(--scr-bg1,#0b0c0b)', stroke: 'var(--scr-brd,#2a2c2a)', 'stroke-width': 2 }); + svgEl(s, 'text', { x: 12, y: 13, 'font-size': 7, 'font-family': 'var(--mono)', fill: 'var(--scr-hi,#e8eae8)' }).textContent = 'C1:START'; + svgEl(s, 'text', { x: 78, y: 13, 'font-size': 7, 'font-family': 'var(--mono)', fill: 'var(--scr-hi,#e8eae8)' }).textContent = 'C2:OFF'; + svgEl(s, 'text', { x: 136, y: 13, 'font-size': 7, 'font-family': 'var(--mono)', fill: 'var(--scr-hi,#e8eae8)' }).textContent = 'C3:END'; + svgEl(s, 'line', { x1: x0, y1: yMid, x2: x1, y2: yMid, stroke: '#3a3c3a', 'stroke-width': .6, 'stroke-dasharray': '1.5 2' }); + const bars = []; + for (let i = 0; i < N; i++) { + const x = x0 + (i / (N - 1)) * (x1 - x0); + const env = Math.abs(Math.sin(i * 0.19)) * Math.abs(Math.sin(i * 0.045)) * (i % 17 < 11 ? 1 : .25); + const h = Math.max(1.2, env * 26); + bars.push(svgEl(s, 'rect', { x: x - 0.8, y: yMid - h, width: 1.6, height: h * 2, fill: 'var(--scr-dim,#5a5c5a)' })); + } + const flag = lbl => { + const g = svgEl(s, 'g', {}); + svgEl(g, 'line', { x1: 0, y1: 18, x2: 0, y2: 78, stroke: 'var(--scr-hi,#f2f4f2)', 'stroke-width': 1 }); + svgEl(g, 'rect', { x: lbl === 'S' ? 0 : -8, y: 70, width: 8, height: 8, fill: 'var(--scr-hi,#f2f4f2)' }); + svgEl(g, 'text', { x: lbl === 'S' ? 4 : -4, y: 76.5, 'text-anchor': 'middle', 'font-size': 6.5, 'font-weight': 700, 'font-family': 'var(--mono)', fill: 'var(--scr-bg1,#0b0c0b)' }).textContent = lbl; + return g; + }; + const flagS = flag('S'), flagE = flag('E'); + svgEl(s, 'rect', { x: 10, y: 84, width: 78, height: 11, fill: 'var(--scr-hi,#e8eae8)' }); + svgEl(s, 'text', { x: 14, y: 92.5, 'font-size': 6.5, 'font-family': 'var(--mono)', fill: 'var(--scr-bg1,#0b0c0b)' }).textContent = 'ENC:ZOOM(1x)'; + svgEl(s, 'text', { x: 96, y: 92.5, 'font-size': 6.5, 'font-family': 'var(--mono)', fill: 'var(--scr-hi,#e8eae8)' }).textContent = 'A-3 M:[S]'; + svgEl(s, 'rect', { x: 148, y: 84, width: 32, height: 11, fill: 'var(--scr-hi,#e8eae8)' }); + svgEl(s, 'text', { x: 164, y: 92.5, 'text-anchor': 'middle', 'font-size': 6.5, 'font-weight': 700, 'font-family': 'var(--mono)', fill: 'var(--scr-bg1,#0b0c0b)' }).textContent = 'MENU'; + s.style.cursor = 'ew-resize'; + let S, E; + const set = (sv, ev) => { + S = Math.max(0, Math.min(96, sv)); E = Math.max(4, Math.min(100, ev)); + const xOf = p => x0 + p / 100 * (x1 - x0); + flagS.setAttribute('transform', `translate(${xOf(S)},0)`); + flagE.setAttribute('transform', `translate(${xOf(E)},0)`); + bars.forEach((b, i) => { + const p = i / (bars.length - 1) * 100; + b.setAttribute('fill', (p >= S && p <= E) ? 'var(--scr-hi,#f2f4f2)' : 'var(--scr-dim,#5a5c5a)'); + }); + onChange({ s: S, e: E }, `S ${Math.round(S)}% · E ${Math.round(E)}%`); + }; + dragX(s, pct => { + /* move whichever flag is nearer the pointer */ + if (Math.abs(pct - S) <= Math.abs(pct - E)) set(Math.min(pct, E - 4), E); + else set(S, Math.max(pct, S + 4)); + }); + set(opts.start !== undefined ? opts.start : 22, opts.end !== undefined ? opts.end : 76); + return { el: s, get: () => ({ s: S, e: E }), set }; +}; + +/* R20 drum roller selector — numbered paper drum in a window, center chip reads it */ +GW.drumRoller = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const chans = (opts.channels || [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }]).map(c => ({ name: c.name, v: c.v })); + const s = stageSvg(host, 'rsvg', 130, 140), cy = 76, step = 17; + gradDef('thRail', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]); + gradDef('drPaper', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#cfc6a8'], ['.5', '#ece4c8'], ['1', '#c6bd9e']]); + gradDef('drCurve', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'rgba(0,0,0,.4)'], ['.28', 'rgba(0,0,0,0)'], ['.72', 'rgba(0,0,0,0)'], ['1', 'rgba(0,0,0,.4)']]); + const localDefs = svgEl(s, 'defs', {}); + svgEl(s, 'text', { x: 65, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.title || 'EQUALIZER'; + const grps = []; + const set = (i, v) => { + v = Math.max(1, Math.min(10, v)); chans[i].v = v; + const p = grps[i]; + p.nums.style.transform = `translateY(${(v - p.base) * step}px)`; + p.chip.textContent = Math.round(v); + onChange(chans.map(c => c.v), `${chans[0].name} ${Math.round(chans[0].v)} · ${chans[1].name} ${Math.round(chans[1].v)}`); + }; + chans.forEach((ch, i) => { + const x = 45 + i * 50, cid = uid('drClip'); + svgEl(s, 'text', { x: x + 18, y: 34, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '+'; + svgEl(s, 'text', { x: x + 18, y: 124, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '−'; + svgEl(s, 'rect', { x: x - 12, y: 24, width: 24, height: 104, rx: 4, fill: 'url(#thRail)', stroke: '#0a0908', 'stroke-width': 1.2 }); + const clip = svgEl(localDefs, 'clipPath', { id: cid }); + svgEl(clip, 'rect', { x: x - 9, y: 28, width: 18, height: 96 }); + svgEl(s, 'rect', { x: x - 9, y: 28, width: 18, height: 96, fill: 'url(#drPaper)' }); + const g = svgEl(s, 'g', { 'clip-path': `url(#${cid})` }); + const nums = svgEl(g, 'g', {}); nums.style.transition = 'transform .12s'; + for (let n = 1; n <= 10; n++) + svgEl(nums, 'text', { x, y: cy + 3 + (ch.v - n) * step, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#2b2418' }).textContent = n; + svgEl(g, 'rect', { x: x - 9, y: 28, width: 18, height: 96, fill: 'url(#drCurve)', 'pointer-events': 'none' }); + svgEl(s, 'rect', { x: x - 8, y: cy - 6.5, width: 16, height: 13, rx: 1.5, fill: '#1a1613', opacity: .92 }); + const chip = svgEl(s, 'text', { x, y: cy + 3, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: 'var(--cream)' }); + svgEl(s, 'text', { x, y: 136, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = ch.name; + const hit = svgEl(s, 'rect', { x: x - 14, y: 24, width: 28, height: 104, fill: 'transparent' }); + hit.style.cursor = 'ns-resize'; + grps.push({ nums, chip, base: ch.v }); + dragDelta(hit, () => chans[i].v, v => set(i, v), { min: 1, max: 10, sens: .08 }); + }); + set(0, chans[0].v); set(1, chans[1].v); + return { el: s, get: () => chans.map(c => c.v), set }; +}; + +/* R21 LED program row — exclusive select, the LED above the key carries the state */ +GW.ledRow = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const items = opts.items || ['Sm Hall B', 'VocPlate', 'Lg Hall B', 'Chamber', 'ParcPlate', 'Sm Hall A', 'Room A', 'Const Plate']; + const s = stageSvg(host, 'rsvg', 190, 58); + gradDef('lrKey', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#2b2823'], ['1', '#171512']]); + const leds = []; + let idx; + const set = i => { + idx = i; + leds.forEach((l, k) => { + const on = k === i; + l.setAttribute('fill', on ? 'var(--jewel-r)' : '#3a0f0a'); + l.setAttribute('style', on ? 'filter:drop-shadow(0 0 3px rgba(255,91,69,.8))' : ''); + }); + onChange(idx, `${idx + 1} · ${items[idx]}`); + }; + items.forEach((_, i) => { + const x = 16 + i * 22.6; + svgEl(s, 'text', { x, y: 9, 'text-anchor': 'middle', 'font-size': 6, 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = i + 1; + leds.push(svgEl(s, 'circle', { cx: x, cy: 17, r: 2.4, fill: '#3a0f0a' })); + const g = svgEl(s, 'g', {}); g.style.cursor = 'pointer'; g.style.transition = 'transform .07s'; + svgEl(g, 'rect', { x: x - 8.5, y: 24, width: 17, height: 22, rx: 2.5, fill: 'url(#lrKey)', stroke: '#0a0908', 'stroke-width': 1 }); + svgEl(g, 'rect', { x: x - 6.5, y: 26.5, width: 13, height: 3, rx: 1.5, fill: 'rgba(255,255,255,.06)' }); + g.addEventListener('click', () => { g.style.transform = 'translateY(1.5px)'; setTimeout(() => g.style.transform = '', 80); set(i); }); + }); + svgEl(s, 'text', { x: 95, y: 56, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.16em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.label || 'PROGRAM'; + set(opts.index !== undefined ? opts.index : 5); + return { el: s, get: () => idx, set }; +}; + +/* R22 three-position slide — chrome pill between detents, honest LED pair */ +GW.pillSlide = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const positions = opts.positions || ['A', 'AB', 'B']; + const s = stageSvg(host, 'rsvg press', 110, 64), detX = [34, 55, 76]; + gradDef('sfChrome', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f0efec'], ['.45', '#c4c1b9'], ['1', '#75726a']]); + svgEl(s, 'text', { x: 55, y: 10, 'text-anchor': 'middle', 'font-size': 5.5, 'letter-spacing': '.16em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.title || 'BASIC · VARIATION'; + svgEl(s, 'rect', { x: 20, y: 16, width: 70, height: 14, rx: 7, fill: '#0b0a09', stroke: '#000', 'stroke-width': 1 }); + svgEl(s, 'rect', { x: 21, y: 17, width: 68, height: 5, rx: 2.5, fill: 'rgba(255,255,255,.04)' }); + const pill = svgEl(s, 'rect', { x: -13, y: 18.5, width: 26, height: 9, rx: 4.5, fill: 'url(#sfChrome)', stroke: '#4e4a42', 'stroke-width': .6 }); + pill.style.transition = 'transform .12s'; + const lbls = positions.map((lbl, i) => { + const t = svgEl(s, 'text', { x: detX[i], y: 40, 'text-anchor': 'middle', 'font-size': 6.5, 'font-family': 'var(--mono)', fill: 'var(--dim)' }); + t.textContent = lbl; return t; + }); + svgEl(s, 'rect', { x: 32, y: 45, width: 46, height: 11, rx: 2, fill: '#141210', stroke: '#060505' }); + const leds = [44, 66].map(x => svgEl(s, 'circle', { cx: x, cy: 50.5, r: 2.4, fill: '#3a0f0a' })); + let idx; + const set = i => { + idx = i; + pill.setAttribute('transform', `translate(${detX[i]},0)`); + lbls.forEach((t, k) => t.setAttribute('fill', k === i ? 'var(--gold-hi)' : 'var(--dim)')); + const lit = [i === 0 || i === 1, i === 2 || i === 1]; + leds.forEach((l, k) => l.setAttribute('fill', lit[k] ? 'var(--jewel-r)' : '#3a0f0a')); + onChange(idx, positions[i]); + }; + s.addEventListener('click', e => { + const r = s.getBoundingClientRect(); + const x = (e.clientX - r.left) / r.width * 110; + set(x < 45 ? 0 : x < 66 ? 1 : 2); + }); + set(opts.index !== undefined ? opts.index : 1); + return { el: s, get: () => idx, set }; +}; + +/* R23 spun-aluminum knob — machined rings in a knurled grip, red index */ +GW.spunKnob = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg drag', 110, 110), cx = 55, cy = 52; + gradDef('spunFace', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#e8e6e0'], ['.7', '#b3b0a8'], ['1', '#8a877e']]); + for (let k = 0; k < 8; k++) { const [x, y] = polar(cx, cy, 45, -135 + k * 38.6); svgEl(s, 'circle', { cx: x, cy: y, r: 1.3, fill: 'var(--steel)' }); } + for (let k = 0; k < 22; k++) { const [x, y] = polar(cx, cy, 36, k * 16.36); svgEl(s, 'circle', { cx: x, cy: y, r: 2.6, fill: '#14110e' }); } + svgEl(s, 'circle', { cx, cy, r: 36, fill: '#1a1714', stroke: '#000', 'stroke-width': .8 }); + svgEl(s, 'circle', { cx, cy, r: 30, fill: 'url(#spunFace)', stroke: '#6e6b63', 'stroke-width': .8 }); + for (const r of [24, 18, 12, 6]) + svgEl(s, 'circle', { cx, cy, r, fill: 'none', stroke: r % 12 === 0 ? 'rgba(0,0,0,.12)' : 'rgba(255,255,255,.3)', 'stroke-width': .6 }); + const idx = svgEl(s, 'line', { x1: cx, y1: cy - 28, x2: cx, y2: cy - 10, stroke: 'var(--fail)', 'stroke-width': 2.6, 'stroke-linecap': 'round' }); + svgEl(s, 'ellipse', { cx: cx - 9, cy: cy - 11, rx: 9, ry: 5, fill: 'rgba(255,255,255,.28)', transform: `rotate(-32,${cx - 9},${cy - 11})` }); + svgEl(s, 'text', { x: cx, y: 103, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.16em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.label || 'SPEED'; + let val; + const set = v => { + val = Math.max(0, Math.min(100, v)); + idx.setAttribute('transform', `rotate(${-135 + val / 100 * 270},${cx},${cy})`); + onChange(val, Math.round(val) + '%'); + }; + dragDelta(s, () => val, set, { min: 0, max: 100 }); + set(opts.value !== undefined ? opts.value : 62); + return { el: s, get: () => val, set }; +}; + +/* R24 stomp switch + jewel — press to engage, the jewel reports */ +GW.stompSwitch = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 110, 110), cx = 55; + gradDef('stompDome', 'radialGradient', { cx: '40%', cy: '30%', r: '80%' }, [['0', '#f4f2ec'], ['.55', '#b9b6ae'], ['1', '#6e6b63']]); + gradDef('stompJewel', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#ffe9b0'], ['.45', 'var(--jewel-a)'], ['1', '#5c3d0c']]); + 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 }); + }); + const glow = svgEl(s, 'circle', { cx, cy: 22, r: 14, fill: 'rgba(255,180,58,.35)', filter: 'url(#avGlow)', opacity: 0 }); + const jewel = svgEl(s, 'circle', { cx, cy: 22, r: 9, fill: '#241f1b', stroke: '#0a0908', 'stroke-width': 1.5 }); + for (let k = 0; k < 6; k++) { + const a1 = k * 60, a2 = k * 60 + 30; + const [x1, y1] = polar(cx, 22, 9, a1), [x2, y2] = polar(cx, 22, 9, a2); + svgEl(s, 'path', { d: `M ${cx} 22 L ${x1} ${y1} A 9 9 0 0 1 ${x2} ${y2} Z`, fill: k % 2 ? 'rgba(255,255,255,.09)' : 'rgba(0,0,0,.10)', 'pointer-events': 'none' }); + } + for (let k = 0; k < 20; k++) { const [x, y] = polar(cx, 72, 25, k * 18); svgEl(s, 'circle', { cx: x, cy: y, r: 2.4, fill: '#14110e' }); } + svgEl(s, 'circle', { cx, cy: 72, r: 25, fill: '#1a1714', stroke: '#000', 'stroke-width': .8 }); + const dome = svgEl(s, 'g', {}); dome.style.transition = 'transform .08s'; + svgEl(dome, 'circle', { cx, cy: 72, r: 19, fill: 'url(#stompDome)', stroke: '#55524a', 'stroke-width': .8 }); + svgEl(dome, 'ellipse', { cx: cx - 6, cy: 64, rx: 8, ry: 4.5, fill: 'rgba(255,255,255,.5)', transform: `rotate(-24,${cx - 6},64)` }); + let on; + const set = v => { + on = !!v; + jewel.setAttribute('fill', on ? 'url(#stompJewel)' : '#241f1b'); + glow.setAttribute('opacity', on ? '1' : '0'); + onChange(on, on ? 'ENGAGED' : 'bypass'); + }; + s.addEventListener('click', () => { dome.style.transform = 'translateY(1.5px)'; setTimeout(() => dome.style.transform = '', 90); set(!on); }); + set(opts.on !== undefined ? opts.on : false); + return { el: s, get: () => on, set }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
