diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 22:09:26 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 22:09:26 -0500 |
| commit | f210e7fce4df8daf69d87f7c8c7d83d4f13673c4 (patch) | |
| tree | 9f6c84b05a26991cdd265266d173cdfb34ad8e90 | |
| parent | cace2002494222dcbecf6706ebb29c8d43228edf (diff) | |
| download | archsetup-f210e7fce4df8daf69d87f7c8c7d83d4f13673c4.tar.gz archsetup-f210e7fce4df8daf69d87f7c8c7d83d4f13673c4.zip | |
refactor(gallery): extract meters N11-N18 into GW builders
Scope, EQ bars, and strip chart take value-driven handles (set samples / push); the eqBands oscillator and the demo signal stay page-side. The N11 screen-family chips now reach the scope through the card handle instead of the retired scopep id.
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 75 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 172 |
2 files changed, 193 insertions, 54 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index beb735b..7862c5e 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -1739,34 +1739,28 @@ card(M,'17','Waveform strip', (st,rd)=>GW.waveStrip(st,{onChange:(v,t)=>rd(t)}), '<b>audio waveform / scope.</b> A richer signal view. Live trace; readout shows amplitude. <b>Needs a drawing surface.</b>'); card(M,'N11','Oscilloscope', - `<span class="scope"><span class="grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline id="scopep"/></svg></span>`, + (st,rd)=>GW.scope(st,{onChange:(v,t)=>rd(t)}), '<b>a live phosphor trace.</b> A waveform swept over a graticule. Live; readout shows Vpp. <b>Needs a Cairo/GTK area for a smooth trace.</b>'); card(M,'N12','Spectrum / EQ bars', - `<span class="eq" id="eq"></span>`, + (st,rd)=>GW.eqBars(st,{onChange:(v,t)=>rd(t)}), '<b>level across bands.</b> A live multi-band bar graph — spectrum, per-core CPU. Live; readout shows the peak band.'); card(M,'N13','Crossed-needle meter', - `<div class="crossm"><div class="face" id="crossm13"><div class="arc"></div><div class="nA" id="cnA"></div><div class="nB" id="cnB"></div></div> - <div class="lbl"><span>FWD</span><span>RFL</span></div></div>`, + (st,rd)=>GW.crossNeedle(st,{value:55,onChange:(v,t)=>rd(t)}), '<b>two readings, one face.</b> Forward + reflected needles cross. Drag up/down to drive power; readout shows both. <b>Round build wants a drawing surface.</b>'); card(M,'N14','Thermometer column', - `<div class="thermo" id="thermo14"><div class="scale"><span>90</span><span>60</span><span>30</span></div> - <div class="wrapcol"><div class="tube"><div class="fill" id="thermf" style="height:58%"></div></div><div class="bulb"></div></div></div>`, + (st,rd)=>GW.thermometer(st,{value:58,onChange:(v,t)=>rd(t)}), '<b>a rising column.</b> The classic mercury tube — temperature, a climbing load. Drag up/down; readout shows the value.'); card(M,'N15','Bourdon pressure gauge', - `<div class="bourdon" id="bourdon15"> - <svg viewBox="0 0 82 82"><path d="M14 62 A34 34 0 0 1 68 62" fill="none" stroke="#2c2f32" stroke-width="3"/> - <path d="M52 22 A34 34 0 0 1 68 62" fill="none" stroke="#cb6b4d" stroke-width="3"/></svg> - <div class="ndl" id="bourd" style="transform:rotate(30deg)"></div><div class="hub"></div><div class="cap">PSI</div></div>`, + (st,rd)=>GW.bourdon(st,{value:45,onChange:(v,t)=>rd(t)}), '<b>a round dial with a red zone.</b> Needle over a printed arc, redline in terracotta. Drag to set PSI; readout tracks. <b>Round sweep is a drawing-area job.</b>'); card(M,'N16','Strip-chart recorder', - `<span class="strip"><span class="rule"></span><svg viewBox="0 0 176 62" preserveAspectRatio="none"><polyline id="stripp"/></svg><span class="pen" id="strippen" style="top:31px"></span></span>`, + (st,rd)=>GW.stripChart(st,{onChange:(v,t)=>rd(t)}), '<b>a scrolling pen trace.</b> History scrolls left, the pen writes at the right edge. Live; readout shows the current value. <b>Real trace wants a drawing surface.</b>'); card(M,'N17','Correlation meter (±)', - `<div class="corr" id="corr17"><div class="face"><div class="arc"></div><div class="zero"></div><div class="ndl" id="corrn" style="transform:rotate(8deg)"></div></div> - <div class="lbl"><span>−1</span><span>0</span><span>+1</span></div></div>`, + (st,rd)=>GW.corrMeter(st,{value:58,onChange:(v,t)=>rd(t)}), '<b>deviation from center.</b> The needle rests at 0 and swings ±. Drag left/right; readout shows the signed value.'); card(M,'N18','Battery-cell gauge', - `<div class="batt" id="batt18"><div class="cells" id="battc"></div><span class="nub"></span></div>`, + (st,rd)=>GW.battCells(st,{value:62,onChange:(v,t)=>rd(t)}), '<b>charge as discrete cells.</b> Filled cells count up, the last warn in terracotta. Drag left/right to set; readout shows %.'); card(M,'R01','Moving-coil VU meter', `<svg id="mcvu" class="rsvg" viewBox="0 0 150 96" width="150" height="96"></svg>`, @@ -1904,28 +1898,7 @@ card(I,'R52','Blinkenlights front panel', /* 01-09, 24, 25, N01-N10 extracted to widgets.js (GW.*) — wired at their card records */ /* ============ METER WIRING (drag-driven) ============ */ -/* 10-17 extracted to widgets.js (GW.*) — wired at their card records */ -/* N13 crossed-needle */ -let cnPow=55; -function setCross(v){cnPow=v;const fwd=v,rfl=v*0.68;$('cnA').style.transform=`rotate(${-42+fwd/100*84}deg)`; - $('cnB').style.transform=`rotate(${42-rfl/100*84}deg)`;setRd('N13','FWD '+Math.round(fwd)+' · RFL '+Math.round(rfl));} -dragDelta($('crossm13'),()=>cnPow,setCross,{min:0,max:100});setCross(55); -/* N14 thermometer */ -let thV=58; -function setThermo(v){thV=v;$('thermf').style.height=v+'%';setRd('N14',Math.round(30+v/100*60)+'°');} -dragDelta($('thermo14'),()=>thV,setThermo,{min:0,max:100});setThermo(58); -/* N15 bourdon */ -let bV=45; -function setBour(v){bV=v;$('bourd').style.transform=`rotate(${-60+v/100*120}deg)`;setRd('N15',Math.round(v/100*160)+' PSI');} -dragDelta($('bourdon15'),()=>bV,setBour,{min:0,max:100});setBour(45); -/* N17 correlation */ -function setCorr(p){const v=(p-50)/50;$('corrn').style.transform=`rotate(${v*38}deg)`;setRd('N17',(v>=0?'+':'')+v.toFixed(2));} -dragX($('corr17'),setCorr);setCorr(58); -/* N18 battery */ -(function(){const bc=$('battc');for(let i=0;i<8;i++){const c=document.createElement('span');c.className='cell';bc.appendChild(c);}})(); -function setBatt(p){const cells=$('battc').children,lit=Math.round(p/100*cells.length); - for(let i=0;i<cells.length;i++)cells[i].className='cell'+(i<lit?' on':'')+((p<=25&&i<lit)?' warn':'');setRd('N18',Math.round(p)+'%');} -dragX($('batt18'),setBatt);setBatt(62); +/* 10-17, N11-N18 extracted to widgets.js (GW.*) — wired at their card records */ /* ============ INDICATOR WIRING ============ */ /* 18 status lamp */ @@ -2438,9 +2411,8 @@ setRgraph(1200,6.5); /* ============ LIVE SIGNAL LOOPS ============ */ /* extracted meters: the page keeps the clock + demo signal, drives card handles */ -const MH={vu:$('card-11').gw,mini:$('card-12').gw,spark:$('card-16').gw,wave:$('card-17').gw}; -(function(){const eq=$('eq');for(let b=0;b<11;b++){const band=document.createElement('span');band.className='band'; - for(let s=0;s<9;s++)band.appendChild(document.createElement('i'));eq.appendChild(band);}})(); +const MH={vu:$('card-11').gw,mini:$('card-12').gw,spark:$('card-16').gw,wave:$('card-17').gw, + scope:$('card-N11').gw,eq:$('card-N12').gw,strip:$('card-N16').gw}; (function(){const cl=$('clock');for(let i=0;i<12;i++){const t=document.createElement('span');t.className='tk'; t.style.transform=`rotate(${i*30}deg)`;cl.insertBefore(t,cl.firstChild);}})(); const counterDigits=[4,7,1,3,0,0]; @@ -2452,10 +2424,6 @@ function setWheel(w,d){w.querySelector('.col').style.top=(-(d+1)*34)+'px';} let ph=0; const eqBands=Array.from({length:11},(_,i)=>({v:0.4,ph:i*0.6})); -const stripHist=Array.from({length:60},()=>31); -function paintEq(){const bands=$('eq').children;let peak=0; - for(let b=0;b<bands.length;b++){const cells=bands[b].children,n=cells.length,val=eqBands[b].v;const lit=Math.round(val*n);peak=Math.max(peak,val); - for(let k=0;k<n;k++){let c='';if(k<lit)c=(k>=n-1?'clip':k>=n-3?'hot':'on');cells[k].className=c?c:'';}}return peak;} function lvl(){return Math.max(0,Math.min(1,0.5+0.4*Math.sin(ph*1.3)+(Math.random()<0.15?Math.random()*0.4:0)-Math.random()*0.08));} function fastTick(){ ph+=0.09; @@ -2465,14 +2433,12 @@ function fastTick(){ const env=Math.min(1,0.6+0.3*Math.sin(ph*0.4)); const smp=[];for(let x=0;x<=170;x+=3)smp.push(Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)); MH.wave.set(smp,env); - let sp='';for(let x=0;x<=176;x+=3){const y=39+Math.sin(x*0.16+ph*3.2)*Math.min(1,0.6+0.3*Math.sin(ph*0.4))*22;sp+=`${x},${y.toFixed(1)} `;} - $('scopep').setAttribute('points',sp.trim());setRd('N11','Vpp '+Math.round((0.6+0.3*Math.sin(ph*0.4))*100)); + const senv=Math.min(1,0.6+0.3*Math.sin(ph*0.4)); + const ssmp=[];for(let x=0;x<=176;x+=3)ssmp.push(Math.sin(x*0.16+ph*3.2)*senv); + MH.scope.set(ssmp,0.6+0.3*Math.sin(ph*0.4)); eqBands.forEach(bd=>{bd.ph+=0.16;bd.v=Math.max(0.05,Math.min(1,0.5+0.42*Math.sin(bd.ph)+(Math.random()<0.2?Math.random()*0.3:0)-Math.random()*0.06));}); - const peak=paintEq();setRd('N12','peak '+Math.round(peak*100)+'%'); - stripHist.push(31-24*Math.sin(ph*0.7)*(0.6+0.4*Math.sin(ph*0.13))+(Math.random()-0.5)*4);stripHist.shift(); - const last=Math.max(3,Math.min(59,stripHist[stripHist.length-1])); - $('stripp').setAttribute('points',stripHist.map((v,i)=>`${i/(stripHist.length-1)*176},${Math.max(3,Math.min(59,v)).toFixed(1)}`).join(' ')); - $('strippen').style.top=last+'px';setRd('N16',Math.round((59-last)/56*100)); + MH.eq.set(eqBands.map(bd=>bd.v)); + MH.strip.push((28+24*Math.sin(ph*0.7)*(0.6+0.4*Math.sin(ph*0.13))-(Math.random()-0.5)*4)/56); } /* clock */ function tickClock(){const d=new Date();const s=d.getSeconds(),m=d.getMinutes(),h=d.getHours()%12; @@ -2518,9 +2484,10 @@ function paintStatic(){ MH.vu.set(0.55,0.5);MH.mini.set(0.55); MH.spark.fill(0.5); MH.wave.set([],0.6); - eqBands.forEach((bd,i)=>bd.v=0.3+0.5*Math.abs(Math.sin(i*0.9)));setRd('N12','peak '+Math.round(paintEq()*100)+'%'); - $('scopep').setAttribute('points',Array.from({length:60},(_,i)=>`${i*3},${39+Math.sin(i*0.5)*18}`).join(' '));setRd('N11','Vpp 74'); - $('stripp').setAttribute('points',stripHist.map((v,i)=>`${i/(stripHist.length-1)*176},${31-Math.sin(i*0.3)*16}`).join(' '));setRd('N16','50'); + eqBands.forEach((bd,i)=>bd.v=0.3+0.5*Math.abs(Math.sin(i*0.9))); + MH.eq.set(eqBands.map(bd=>bd.v)); + MH.scope.set(Array.from({length:60},(_,i)=>Math.sin(i*0.5)*18/22),0.74); + MH.strip.set(Array.from({length:60},(_,i)=>0.5+Math.sin(i*0.3)*16/56),0.5); tickClock();renderSeven();setRd('N20','DNS');setRd('N22','scrolling');setRd('N25',String(ctrVal).padStart(6,'0')); setMcVu(.5); } @@ -2954,7 +2921,7 @@ screenChips('R10',$('dmx'),['amber','green','red','blue','vfd'],'amber'); screenChips('R17',$('rcrt'),['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('N11',$('scopep').closest('.scope'),['green','amber','red','blue','vfd'],'green'); +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 266cdac..d7b66f3 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -1975,6 +1975,178 @@ GW.waveStrip = function (host, opts = {}) { return { el, get: () => amp, set }; }; +/* N11 oscilloscope — sampled phosphor trace; set(samples, vpp) with samples in -1..1 */ +GW.scope = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const el = document.createElement('span'); el.className = 'scope'; + el.innerHTML = '<span class="grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline/></svg>'; + host.appendChild(el); + const line = el.querySelector('polyline'); + let vpp = 0; + function set(samples, v) { + vpp = v; + const n = samples.length; + line.setAttribute('points', samples.map((s, i) => `${(i / (n - 1) * 176).toFixed(1)},${(39 + s * 22).toFixed(1)}`).join(' ')); + onChange(vpp, 'Vpp ' + Math.round(vpp * 100)); + } + return { el, get: () => vpp, set }; +}; + +/* N12 spectrum / EQ bars — set(values) paints one column per band */ +GW.eqBars = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const bands = opts.bands || 11, cells = opts.cells || 9; + const el = document.createElement('span'); el.className = 'eq'; + host.appendChild(el); + for (let b = 0; b < bands; b++) { + const band = document.createElement('span'); band.className = 'band'; + for (let s = 0; s < cells; s++) band.appendChild(document.createElement('i')); + el.appendChild(band); + } + let vals = []; + function set(values) { + vals = values; + let peak = 0; + for (let b = 0; b < bands; b++) { + const col = el.children[b].children, val = values[b], lit = Math.round(val * cells); + peak = Math.max(peak, val); + for (let k = 0; k < cells; k++) { + let c = ''; + if (k < lit) c = (k >= cells - 1 ? 'clip' : k >= cells - 3 ? 'hot' : 'on'); + col[k].className = c; + } + } + onChange(vals, 'peak ' + Math.round(peak * 100) + '%'); + } + return { el, get: () => vals, set }; +}; + +/* N13 crossed-needle meter — one drive value, FWD and RFL needles cross */ +GW.crossNeedle = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let v = opts.value ?? 55; + const el = document.createElement('div'); el.className = 'crossm'; + el.innerHTML = `<div class="face"><div class="arc"></div><div class="nA"></div><div class="nB"></div></div> + <div class="lbl"><span>FWD</span><span>RFL</span></div>`; + host.appendChild(el); + const face = el.querySelector('.face'), nA = el.querySelector('.nA'), nB = el.querySelector('.nB'); + function set(nv) { + v = nv; + const fwd = v, rfl = v * 0.68; + nA.style.transform = `rotate(${-42 + fwd / 100 * 84}deg)`; + nB.style.transform = `rotate(${42 - rfl / 100 * 84}deg)`; + onChange(v, 'FWD ' + Math.round(fwd) + ' · RFL ' + Math.round(rfl)); + } + dragDelta(face, () => v, set, { min: 0, max: 100 }); + set(v); + return { el, get: () => v, set }; +}; + +/* N14 thermometer column — mercury rises with the value */ +GW.thermometer = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let v = opts.value ?? 58; + const el = document.createElement('div'); el.className = 'thermo'; + el.innerHTML = `<div class="scale"><span>90</span><span>60</span><span>30</span></div> + <div class="wrapcol"><div class="tube"><div class="fill"></div></div><div class="bulb"></div></div>`; + host.appendChild(el); + const fill = el.querySelector('.fill'); + function set(nv) { + v = nv; + fill.style.height = v + '%'; + onChange(v, Math.round(30 + v / 100 * 60) + '°'); + } + dragDelta(el, () => v, set, { min: 0, max: 100 }); + set(v); + return { el, get: () => v, set }; +}; + +/* N15 bourdon pressure gauge — needle over a printed arc with a redline */ +GW.bourdon = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let v = opts.value ?? 45; + const el = document.createElement('div'); el.className = 'bourdon'; + el.innerHTML = `<svg viewBox="0 0 82 82"><path d="M14 62 A34 34 0 0 1 68 62" fill="none" stroke="#2c2f32" stroke-width="3"/> + <path d="M52 22 A34 34 0 0 1 68 62" fill="none" stroke="#cb6b4d" stroke-width="3"/></svg> + <div class="ndl"></div><div class="hub"></div><div class="cap">PSI</div>`; + host.appendChild(el); + const ndl = el.querySelector('.ndl'); + function set(nv) { + v = nv; + ndl.style.transform = `rotate(${-60 + v / 100 * 120}deg)`; + onChange(v, Math.round(v / 100 * 160) + ' PSI'); + } + dragDelta(el, () => v, set, { min: 0, max: 100 }); + set(v); + return { el, get: () => v, set }; +}; + +/* N16 strip-chart recorder — scrolling history, pen rides the newest value; + push(v) appends, set(samples, current) replaces the trace (values 0..1) */ +GW.stripChart = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const n = opts.samples || 60; + const hist = Array.from({ length: n }, () => opts.value ?? 0.5); + const el = document.createElement('span'); el.className = 'strip'; + el.innerHTML = '<span class="rule"></span><svg viewBox="0 0 176 62" preserveAspectRatio="none"><polyline/></svg><span class="pen" style="top:31px"></span>'; + host.appendChild(el); + const line = el.querySelector('polyline'), pen = el.querySelector('.pen'); + const clamp = x => Math.max(0, Math.min(1, x)); + const yPx = v => 59 - clamp(v) * 56; + function paint(current) { + line.setAttribute('points', hist.map((v, i) => `${i / (n - 1) * 176},${yPx(v).toFixed(1)}`).join(' ')); + pen.style.top = yPx(current) + 'px'; + onChange(current, String(Math.round(clamp(current) * 100))); + } + function push(v) { hist.push(v); hist.shift(); paint(v); } + function set(samples, current) { + for (let i = 0; i < n; i++) hist[i] = samples[i] ?? 0.5; + paint(current ?? hist[n - 1]); + } + return { el, get: () => hist[n - 1], push, set }; +}; + +/* N17 correlation meter — needle rests at 0 and swings ±; drag left/right */ +GW.corrMeter = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let p = opts.value ?? 58; + const el = document.createElement('div'); el.className = 'corr'; + el.innerHTML = `<div class="face"><div class="arc"></div><div class="zero"></div><div class="ndl"></div></div> + <div class="lbl"><span>−1</span><span>0</span><span>+1</span></div>`; + host.appendChild(el); + const ndl = el.querySelector('.ndl'); + function set(np) { + p = np; + const v = (p - 50) / 50; + ndl.style.transform = `rotate(${v * 38}deg)`; + onChange(v, (v >= 0 ? '+' : '') + v.toFixed(2)); + } + dragX(el, set); + set(p); + return { el, get: () => p, set }; +}; + +/* N18 battery-cell gauge — charge as discrete cells, the low end warns */ +GW.battCells = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const cells = opts.cells || 8, warnAt = opts.warnAt ?? 25; + let p = opts.value ?? 62; + const el = document.createElement('div'); el.className = 'batt'; + el.innerHTML = '<div class="cells"></div><span class="nub"></span>'; + host.appendChild(el); + const cc = el.querySelector('.cells'); + for (let i = 0; i < cells; i++) { const c = document.createElement('span'); c.className = 'cell'; cc.appendChild(c); } + function set(np) { + p = np; + const lit = Math.round(p / 100 * cells); + for (let i = 0; i < cells; i++) cc.children[i].className = 'cell' + (i < lit ? ' on' : '') + ((p <= warnAt && i < lit) ? ' warn' : ''); + onChange(p, Math.round(p) + '%'); + } + dragX(el, set); + set(p); + return { el, get: () => p, set }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
