aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
blob: b88981edf9ccb53de55d611c9f451b495ab03063 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/* widgets.js — retro-instrument widget library (GW namespace).
   Classic script: load after the token :root block (tokens.json → gen_tokens.py),
   before any GW.* call. Works from file:// — no modules, no build step.
   Contract: each gallery card in panel-widget-gallery.html is the visual +
   behavioral spec its builder is judged against.
   Spec: docs/specs/2026-07-12-component-generation-spec.org */
(function () {
'use strict';
const GW = {};

/* ================= shared engine ================= */

const SVGNS = 'http://www.w3.org/2000/svg';
function svgEl(parent, tag, attrs) {
  const e = document.createElementNS(SVGNS, tag);
  for (const k in attrs) e.setAttribute(k, attrs[k]);
  parent.appendChild(e); return e;
}
function polar(cx, cy, r, deg) {
  const a = deg * Math.PI / 180;
  return [cx + r * Math.sin(a), cy - r * Math.cos(a)];
}

/* ---- pointer-drag helpers (rect-ratio based, so CSS zoom is transparent) ---- */
function dragX(el, onPct) {
  el.style.touchAction = 'none';
  const at = e => { 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 = `<svg class="seg7 ${cls || ''}" viewBox="0 0 24 44">`;
  for (const k of 'abcdefg') {
    const on = lit.includes(k);
    const col = on ? (cls === 'red' ? 'var(--sevred)' : 'var(--sevgrn)') : 'var(--sevoff)';
    s += `<polygon points="${SEGPTS[k]}" fill="${col}"/>`;
  }
  return s + '</svg>';
}

function buildBars(el, n) { el.innerHTML = ''; for (let k = 0; k < n; k++) el.appendChild(document.createElement('i')); }

/* ---- VU law: authentic nonlinear dB scale, dB → 0..1 sweep position ---- */
const VUDB = [[-20, 0], [-10, .20], [-7, .31], [-5, .40], [-3, .50], [-2, .58], [-1, .66], [0, .75], [1, .84], [2, .91], [3, 1]];
/* interpolate a dB reading from a 0..1 sweep position */
function vuDb(t) {
  let i = 0; while (i < VUDB.length - 2 && VUDB[i + 1][1] < t) i++;
  const [d1, t1] = VUDB[i], [d2, t2] = VUDB[i + 1];
  return d1 + (d2 - d1) * Math.max(0, Math.min(1, (t - t1) / (t2 - t1)));
}

/* ---- screen-color families: period phosphor/LCD palettes as scoped CSS vars.
   Shipped literals are the fallbacks, so defaults are pixel-identical until applied. ---- */
const SCREEN_FAMS = {
  green: { '--scr-hi': '#7fe0a0', '--scr-ink': '#58b87e', '--scr-dim': '#3d5c46',
    '--scr-bg1': '#0a120c', '--scr-bg2': '#050c07', '--scr-bgc': '#04140a', '--scr-bge': '#020a05', '--scr-brd': '#123018',
    '--scr-grat': 'rgba(111,206,51,.18)', '--scr-gratc': 'rgba(111,206,51,.35)', '--scr-glow': 'rgba(127,224,160,.8)',
    '--crt-face1': '#b9d8c0', '--crt-face2': '#8bb296', '--crt-glow': '#eef7ee' },
  amber: { '--scr-hi': '#ffbe54', '--scr-ink': '#e2a038', '--scr-dim': '#7d5c16',
    '--scr-bg1': '#140d06', '--scr-bg2': '#0a0705', '--scr-bgc': '#140d06', '--scr-bge': '#0a0603', '--scr-brd': '#33230e',
    '--scr-grat': 'rgba(226,160,56,.18)', '--scr-gratc': 'rgba(226,160,56,.35)', '--scr-glow': 'rgba(255,190,84,.8)',
    '--crt-face1': '#d8cba6', '--crt-face2': '#b3a276', '--crt-glow': '#ffe7c0' },
  red: { '--scr-hi': '#ff9a4c', '--scr-ink': '#e0742e', '--scr-dim': '#5c3416',
    '--scr-bg1': '#140a05', '--scr-bg2': '#0c0603', '--scr-bgc': '#140a05', '--scr-bge': '#0a0502', '--scr-brd': '#331c0c',
    '--scr-grat': 'rgba(224,116,46,.18)', '--scr-gratc': 'rgba(224,116,46,.35)', '--scr-glow': 'rgba(255,154,76,.8)',
    '--crt-face1': '#d8b9a6', '--crt-face2': '#b39276', '--crt-glow': '#ffdcc0' },
  blue: { '--scr-hi': '#cfe4ff', '--scr-ink': '#94b8d8', '--scr-dim': '#3a4c60',
    '--scr-bg1': '#0a0e14', '--scr-bg2': '#05080c', '--scr-bgc': '#0a1018', '--scr-bge': '#04070c', '--scr-brd': '#16283a',
    '--scr-grat': 'rgba(148,184,216,.18)', '--scr-gratc': 'rgba(148,184,216,.35)', '--scr-glow': 'rgba(207,228,255,.8)',
    '--crt-face1': '#c2d2dd', '--crt-face2': '#93a8b6', '--crt-glow': '#eaf2ff' },
  vfd: { '--scr-hi': '#63e6c8', '--scr-ink': '#46b89e', '--scr-dim': '#1e4a40',
    '--scr-bg1': '#06100d', '--scr-bg2': '#040b09', '--scr-bgc': '#041410', '--scr-bge': '#020a08', '--scr-brd': '#0e3028',
    '--scr-grat': 'rgba(70,184,158,.18)', '--scr-gratc': 'rgba(70,184,158,.35)', '--scr-glow': 'rgba(99,230,200,.8)',
    '--crt-face1': '#b0d8cd', '--crt-face2': '#84ada2', '--crt-glow': '#dcfff5' },
  white: { '--scr-hi': '#f2f4f2', '--scr-ink': '#c8cac8', '--scr-dim': '#5a5c5a',
    '--scr-bg1': '#0b0c0b', '--scr-bg2': '#070807', '--scr-bgc': '#0d0e0d', '--scr-bge': '#060706', '--scr-brd': '#2a2c2a',
    '--scr-grat': 'rgba(200,202,200,.18)', '--scr-gratc': 'rgba(200,202,200,.35)', '--scr-glow': 'rgba(242,244,242,.7)',
    '--crt-face1': '#d0d2d0', '--crt-face2': '#a8aaa8', '--crt-glow': '#f7f7f7' },
};

/* ---- widget CSS: injected once, grows as builders move in ---- */
const GW_CSS = ``;
function ensureCss() {
  if (document.getElementById('gw-css') || !GW_CSS) return;
  const st = document.createElement('style'); st.id = 'gw-css'; st.textContent = GW_CSS;
  document.head.appendChild(st);
}
if (document.head) ensureCss();
else document.addEventListener('DOMContentLoaded', ensureCss);

Object.assign(GW, { SVGNS, svgEl, polar, dragX, dragY, dragDelta, SEG, seg7, buildBars, VUDB, vuDb, SCREEN_FAMS });
window.GW = GW;
})();