diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 21:26:16 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 21:26:16 -0500 |
| commit | 7e081036a5a29316b702a58b28a1ce21c685d4a4 (patch) | |
| tree | 0c8ebfa224ca689cadc2d89cccda8d31686679c2 /docs/prototypes/widgets.js | |
| parent | 933a5f8658498bbcaa9bf47006a94c8b260e80a5 (diff) | |
| download | archsetup-7e081036a5a29316b702a58b28a1ce21c685d4a4.tar.gz archsetup-7e081036a5a29316b702a58b28a1ce21c685d4a4.zip | |
refactor(gallery): extract controls N01-N10 into GW builders
Rocker, transport cluster, preset bank, dual knob, encoder, key switch, crossfader, thumbwheel, DIP bank, and jog wheel move into widgets.js; their inline onclick globals (transport/preset/keyTurn/dip) become listeners inside the builders. Transport takes an explicit animate option so reduced-motion behavior is the consumer's call; its initial paint leaves reel play-state to CSS, matching load behavior. Probes green plus the per-widget behavioral pass (43 checks).
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 9e0f73d..eba1fd3 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -320,6 +320,205 @@ GW.slideRule = function (host, opts = {}) { return { el: t, get: () => values[idx], set }; }; +/* N01 rocker power switch — hard on/off, lit legend */ +GW.rocker = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const r = document.createElement('span'); r.className = 'rocker'; + r.innerHTML = `<span class="half top">${opts.onLabel || 'ON'}</span><span class="half bot">${opts.offLabel || 'OFF'}</span>`; + host.appendChild(r); + let on; + const set = v => { on = !!v; r.classList.toggle('on', on); onChange(on, on ? 'ON' : 'OFF'); }; + r.addEventListener('click', () => set(!on)); + set(opts.on !== undefined ? opts.on : true); + return { el: r, get: () => on, set }; +}; + +/* N02 transport cluster — rew/play/stop/rec, one lit; reels turn while playing. + opts.animate: reels obey play state (default: not prefers-reduced-motion) */ +GW.transport = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const animate = opts.animate !== undefined ? opts.animate : !matchMedia('(prefers-reduced-motion: reduce)').matches; + const MODES = { rew: '⏮', play: '▶', stop: '⏹', rec: '⏺' }; + const NAMES = { rew: 'REW', play: 'PLAY', stop: 'STOP', rec: 'REC' }; + const wrap = document.createElement('div'); + wrap.style.cssText = 'display:flex;flex-direction:column;gap:6px;align-items:center'; + wrap.innerHTML = `<div class="reels"><span class="reel spin"><i></i><i></i><i></i></span><span class="reel spin"><i></i><i></i><i></i></span></div><div class="transport"></div>`; + const bar = wrap.querySelector('.transport'); + const btns = {}; + for (const m of Object.keys(MODES)) { + const b = document.createElement('button'); b.className = 'tbtn' + (m === 'rec' ? ' rec' : ''); + b.textContent = MODES[m]; bar.appendChild(b); btns[m] = b; + b.addEventListener('click', () => set(m)); + } + host.appendChild(wrap); + let mode; + const set = m => { + mode = m; + for (const k in btns) btns[k].classList.toggle('on', k === m); + wrap.querySelectorAll('.reel.spin').forEach(r => r.style.animationPlayState = (m === 'play' && animate) ? 'running' : 'paused'); + onChange(m, NAMES[m]); + }; + /* initial: light the mode but leave reel play-state to CSS, matching load behavior */ + mode = opts.mode || 'play'; + btns[mode].classList.add('on'); + onChange(mode, NAMES[mode]); + return { el: wrap, get: () => mode, set }; +}; + +/* N03 radio preset bank — mechanically exclusive presets */ +GW.presetBank = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const items = opts.items || ['WIFI', 'ETH', 'CELL', 'OFF']; + const bank = document.createElement('div'); bank.className = 'radiobank'; host.appendChild(bank); + const btns = items.map(t => { const b = document.createElement('button'); b.className = 'preset'; b.textContent = t; bank.appendChild(b); return b; }); + let idx; + const set = i => { idx = i; btns.forEach((b, j) => b.classList.toggle('on', j === i)); onChange(i, items[i]); }; + btns.forEach((b, i) => b.addEventListener('click', () => set(i))); + set(opts.active || 0); + return { el: bank, get: () => idx, set }; +}; + +/* N04 concentric dual knob — two values on one spindle, outer ring + inner cap */ +GW.dualKnob = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const dk = document.createElement('span'); dk.className = 'dualknob'; + dk.innerHTML = `<span class="outer"><span class="tick" style="transform:rotate(-120deg)"></span><span class="tick" style="transform:rotate(120deg)"></span></span><span class="inner"><span class="ind"></span></span>`; + host.appendChild(dk); + const o = dk.querySelector('.outer'), n = dk.querySelector('.inner'); + let oV, iV; + const paint = () => { + o.style.transform = `rotate(${-150 + oV / 100 * 300}deg)`; + n.style.transform = `rotate(${-150 + iV / 100 * 300}deg)`; + onChange(oV, iV); + }; + dragDelta(o, () => oV, v => { oV = v; paint(); }, { min: 0, max: 100 }); + dragDelta(n, () => iV, v => { iV = v; paint(); }, { min: 0, max: 100, stop: true }); + oV = opts.outer !== undefined ? opts.outer : 50; + iV = opts.inner !== undefined ? opts.inner : 50; + paint(); + return { el: dk, get: () => [oV, iV], set: (a, b) => { if (a !== undefined) oV = a; if (b !== undefined) iV = b; paint(); } }; +}; + +/* N05 rotary encoder + LED ring — endless dial, lit arc tracks the level */ +GW.encoder = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const enc = document.createElement('span'); enc.className = 'encoder'; host.appendChild(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 = '<span class="ind"></span>'; enc.appendChild(k); + let val; + const set = v => { + val = v; + const lit = ((Math.floor(val) % 12) + 12) % 12; + enc.querySelectorAll('.led').forEach((l, i) => l.classList.toggle('on', i <= lit)); + k.style.transform = `rotate(${val * 30}deg)`; + onChange(val, 'pos ' + Math.round(val)); + }; + dragDelta(enc, () => val, set, { min: 0, max: 100000, sens: 0.25 }); + set(opts.value !== undefined ? opts.value : 7); + return { el: enc, get: () => val, set }; +}; + +/* N06 keyed mode switch — guarded three-position mode, key-bit points at the live one */ +GW.keySwitch = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const items = opts.items || ['OFF', 'ON', 'RUN']; + const ANG = [-70, 0, 70], POS = [[16, 64], [50, 12], [84, 64]]; + const kl = document.createElement('span'); kl.className = 'keylock'; + items.forEach((t, i) => { + const p = document.createElement('span'); p.className = 'kpos'; + p.style.left = POS[i][0] + '%'; p.style.top = POS[i][1] + '%'; p.textContent = t; + kl.appendChild(p); + }); + const body = document.createElement('span'); body.className = 'body'; + body.innerHTML = '<span class="bit"></span><span class="barrel"></span>'; + kl.appendChild(body); host.appendChild(kl); + let idx; + const set = i => { + idx = ((i % items.length) + items.length) % items.length; + body.querySelector('.bit').style.transform = `rotate(${ANG[idx]}deg)`; + kl.querySelectorAll('.kpos').forEach((p, j) => p.classList.toggle('on', j === idx)); + onChange(idx, items[idx]); + }; + body.addEventListener('click', () => set(idx + 1)); + set(opts.index !== undefined ? opts.index : 1); + return { el: kl, get: () => idx, set }; +}; + +/* N07 center-detented crossfader — throw to either side of zero */ +GW.crossfader = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const xf = document.createElement('div'); xf.className = 'xfader'; + xf.innerHTML = `<div class="slot"></div><div class="detent"></div><span class="end" style="left:0">${opts.aLabel || 'A'}</span><span class="end" style="right:0">${opts.bLabel || 'B'}</span><div class="cap"></div>`; + host.appendChild(xf); + let pct; + const set = p => { + pct = Math.max(0, Math.min(100, p)); + xf.querySelector('.cap').style.left = pct + '%'; + const v = Math.round(pct - 50); + onChange(v, (v > 0 ? '+' : '') + v); + }; + dragX(xf, set); + set(opts.value !== undefined ? opts.value : 50); + return { el: xf, get: () => Math.round(pct - 50), set }; +}; + +/* N08 thumbwheel — knurled edge-wheel with a windowed two-digit value */ +GW.thumbwheel = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const w = document.createElement('div'); w.className = 'thumbw'; + w.innerHTML = '<span class="thumbwheel"></span><span class="win"></span>'; + host.appendChild(w); + let val; + const set = v => { + val = Math.round(v); + const s = ((val % 100) + 100) % 100; + w.querySelector('.win').textContent = String(s).padStart(2, '0'); + onChange(s, 'value ' + s); + }; + dragDelta(w.querySelector('.thumbwheel'), () => val, set, { min: 0, max: 99, sens: 0.2 }); + set(opts.value !== undefined ? opts.value : 42); + return { el: w, get: () => ((val % 100) + 100) % 100, set }; +}; + +/* N09 DIP-switch bank — hard flags, up is on; readout is the binary word */ +GW.dipBank = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const bits = opts.bits || [true, false, true, true, false, false]; + const bank = document.createElement('span'); bank.className = 'dip'; host.appendChild(bank); + const sws = bits.map(on => { + const s = document.createElement('span'); s.className = 'dipsw' + (on ? ' on' : ''); + s.innerHTML = '<i></i>'; bank.appendChild(s); return s; + }); + const word = () => sws.map(x => x.classList.contains('on') ? '1' : '0').join(''); + const upd = () => onChange(word(), word()); + sws.forEach(s => s.addEventListener('click', () => { s.classList.toggle('on'); upd(); })); + upd(); + return { el: bank, get: word, set: w => { sws.forEach((s, i) => s.classList.toggle('on', w[i] === '1')); upd(); } }; +}; + +/* N10 jog / shuttle wheel — scrub fine; position accumulates without limit */ +GW.jogWheel = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const j = document.createElement('span'); j.className = 'jog'; + j.innerHTML = `<span class="shuttle" style="--sh:40deg"></span><span class="inner"><span class="dimple"></span></span>`; + host.appendChild(j); + let val; + const set = v => { + val = v; + j.querySelector('.inner').style.transform = `rotate(${val * 4}deg)`; + onChange(val, 'pos ' + Math.round(val)); + }; + dragDelta(j, () => val, set, { min: -100000, max: 100000, sens: 0.5 }); + set(opts.value !== undefined ? opts.value : 0); + return { el: j, get: () => val, set }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
