aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-12 22:04:50 -0500
committerCraig Jennings <c@cjennings.net>2026-07-12 22:04:50 -0500
commitcace2002494222dcbecf6706ebb29c8d43228edf (patch)
treeef4b575363b39e3020d4ac845ca83a57ac52b2ff /docs/prototypes/widgets.js
parent9758f1710161b0353654e83da04af8e13253e561 (diff)
downloadarchsetup-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.js169
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() {