diff options
Diffstat (limited to 'docs/prototypes')
| -rw-r--r-- | docs/prototypes/widgets.js | 164 |
1 files changed, 129 insertions, 35 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index dc37bd4..bd5c79f 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -3746,7 +3746,17 @@ DUPRE.edgeMeter = function (host, opts = {}) { }; /* R17 round CRT scope — pale phosphor face; the trace is the instrument's own - animation, so it lives here with its reduced-motion gate */ + animation, so it lives here with its reduced-motion gate. + + Contract (everything a consumer needs; no page globals touched): + opts: onChange(amp, 'Vpp N') fires on every animation tick, including + the initial paint — the demo waveform is instrument-owned. + handle: el, tick() — one animation step (the reduced-motion path paints + once and never ticks again on its own). + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate, except the + face gradient, which reads screen-family vars from this instrument's + subtree and so lives in a local defs. */ DUPRE.roundCrt = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 110, 104); @@ -3797,7 +3807,16 @@ DUPRE.roundCrt = function (host, opts = {}) { return { el: s, tick }; }; -/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */ +/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag. + + Contract (everything a consumer needs; no page globals touched): + opts: bank (initial degrees, -60..60, default 0); pitch (initial + degrees, -20..20, default 0); onChange({bank, pitch}, 'BANK … + · PITCH …') fires on every set, including the initial paint. + handle: el, get() ({bank, pitch}), set(bank, pitch) — both clamp to + their ranges; the stage itself is a 2D drag surface. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate. */ DUPRE.attitudeIndicator = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 130, 130); @@ -3853,7 +3872,16 @@ DUPRE.attitudeIndicator = function (host, opts = {}) { }; /* R44 heading bug + servo needle — drag parks the command; the needle chases - with honest servo lag (instrument-owned animation, reduced-motion gated) */ + with honest servo lag (instrument-owned animation, reduced-motion gated). + + Contract (everything a consumer needs; no page globals touched): + opts: value (initial commanded heading in degrees, default 90 — + normalized to 0..360); onChange({cmd, act}, 'CMD … · ACT …') + fires on every set and on every servo step. + handle: el, get() ({cmd, act}), set(v) — v normalizes to 0..360; + dragging the stage moves the command bug, the needle chases. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate. */ DUPRE.headingBug = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 130, 130); @@ -3884,7 +3912,7 @@ DUPRE.headingBug = function (host, opts = {}) { fill: '#bfc4d0', stroke: '#4a4e58', 'stroke-width': .6 }); svgEl(s, 'circle', { cx, cy, r: 4.5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 }); - let cmd = opts.value ?? 90, act = cmd; + let cmd = ((opts.value ?? 90) % 360 + 360) % 360, act = cmd; const draw = () => { bug.setAttribute('transform', `rotate(${cmd.toFixed(1)},${cx},${cy})`); needle.setAttribute('transform', `rotate(${act.toFixed(1)},${cx},${cy})`); @@ -3904,7 +3932,15 @@ DUPRE.headingBug = function (host, opts = {}) { }; /* R53 circular chart recorder — a day per revolution; the pen draws the cycle - (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper */ + (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper. + + Contract (everything a consumer needs; no page globals touched): + opts: onChange(hour, 'HH:MM · N%') fires on every pen step — the demo + day-cycle signal is instrument-owned. + handle: el, get() (current hour, 0..24), reset() — fresh paper, pen to + 00:00; clicking the stage does the same. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate. */ DUPRE.chartRecorder = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 130, 130); @@ -3954,7 +3990,16 @@ DUPRE.chartRecorder = function (host, opts = {}) { return { el: s, get: () => hour, reset }; }; -/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive */ +/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive. + + Contract (everything a consumer needs; no page globals touched): + opts: value (initial units, 5..35 on the printed RPM x100 scale, + default 24); onChange(value, 'RPM N') fires on every set, + including the initial paint. + handle: el, get() (units), set(v) — v clamps 5..35; the stage drags + vertically. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate. */ DUPRE.verticalTape = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 70, 130); @@ -3996,7 +4041,16 @@ DUPRE.verticalTape = function (host, opts = {}) { return { el: s, get: () => v, set }; }; -/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles */ +/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles. + + Contract (everything a consumer needs; no page globals touched): + opts: fuel (initial, 0..4, default 2.4); oil (initial, 0..4, default + 3.1); onChange([fuel, oil], 'FUEL n.n · OIL n.n') fires on every + set, including the initial paint. + handle: el, get() ([fuel, oil]), set(fuel, oil) — both clamp 0..4; + each half of the stage drags its own needle vertically. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate. */ DUPRE.twinNeedle = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 120, 110); @@ -4045,7 +4099,7 @@ DUPRE.twinNeedle = function (host, opts = {}) { }; const nF = needle('#e0523a'), nO = needle('#bfc4d0'); svgEl(s, 'circle', { cx, cy, r: 5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 }); - let vF = opts.fuel ?? 2.4, vO = opts.oil ?? 3.1; + let vF, vO; const draw = () => { nF.setAttribute('transform', `rotate(${angL(vF).toFixed(1)},${cx},${cy})`); nO.setAttribute('transform', `rotate(${angR(vO).toFixed(1)},${cx},${cy})`); @@ -4060,11 +4114,21 @@ DUPRE.twinNeedle = function (host, opts = {}) { }; half(0, () => vF, v => vF = v); half(60, () => vO, v => vO = v); - draw(); + set(opts.fuel ?? 2.4, opts.oil ?? 3.1); return { el: s, get: () => [vF, vO], set }; }; -/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts */ +/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts. + + Contract (everything a consumer needs; no page globals touched): + opts: temp (initial °F, 40..100, default 72); humidity (initial %, + 0..100, default 45); onChange([temp, humidity], 'NF · N% RH · + VERDICT') fires on every set, including the initial paint. + handle: el, get() ([temp, humidity]), set(temp, humidity) — both clamp + to their scales; the left half of the stage drags temp, the + right half humidity. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; gradients register in the shared defs plate. */ DUPRE.comfortMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 130, 122); @@ -4137,7 +4201,7 @@ DUPRE.comfortMeter = function (host, opts = {}) { const nT = needle(), nH = needle(); nT.setAttribute('transform', `translate(${PT[0]},${PT[1]})`); nH.setAttribute('transform', `translate(${PH[0]},${PH[1]})`); - let vT = opts.temp ?? 72, vH = opts.humidity ?? 45; + let vT, vH; const verdict = () => vT > 78 ? 'warm' : vT < 62 ? 'cold' : vH > 60 ? 'humid' : vH < 30 ? 'dry' : 'right'; const draw = () => { nT.setAttribute('transform', `translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`); @@ -4154,7 +4218,7 @@ DUPRE.comfortMeter = function (host, opts = {}) { }; half(0, () => vT, v => vT = v, 40, 100); half(65, () => vH, v => vH = v, 0, 100); - draw(); + set(opts.temp ?? 72, opts.humidity ?? 45); return { el: s, get: () => [vT, vH], set }; }; @@ -4192,15 +4256,25 @@ DUPRE.statusLamps = function (host, opts = {}) { return { el: wrap, get: () => states.slice(), set }; }; -/* 19 badges — labelled flags; click a badge to cycle its variant */ +/* 19 badges — labelled flags; click a badge to cycle its variant. + + Contract (everything a consumer needs; no page globals touched): + opts: items (array of [label, variant] pairs, variant 0 gold · 1 red · + 2 ghost, default a three-badge demo set); onChange(variants array + copy, label) fires per change and once at build with a count + summary. + handle: el, get() (variants copy), set(i, v) — v wraps modulo 3. + CSS lives in the .dupre-badge block of DUPRE_CSS; no other styles + involved. */ DUPRE.badges = function (host, opts = {}) { const onChange = opts.onChange || noop; - const CLS = ['badge', 'badge red', 'badge ghost']; + const CLS = ['dupre-badge', 'dupre-badge dupre-red', 'dupre-badge dupre-ghost']; + const norm = v => (((v | 0) % CLS.length) + CLS.length) % CLS.length; const items = opts.items || [['TUNNEL', 0], ['LOW BATT', 1], ['2.4G', 2]]; - const vars = items.map(it => it[1]); + const vars = items.map(it => norm(it[1])); const wrap = document.createElement('span'); - const els = items.map(([txt, v], i) => { - const b = document.createElement('span'); b.className = CLS[v]; b.textContent = txt; b.style.cursor = 'pointer'; + const els = items.map(([txt], i) => { + const b = document.createElement('span'); b.className = CLS[vars[i]]; b.textContent = txt; b.style.cursor = 'pointer'; b.addEventListener('click', () => set(i, vars[i] + 1)); wrap.appendChild(b); if (i < items.length - 1) wrap.appendChild(document.createTextNode(' ')); @@ -4208,7 +4282,7 @@ DUPRE.badges = function (host, opts = {}) { }); host.appendChild(wrap); function set(i, v) { - vars[i] = ((v % CLS.length) + CLS.length) % CLS.length; + vars[i] = norm(v); els[i].className = CLS[vars[i]]; onChange(vars.slice(), items[i][0]); } @@ -4217,13 +4291,24 @@ DUPRE.badges = function (host, opts = {}) { }; /* 20 tabular readout — mm:ss countdown; builder owns the state, the page - drives tick() on its own clock (tick contract); click pauses/resumes */ + drives tick() on its own clock (tick contract); click pauses/resumes. + + Contract (everything a consumer needs; no page globals touched): + opts: secs (initial seconds, wraps modulo 3600, default 24:10); run + (initial running state, default true); unit (caption under the + digits, default 'timer'); onChange(secs, 'running'/'paused') + fires on every set, including the initial paint. + handle: el, get() (secs), set(v) — v wraps modulo 3600; tick() — one + second down when running (the page's clock drives it); clicking + the digits pauses/resumes. + CSS lives in the .dupre-readout block of DUPRE_CSS; no other styles + involved. */ DUPRE.tabularReadout = function (host, opts = {}) { const onChange = opts.onChange || noop; const wrap = document.createElement('div'); wrap.style.textAlign = 'center'; - wrap.innerHTML = '<div class="readout"></div><span class="u"></span>'; - const out = wrap.querySelector('.readout'); - wrap.querySelector('.u').textContent = opts.unit || 'timer'; + wrap.innerHTML = '<div class="dupre-readout"></div><span class="dupre-unit"></span>'; + const out = wrap.querySelector('.dupre-readout'); + wrap.querySelector('.dupre-unit').textContent = opts.unit || 'timer'; host.appendChild(wrap); let secs, run = opts.run !== undefined ? !!opts.run : true; const draw = () => { @@ -4236,12 +4321,21 @@ DUPRE.tabularReadout = function (host, opts = {}) { return { el: wrap, get: () => secs, set, tick: () => { if (run) set(secs - 1); } }; }; -/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9 */ +/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9. + + Contract (everything a consumer needs; no page globals touched): + opts: label (caption text, default 'outputs'); count (initial count, + default 3); onChange(count, 'count N') fires on every set, + including the initial paint. + handle: el, get() (count), set(v); clicking the label cycles the count + 1..9. + CSS lives in the .dupre-engrave block of DUPRE_CSS; no other styles + involved. */ DUPRE.engravedLabel = function (host, opts = {}) { const onChange = opts.onChange || noop; - const e = document.createElement('span'); e.className = 'engrave'; + const e = document.createElement('span'); e.className = 'dupre-engrave'; e.append(opts.label || 'outputs'); - const c = document.createElement('span'); c.className = 'cnt'; e.appendChild(c); + const c = document.createElement('span'); c.className = 'dupre-cnt'; e.appendChild(c); host.appendChild(e); let n; const set = v => { n = v; c.textContent = '· ' + n; onChange(n, 'count ' + n); }; @@ -5420,9 +5514,9 @@ const DUPRE_CSS = ` .dupre-chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px} .dupre-chip.dupre-on{color:var(--chip-on,var(--gold));border-color:var(--chip-on,var(--gold))} -.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px} -.badge.red{background:var(--fail);color:var(--cream)} -.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)} +.dupre-badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px} +.dupre-badge.dupre-red{background:var(--fail);color:var(--cream)} +.dupre-badge.dupre-ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)} /* fader */ .dupre-fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none} @@ -5489,9 +5583,9 @@ const DUPRE_CSS = ` .dupre-ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums} /* tabular readout */ -.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer} -.readout small{color:var(--dim);font-size:12px;font-weight:400} -.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px} +.dupre-readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer} +.dupre-readout small{color:var(--dim);font-size:12px;font-weight:400} +.dupre-unit{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px} /* sparkline */ .dupre-spark{width:170px;height:44px} @@ -5515,11 +5609,11 @@ const DUPRE_CSS = ` .dupre-seg button.dupre-on{background:var(--seg-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));color:var(--seg-on-ink,var(--panel));font-weight:700} /* engraved section label */ -.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer; +.dupre-engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer; display:flex;align-items:center;gap:9px} -.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1} -.engrave::before{max-width:10px} -.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none} +.dupre-engrave::before,.dupre-engrave::after{content:"";height:1px;background:var(--wash);flex:1} +.dupre-engrave::before{max-width:10px} +.dupre-engrave .dupre-cnt{color:var(--dim);letter-spacing:.1em;text-transform:none} /* waveform strip */ .dupre-wave{width:170px;height:38px} |
