From f210e7fce4df8daf69d87f7c8c7d83d4f13673c4 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Jul 2026 22:09:26 -0500 Subject: 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. --- docs/prototypes/panel-widget-gallery.html | 75 ++++--------- 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)}), 'audio waveform / scope. A richer signal view. Live trace; readout shows amplitude. Needs a drawing surface.'); card(M,'N11','Oscilloscope', - ``, + (st,rd)=>GW.scope(st,{onChange:(v,t)=>rd(t)}), 'a live phosphor trace. A waveform swept over a graticule. Live; readout shows Vpp. Needs a Cairo/GTK area for a smooth trace.'); card(M,'N12','Spectrum / EQ bars', - ``, + (st,rd)=>GW.eqBars(st,{onChange:(v,t)=>rd(t)}), 'level across bands. A live multi-band bar graph — spectrum, per-core CPU. Live; readout shows the peak band.'); card(M,'N13','Crossed-needle meter', - `
-
FWDRFL
`, + (st,rd)=>GW.crossNeedle(st,{value:55,onChange:(v,t)=>rd(t)}), 'two readings, one face. Forward + reflected needles cross. Drag up/down to drive power; readout shows both. Round build wants a drawing surface.'); card(M,'N14','Thermometer column', - `
906030
-
`, + (st,rd)=>GW.thermometer(st,{value:58,onChange:(v,t)=>rd(t)}), 'a rising column. The classic mercury tube — temperature, a climbing load. Drag up/down; readout shows the value.'); card(M,'N15','Bourdon pressure gauge', - `
- - -
PSI
`, + (st,rd)=>GW.bourdon(st,{value:45,onChange:(v,t)=>rd(t)}), 'a round dial with a red zone. Needle over a printed arc, redline in terracotta. Drag to set PSI; readout tracks. Round sweep is a drawing-area job.'); card(M,'N16','Strip-chart recorder', - ``, + (st,rd)=>GW.stripChart(st,{onChange:(v,t)=>rd(t)}), 'a scrolling pen trace. History scrolls left, the pen writes at the right edge. Live; readout shows the current value. Real trace wants a drawing surface.'); card(M,'N17','Correlation meter (±)', - `
-
−10+1
`, + (st,rd)=>GW.corrMeter(st,{value:58,onChange:(v,t)=>rd(t)}), 'deviation from center. The needle rests at 0 and swings ±. Drag left/right; readout shows the signed value.'); card(M,'N18','Battery-cell gauge', - `
`, + (st,rd)=>GW.battCells(st,{value:62,onChange:(v,t)=>rd(t)}), 'charge as discrete cells. Filled cells count up, the last warn in terracotta. Drag left/right to set; readout shows %.'); card(M,'R01','Moving-coil VU meter', ``, @@ -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({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=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 = ''; + 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 = `
+
FWDRFL
`; + 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 = `
906030
+
`; + 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 = ` + +
PSI
`; + 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 = ''; + 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 = `
+
−10+1
`; + 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 = '
'; + 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() { -- cgit v1.2.3