From 5fdded5daccc5df5d56af080cbe79b294aac436f Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 11 Jul 2026 13:40:29 -0500 Subject: feat(gallery): make every widget interactive and add instrument idioms Turn the panel widget gallery into a driveable kit. Drag the faders, rotary and concentric knobs, encoder, crossfader, thumbwheel, jog wheel, and the gauge needles; click the switches, keys, DIP bank, keyed switch, transport, radio bank, patch-bay, and lamps. Every card shows a live readout that tracks the control. Adds a batch of new console idioms across controls, meters, and readouts (oscilloscope, EQ bars, strip-chart, split-flap, VFD marquee, annunciator, jewel lamps, tape counter, and more). --- .../2026-07-03-panel-widget-gallery-prototype.html | 954 ++++++++++++++++++--- 1 file changed, 839 insertions(+), 115 deletions(-) diff --git a/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html b/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html index 19323bf..65973bd 100644 --- a/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html +++ b/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html @@ -3,7 +3,7 @@ -Panel widget gallery — dupre instrument console +Panel widget gallery — dupre instrument console (interactive)
-
archsetup · dupre panel family
-

Widget gallery — the instrument-console kit

-

Every control + display idiom we can build in the dupre faceplate language, all rendering from - the same tokens the net / bt / sound panels use. Controls take input; meters & - gauges show a live analog value; indicators & readouts show state or a number. - Live ones animate. Pick what fits each job — most cost pure CSS; the two that need a real - drawing surface (needle gauge, waveform) are flagged in their notes.

+
archsetup · dupre panel family · live kit
+

Widget gallery — the instrument-console kit, fully driveable

+

Every control + display idiom in the dupre faceplate language, merged into one live console — the base kit + plus the candidate batch, ~55 widgets rendering from the same tokens the net / bt / sound panels use. + Controls take input; meters & gauges show a live analog value; indicators & + readouts show state or a number. Every widget is operable: drag the faders, knobs and gauges; click the + switches, keys and lamps. Each card carries a live readout that tracks what you do. The trace displays run + a live signal; the pointer types drive from your drag.

Controls — take input

@@ -251,152 +521,606 @@ const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; function card(host, no, name, html, note){ const c=document.createElement('div'); c.className='card'; c.innerHTML=`
${no}${name}
`+ - `
${html}
${note}
`; + `
${html}
${note}
`; host.appendChild(c); return c; } +function setRd(no,txt){const e=$('rd-'+no);if(e)e.textContent=txt;} function buildBars(el,n){el.innerHTML='';for(let k=0;k{const r=el.getBoundingClientRect();onPct(Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100)));}; + el.addEventListener('pointerdown',e=>{el.setPointerCapture(e.pointerId);at(e); + const mv=ev=>at(ev),up=()=>{el.removeEventListener('pointermove',mv);el.removeEventListener('pointerup',up);el.removeEventListener('pointercancel',up);}; + el.addEventListener('pointermove',mv);el.addEventListener('pointerup',up);el.addEventListener('pointercancel',up);e.preventDefault();}); +} +function dragY(el,onPct){ el.style.touchAction='none'; + const at=e=>{const r=el.getBoundingClientRect();onPct(Math.max(0,Math.min(100,(r.bottom-e.clientY)/r.height*100)));}; + el.addEventListener('pointerdown',e=>{el.setPointerCapture(e.pointerId);at(e); + const mv=ev=>at(ev),up=()=>{el.removeEventListener('pointermove',mv);el.removeEventListener('pointerup',up);el.removeEventListener('pointercancel',up);}; + el.addEventListener('pointermove',mv);el.addEventListener('pointerup',up);el.addEventListener('pointercancel',up);e.preventDefault();}); +} +function dragDelta(el,get,set,opts){ opts=opts||{}; el.style.touchAction='none'; + const min=opts.min==null?0:opts.min, max=opts.max==null?100:opts.max, sens=opts.sens||((max-min)/160); + el.addEventListener('pointerdown',e=>{if(opts.stop)e.stopPropagation();el.setPointerCapture(e.pointerId); + const y0=e.clientY, v0=get(); + const mv=ev=>{let v=v0+(y0-ev.clientY)*sens;set(Math.max(min,Math.min(max,v)));}; + const up=()=>{el.removeEventListener('pointermove',mv);el.removeEventListener('pointerup',up);el.removeEventListener('pointercancel',up);}; + el.addEventListener('pointermove',mv);el.addEventListener('pointerup',up);el.addEventListener('pointercancel',up);e.preventDefault();}); +} + +/* ---- seven-segment digit builder ---- */ +const SEG={'0':'abcdef','1':'bc','2':'abged','3':'abgcd','4':'fbgc','5':'afgcd','6':'afgedc','7':'abc','8':'abcdefg','9':'abcdfg','-':'g',' ':''}; +function hseg(cy){return `5,${cy} 7,${cy-2} 17,${cy-2} 19,${cy} 17,${cy+2} 7,${cy+2}`;} +function vseg(cx,ty,by){return `${cx},${ty} ${cx-2},${ty+2} ${cx-2},${by-2} ${cx},${by} ${cx+2},${by-2} ${cx+2},${ty+2}`;} +const SEGPTS={a:hseg(3),g:hseg(22),d:hseg(41),f:vseg(5,4,21),b:vseg(19,4,21),e:vseg(5,23,40),c:vseg(19,23,40)}; +function seg7(ch,cls){ + const lit=SEG[ch]||''; let s=``; + for(const k of 'abcdefg'){const on=lit.includes(k); + const col=on?(cls==='red'?'var(--sevred)':'var(--sevgrn)'):'var(--sevoff)'; + s+=``;} + return s+''; +} + /* ============ CONTROLS ============ */ const C=$('controls'); card(C,'01','Toggle switch', - ` - `, - 'on / off / muted. The faceplate master control — wifi radio, bt power, master-mute. Click to flip.'); + ``, + 'on / off. The faceplate master control — power, mute, enable. Click to flip; readout shows the state.'); card(C,'02','Console key', - ``, - 'physical push button. DOCTOR / SPEED TEST / mic mode. Gold = engaged, terracotta = off.'); + ``, + 'physical push button. Mutually exclusive — click one to engage. Gold = live, terracotta = muted.'); card(C,'03','Horizontal fader', `
`, - 'continuous 0-100. Per-device volume, brightness, kbd backlight. Drag; the gold cap tracks.'); + 'continuous 0-100. Per-device volume, brightness. Drag the gold cap; readout tracks.'); card(C,'04','Vertical fader', - `
-
`, - 'channel-strip style. A mixer column per device if you want the classic board look.'); + `
+
`, + 'channel-strip style. A mixer column per device. Drag each fader up or down.'); card(C,'05','Rotary knob', - ``, - 'dial in a value. Volume/gain the analog way. Click to turn; drag in the real build. Pairs with a scale arc.'); + ``, + 'dial in a value. Volume/gain the analog way. Drag up/down to turn; readout shows the level.'); card(C,'06','Segmented selector', - `
`, - 'pick one of a few. Timer type, layout mode, theme. One press-lit segment.'); + `
`, + 'pick one of a few. Timer type, layout mode. Click a segment; readout names the choice.'); card(C,'07','Chip toggle', - `discoverable on`, - 'inline binary. A soft toggle inside a line of text — discoverable, auto-dim, DND. Gold when on.'); + `discoverable on`, + 'inline binary. A soft toggle inside a line of text. Click to flip; gold when on.'); card(C,'08','Arm-to-fire', - ``, - 'two-stage confirm. Destructive/disruptive actions — forget network, disconnect. First click arms (red), second fires.'); + ``, + 'two-stage confirm. Destructive actions. First click arms (red), second fires. Readout shows the stage.'); card(C,'09','Lamp row', - `
WH-1000XM4tap to connect
`, - 'actionable list item. The net/bt/sound row: lamp + name + status, click acts. The workhorse.'); + `
WH-1000XM4tap to connect
`, + 'actionable list item. Lamp + name + status. Click to cycle idle → connecting → connected.'); card(C,'24','Rotary selector', - ` + ` 4681012 - + `, - 'pick one of N by position. The source/speaker selector — printed detents, the pointer names the value. Read from where it points, not from a lit cell. Ideal for board size or grade. Click to turn.'); + 'pick one of N by position. Printed detents, the pointer names the value. Click to turn; readout shows it.'); card(C,'25','Slide-rule dial', - ` + ` 4681012 `, - 'value on a printed scale. The illuminated tuner face — a lit pointer glides a warm-backlit strip of marks. Unmistakably hi-fi; great for board size. Click to step.'); + 'value on a printed scale. A lit pointer glides a warm-backlit strip. Click a mark to jump, or focus it and press ←/→ (↑/↓); readout shows it.'); +card(C,'N01','Rocker power switch', + `ONOFF`, + 'hard on / off, lit legend. A master power paddle — the pressed half glows. Click to rock.'); +card(C,'N02','Transport cluster', + `
+
+
+ + + + +
`, + 'run / pause / capture. One mode lit at a time, reels turn while it plays. Click a button.'); +card(C,'N03','Radio preset bank', + `
+ + + + +
`, + 'pick one, the rest release. The car-radio preset row — mechanically exclusive. Click one.'); +card(C,'N04','Concentric dual knob', + ` + + + `, + 'two values on one spindle. Bass / treble, coarse / fine. Drag the outer ring or inner cap — each is independent.'); +card(C,'N05','Rotary encoder + LED ring', + ``, + 'endless dial, lit position. A detentless encoder; the LED arc lights to the level and the value keeps accumulating. Drag to turn.'); +card(C,'N06','Keyed mode switch', + ` + OFFONRUN + + `, + 'guarded three-position mode. Off · on · run. The key-bit points at the live position. Click to turn.'); +card(C,'N07','Center-detented crossfader', + `
+
+ AB +
+
`, + 'throw to either side of zero. A fader notched at center — balance, L/R mix. Drag; readout shows the signed value.'); +card(C,'N08','Thumbwheel', + `
42
`, + 'ribbed edge-wheel, one value. A knurled wheel with a windowed number. Drag up/down to roll.'); +card(C,'N09','DIP-switch bank', + ` + + + + `, + 'a bank of hard flags. Hardware slide switches — up is on. Click each to flip; readout is the binary word.'); +card(C,'N10','Jog / shuttle wheel', + ``, + 'scrub fine, shuttle fast. The edit-deck wheel. Drag to jog the inner wheel; readout shows the position.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); card(M,'10','Needle gauge', - `
+ `
0%
`, - 'analog dial. Throughput, battery, volume level. Needs a Cairo/GTK drawing area — CSS can fake a fixed angle but not a smooth sweep in waybar.'); + 'analog dial. Throughput, battery, volume. Drag up/down to sweep the needle; readout tracks. Smooth sweep wants a Cairo/GTK area.'); card(M,'11','Stereo VU (LED bar)', `
L
R
`, - 'live signal level. The sound panel\'s second meter row. Peak-hold outline. Pure CSS — pango/box segments.'); + 'live signal level. The sound panel meter row, peak-hold outline. Runs a live signal; readout shows L/R.'); card(M,'12','Mini signal (4-bar)', ``, - 'compact activity. Per-row "is this device playing" indicator. Cheap enough to sit in every list row.'); + 'compact activity. Per-row "is this device playing". Live signal; readout shows activity %.'); card(M,'13','Signal ladder', - ``, - 'discrete strength. Wifi bars, bt RSSI — a stepped 0-4. Already in the net panel.'); + ``, + 'discrete strength. Wifi bars, bt RSSI — a stepped 0-4. Click to cycle strength; readout shows bars.'); card(M,'14','Linear fuel bar', - `
`, - 'a single 0-100. Battery, disk, download progress. Warn tint under threshold. Trivial in CSS.'); + `
`, + 'a single 0-100. Battery, disk, download. Drag either bar; warn tint under threshold; readout tracks.'); card(M,'15','Radial ring', - `68`, - 'percentage as a donut. CPU, battery, a single meter where a needle is overkill. conic-gradient, pure CSS.'); + `68`, + 'percentage as a donut. CPU, battery. Drag up/down to set; conic-gradient tracks the readout.'); card(M,'16','Sparkline', ``, - 'recent history. Throughput/CPU over the last minute. SVG here; a drawing area in GTK.'); + 'recent history. Throughput/CPU over the last minute. Live trace; readout shows the current value.'); card(M,'17','Waveform strip', ``, - 'audio waveform / scope. A richer signal view for the sound panel. Needs a drawing surface.'); + 'audio waveform / scope. A richer signal view. Live trace; readout shows amplitude. Needs a drawing surface.'); +card(M,'N11','Oscilloscope', + ``, + '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', + ``, + '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
`, + '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
+
`, + '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
`, + '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', + ``, + '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
`, + 'deviation from center. The needle rests at 0 and swings ±. Drag left/right; readout shows the signed value.'); +card(M,'N18','Battery-cell gauge', + `
`, + 'charge as discrete cells. Filled cells count up, the last warn in terracotta. Drag left/right to set; readout shows %.'); /* ============ INDICATORS & READOUTS ============ */ const I=$('indicators'); card(I,'18','Status lamp', - ``, - 'one-glance health. Green ok · gold engaged · red fail · dim off · pulsing busy. The family signature.'); + ``, + 'one-glance health. Green ok · gold engaged · red fail · dim off · pulsing busy. Click any lamp to cycle its state.'); card(I,'19','Badge / tag', - `TUNNEL LOW BATT 2.4G`, - 'a labelled flag. On the faceplate or a row — MUTED, AIRPLANE, DEF, a band tag.'); + `TUNNEL LOW BATT 2.4G`, + 'a labelled flag. MUTED, AIRPLANE, a band tag. Click a badge to cycle its variant.'); card(I,'20','Tabular readout', - `
24:10
timer
-
68%
`, - 'a precise number. Clock, countdown, volume %. BerkeleyMono tabular-nums so digits don\'t jitter.'); + `
24:10
timer
`, + 'a precise number. Clock, countdown, volume %. Runs a live countdown; click to pause/resume.'); card(I,'21','Engraved label', - `outputs· 3`, - 'section divider. The hairline-flanked caps label with a count. Groups a panel into readable blocks.'); + `outputs· 3`, + 'section divider. The hairline-flanked caps label with a count. Click to bump the count.'); card(I,'22','Output well', - `
Linkwlp170s0 · @Hyatt
+ `
Linkwlp170s0 · @Hyatt
DNSresolving…
`, - 'streaming step log. The doctor/scan output — lamp-per-step with evidence. For any run-and-report action.'); + 'streaming step log. Lamp-per-step with evidence. Click to stream the next step.'); card(I,'23','Toast / status line', - `joined @Hyatt_WiFi — saved`, - 'transient confirmation. The one-line result after an action. Auto-dismiss; red variant for errors.'); + `joined @Hyatt_WiFi — saved`, + 'transient confirmation. The one-line result after an action. Click to fire the next toast.'); card(I,'26','Nixie tube', - `08`, - 'a single warm-glowing numeral. The nixie/VFD readout — one lit digit per tube, leading zeros dark, no matrix of cells. For a count that should feel analog: board size, a channel number.'); + `08`, + 'a single warm-glowing numeral. One lit digit per tube, leading zeros dark. Click to increment the count.'); card(I,'27','Magic-eye tube', - ``, - 'the glowing tuning eye. A phosphor-green tube whose dark wedge narrows as a value peaks — signal lock, level, how close to solved. Pure nostalgia; a soft alternative to the ring.'); - -/* ---- live animation ---- */ -let ph=0, kang=140; -function bumpKnob(){ kang=(kang+35)%300-0; $('kind').style.transform=`rotate(${kang-150}deg)`; } -function armFire(){ const a=$('arm'); if(a.classList.contains('armed')){a.classList.remove('armed');a.textContent='forget';} - else{a.classList.add('armed');a.textContent='forget? again';} } -let rotIdx=2; const rotAng=[-70,-35,0,35,70]; -function rotSel(knob){ const poss=knob.parentElement.querySelectorAll('.pos'); - rotIdx=(rotIdx+1)%5; knob.querySelector('.ind').style.transform=`rotate(${rotAng[rotIdx]}deg)`; - poss.forEach((p,i)=>p.classList.toggle('on',i===rotIdx)); } -let tunIdx=2; const tunX=[12,51,90,129,168]; -function tuneNext(){ const mks=$('tuner').querySelectorAll('.mk'); tunIdx=(tunIdx+1)%5; - $('tundl').style.left=tunX[tunIdx]+'px'; mks.forEach((m,i)=>m.classList.toggle('on',i===tunIdx)); } -buildBars($('vuL'),16); buildBars($('vuR'),16); buildBars($('mini'),0); -$('mini').innerHTML=''; -$('kind').style.transform=`rotate(${kang-150}deg)`; + ``, + 'the glowing tuning eye. A phosphor tube whose dark wedge narrows as the value peaks. Drag up/down to set; readout tracks.'); +card(I,'N20','Split-flap display', + `DNS`, + 'flips to the new value. Each card drops to the next glyph with the mechanical clack. Auto-flips; click to flip now.'); +card(I,'N21','Seven-segment display', + ``, + 'a lit-segment number. Green segments, distinct from the nixie glow. Live countdown; click to add a minute.'); +card(I,'N22','VFD marquee', + `ARCHSETUP · NET OK · BT 2 · SND 62%`, + 'scrolling status line. Teal glow, dot-matrix mesh, text tracks across. Scrolls live; click to change the message.'); +card(I,'N23','Annunciator panel', + `
+ SYNCLOW BATTLINK + NO DNSVPNDISK +
`, + 'a grid of named alarms. Cells light amber to warn, red to fault (faults pulse). Click a cell to cycle its state.'); +card(I,'N24','Jewel pilot lamps', + ` + `, + 'a faceted glass indicator. The chunky bezel pilot lamp with a real bloom. Click a jewel to cycle red · amber · green · dark.'); +card(I,'N25','Tape counter', + ``, + 'rolling odometer digits. Numbered wheels roll to the next figure. Rolls live; readout shows the total.'); +card(I,'N26','Analog clock', + ``, + 'time on a real face. Engraved ticks, three hands, live seconds sweep. Runs live; readout shows the time.'); +card(I,'N27','Frequency-dial scale', + `MHz`, + 'a printed log scale. Marks crowd low, spread high — a real logarithmic axis. Drag the pointer; readout shows the frequency.'); +card(I,'N28','Patch-bay jack field', + `
`, + 'routing shown as patches. A grid of jacks with cables between connected pairs. Click one jack then another to add/remove a cable.'); + +/* ============ CONTROL WIRING ============ */ +/* 01 toggle */ +(function(){const sw=$('toggle01').querySelector('.switch'); + function upd(){setRd('01',sw.classList.contains('on')?'ON':'OFF');} + sw.onclick=()=>{sw.classList.toggle('on');upd();};upd();})(); +/* 02 console key */ +$('keys02').querySelectorAll('.key').forEach(k=>k.onclick=()=>{ + $('keys02').querySelectorAll('.key').forEach(x=>x.classList.remove('on','red')); + k.classList.add(k.textContent==='MUTED'?'red':'on');setRd('02',k.textContent);}); +setRd('02','LIVE'); +/* 03 horizontal fader */ +function setF1(p){const f=$('f1');f.querySelector('.fill').style.width=p+'%';f.querySelector('.cap').style.left=p+'%';setRd('03','level '+Math.round(p));} +dragX($('f1'),setF1);setF1(68); +/* 04 vertical faders */ +let vfA=60,vfB=35; +function vfUpd(){setRd('04','A '+Math.round(vfA)+' · B '+Math.round(vfB));} +function setVf(el,p){el.querySelector('.fill').style.height=p+'%';el.querySelector('.cap').style.bottom=p+'%';} +dragY($('v1'),p=>{vfA=p;setVf($('v1'),p);vfUpd();}); +dragY($('v2'),p=>{vfB=p;setVf($('v2'),p);vfUpd();}); +setVf($('v1'),60);setVf($('v2'),35);vfUpd(); +/* 05 knob */ +let knobV=53; +function setKnob(v){knobV=v;$('kind').style.transform=`rotate(${-150+v/100*300}deg)`;setRd('05','gain '+Math.round(v));} +dragDelta($('knob'),()=>knobV,setKnob,{min:0,max:100});setKnob(53); +/* 06 segmented */ +$('seg06').querySelectorAll('button').forEach(b=>b.onclick=()=>{ + $('seg06').querySelectorAll('button').forEach(x=>x.classList.remove('on'));b.classList.add('on');setRd('06',b.textContent);}); +setRd('06','TIMER'); +/* 07 chip */ +$('chip07').onclick=()=>{$('chip07').classList.toggle('on');setRd('07',$('chip07').classList.contains('on')?'ON':'OFF');}; +setRd('07','ON'); +/* 08 arm */ +let armed=false; +$('arm').onclick=()=>{const a=$('arm');if(!armed){armed=true;a.classList.add('armed');a.textContent='forget? again';setRd('08','ARMED');} + else{armed=false;a.classList.remove('armed');a.textContent='forget';setRd('08','FIRED · reset');}}; +setRd('08','SAFE'); +/* 09 lamp row */ +const lrStates=[['gold','tap to connect'],['busy','connecting…'],['','connected']];let lrI=0; +$('lrow9').onclick=()=>{lrI=(lrI+1)%3;const s=lrStates[lrI];$('lrow9').querySelector('.lamp').className='lamp '+s[0]; + $('lrow9').querySelector('.what').textContent=s[1];setRd('09',s[1]);}; +setRd('09','tap to connect'); +/* 24 rotary selector */ +const rotVals=[4,6,8,10,12];let rotIdx=2;const rotAng=[-70,-35,0,35,70]; +$('rotsel24').querySelector('.knob').onclick=()=>{rotIdx=(rotIdx+1)%5; + $('rotsel24').querySelector('.ind').style.transform=`rotate(${rotAng[rotIdx]}deg)`; + $('rotsel24').querySelectorAll('.pos').forEach((p,i)=>p.classList.toggle('on',i===rotIdx));setRd('24','size '+rotVals[rotIdx]);}; +setRd('24','size 8'); +/* 25 slide-rule */ +const tunVals=[4,6,8,10,12];let tunIdx=2;const tunX=[12,51,90,129,168]; +function setTun(i){tunIdx=Math.max(0,Math.min(tunVals.length-1,i));$('tundl').style.left=tunX[tunIdx]+'px'; + $('tuner').querySelectorAll('.mk').forEach((m,j)=>m.classList.toggle('on',j===tunIdx));setRd('25','pos '+tunVals[tunIdx]);} +$('tuner').onclick=(e)=>{const r=$('tuner').getBoundingClientRect(),x=e.clientX-r.left; + let best=0,bd=1e9;tunX.forEach((tx,j)=>{const d=Math.abs(tx-x);if(d{if(e.key==='ArrowLeft'||e.key==='ArrowDown'){e.preventDefault();setTun(tunIdx-1);} + else if(e.key==='ArrowRight'||e.key==='ArrowUp'){e.preventDefault();setTun(tunIdx+1);}}; +setRd('25','pos 8'); +/* N01 rocker */ +$('rocker').onclick=()=>{$('rocker').classList.toggle('on');setRd('N01',$('rocker').classList.contains('on')?'ON':'OFF');}; +setRd('N01','ON'); +/* N02 transport */ +function transport(b){b.parentElement.querySelectorAll('.tbtn').forEach(x=>x.classList.remove('on'));b.classList.add('on'); + const playing=b.dataset.t==='play';document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState=playing&&!reduced?'running':'paused'); + setRd('N02',{rew:'REW',play:'PLAY',stop:'STOP',rec:'REC'}[b.dataset.t]);} +setRd('N02','PLAY'); +/* N03 radio preset */ +function preset(b){b.parentElement.querySelectorAll('.preset').forEach(x=>x.classList.remove('on'));b.classList.add('on');setRd('N03',b.textContent);} +setRd('N03','WIFI'); +/* N04 concentric dual knob */ +let dkoV=50,dkiV=50; +function setDual(){$('dko').style.transform=`rotate(${-150+dkoV/100*300}deg)`;$('dki').style.transform=`rotate(${-150+dkiV/100*300}deg)`; + setRd('N04','bass '+Math.round(dkoV)+' · treble '+Math.round(dkiV));} +dragDelta($('dko'),()=>dkoV,v=>{dkoV=v;setDual();},{min:0,max:100}); +dragDelta($('dki'),()=>dkiV,v=>{dkiV=v;setDual();},{min:0,max:100,stop:true}); +setDual(); +/* N05 encoder */ +let encV=7; +function paintEnc(){const leds=$('enc').querySelectorAll('.led');const lit=((Math.floor(encV)%12)+12)%12; + leds.forEach((l,i)=>l.classList.toggle('on',i<=lit));} +function setEnc(v){encV=v;paintEnc();const ind=$('enc').querySelector('.knob .ind');if(ind)ind.parentElement.style.transform=`rotate(${encV*30}deg)`; + setRd('N05','pos '+Math.round(encV));} +(function(){const enc=$('enc');const R=27,cx=33,cy=33; + for(let i=0;i<12;i++){const a=(i*30-90)*Math.PI/180;const d=document.createElement('span');d.className='led'; + d.style.left=(cx+R*Math.cos(a))+'px';d.style.top=(cy+R*Math.sin(a))+'px';enc.appendChild(d);} + const k=document.createElement('span');k.className='knob';k.innerHTML='';enc.appendChild(k);})(); +dragDelta($('enc'),()=>encV,setEnc,{min:0,max:100000,sens:0.25});setEnc(7); +/* N06 keylock */ +let keyIdx=1;const keyAng=[-70,0,70];const keyNm=['OFF','ON','RUN']; +function keyTurn(){keyIdx=(keyIdx+1)%3;$('kbit').style.transform=`rotate(${keyAng[keyIdx]}deg)`; + $('keylock6').querySelectorAll('.kpos').forEach((p,i)=>p.classList.toggle('on',i===keyIdx));setRd('N06',keyNm[keyIdx]);} +setRd('N06','ON'); +/* N07 crossfader */ +function setXf(p){$('xfcap').style.left=p+'%';const v=Math.round(p-50);setRd('N07',(v>0?'+':'')+v);} +dragX($('xf'),setXf);setXf(50); +/* N08 thumbwheel */ +let thumbV=42; +function setThumb(v){thumbV=Math.round(v);const s=(((thumbV%100)+100)%100);$('thval').textContent=String(s).padStart(2,'0');setRd('N08','value '+s);} +dragDelta($('thumbwheel'),()=>thumbV,setThumb,{min:0,max:99,sens:0.2});setThumb(42); +/* N09 DIP */ +function dipUpd(){const bits=[...$('dips').querySelectorAll('.dipsw')].map(x=>x.classList.contains('on')?'1':'0').join('');setRd('N09',bits);} +function dip(s){s.classList.toggle('on');dipUpd();} +dipUpd(); +/* N10 jog */ +let jogV=0; +function setJog(v){jogV=v;$('jogi').style.transform=`rotate(${jogV*4}deg)`;setRd('N10','pos '+Math.round(jogV));} +dragDelta($('jog10'),()=>jogV,setJog,{min:-100000,max:100000,sens:0.5});setJog(0); + +/* ============ METER WIRING (drag-driven) ============ */ +/* 10 needle gauge */ +let gV=50; +function setGauge(v){gV=v;$('g1').style.transform=`rotate(${-60+v/100*120}deg)`;$('g1v').textContent=Math.round(v);setRd('10','value '+Math.round(v)+'%');} +dragDelta($('gauge10'),()=>gV,setGauge,{min:0,max:100});setGauge(50); +/* 13 signal ladder */ +let ladV=3; +function setLadder(v){ladV=v;const bars=$('ladder').children; + for(let i=0;isetLadder((ladV+1)%5);setLadder(3); +/* 14 fuel bars */ +function setBar(id,no,p){const b=$(id);b.querySelector('span').style.width=p+'%';b.classList.toggle('warn',p<20);setRd(no,Math.round(p)+'%');} +dragX($('bar14'),p=>setBar('bar14','14',p));dragX($('bar14b'),p=>setBar('bar14b','14',p)); +setBar('bar14','14',72); +/* 15 radial ring */ +let ringV=68; +function setRing(v){ringV=v;const r=$('ring15');r.style.setProperty('--p',v);r.querySelector('b').textContent=Math.round(v);setRd('15',Math.round(v)+'%');} +dragDelta($('ring15'),()=>ringV,setRing,{min:0,max:100});setRing(68); +/* 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{l.style.cursor='pointer';l.onclick=()=>{ + let i=lst.indexOf(l.className);if(i<0)i=0;i=(i+1)%lst.length;l.className=lst[i];setRd('18',nm[i]);};});})(); +setRd('18','five states'); +/* 19 badge */ +(function(){const bv=['badge','badge red','badge ghost']; + $('badge19').querySelectorAll('.badge').forEach(b=>{b.style.cursor='pointer';b.onclick=()=>{ + let i=bv.indexOf(b.className);if(i<0)i=0;i=(i+1)%bv.length;b.className=bv[i];setRd('19',b.textContent);};});})(); +setRd('19','three variants'); +/* 21 engraved */ +let engN=3;$('engrave').onclick=()=>{engN=engN%9+1;$('engc').textContent='· '+engN;setRd('21','count '+engN);}; +setRd('21','count 3'); +/* 22 output well */ +const owSteps=[['gold','Probe','8.8.8.8 …'],['','Gateway','10.0.0.1 ok'],['','DNS','1.1.1.1 ok'],['red','Retry','timeout']];let owI=0; +$('owell').onclick=()=>{owI=(owI+1)%owSteps.length;const s=owSteps[owI];const d=document.createElement('div');d.className='ostep'; + d.innerHTML=`${s[1]}${s[2]}`; + $('owell').appendChild(d);while($('owell').children.length>5)$('owell').removeChild($('owell').firstChild);setRd('22','+ '+s[1]);}; +setRd('22','click to stream'); +/* 23 toast */ +const toastMsgs=['link up · 300 Mbps','bt paired — WH-1000XM4','profile switched','joined @Hyatt_WiFi — saved'];let toastN=0; +$('toast').onclick=()=>{toastN++;$('toast').textContent=toastMsgs[toastN%toastMsgs.length]; + $('toast').style.opacity='0';requestAnimationFrame(()=>{$('toast').style.transition='opacity .3s';$('toast').style.opacity='1';});setRd('23','fired '+toastN);}; +setRd('23','click to fire'); +/* 26 nixie */ +let nixV=8; +function setNix(v){nixV=v;const t=$('nixie').children,s=String(v).padStart(2,'0'); + t[0].classList.toggle('off',v<10);t[0].querySelector('b').textContent=s[0];t[1].querySelector('b').textContent=s[1];setRd('26',s);} +$('nixie').onclick=()=>setNix((nixV+1)%100);setNix(8); +/* 27 magic-eye */ +let meV=48; +function setEye(v){meV=v;$('meye').querySelector('.fan').style.setProperty('--gap',(8+(100-v)/100*118)+'deg');setRd('27',Math.round(v)+'%');} +dragDelta($('meye'),()=>meV,setEye,{min:0,max:100});setEye(48); +/* N23 annunciator */ +(function(){const st=['acell','acell warn','acell fault'];const nm=['ok','warn','fault']; + $('annun').querySelectorAll('.acell').forEach(cell=>{const label=cell.textContent;cell.onclick=()=>{ + let cur=cell.className.trim();let i=st.indexOf(cur);if(i<0)i=0;i=(i+1)%st.length;cell.className=st[i];setRd('N23',label+' → '+nm[i]);};});})(); +setRd('N23','click a cell'); +/* N24 jewel */ +(function(){const cols=['var(--jewel-r)','var(--jewel-a)','var(--jewel-g)'];const nm=['red','amber','green']; + $('jewels').querySelectorAll('.jewel').forEach(j=>{j.onclick=()=>{ + if(j.classList.contains('dim')){j.classList.remove('dim');j.style.setProperty('--jc',cols[0]);setRd('N24','red');return;} + let cur=j.style.getPropertyValue('--jc').trim();let i=cols.indexOf(cur); + if(i>=cols.length-1||i<0){j.classList.add('dim');setRd('N24','dark');}else{j.style.setProperty('--jc',cols[i+1]);setRd('N24',nm[i+1]);}};});})(); +setRd('N24','click to cycle'); +/* N27 frequency-dial + pointer */ +(function(){const fs=$('fscale');const W=184;const marks=[0.5,1,2,3,5,7,10,14,20];const lo=Math.log10(0.5),hi=Math.log10(20); + marks.forEach(m=>{const x=6+((Math.log10(m)-lo)/(hi-lo))*(W-12); + const tk=document.createElement('span');tk.className='tick';tk.style.left=x+'px';tk.style.height='11px';fs.appendChild(tk); + const mk=document.createElement('span');mk.className='mk';mk.style.left=x+'px';mk.textContent=m;fs.appendChild(mk);}); + for(let m=0.5;m<=20;m+=(m<2?0.25:m<10?1:2)){const x=6+((Math.log10(m)-lo)/(hi-lo))*(W-12); + const tk=document.createElement('span');tk.className='tick';tk.style.left=x+'px';tk.style.height='6px';tk.style.opacity='.5';fs.appendChild(tk);} + const ptr=document.createElement('span');ptr.className='fptr';fs.appendChild(ptr); + function freqAt(p){return Math.pow(10,lo+(p/100)*(hi-lo));} + window.setFreq=function(p){ptr.style.left=(6+p/100*(W-12))+'px';const f=freqAt(p);setRd('N27',(f<10?f.toFixed(1):Math.round(f))+' MHz');}; + dragX(fs,window.setFreq);window.setFreq(45);})(); +/* N28 patch-bay */ +(function(){const patch=$('patch');const rows=2,cols=4;const jacks=[]; + for(let r=0;rj.classList.remove('hot')); + conns.forEach(pair=>{const[a,b]=pair.split('-').map(Number); + const ra=jacks[a].getBoundingClientRect(),rb=jacks[b].getBoundingClientRect(); + const x1=ra.left-pr.left+ra.width/2,y1=ra.top-pr.top+ra.height/2; + const x2=rb.left-pr.left+rb.width/2,y2=rb.top-pr.top+rb.height/2; + const p=document.createElementNS('http://www.w3.org/2000/svg','path'); + const my=Math.max(y1,y2)+16;p.setAttribute('d',`M${x1} ${y1} C${x1} ${my} ${x2} ${my} ${x2} ${y2}`);svg.appendChild(p); + jacks[a].classList.add('hot');jacks[b].classList.add('hot');}); + setRd('N28',conns.length?conns.map(c=>{const[a,b]=c.split('-');return a+'↔'+b;}).join(' '):'no cables');} + jacks.forEach(j=>{j.onclick=()=>{const id=+j.dataset.id; + if(pending==null){pending=id;j.classList.add('sel');return;} + if(pending===id){pending=null;j.classList.remove('sel');return;} + const k=key(pending,id);const i=conns.indexOf(k);if(i>=0)conns.splice(i,1);else conns.push(k); + jacks[pending].classList.remove('sel');pending=null;draw();};}); + conns=['0-5','2-7'];draw();window.addEventListener('resize',draw);})(); + +/* ============ 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'; + for(let s=0;s<9;s++)band.appendChild(document.createElement('i'));eq.appendChild(band);}})(); +(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]; +(function(){const ct=$('counter');counterDigits.forEach((d,idx)=>{const w=document.createElement('span'); + w.className='cwheel'+(idx>=4?' redw':'');const col=document.createElement('span');col.className='col'; + for(let n=-1;n<=10;n++){const s=document.createElement('span');s.textContent=((n+10)%10);col.appendChild(s);} + w.appendChild(col);ct.appendChild(w);setWheel(w,d);});})(); +function setWheel(w,d){w.querySelector('.col').style.top=(-(d+1)*34)+'px';} + +let ph=0; const hist=Array.from({length:40},()=>0.5); +const eqBands=Array.from({length:11},(_,i)=>({v:0.4,ph:i*0.6})); +const stripHist=Array.from({length:60},()=>31); +const pkL={v:0},pkR={v:0}; function paintVU(el,l,pk){const b=el.children,n=b.length,lit=Math.round(l*n); pk.v=Math.max(lit,(pk.v||0)-0.4);const p=Math.round(pk.v); for(let k=0;k=n-2?'clip':k>=n-4?'hot':'on'):'';if(p>0&&k===p-1)c=(c?c+' ':'')+'peak';b[k].className=c;}} -const pkL={v:0},pkR={v:0}; function paintMini(el,l){const b=el.children,lit=Math.round(l*4);for(let k=0;k<4;k++)b[k].className=k=3?'clip':k>=2?'hot':'on'):'';} -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 tick(){ +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; - const a=lvl(), b=lvl(); - paintVU($('vuL'),a,pkL); paintVU($('vuR'),b,pkR); paintMini($('mini'),a); - // needle sweeps 0..100 - const gv=Math.round(50+45*Math.sin(ph*0.7)); - $('g1').style.transform=`rotate(${-60+gv/100*120}deg)`; $('g1v').textContent=gv; - // sparkline - hist.push(0.5+0.42*Math.sin(ph*0.9)+ (Math.random()-0.5)*0.25); hist.shift(); + const a=lvl(),b=lvl(); + paintVU($('vuL'),a,pkL);paintVU($('vuR'),b,pkR);paintMini($('mini'),a); + setRd('11','L '+Math.round(a*100)+' · R '+Math.round(b*100)); + setRd('12',Math.round(a*100)+'%'); + hist.push(0.5+0.42*Math.sin(ph*0.9)+(Math.random()-0.5)*0.25);hist.shift(); $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-Math.max(0,Math.min(1,v))*40-2}`).join(' ')); - // waveform - let d='M0 19'; for(let x=0;x<=170;x+=3){const y=19+Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)*14; d+=` L${x} ${y.toFixed(1)}`;} - $('wavep').setAttribute('d',d); + setRd('16',Math.round(Math.max(0,Math.min(1,hist[hist.length-1]))*100)); + let d='M0 19';const env=Math.min(1,0.6+0.3*Math.sin(ph*0.4)); + for(let x=0;x<=170;x+=3){const y=19+Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)*14;d+=` L${x} ${y.toFixed(1)}`;} + $('wavep').setAttribute('d',d);setRd('17','amp '+Math.round(env*100)+'%'); + 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)); + 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)); +} +/* clock */ +function tickClock(){const d=new Date();const s=d.getSeconds(),m=d.getMinutes(),h=d.getHours()%12; + $('clks').style.transform=`rotate(${s*6}deg)`;$('clkm').style.transform=`rotate(${m*6+s*0.1}deg)`;$('clkh').style.transform=`rotate(${h*30+m*0.5}deg)`; + setRd('N26',String(d.getHours()).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0'));} +/* split-flap */ +const flapWords=['DNS ','LINK','SYNC','WIFI','SCAN'];let flapI=0; +function tickFlap(){flapI=(flapI+1)%flapWords.length;const w=flapWords[flapI].padEnd(4,' '); + $('flap').querySelectorAll('.flapd').forEach((dd,i)=>{const ch=w[i]||' ';const bel=dd.querySelector('b'); + if(bel.textContent!==ch){bel.textContent=ch;if(!reduced){dd.classList.remove('flip');void dd.offsetWidth;dd.classList.add('flip');}}}); + setRd('N20',flapWords[flapI].trim());} +$('flap').onclick=tickFlap; +/* seven-segment countdown */ +let sevenSecs=24*60+10; +function renderSeven(){const mm=String(Math.floor(sevenSecs/60)).padStart(2,'0'),ss=String(sevenSecs%60).padStart(2,'0'); + $('seven').innerHTML=seg7(mm[0])+seg7(mm[1])+''+seg7(ss[0])+seg7(ss[1]);setRd('N21',mm+':'+ss);} +function tickSeven(){sevenSecs=(sevenSecs-1+3600)%3600;renderSeven();} +$('seven').onclick=()=>{sevenSecs=(sevenSecs+60)%3600;renderSeven();}; +renderSeven(); +/* tabular timer */ +let tSecs=24*60+10,tRun=true; +function renderT(){$('treadout').textContent=String(Math.floor(tSecs/60)).padStart(2,'0')+':'+String(tSecs%60).padStart(2,'0'); + setRd('20',tRun?'running':'paused');} +function tickT(){if(tRun){tSecs=(tSecs-1+3600)%3600;renderT();}} +$('treadout').onclick=()=>{tRun=!tRun;renderT();};renderT(); +/* VFD marquee */ +const vfdMsgs=['ARCHSETUP · NET OK · BT 2 · SND 62%','WIFI @Hyatt · 300 Mbps · VPN UP','BATTERY 84% · DISK 61% · TEMP 47C'];let vfdMi=0,vfdX=176; +function tickVfd(){const t=$('vfdt');vfdX-=1.1;if(vfdX< -t.offsetWidth)vfdX=176;t.style.left=vfdX+'px';} +$('vfdm').onclick=()=>{vfdMi=(vfdMi+1)%vfdMsgs.length;$('vfdt').textContent=vfdMsgs[vfdMi];vfdX=176;setRd('N22','msg '+(vfdMi+1)+'/'+vfdMsgs.length);}; +setRd('N22','scrolling'); +/* tape counter */ +let ctrVal=471300; +function tickCounter(){ctrVal+=137;const s=String(ctrVal).padStart(6,'0'); + $('counter').querySelectorAll('.cwheel').forEach((w,i)=>setWheel(w,+s[i]));setRd('N25',s);} +setRd('N25',String(ctrVal).padStart(6,'0')); + +/* reels spin keyframe */ +(function(){const st=document.createElement('style');st.textContent='@keyframes reelspin{to{transform:rotate(360deg)}}';document.head.appendChild(st);})(); +document.querySelectorAll('.reel.spin').forEach(r=>{r.style.animation='reelspin 2.6s linear infinite';}); + +/* static paint for reduced motion */ +function paintStatic(){ + paintVU($('vuL'),0.55,pkL);paintVU($('vuR'),0.5,pkR);paintMini($('mini'),0.55); + setRd('11','L 55 · R 50');setRd('12','55%'); + $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-0.5*40-2}`).join(' '));setRd('16','50'); + $('wavep').setAttribute('d','M0 19 L170 19');setRd('17','amp 60%'); + 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'); + tickClock();renderSeven();setRd('N20','DNS');setRd('N22','scrolling');setRd('N25',String(ctrVal).padStart(6,'0')); +} + +if(!reduced){ + setInterval(fastTick,80); + setInterval(tickClock,1000);tickClock(); + setInterval(tickFlap,1500); + setInterval(tickVfd,32); + setInterval(tickCounter,1600); + setInterval(tickSeven,1000); + setInterval(tickT,1000); + document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='running'); +}else{ + paintStatic(); + document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused'); } -if(!reduced) setInterval(tick,80); else tick(); -- cgit v1.2.3