From 142f8180fe679ce00d212248cef54364c0ae5685 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Jul 2026 21:50:14 -0500 Subject: refactor(gallery): extract controls R27-R29, R32-R34, R37, R38 into GW builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Winged gain selector, rotary disc switch, guarded toggle, mechanical timer dial, four-way rocker, four-way toggle selector, pin routing matrix, and dead-man button. The timer dial keeps its reduced-motion-gated 1 Hz wind-down inside the builder — it's widget behavior, not a gallery meter loop. discRed moves to the shared defs so the guarded toggle renders standalone. Behavioral pass now 96 checks; both probes green. --- docs/prototypes/panel-widget-gallery.html | 313 +----------------------------- 1 file changed, 8 insertions(+), 305 deletions(-) (limited to 'docs/prototypes/panel-widget-gallery.html') diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 709b2e8..e08db62 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -1662,28 +1662,28 @@ card(C,'R24','Stomp switch + jewel', (st,rd)=>GW.stompSwitch(st,{on:false,onChange:(v,t)=>rd(t)}), 'the engage pair. 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', - ``, + (st,rd)=>GW.wingSelector(st,{index:7,onChange:(v,t)=>rd(t)}), 'the red T-bar, stepped. A bar-grip handle over a dot ring, snapping between labeled gain detents. Drag up/down to step it. After a classic mic-preamp gain switch.'); card(C,'R28','Rotary disc switch', - ``, + (st,rd)=>GW.discSwitch(st,{index:1,onChange:(v,t)=>rd(t)}), 'the master power disc. The whole red disc turns, grip bar molded in — a heavy three-position selector for OFF, ON, and COMBINE. Click to step. After a marine battery switch.'); card(C,'R29','Guarded toggle', - ``, + (st,rd)=>GW.guardedToggle(st,{on:true,onChange:(v,t)=>rd(t)}), 'friction sized to consequence. Guard posts flank the lever and a red collar marks it critical — the avionics way to prevent an accidental throw. Click to throw it deliberately. After a flight-deck switch panel.'); card(C,'R32','Mechanical timer dial', - ``, + (st,rd)=>GW.timerDial(st,{minutes:0,onChange:(v,t)=>rd(t)}), 'turn to start, push to stop. Drag the knurled dial to wind minutes onto the fixed red index; it winds back down to OFF on its own (demo runs a minute per second). Click the red knob to stop it dead. After a mechanical interval timer.'); card(C,'R33','Four-way rocker', - ``, + (st,rd)=>GW.rockerPad(st,{onChange:(v,t)=>rd(t)}), 'discrete direction, one press at a time. Click an arrow (the whole quadrant is the target) to step that way; the readout tracks the cursor position it drives. After a remote-control navigation pad.'); card(C,'R34','Four-way toggle selector', - ``, + (st,rd)=>GW.fourWayToggle(st,{position:'C',onChange:(v,t)=>rd(t)}), 'a lever that lives in one of four states. Click a quadrant and the ball lever throws to A, B, C or D; the matching corner lamp takes the light. The rocker steps — this one selects. After a test-panel quadrant switch.'); card(C,'R37','Pin routing matrix', - ``, + (st,rd)=>GW.pinMatrix(st,{onChange:(v,t)=>rd(t)}), 'many-to-many routing you can read. Sources are rows, destinations are columns; click an intersection to seat a pin and the route exists. Every simultaneous route stays visible. After the EMS VCS3 pin matrix.'); card(C,'R38','Dead-man button', - ``, + (st,rd)=>GW.deadMan(st,{onChange:(v,t)=>rd(t)}), 'active only while held. Press and hold — the ring arms, the lamp runs, the dwell clock counts. Let go anywhere and it drops to SAFE. The kit has toggles and momentaries; this is the only control whose state IS your grip.'); card(C,'R39','Rotary telephone dial', ``, @@ -2299,87 +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); -/* 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; -(function(){const s=$('wingk'),cx=55,cy=52; - const defs=svgEl(s,'defs',{}); - const rg2=svgEl(defs,'radialGradient',{id:'wingRed',cx:'40%',cy:'32%',r:'80%'}); - svgEl(rg2,'stop',{offset:'0','stop-color':'#d98a6f'}); - svgEl(rg2,'stop',{offset:'.6','stop-color':'var(--fail)'}); - svgEl(rg2,'stop',{offset:'1','stop-color':'#6e2415'}); - WINGK_STEPS.forEach((db,i)=>{const a=-135+i/(WINGK_STEPS.length-1)*270; - const [dx,dy]=polar(cx,cy,38,a); - svgEl(s,'circle',{cx:dx,cy:dy,r:1.5,fill:'var(--steel)'}); - if(i%2===1||db===0||db===10||db===-80){const [tx,ty]=polar(cx,cy,46.5,a); - svgEl(s,'text',{x:tx,y:ty+2.2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=db>0?'+'+db:db;}}); - wingGrp=svgEl(s,'g',{});wingGrp.style.transition='transform .09s'; - svgEl(wingGrp,'rect',{x:cx-6.5,y:cy-30,width:13,height:60,rx:6,fill:'url(#wingRed)',stroke:'#4e150a','stroke-width':.8}); - svgEl(wingGrp,'circle',{cx,cy,r:15,fill:'url(#wingRed)',stroke:'#4e150a','stroke-width':.8}); - svgEl(wingGrp,'line',{x1:cx,y1:cy-28,x2:cx,y2:cy-18,stroke:'var(--cream)','stroke-width':2,'stroke-linecap':'round'}); - svgEl(wingGrp,'ellipse',{cx:cx-4,cy:cy-8,rx:5,ry:3,fill:'rgba(255,255,255,.18)',transform:`rotate(-30,${cx-4},${cy-8})`});})(); -function setWing(v){const i=Math.max(0,Math.min(WINGK_STEPS.length-1,Math.round(v)));wingI=i; - wingGrp.style.transform=`rotate(${-135+i/(WINGK_STEPS.length-1)*270}deg)`; - wingGrp.style.transformOrigin='55px 52px'; - const db=WINGK_STEPS[i]; - setRd('R27',(db>0?'+':'')+db+' dB');} -dragDelta($('wingk'),()=>wingI,setWing,{min:0,max:WINGK_STEPS.length-1,sens:.05});setWing(7); - -/* R28 rotary disc switch: the whole disc turns between heavy positions */ -const DISCSW_POS=[['OFF',-45],['ON',45],['COMBINE',135]]; -let discI=1,discGrp=null,discLbls=[]; -(function(){const s=$('discsw'),cx=55,cy=58; - const defs=svgEl(s,'defs',{}); - const rr=svgEl(defs,'radialGradient',{id:'discRed',cx:'42%',cy:'34%',r:'85%'}); - svgEl(rr,'stop',{offset:'0','stop-color':'#d98a6f'}); - svgEl(rr,'stop',{offset:'.65','stop-color':'var(--fail)'}); - svgEl(rr,'stop',{offset:'1','stop-color':'#7a2a1a'}); - svgEl(s,'circle',{cx,cy,r:48,fill:'#1a1714',stroke:'#060505','stroke-width':1.5}); - for(let k=0;k<4;k++){const [x,y]=polar(cx,cy,44,45+k*90); - svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5});} - DISCSW_POS.forEach(([lbl,a],i)=>{const [tx,ty]=polar(cx,cy,42,a); - const t=svgEl(s,'text',{x:tx,y:ty+2.4,'text-anchor':'middle','font-size':5.8,'letter-spacing':'.06em', - 'font-family':'var(--mono)',fill:'var(--dim)'});t.textContent=lbl;discLbls.push(t);}); - svgEl(s,'circle',{cx,cy,r:36,fill:'url(#discRed)',stroke:'#4e150a','stroke-width':1}); - discGrp=svgEl(s,'g',{});discGrp.style.transition='transform .15s';discGrp.style.transformOrigin=`${cx}px ${cy}px`; - svgEl(discGrp,'rect',{x:cx-27,y:cy-8,width:54,height:16,rx:8,fill:'url(#discRed)',stroke:'#8f3520','stroke-width':1}); - svgEl(discGrp,'rect',{x:cx-25,y:cy-6.5,width:50,height:4,rx:2,fill:'rgba(255,255,255,.16)'}); - svgEl(discGrp,'line',{x1:cx+20,y1:cy,x2:cx+26,y2:cy,stroke:'var(--cream)','stroke-width':2.5,'stroke-linecap':'round'}); - svgEl(s,'ellipse',{cx:cx-12,cy:cy-14,rx:12,ry:6,fill:'rgba(255,255,255,.10)',transform:`rotate(-28,${cx-12},${cy-14})`}); - $('discsw').onclick=()=>setDisc((discI+1)%DISCSW_POS.length);})(); -function setDisc(i){discI=i; - discGrp.style.transform=`rotate(${DISCSW_POS[i][1]}deg)`; - discLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)')); - setRd('R28',DISCSW_POS[i][0]);} -setDisc(1); - -/* R29 guarded toggle: guard posts + red collar mark the critical throw */ -let guardOn=true,guardLever=null,guardLblOn=null,guardLblOff=null; -(function(){const s=$('guardsw'),cx=45,cy=52; - guardLblOn=svgEl(s,'text',{x:cx,y:12,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--gold-hi)'});guardLblOn.textContent='ON'; - guardLblOff=svgEl(s,'text',{x:cx,y:96,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--dim)'});guardLblOff.textContent='OFF'; - svgEl(s,'circle',{cx,cy,r:13,fill:'url(#discRed)',stroke:'#4e150a','stroke-width':1}); - const pts=[];for(let k=0;k<6;k++){const a=(k*60+30)*Math.PI/180;pts.push((cx+11*Math.cos(a))+','+(cy+11*Math.sin(a)));} - svgEl(s,'polygon',{points:pts.join(' '),fill:'url(#nutG)',stroke:'#2b2822'}); - svgEl(s,'circle',{cx,cy,r:6,fill:'#14110e',stroke:'#000'}); - for(const gx of [16,74]){ - svgEl(s,'rect',{x:gx-6,y:cy-24,width:12,height:48,rx:6,fill:'url(#nutG)',stroke:'#2b2822','stroke-width':1}); - svgEl(s,'rect',{x:gx-4,y:cy-21,width:3.5,height:42,rx:2,fill:'rgba(255,255,255,.14)'});} - guardLever=svgEl(s,'g',{}); - svgEl(guardLever,'path',{d:`M ${cx-3} ${cy} L ${cx-2} 26 L ${cx+2} 26 L ${cx+3} ${cy} Z`,fill:'url(#chromeG)',stroke:'#4e4a42','stroke-width':.5}); - svgEl(guardLever,'rect',{x:cx-4.5,y:16,width:9,height:12,rx:2,fill:'url(#tipG)',stroke:'#5e5a52','stroke-width':.6}); - for(let r=0;r<4;r++)svgEl(guardLever,'line',{x1:cx-3.5,y1:18.5+r*2.4,x2:cx+3.5,y2:18.5+r*2.4,stroke:'rgba(0,0,0,.25)','stroke-width':.8}); - guardLever.style.transformOrigin=`${cx}px ${cy}px`;guardLever.style.transition='transform .12s'; - $('guardsw').onclick=()=>setGuard(!guardOn);})(); -function setGuard(on){guardOn=on;guardLever.style.transform=on?'rotate(0deg)':'rotate(180deg)'; - guardLblOn.setAttribute('fill',on?'var(--gold-hi)':'var(--dim)'); - guardLblOff.setAttribute('fill',on?'var(--dim)':'var(--gold-hi)'); - setRd('R29',on?'ON':'OFF');} -setGuard(true); - /* 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; @@ -2648,162 +2567,6 @@ if(!reduced){ document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused'); } -/* R32 mechanical timer dial: dial rotates under a fixed index; wind by drag, stop by the red knob */ -let mtMin=0,mtRot=null; -(function(){const s=$('mtimer'),cx=62,cy=58; - const defs=svgEl(s,'defs',{}); - const fc=svgEl(defs,'radialGradient',{id:'mtFace',cx:'50%',cy:'42%',r:'75%'}); - svgEl(fc,'stop',{offset:'0','stop-color':'#282320'}); - svgEl(fc,'stop',{offset:'1','stop-color':'#14110e'}); - const rd=svgEl(defs,'radialGradient',{id:'mtRed',cx:'38%',cy:'32%',r:'80%'}); - svgEl(rd,'stop',{offset:'0','stop-color':'#e0523a'}); - svgEl(rd,'stop',{offset:'1','stop-color':'#8f2416'}); - const hb=svgEl(defs,'linearGradient',{id:'mtHub',x1:0,y1:0,x2:0,y2:1}); - svgEl(hb,'stop',{offset:'0','stop-color':'#c9c4b8'}); - svgEl(hb,'stop',{offset:'1','stop-color':'#6e685c'}); - // bezel + knurled coin edge - svgEl(s,'circle',{cx,cy,r:50,fill:'#17140f',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:47.5,fill:'none',stroke:'#3a352c','stroke-width':4,'stroke-dasharray':'2 1.6'}); - svgEl(s,'circle',{cx,cy,r:44,fill:'url(#mtFace)',stroke:'#0a0908','stroke-width':1}); - // rotating scale: OFF at 0, minutes climb clockwise at 4.5 deg/min (60 at 270) - mtRot=svgEl(s,'g',{}); - for(let m=0;m<=60;m+=5){const major=m%20===0; - const [x1,y1]=polar(cx,cy,40,m*4.5),[x2,y2]=polar(cx,cy,major?34:37,m*4.5); - svgEl(mtRot,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':major?1.2:.55,opacity:major?.9:.55});} - for(const m of [20,40,60]){const [x,y]=polar(cx,cy,27,m*4.5); - svgEl(mtRot,'text',{x,y:y+3,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=String(m);} - const [ox,oy]=polar(cx,cy,27,0); - svgEl(mtRot,'text',{x:ox,y:oy+3,'text-anchor':'middle','font-size':7.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'#e0523a'}).textContent='OFF'; - const [mx,my]=polar(cx,cy,16,180); - svgEl(mtRot,'text',{x:mx,y:my+2.5,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='MINUTES'; - // hub with screw slot - svgEl(s,'circle',{cx,cy,r:9,fill:'url(#mtHub)',stroke:'#3c382f','stroke-width':1}); - svgEl(s,'line',{x1:cx-5.5,y1:cy,x2:cx+5.5,y2:cy,stroke:'#4a4438','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:2.2,fill:'#8f897b'}); - // fixed red index at top + winding-direction arrow - svgEl(s,'line',{x1:cx,y1:cy-44,x2:cx,y2:cy-32,stroke:'#e0523a','stroke-width':1.6}); - svgEl(s,'path',{d:'M 122 14 A 26 26 0 0 1 136 26',fill:'none',stroke:'var(--steel)','stroke-width':1.6}); - svgEl(s,'polygon',{points:'136,26 130.5,24.5 134.5,19.5',fill:'var(--steel)'}); - // engraved plate captions - const cap=(t,y)=>svgEl(s,'text',{x:4,y,'font-size':6.2,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=t; - cap('PUSH TO STOP',118); cap('TURN TO START',128); - // dial drag surface (under the knob in DOM so the knob wins the overlap) - const hit=svgEl(s,'circle',{cx,cy,r:50,fill:'transparent'}); - hit.style.cursor='grab'; - dragDelta(hit,()=>mtMin,v=>setMtimer(v),{min:0,max:60}); - // red stop knob: fluted edge, domed center - const knob=svgEl(s,'g',{}); - svgEl(knob,'circle',{cx:126,cy:104,r:20,fill:'url(#mtRed)',stroke:'#5c150c','stroke-width':1.5}); - svgEl(knob,'circle',{cx:126,cy:104,r:18,fill:'none',stroke:'rgba(0,0,0,.4)','stroke-width':3.4,'stroke-dasharray':'3 2.4'}); - svgEl(knob,'circle',{cx:126,cy:104,r:8.5,fill:'url(#mtRed)',stroke:'rgba(0,0,0,.3)','stroke-width':.8}); - svgEl(knob,'ellipse',{cx:121,cy:98,rx:6,ry:3.4,fill:'rgba(255,255,255,.18)',transform:'rotate(-28,121,98)'}); - knob.style.cursor='pointer'; - knob.addEventListener('click',()=>{setMtimer(0);setRd('R32','STOP · OFF');});})(); -function setMtimer(m){mtMin=Math.max(0,Math.min(60,m)); - mtRot.setAttribute('transform',`rotate(${(-mtMin*4.5).toFixed(2)},62,58)`); - setRd('R32',mtMin>0.5?'T-'+Math.round(mtMin)+' MIN':'OFF');} -setMtimer(0); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{if(mtMin>0){setMtimer(mtMin-1);if(mtMin===0)setRd('R32','DING · OFF');}},1000); - -/* R33 four-way rocker: quadrant clicks step a tracked cursor; arrows flash on press */ -(function(){const s=$('rocker4'),cx=55,cy=55; - const defs=svgEl(s,'defs',{}); - const pd=svgEl(defs,'radialGradient',{id:'rk4Pad',cx:'42%',cy:'34%',r:'85%'}); - svgEl(pd,'stop',{offset:'0','stop-color':'#33302b'}); - svgEl(pd,'stop',{offset:'1','stop-color':'#141210'}); - const ar=svgEl(defs,'linearGradient',{id:'rk4Arr',x1:0,y1:0,x2:0,y2:1}); - svgEl(ar,'stop',{offset:'0','stop-color':'#e9e4d6'}); - svgEl(ar,'stop',{offset:'1','stop-color':'#9d988b'}); - const nb=svgEl(defs,'radialGradient',{id:'rk4Nub',cx:'38%',cy:'30%',r:'85%'}); - svgEl(nb,'stop',{offset:'0','stop-color':'#4a453e'}); - svgEl(nb,'stop',{offset:'1','stop-color':'#0d0c0a'}); - // recessed plate + rubber pad - svgEl(s,'rect',{x:3,y:3,width:104,height:104,rx:18,fill:'#211e1a',stroke:'#0c0b09','stroke-width':1.5}); - svgEl(s,'circle',{cx,cy,r:45,fill:'#0f0d0b'}); - svgEl(s,'circle',{cx,cy,r:41,fill:'url(#rk4Pad)',stroke:'#060505','stroke-width':1}); - // four arrows, pointing outward - const ARR={up:'55,22 45,36 65,36',down:'55,88 45,74 65,74',left:'22,55 36,45 36,65',right:'88,55 74,45 74,65'}; - const arrows={}; - for(const [dir,pts] of Object.entries(ARR)) - arrows[dir]=svgEl(s,'polygon',{points:pts,fill:'url(#rk4Arr)',stroke:'#3c382f','stroke-width':.7}); - // center pivot nub - svgEl(s,'circle',{cx,cy,r:7,fill:'url(#rk4Nub)',stroke:'#060505','stroke-width':.8}); - svgEl(s,'ellipse',{cx:cx-2,cy:cy-2.5,rx:2.6,ry:1.7,fill:'rgba(255,255,255,.25)'}); - // quadrant hit zones (drawn last so they win), press flash + tracked position - let rx=0,ry=0; - const press=(dir,dx,dy)=>{rx+=dx;ry+=dy; - setRd('R33',dir.toUpperCase()+' · x '+rx+' y '+ry); - arrows[dir].setAttribute('fill','var(--gold-hi)'); - setTimeout(()=>arrows[dir].setAttribute('fill','url(#rk4Arr)'),160);}; - const zone=(dir,pts,dx,dy)=>{const z=svgEl(s,'polygon',{points:pts,fill:'transparent'}); - z.style.cursor='pointer';z.addEventListener('click',()=>press(dir,dx,dy));}; - zone('up',`${cx},${cy} 20,20 90,20`,0,1); - zone('down',`${cx},${cy} 20,90 90,90`,0,-1); - zone('left',`${cx},${cy} 20,20 20,90`,-1,0); - zone('right',`${cx},${cy} 90,20 90,90`,1,0); - setRd('R33','x 0 y 0');})(); - -/* R34 four-way toggle selector: ball lever throws to a diagonal; corner lamps show the state */ -(function(){const s=$('fourway'),cx=65,cy=65; - const defs=svgEl(s,'defs',{}); - const ch=svgEl(defs,'linearGradient',{id:'fw4Chrome',x1:0,y1:0,x2:1,y2:1}); - svgEl(ch,'stop',{offset:'0','stop-color':'#e8e6e0'}); - svgEl(ch,'stop',{offset:'.5','stop-color':'#9a968c'}); - svgEl(ch,'stop',{offset:'1','stop-color':'#55524a'}); - const bl=svgEl(defs,'radialGradient',{id:'fw4Ball',cx:'36%',cy:'30%',r:'80%'}); - svgEl(bl,'stop',{offset:'0','stop-color':'#f2f0ea'}); - svgEl(bl,'stop',{offset:'.55','stop-color':'#8e8a80'}); - svgEl(bl,'stop',{offset:'1','stop-color':'#3c3a34'}); - // printed panel graphics: axes, circle, diagonal square, labels - svgEl(s,'line',{x1:8,y1:cy,x2:122,y2:cy,stroke:'var(--steel)','stroke-width':.7,opacity:.7}); - svgEl(s,'line',{x1:cx,y1:8,x2:cx,y2:122,stroke:'var(--steel)','stroke-width':.7,opacity:.7}); - svgEl(s,'text',{x:cx+2,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='Y'; - svgEl(s,'text',{x:116,y:cy-4,'font-size':7,'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='X'; - svgEl(s,'circle',{cx,cy,r:42,fill:'none',stroke:'var(--steel)','stroke-width':.9,opacity:.8}); - const QUAD={A:315,B:45,C:135,D:225}; - const diag=Object.values(QUAD).map(a=>polar(cx,cy,42,a)); - svgEl(s,'polygon',{points:diag.map(p=>p.join(',')).join(' '),fill:'none',stroke:'var(--steel)','stroke-width':.8,opacity:.8}); - const LAMP_AT={A:[14,14],B:[116,14],C:[116,116],D:[14,116]}; - const lamps={}; - for(const [q,a] of Object.entries(QUAD)){ - const [lx,ly]=polar(cx,cy,42,a); - const [px,py]=LAMP_AT[q]; - svgEl(s,'line',{x1:lx,y1:ly,x2:px,y2:py,stroke:'var(--steel)','stroke-width':.7,opacity:.5}); - svgEl(s,'circle',{cx:lx,cy:ly,r:6.5,fill:'#14110e',stroke:'var(--steel)','stroke-width':.9}); - svgEl(s,'text',{x:lx,y:ly+2.8,'text-anchor':'middle','font-size':7.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=q; - svgEl(s,'circle',{cx:px,cy:py,r:5.5,fill:'#0d0b09',stroke:'#2c2820','stroke-width':1}); - lamps[q]=svgEl(s,'circle',{cx:px,cy:py,r:3.6,fill:'#3a3426'});} - // bezel + boot - svgEl(s,'circle',{cx,cy,r:20,fill:'#0d0c0a',stroke:'#2a2722','stroke-width':1.5}); - svgEl(s,'circle',{cx,cy,r:15,fill:'#17140f',stroke:'#060505','stroke-width':1}); - // lever: shaft + grip rotate to the selected diagonal; ball pivot stays centered - const lever=svgEl(s,'g',{}); - svgEl(lever,'polygon',{points:`${cx-3},${cy} ${cx+3},${cy} ${cx+2},${cy-26} ${cx-2},${cy-26}`,fill:'url(#fw4Chrome)',stroke:'#3c3a34','stroke-width':.6}); - svgEl(lever,'rect',{x:cx-3.4,y:cy-40,width:6.8,height:16,rx:3,fill:'url(#fw4Chrome)',stroke:'#3c3a34','stroke-width':.6}); - svgEl(s,'circle',{cx,cy,r:8.5,fill:'url(#fw4Ball)',stroke:'#26241f','stroke-width':.8}); - svgEl(s,'ellipse',{cx:cx-2.6,cy:cy-3,rx:2.8,ry:1.9,fill:'rgba(255,255,255,.5)'}); - let fwPos=null; - const setFw=q=>{fwPos=q; - lever.setAttribute('transform',`rotate(${QUAD[q]},${cx},${cy})`); - for(const k of Object.keys(QUAD)){ - lamps[k].setAttribute('fill',k===q?'var(--jewel-a)':'#3a3426'); - lamps[k].setAttribute('filter',k===q?'drop-shadow(0 0 4px rgba(255,180,58,.8))':'none');} - setRd('R34','POS '+q);}; - // quadrant click zones (diagonal quadrants, drawn last) - const zone=(q,pts)=>{const z=svgEl(s,'polygon',{points:pts,fill:'transparent'}); - z.style.cursor='pointer';z.addEventListener('click',()=>setFw(q));}; - zone('A',`${cx},${cy} ${cx},5 5,5 5,${cy}`); - zone('B',`${cx},${cy} ${cx},5 125,5 125,${cy}`); - zone('C',`${cx},${cy} 125,${cy} 125,125 ${cx},125`); - zone('D',`${cx},${cy} ${cx},125 5,125 5,${cy}`); - setFw('C');})(); - /* R35 day-date disc calendar: coaxial printed discs rotate so today reads under the fixed hand */ const DD_DAYS=['SUN','MON','TUE','WED','THU','FRI','SAT']; let ddDate=1,ddDay=0,ddDateG=null,ddDayG=null; @@ -2862,66 +2625,6 @@ function setDdcal(d,w){ddDate=d;ddDay=w;const HAND=135; setRd('R36',lit+' / 64 lit');});} setRd('R36',lit+' / 64 lit');})(); -/* R37 pin routing matrix: click an intersection to seat/pull a pin; many-to-many */ -(function(){const s=$('pinmx'); - const ROWS=['OSC1','OSC2','LFO','NOIS','ENV'],COLS=['VCF','VCA','PAN','DLY','OUT','MOD']; - const X0=42,Y0=30,DX=19,DY=15.5; - svgEl(s,'rect',{x:2,y:2,width:156,height:106,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5}); - COLS.forEach((c,j)=>svgEl(s,'text',{x:X0+j*DX,y:16,'text-anchor':'middle','font-size':5.4, - 'letter-spacing':'.05em','font-family':'var(--mono)',fill:'var(--steel)'}).textContent=c); - ROWS.forEach((r,i)=>svgEl(s,'text',{x:34,y:Y0+i*DY+2,'text-anchor':'end','font-size':5.4, - 'letter-spacing':'.05em','font-family':'var(--mono)',fill:'var(--steel)'}).textContent=r); - const pins=new Set(['OSC1>VCF','LFO>PAN','ENV>VCA']); // a legible default patch - ROWS.forEach((r,i)=>COLS.forEach((c,j)=>{ - const cx=X0+j*DX,cy=Y0+i*DY,key=r+'>'+c; - svgEl(s,'circle',{cx,cy,r:3.4,fill:'#0a0908',stroke:'#2c2820','stroke-width':1}); - const cap=svgEl(s,'circle',{cx,cy,r:4.4,fill:'var(--gold)',stroke:'#7d5c16','stroke-width':1, - opacity:pins.has(key)?1:0,filter:'drop-shadow(0 1px 2px rgba(0,0,0,.6))'}); - const hit=svgEl(s,'circle',{cx,cy,r:8,fill:'transparent'}); - hit.style.cursor='pointer'; - hit.addEventListener('click',()=>{const on=!pins.has(key); - if(on)pins.add(key);else pins.delete(key); - cap.setAttribute('opacity',on?1:0); - setRd('R37',(on?r+' → '+c:'pulled '+r+' → '+c)+' · '+pins.size+' routes');});})); - setRd('R37',pins.size+' routes');})(); - -/* R38 dead-man button: state exists only while the pointer holds it down */ -(function(){const s=$('deadman'),cx=60,cy=58; - const defs=svgEl(s,'defs',{}); - const rb=svgEl(defs,'radialGradient',{id:'dmBtn',cx:'40%',cy:'32%',r:'85%'}); - svgEl(rb,'stop',{offset:'0','stop-color':'#3a3631'}); - svgEl(rb,'stop',{offset:'1','stop-color':'#16130f'}); - svgEl(s,'rect',{x:4,y:4,width:112,height:102,rx:12,fill:'#211e1a',stroke:'#0c0b09','stroke-width':1.5}); - svgEl(s,'text',{x:cx,y:18,'text-anchor':'middle','font-size':6.2,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='HOLD TO RUN'; - const lamp=svgEl(s,'circle',{cx:104,cy:16,r:4.5,fill:'#1e2a12'}); - const ring=svgEl(s,'circle',{cx,cy,r:33,fill:'none',stroke:'#2c2820','stroke-width':3}); - const btn=svgEl(s,'g',{}); - svgEl(btn,'circle',{cx,cy,r:28,fill:'url(#dmBtn)',stroke:'#060505','stroke-width':1.5}); - svgEl(btn,'circle',{cx,cy,r:22,fill:'none',stroke:'rgba(255,255,255,.07)','stroke-width':1}); - svgEl(btn,'text',{x:cx,y:cy+2.5,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--silver)'}).textContent='RUN'; - btn.style.cursor='pointer'; - let dmT0=null,dmIv=null; - const stop=()=>{if(dmT0===null)return; - const held=((Date.now()-dmT0)/1000).toFixed(1); - dmT0=null;clearInterval(dmIv); - btn.removeAttribute('transform'); - ring.setAttribute('stroke','#2c2820');ring.removeAttribute('filter'); - lamp.setAttribute('fill','#1e2a12');lamp.removeAttribute('filter'); - setRd('R38','SAFE (held '+held+'s)');}; - btn.addEventListener('pointerdown',e=>{btn.setPointerCapture(e.pointerId); - dmT0=Date.now(); - btn.setAttribute('transform','translate(0,1.5)'); - ring.setAttribute('stroke','var(--gold)');ring.setAttribute('filter','drop-shadow(0 0 5px rgba(var(--glow-lo),.7))'); - lamp.setAttribute('fill','var(--pass)');lamp.setAttribute('filter','drop-shadow(0 0 4px rgba(116,147,47,.8))'); - setRd('R38','RUNNING 0.0s'); - dmIv=setInterval(()=>{if(dmT0!==null)setRd('R38','RUNNING '+((Date.now()-dmT0)/1000).toFixed(1)+'s');},100); - e.preventDefault();}); - btn.addEventListener('pointerup',stop); - btn.addEventListener('pointercancel',stop); - setRd('R38','SAFE');})(); - /* R39 rotary telephone dial: click a hole; the wheel winds to the stop and returns */ (function(){const s=$('teldial'),cx=65,cy=63; const DIGITS=['1','2','3','4','5','6','7','8','9','0']; -- cgit v1.2.3