diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 22:04:50 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 22:04:50 -0500 |
| commit | cace2002494222dcbecf6706ebb29c8d43228edf (patch) | |
| tree | ef4b575363b39e3020d4ac845ca83a57ac52b2ff /docs/prototypes/widgets.js | |
| parent | 9758f1710161b0353654e83da04af8e13253e561 (diff) | |
| download | archsetup-cace2002494222dcbecf6706ebb29c8d43228edf.tar.gz archsetup-cace2002494222dcbecf6706ebb29c8d43228edf.zip | |
refactor(gallery): extract meters 10-17 into GW builders
The page keeps the clock and demo signal; live meters (VU pair, mini signal, sparkline, waveform strip) expose value-driven handles — set/push repaint synchronously and fire onChange like every other builder. Peak-hold and history buffers move into the builders. fastTick and paintStatic now drive card handles.
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index ed0feb4..266cdac 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -1806,6 +1806,175 @@ GW.voiceLoop = function (host, opts = {}) { return { el, get: () => keys.map(k => k.dataset.state) }; }; +/* ---- meters & gauges ---- + Tick contract: the page owns the clock and the signal; live meters expose + value-driven handles (set/push) that repaint synchronously and fire + onChange(value, text) like every other builder. Display-side state that + belongs to the instrument (peak-hold, history buffers) lives in here. */ + +/* 10 needle gauge — drag up/down sweeps the needle over a 120° arc */ +GW.needleGauge = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let v = opts.value ?? 50; + const el = document.createElement('div'); el.className = 'gauge'; + el.innerHTML = `<div class="dial"><div class="arc"></div> + <div class="tk" style="transform:rotate(-60deg)"></div><div class="tk" style="transform:rotate(0)"></div><div class="tk" style="transform:rotate(60deg)"></div> + <div class="ndl"></div><div class="hub"></div></div><div class="gv"><span>0</span>%</div>`; + host.appendChild(el); + const ndl = el.querySelector('.ndl'), num = el.querySelector('.gv span'); + function set(nv) { + v = nv; + ndl.style.transform = `rotate(${-60 + v / 100 * 120}deg)`; + num.textContent = Math.round(v); + onChange(v, 'value ' + Math.round(v) + '%'); + } + dragDelta(el, () => v, set, { min: 0, max: 100 }); + set(v); + return { el, get: () => v, set }; +}; + +/* 11 stereo VU — two LED bars with peak-hold; set(l, r) drives both */ +GW.vuPair = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const n = opts.bars || 16; + const el = document.createElement('div'); el.className = 'vu'; + el.innerHTML = `<div class="vurow"><span class="ch">L</span><span class="vubar"></span></div> + <div class="vurow"><span class="ch">R</span><span class="vubar"></span></div>`; + host.appendChild(el); + const bars = el.querySelectorAll('.vubar'); + bars.forEach(b => buildBars(b, n)); + const pkL = { v: 0 }, pkR = { v: 0 }; + function paint(bar, l, pk) { + const b = bar.children, 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; k++) { + let c = k < lit ? (k >= n - 2 ? 'clip' : k >= n - 4 ? 'hot' : 'on') : ''; + if (p > 0 && k === p - 1) c = (c ? c + ' ' : '') + 'peak'; + b[k].className = c; + } + } + let lv = 0, rv = 0; + function set(l, r) { + lv = l; rv = r; + paint(bars[0], l, pkL); paint(bars[1], r, pkR); + onChange([l, r], 'L ' + Math.round(l * 100) + ' · R ' + Math.round(r * 100)); + } + return { el, get: () => [lv, rv], set }; +}; + +/* 12 mini 4-bar signal — compact activity meter; set(level) */ +GW.miniSig = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const el = document.createElement('span'); el.className = 'sig'; + el.innerHTML = '<i></i><i></i><i></i><i></i>'; + host.appendChild(el); + let v = 0; + function set(l) { + v = l; + const b = el.children, lit = Math.round(l * 4); + for (let k = 0; k < 4; k++) b[k].className = k < lit ? (k >= 3 ? 'clip' : k >= 2 ? 'hot' : 'on') : ''; + onChange(l, Math.round(l * 100) + '%'); + } + return { el, get: () => v, set }; +}; + +/* 13 signal ladder — stepped 0-4 strength; click cycles */ +GW.signalLadder = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let v = opts.value ?? 3; + const el = document.createElement('span'); el.className = 'ladder'; + el.innerHTML = '<i></i><i></i><i></i><i></i>'; + host.appendChild(el); + function set(nv) { + v = nv; + const bars = el.children; + for (let i = 0; i < bars.length; i++) bars[i].style.background = i < v ? 'var(--gold)' : 'var(--wash)'; + onChange(v, v + '/4'); + } + el.onclick = () => set((v + 1) % 5); + set(v); + return { el, get: () => v, set }; +}; + +/* 14 linear fuel bar — one 0-100 bar, warn tint under the threshold; drag to set */ +GW.fuelBar = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const warnAt = opts.warnAt ?? 20; + let v = opts.value ?? 50; + const el = document.createElement('div'); el.className = 'bar'; + el.innerHTML = '<span></span>'; + host.appendChild(el); + const fill = el.querySelector('span'); + function set(p) { + v = p; + fill.style.width = p + '%'; + el.classList.toggle('warn', p < warnAt); + onChange(p, Math.round(p) + '%'); + } + dragX(el, set); + set(v); + return { el, get: () => v, set }; +}; + +/* 15 radial ring — percentage donut; drag up/down to set */ +GW.radialRing = function (host, opts = {}) { + const onChange = opts.onChange || noop; + let v = opts.value ?? 68; + const el = document.createElement('span'); el.className = 'ring'; + el.innerHTML = '<b></b>'; + host.appendChild(el); + const num = el.querySelector('b'); + function set(nv) { + v = nv; + el.style.setProperty('--p', v); + num.textContent = Math.round(v); + onChange(v, Math.round(v) + '%'); + } + dragDelta(el, () => v, set, { min: 0, max: 100 }); + set(v); + return { el, get: () => v, set }; +}; + +/* 16 sparkline — rolling history trace; push(v) appends a sample, fill(v) levels it */ +GW.sparkline = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const n = opts.samples || 40; + const hist = Array.from({ length: n }, () => opts.value ?? 0.5); + const el = document.createElement('span'); el.className = 'spark'; + el.innerHTML = '<svg viewBox="0 0 170 44" preserveAspectRatio="none"><polyline fill="none" stroke="var(--gold-hi)" stroke-width="1.5"/></svg>'; + host.appendChild(el); + const line = el.querySelector('polyline'); + const clamp = x => Math.max(0, Math.min(1, x)); + function paint() { + line.setAttribute('points', hist.map((v, i) => `${i / (n - 1) * 170},${44 - clamp(v) * 40 - 2}`).join(' ')); + onChange(hist[n - 1], String(Math.round(clamp(hist[n - 1]) * 100))); + } + function push(v) { hist.push(v); hist.shift(); paint(); } + function fill(v) { hist.fill(v); paint(); } + return { el, get: () => hist[n - 1], push, fill }; +}; + +/* 17 waveform strip — sampled trace; set(samples, amp) with samples in -1..1 */ +GW.waveStrip = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const el = document.createElement('span'); el.className = 'wave'; + el.innerHTML = '<svg viewBox="0 0 170 38" preserveAspectRatio="none"><path fill="none" stroke="var(--gold)" stroke-width="1.2"/></svg>'; + host.appendChild(el); + const path = el.querySelector('path'); + let amp = 0; + function set(samples, a) { + amp = a; + let d = 'M0 19'; + if (samples.length < 2) d += ' L170 19'; + else for (let i = 0; i < samples.length; i++) + d += ` L${(i / (samples.length - 1) * 170).toFixed(1)} ${(19 + samples[i] * 14).toFixed(1)}`; + path.setAttribute('d', d); + onChange(amp, 'amp ' + Math.round(amp * 100) + '%'); + } + set([], opts.amp ?? 0.6); + return { el, get: () => amp, set }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
