aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/widgets.js')
-rw-r--r--docs/prototypes/widgets.js199
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() {