From d766c6b478dc489ab9e075c61f42cc5dc0b2351b Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Jul 2026 00:11:19 -0500 Subject: feat(gallery): add R05-R07 filter bank, chicken-head selector, round meter Three more reference-batch widgets as inline SVG, from the Berna-style faceplate set: - R05 filter slider bank: eight band faders with teal double-arrow caps on black tracks, frequency labels up top, a dB rail at the side. Per-band drag; the readout names the band and its attenuation. - R06 chicken-head selector: the tapered lever aims at the engraved position (OFF/LO/MID/HI); the active legend lights. Click steps through. - R07 round panel meter: porthole black bezel, cream dial, printed dB arc with red zone, drag-driven needle. R07 shares the VU-law dB interpolation with R01 through a new vuDb helper, so the nonlinear scale lives in one place. I verified all three in headless Chrome: band drags update cap and readout, the selector steps and relights, the meter needle tracks drags, no console exceptions. The gallery is at 60 cards. --- docs/prototypes/panel-widget-gallery.html | 103 +++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 3 deletions(-) (limited to 'docs/prototypes') diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 944657b..fac4762 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -697,6 +697,12 @@ card(C,'R03','Bat-handle toggle', card(C,'R04','Bakelite fluted knob', ``, 'the skirted console knob. Scalloped bakelite skirt, glossy dome, amber index over a printed 0-10 scale. Drag up/down to turn. After a vintage console mixer knob.'); +card(C,'R05','Filter slider bank', + ``, + 'a wall of band faders. One slider per band, arrow-head caps on black tracks, dB rail at the side. Drag any cap; readout names band and level. After a variable multi-band filter.'); +card(C,'R06','Chicken-head selector', + ``, + 'the pointer-lever switch. The tapered bakelite lever IS the indicator — it aims at the engraved position. Click to step through. After a modulator mode switch.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); @@ -760,6 +766,9 @@ card(M,'N18','Battery-cell gauge', card(M,'R01','Moving-coil VU meter', ``, 'the classic cream-faced VU. Printed dB arc, red zone past 0, ballistic needle. Runs the live signal; readout shows VU. After a vintage console meter pair.'); +card(M,'R07','Round panel meter', + ``, + 'the porthole lab meter. Cream dial in a round black bezel, printed dB arc, black needle. Drag up/down to drive it; readout shows dB. After a modulator front meter.'); /* ============ INDICATORS & READOUTS ============ */ const I=$('indicators'); @@ -1081,9 +1090,7 @@ const mcvu={t:.35,needle:null}; mcvu.sweep=sweep;})(); function setMcVu(t){t=Math.max(0,Math.min(1.02,t));mcvu.t=t; mcvu.needle.setAttribute('transform',`rotate(${mcvu.sweep(Math.min(1,t))},75,112)`); - let i=0;while(i0?'+':'')+db.toFixed(1)+' VU');} function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12); setMcVu(mcvu.t+(target-mcvu.t)*(target>mcvu.t?0.32:0.10));} @@ -1174,6 +1181,96 @@ function setBake(v){bakeV=v;bakeIdx.setAttribute('transform',`rotate(${v*2.7-135 setRd('R04',(v/10).toFixed(1)+' / 10');} dragDelta($('bakek'),()=>bakeV,setBake,{min:0,max:100,sens:0.25});setBake(63); +/* shared: interpolate a dB reading from a 0..1 sweep position (VU law) */ +function vuDb(t){let i=0;while(iy0+v/60*(y1-y0); + for(const [lbl,y] of [['0',y0],['20',y0+(y1-y0)/3],['40',y0+2*(y1-y0)/3],['60',y1]]){ + svgEl(s,'text',{x:14,y:y+2,'text-anchor':'end','font-size':6,'font-family':'var(--mono)', + fill:'var(--steel)'}).textContent=lbl;} + svgEl(s,'text',{x:14,y:94,'text-anchor':'end','font-size':5.5,'font-family':'var(--mono)', + fill:'var(--dim)'}).textContent='dB'; + FB_FREQ.forEach((f,i)=>{const x=x0+i*dx; + svgEl(s,'text',{x,y:8,'text-anchor':'middle','font-size':5.2,'font-family':'var(--mono)', + fill:'var(--steel)'}).textContent=f<1000?f:(f/1000)+'k'; + svgEl(s,'line',{x1:x,y1:y0,x2:x,y2:y1,stroke:'#0a0908','stroke-width':3.4,'stroke-linecap':'round'}); + svgEl(s,'line',{x1:x-.7,y1:y0,x2:x-.7,y2:y1,stroke:'rgba(255,255,255,.05)','stroke-width':.7}); + const cap=svgEl(s,'g',{}); + svgEl(cap,'polygon',{points:`${x-8.5},${-4.5} ${x-2.5},0 ${x-8.5},${4.5}`,fill:'var(--vfd)',stroke:'#123028','stroke-width':.6}); + svgEl(cap,'polygon',{points:`${x+8.5},${-4.5} ${x+2.5},0 ${x+8.5},${4.5}`,fill:'var(--vfd)',stroke:'#123028','stroke-width':.6}); + svgEl(cap,'circle',{cx:x,cy:0,r:1.5,fill:'#123028'}); + fbank.caps.push(cap); + const hit=svgEl(s,'rect',{x:x-9.5,y:y0-6,width:19,height:y1-y0+12,fill:'transparent'}); + hit.style.cursor='ns-resize'; + dragY(hit,pct=>setFbank(i,(100-pct)/100*60));}); + })(); +function setFbank(i,db){db=Math.max(0,Math.min(60,db));fbank.vals[i]=db; + fbank.caps[i].setAttribute('transform',`translate(0,${14+db/60*70})`); + setRd('R05',`${FB_FREQ[i]<1000?FB_FREQ[i]:(FB_FREQ[i]/1000)+'k'} Hz · −${Math.round(db)} dB`);} +fbank.vals.forEach((v,i)=>setFbank(i,v));setRd('R05','drag a band'); + +/* R06 chicken-head selector: the tapered lever aims at the position */ +const CHICK_POS=[['OFF',-60],['LO',-20],['MID',20],['HI',60]]; +let chickI=2,chickLever=null,chickLbls=[]; +(function(){const s=$('chick'),cx=55,cy=58; + const defs=svgEl(s,'defs',{}); + const bg=svgEl(defs,'radialGradient',{id:'chickG',cx:'42%',cy:'34%',r:'80%'}); + svgEl(bg,'stop',{offset:'0','stop-color':'#35312b'}); + svgEl(bg,'stop',{offset:'1','stop-color':'#070606'}); + CHICK_POS.forEach(([lbl,a],i)=>{const [tx,ty]=polar(cx,cy,42,a); + const t=svgEl(s,'text',{x:tx,y:ty+2.5,'text-anchor':'middle','font-size':6.5,'letter-spacing':'.06em', + 'font-family':'var(--mono)',fill:'var(--dim)'});t.textContent=lbl;chickLbls.push(t); + const [mx1,my1]=polar(cx,cy,33,a),[mx2,my2]=polar(cx,cy,29,a); + svgEl(s,'line',{x1:mx1,y1:my1,x2:mx2,y2:my2,stroke:'var(--steel)','stroke-width':1.2});}); + svgEl(s,'circle',{cx,cy,r:12,fill:'#0c0b0a'}); + chickLever=svgEl(s,'g',{}); + svgEl(chickLever,'path',{d:`M ${cx} ${cy-29} L ${cx+8.5} ${cy-4} Q ${cx+9} ${cy+8} ${cx+5} ${cy+11} A 7 7 0 0 1 ${cx-5} ${cy+11} Q ${cx-9} ${cy+8} ${cx-8.5} ${cy-4} Z`, + fill:'url(#chickG)',stroke:'#000','stroke-width':.7}); + svgEl(chickLever,'line',{x1:cx,y1:cy-26,x2:cx,y2:cy+6,stroke:'#4a463e','stroke-width':1.4,'stroke-linecap':'round'}); + chickLever.style.transformOrigin=`${cx}px ${cy}px`;chickLever.style.transition='transform .12s'; + $('chick').onclick=()=>setChick((chickI+1)%CHICK_POS.length);})(); +function setChick(i){chickI=i;chickLever.style.transform=`rotate(${CHICK_POS[i][1]}deg)`; + chickLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)')); + setRd('R06',CHICK_POS[i][0]);} +setChick(2); + +/* R07 round panel meter: porthole bezel, same VU law as R01 */ +let rndT=.5,rndNeedle=null; +(function(){const s=$('rndm'),cx=55,fy=54,py=74,sweep=t=>-40+t*80; + const defs=svgEl(s,'defs',{}); + const clip=svgEl(defs,'clipPath',{id:'rndFace'}); + svgEl(clip,'circle',{cx,cy:fy,r:40}); + svgEl(s,'circle',{cx,cy:fy,r:48,fill:'#141110',stroke:'#060505','stroke-width':2}); + svgEl(s,'circle',{cx,cy:fy,r:40,fill:'var(--cream)'}); + const g=svgEl(s,'g',{'clip-path':'url(#rndFace)'}); + const pt=(r,t)=>polar(cx,py,r,sweep(t)); + const [ax1,ay1]=pt(44,0),[ax2,ay2]=pt(44,1); + svgEl(g,'path',{d:`M ${ax1} ${ay1} A 44 44 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'#3a3128','stroke-width':1.1}); + const [rzx,rzy]=pt(44,.75); + svgEl(g,'path',{d:`M ${rzx} ${rzy} A 44 44 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'var(--fail)','stroke-width':3}); + for(const [db,t] of VUDB){const [x1,y1]=pt(41.5,t),[x2,y2]=pt(47,t); + svgEl(g,'line',{x1,y1,x2,y2,stroke:db>0?'var(--fail)':'#3a3128','stroke-width':1});} + for(const [db,t] of VUDB){if(![-20,-10,-5,-3,0,3].includes(db))continue; + const [x,y]=pt(52,t); + svgEl(g,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)', + fill:db>0?'var(--fail)':'#3a3128'}).textContent=Math.abs(db);} + svgEl(g,'text',{x:cx,y:fy+14,'text-anchor':'middle','font-size':8,'font-weight':700, + 'font-family':'var(--mono)',fill:'#3a3128'}).textContent='dB'; + rndNeedle=svgEl(g,'line',{x1:cx,y1:py,x2:cx,y2:py-46,stroke:'#1a1613','stroke-width':1.5, + transform:`rotate(0,${cx},${py})`}); + svgEl(g,'circle',{cx,cy:py,r:5,fill:'#141110'}); + svgEl(s,'ellipse',{cx:cx-8,cy:fy-22,rx:20,ry:9,fill:'rgba(255,255,255,.07)',transform:`rotate(-18,${cx-8},${fy-22})`});})(); +function setRnd(v){const t=Math.max(0,Math.min(1,v/100));rndT=t; + rndNeedle.setAttribute('transform',`rotate(${-40+t*80},55,74)`); + const db=vuDb(t);setRd('R07',(db>0?'+':'')+db.toFixed(1)+' dB');} +dragDelta($('rndm'),()=>rndT*100,setRnd,{min:0,max:100});setRnd(50); + /* ============ 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