From d30bbbc29f796513416fb102d5b55352d68d9295 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Jul 2026 00:28:49 -0500 Subject: feat(gallery): add R14 spade-pointer knob and R15 multi-band dial Two radio-receiver widgets as inline SVG, after a 1920s GE receiver panel and a Hallicrafters shortwave dial pair: - R14 spade-pointer tuning knob: a knurled bakelite knob carries a bright spade pointer over a 0-10 scale rendered as engraved relief (each mark drawn with a light offset twin, ink-free like the source panel). The knurl ring turns with the knob. - R15 multi-band dial: four nested band arcs on a cream face read by one long needle; the active ring draws bold and the rest fade. Dragging the main dial tunes within the active band's frequency range, and clicking the bandspread dial steps bands, turning its own needle. I verified both in headless Chrome: the spade knob tracks drags, tuning moves the needle and readout, the band click switches range and re-weights the rings, no console exceptions. The gallery is at 68 cards. --- docs/prototypes/panel-widget-gallery.html | 84 +++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) (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 890c862..8268935 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -706,6 +706,12 @@ card(C,'R06','Chicken-head selector', card(C,'R12','Chrome slot fader', ``, 'the console gain slide. A chrome T-handle rides a recessed slot, the dB scale engraved on the plate — gain above zero, cut below. Drag; readout shows signed dB. After a vintage channel-strip output fader.'); +card(C,'R14','Spade-pointer tuning knob', + ``, + 'the pointer rides the knob. A knurled bakelite knob carries a bright spade pointer over a scale engraved into the panel itself. Drag up/down to tune. After a 1920s radio receiver dial.'); +card(C,'R15','Multi-band dial', + ``, + 'four scales, one needle. 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.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); @@ -1470,6 +1476,84 @@ function setEdge(f){edgeFrac=Math.max(.08,Math.min(.94,f)); setRd('R13','−'+db.toFixed(1)+' dB');} setEdge(.62); +/* R14 spade-pointer tuning knob: engraved panel arc, pointer rides the knob */ +let spadeV=8.3,spadeGrp=null; +(function(){const s=$('spade'),cx=70,cy=82,sweep=v=>-80+v/10*160; + const defs=svgEl(s,'defs',{}); + const bk=svgEl(defs,'radialGradient',{id:'spadeKnob',cx:'42%',cy:'34%',r:'80%'}); + svgEl(bk,'stop',{offset:'0','stop-color':'#33302a'}); + svgEl(bk,'stop',{offset:'1','stop-color':'#080706'}); + const mt=svgEl(defs,'linearGradient',{id:'spadeMetal',x1:0,y1:0,x2:1,y2:1}); + svgEl(mt,'stop',{offset:'0','stop-color':'#e9e7e0'}); + svgEl(mt,'stop',{offset:'1','stop-color':'#8d897f'}); + // engraved arc: every stroke carries a faint light twin offset below (incised relief) + const engrave=(x1,y1,x2,y2,w)=>{ + svgEl(s,'line',{x1,y1:y1+.8,x2,y2:y2+.8,stroke:'rgba(255,255,255,.13)','stroke-width':w}); + svgEl(s,'line',{x1,y1,x2,y2,stroke:'#050404','stroke-width':w});}; + for(let i=0;i<=40;i++){const v=i/4,a=sweep(v),major=i%4===0; + const [x1,y1]=polar(cx,cy,major?50:52,a),[x2,y2]=polar(cx,cy,58,a); + engrave(x1,y1,x2,y2,major?1.6:.9); + if(major){const [nx,ny]=polar(cx,cy,65,a); + svgEl(s,'text',{x:nx,y:ny+3.2,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)', + fill:'rgba(255,255,255,.13)'}).textContent=v; + svgEl(s,'text',{x:nx,y:ny+2.4,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)', + fill:'#0a0908'}).textContent=v;}} + spadeGrp=svgEl(s,'g',{}); + svgEl(spadeGrp,'path',{d:`M ${cx-2} ${cy-28} L ${cx-4.5} ${cy-42} L ${cx} ${cy-54} L ${cx+4.5} ${cy-42} L ${cx+2} ${cy-28} Z`, + fill:'url(#spadeMetal)',stroke:'#55524a','stroke-width':.6}); + svgEl(spadeGrp,'circle',{cx,cy,r:30,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.8}); + svgEl(spadeGrp,'circle',{cx,cy,r:28.5,fill:'none',stroke:'#0a0908','stroke-width':3,'stroke-dasharray':'2.2 2.2'}); + svgEl(spadeGrp,'circle',{cx,cy,r:22,fill:'url(#spadeKnob)'}); + svgEl(spadeGrp,'ellipse',{cx:cx-8,cy:cy-9,rx:8,ry:5,fill:'rgba(255,255,255,.08)',transform:`rotate(-30,${cx-8},${cy-9})`});})(); +function setSpade(v){spadeV=Math.max(0,Math.min(10,v)); + spadeGrp.setAttribute('transform',`rotate(${-80+spadeV/10*160},70,82)`); + setRd('R14',spadeV.toFixed(1));} +dragDelta($('spade'),()=>spadeV,setSpade,{min:0,max:10,sens:.05});setSpade(8.3); + +/* R15 multi-band dial: nested arcs, one needle; bandspread selects the ring */ +const MBAND_RANGES=[[0.54,1.6],[1.6,5.1],[5.1,15.5],[15.5,30.5]]; +let mbandV=45,mbandB=2,mbNeedle=null,mbRings=[],mbSpread=null; +(function(){const s=$('mband'),cx=62,cy=62,sweep=t=>-70+t*140; + const defs=svgEl(s,'defs',{}); + const pf=svgEl(defs,'linearGradient',{id:'mbFace',x1:0,y1:0,x2:1,y2:1}); + svgEl(pf,'stop',{offset:'0','stop-color':'#eee5c8'}); + svgEl(pf,'stop',{offset:'1','stop-color':'#dcd2ae'}); + svgEl(s,'rect',{x:4,y:4,width:182,height:102,rx:12,fill:'#171412',stroke:'#060505'}); + svgEl(s,'rect',{x:9,y:9,width:172,height:92,rx:9,fill:'url(#mbFace)',stroke:'#0a0908'}); + const INK='#2c2318'; + [18,26,34,42].forEach((r,bi)=>{const ring=svgEl(s,'g',{}); + const [x1,y1]=polar(cx,cy,r,-70),[x2,y2]=polar(cx,cy,r,70); + svgEl(ring,'path',{d:`M ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',stroke:INK,'stroke-width':.9}); + for(let i=0;i<=8;i++){const a=sweep(i/8),[tx1,ty1]=polar(cx,cy,r,a),[tx2,ty2]=polar(cx,cy,r+3,a); + svgEl(ring,'line',{x1:tx1,y1:ty1,x2:tx2,y2:ty2,stroke:INK,'stroke-width':.8});} + mbRings.push(ring);}); + mbNeedle=svgEl(s,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'#1a1613','stroke-width':1.6,'stroke-linecap':'round'}); + svgEl(s,'circle',{cx,cy,r:7,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.6}); + svgEl(s,'text',{x:14,y:100,'font-size':5,'letter-spacing':'.12em','font-family':'var(--mono)', + fill:INK,opacity:.8}).textContent='MEGACYCLES'; + // bandspread dial (click = band select) + const bx=142,by=56; + for(let i=0;i<12;i++){const a=i*30,[x1,y1]=polar(bx,by,24,a),[x2,y2]=polar(bx,by,21,a); + svgEl(s,'line',{x1,y1,x2,y2,stroke:INK,'stroke-width':.8});} + svgEl(s,'circle',{cx:bx,cy:by,r:17,fill:'none',stroke:INK,'stroke-width':.7,opacity:.5}); + mbSpread=svgEl(s,'line',{x1:bx,y1:by,x2:bx,y2:by-22,stroke:'#1a1613','stroke-width':1.4,'stroke-linecap':'round'}); + svgEl(s,'circle',{cx:bx,cy:by,r:6,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.6}); + svgEl(s,'text',{x:bx,y:96,'text-anchor':'middle','font-size':5,'letter-spacing':'.12em', + 'font-family':'var(--mono)',fill:INK,opacity:.8}).textContent='BANDSPREAD'; + const dragHit=svgEl(s,'rect',{x:9,y:9,width:100,height:92,fill:'transparent'}); + dragHit.style.cursor='ns-resize'; + dragDelta(dragHit,()=>mbandV,v=>setMband(v,mbandB),{min:0,max:100}); + const clickHit=svgEl(s,'rect',{x:112,y:9,width:69,height:92,fill:'transparent'}); + clickHit.style.cursor='pointer'; + clickHit.addEventListener('click',()=>setMband(mbandV,(mbandB+1)%4));})(); +function setMband(v,b){mbandV=Math.max(0,Math.min(100,v));mbandB=b; + mbNeedle.setAttribute('transform',`rotate(${-70+mbandV/100*140},62,62)`); + mbSpread.setAttribute('transform',`rotate(${-45+b*30},142,56)`); + mbRings.forEach((g,i)=>g.setAttribute('opacity',i===b?'1':'.4')); + const [lo,hi]=MBAND_RANGES[b];const mc=lo+mbandV/100*(hi-lo); + setRd('R15',`B${b+1} · ${mc.toFixed(2)} Mc`);} +setMband(45,2); + /* ============ LIVE SIGNAL LOOPS ============ */ buildBars($('vuL'),16);buildBars($('vuR'),16);$('mini').innerHTML=''; (function(){const eq=$('eq');for(let b=0;b<11;b++){const band=document.createElement('span');band.className='band'; -- cgit v1.2.3