diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-25 15:00:56 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-25 15:00:56 -0500 |
| commit | 2c416f5361ed10cd73eb72eb5062261b2e73c18f (patch) | |
| tree | 5c84c09deb2c8392544020d432ffcd0fccde79f2 /docs/prototypes | |
| parent | d52da959bc416e09bc0f3e54994563bd80e19c64 (diff) | |
| download | archsetup-2c416f5361ed10cd73eb72eb5062261b2e73c18f.tar.gz archsetup-2c416f5361ed10cd73eb72eb5062261b2e73c18f.zip | |
refactor(prototypes): merge casting widgets into Dupre kit
Diffstat (limited to 'docs/prototypes')
| -rw-r--r-- | docs/prototypes/dupre-kit-additions.js | 30 | ||||
| -rw-r--r-- | docs/prototypes/gallery-widget.el | 29 | ||||
| -rw-r--r-- | docs/prototypes/gen_tokens.py | 21 | ||||
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 14 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 160 |
5 files changed, 188 insertions, 66 deletions
diff --git a/docs/prototypes/dupre-kit-additions.js b/docs/prototypes/dupre-kit-additions.js index 005a577..b1846a2 100644 --- a/docs/prototypes/dupre-kit-additions.js +++ b/docs/prototypes/dupre-kit-additions.js @@ -5,12 +5,9 @@ * so they can merge back into the kit with gallery cards. * * Load AFTER widgets.js. Contents: - * - DUPRE.detentFader (NEW) — multi-detent slide attenuator with - * speedbump drag physics. - * - DUPRE.drumRoller (UPGRADE) — backward-compatible redefinition: - * 1..N channels (stock hardcodes exactly two), configurable min/max - * range and stage height. Defaults reproduce the stock instrument. - * - DUPRE.guardedToggle (UPGRADE) — rotateX throw (no planar spin). + * - detentFader, drumRoller and guardedToggle have merged into widgets.js; + * their retired implementation references remain local below while the + * remaining additions continue to load in older casting prototypes. * - DUPRE.batToggle (UPGRADE) — same throw fix. * - DUPRE.slideRule (UPGRADE) — width option; the printed scale * spreads its stops proportionally. Default keeps stock 180px. @@ -101,21 +98,6 @@ const st = document.createElement('style'); st.id = 'dupre-additions-css'; st.textContent = ` -.dupre-dfader{position:relative;display:inline-block;width:230px;height:36px;cursor:ew-resize;touch-action:none} -.dupre-dfader .df-slot{position:absolute;left:8px;right:8px;top:15px;height:6px;border-radius:4px; - background:var(--well,#0a0c0d);border:1px solid #0a0908;box-shadow:inset 0 1px 2px #000} -.dupre-dfader .df-fill{position:absolute;left:9px;top:16px;height:4px;border-radius:3px; - background:linear-gradient(180deg,var(--amber-grad-top,#f2c76a),var(--gold,#e2a038));pointer-events:none} -.dupre-dfader .df-tick{position:absolute;top:9px;width:2px;height:18px;border-radius:1px; - background:var(--steel,#969385);opacity:.55;transform:translateX(-50%);pointer-events:none} -.dupre-dfader .df-tick.df-park{background:var(--gold-hi,#ffbe54);opacity:1; - box-shadow:0 0 5px rgba(var(--glow-hi,255,190,84),.7)} -.dupre-dfader .df-cap{position:absolute;top:5px;width:12px;height:26px;border-radius:3px;transform:translateX(-50%); - background:linear-gradient(90deg,#7e7a70,#e8e5db 45%,#8a867c);border:1px solid #4e4a42; - box-shadow:0 2px 4px #000a;pointer-events:none} -.dupre-dfader .df-cap::after{content:"";position:absolute;left:50%;top:3px;bottom:3px;width:2px; - transform:translateX(-50%);background:rgba(0,0,0,.35);border-radius:1px} -.dupre-dfader.df-parked .df-cap{box-shadow:0 2px 4px #000a,0 0 7px rgba(var(--glow-hi,255,190,84),.55)} .dupre-slsel{display:block;width:220px;cursor:pointer;touch-action:none;user-select:none} .dupre-slsel .sl-cols{display:grid} .dupre-slsel .sl-col{display:flex;flex-direction:column;align-items:center;gap:3px;padding-bottom:5px} @@ -177,7 +159,7 @@ (the detent value or null). CSS lives in the additions block (.dupre-dfader); no other styles involved. */ - DUPRE.detentFader = function (host, opts = {}) { + const mergedDetentFaderReference = function (host, opts = {}) { const onChange = opts.onChange || noop; const detents = (opts.detents || [50]).slice().sort((a, b) => a - b); const MAGNET = opts.magnet !== undefined ? opts.magnet : 3; @@ -255,7 +237,7 @@ each drum drags vertically on its own hit strip. SVG-built: styling is inline attributes plus the shared .rsvg stage block of DUPRE_CSS; gradients are additions-scoped until merge. */ - DUPRE.drumRoller = function (host, opts = {}) { + const mergedDrumRollerReference = function (host, opts = {}) { const onChange = opts.onChange || noop; const MIN = opts.min !== undefined ? opts.min : 1; const MAX = opts.max !== undefined ? opts.max : 10; @@ -313,7 +295,7 @@ foreshortens through the pivot and re-extends downward, never sweeping sideways. Contract unchanged from stock: opts onLabel/offLabel/on/ onChange(on, 'ON'|'OFF'); handle {el, get, set}. */ - DUPRE.guardedToggle = function (host, opts = {}) { + const mergedGuardedToggleReference = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 90, 100), cx = 45, cy = 52; grad('dkaDiscRed', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#d98a6f'], ['.65', 'var(--fail)'], ['1', '#7a2a1a']]); diff --git a/docs/prototypes/gallery-widget.el b/docs/prototypes/gallery-widget.el index 5a80e59..543affe 100644 --- a/docs/prototypes/gallery-widget.el +++ b/docs/prototypes/gallery-widget.el @@ -49,6 +49,12 @@ a typo'd token must fail loudly, not render black." "Needle angle at value 0, degrees from vertical (negative = left).") (defconst gallery-widget--gauge-max-deg 60.0 "Needle angle at value 100.") +(defconst gallery-widget--gauge-cx 48 + "Horizontal center of the needle gauge.") +(defconst gallery-widget--gauge-cy 48 + "Vertical center and baseline of the needle gauge.") +(defconst gallery-widget--gauge-radius 47 + "Outer radius of the needle gauge.") (defun gallery-widget--clamp-value (value) "Validate VALUE is a number and clamp it to the gauge's 0-100 range. @@ -76,27 +82,36 @@ Positive angles swing right, matching the gauge's needle travel." "Format coordinate N with two decimals for stable, readable SVG output." (format "%.2f" n)) +(defun gallery-widget--semicircle-path (cx cy radius &optional close) + "Return an upper semicircle path centered at (CX, CY) with RADIUS. +Append a closing segment when CLOSE is non-nil." + (format "M %g %g A %g %g 0 0 1 %g %g%s" + (- cx radius) cy radius radius (+ cx radius) cy + (if close " Z" ""))) + (defun gallery-widget--node (svg tag &rest attrs) "Append a TAG element with ATTRS plist to SVG and return it." (let ((node (dom-node tag (cl-loop for (k v) on attrs by #'cddr collect (cons (intern (substring (symbol-name k) 1)) v))))) - (svg--append svg node) + (dom-append-child svg node) node)) (defun gallery-widget-needle-gauge (value) "Render the gallery's needle gauge at VALUE (0-100) as an svg.el object. Geometry mirrors the web card: 96px-wide semicircular dial, pivot at its bottom center, 40px needle sweeping -60..+60 degrees, readout below." - (let* ((w 96) (h 64) (cx 48.0) (cy 48.0) + (let* ((w 96) (h 64) + (cx gallery-widget--gauge-cx) + (cy gallery-widget--gauge-cy) ;; clamp once; the angle and the readout render the same value (value (gallery-widget--clamp-value value)) (angle (gallery-widget--needle-angle value)) (tip (gallery-widget--polar cx cy 40.0 angle)) (svg (svg-create w h :viewBox (format "0 0 %d %d" w h)))) ;; glow filter: the SVG equivalent of the web card's box-shadow bloom - (svg--append + (dom-append-child svg (dom-node 'defs nil (dom-node 'filter @@ -107,13 +122,15 @@ bottom center, 40px needle sweeping -60..+60 degrees, readout below." (stdDeviation . "2")))))) ;; arc: 2px wash stroke, centerline radius 47 (96px circle, 2px border) (gallery-widget--node svg 'path - :d "M 1 48 A 47 47 0 0 1 95 48" + :d (gallery-widget--semicircle-path + cx cy gallery-widget--gauge-radius) :fill "none" :stroke (gallery-widget-token 'wash) :stroke-width "2") ;; ticks: 8px steel radials at -60 / 0 / +60 (dolist (deg '(-60.0 0.0 60.0)) - (let ((outer (gallery-widget--polar cx cy 47.0 deg)) + (let ((outer (gallery-widget--polar + cx cy gallery-widget--gauge-radius deg)) (inner (gallery-widget--polar cx cy 39.0 deg))) (gallery-widget--node svg 'line :class "tick" @@ -148,7 +165,7 @@ bottom center, 40px needle sweeping -60..+60 degrees, readout below." ;; lower half of its 8px circle; here the dome is drawn directly) (gallery-widget--node svg 'path :class "hub" - :d "M 44 48 A 4 4 0 0 1 52 48 Z" + :d (gallery-widget--semicircle-path cx cy 4 t) :fill (gallery-widget-token 'gold)) ;; value readout, matching the web card's cream bold figure (svg-text svg (format "%d%%" (round value)) diff --git a/docs/prototypes/gen_tokens.py b/docs/prototypes/gen_tokens.py index 9f011e9..7c80985 100644 --- a/docs/prototypes/gen_tokens.py +++ b/docs/prototypes/gen_tokens.py @@ -70,6 +70,12 @@ def resolve_color(tokens, key): raise KeyError(key) +def iter_section_items(tokens, sections): + """Yield (name, value) pairs from SECTIONS in their declared order.""" + for section in sections: + yield from tokens.get(section, {}).items() + + def _el_str(v): """Quote a value as an elisp string literal, escaping backslashes/quotes.""" return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"' @@ -78,9 +84,8 @@ def _el_str(v): def emit_web_css(tokens): """The inner :root block: hyphenated --vars, glows as rgb triples.""" lines = [] - for section in COLOR_SECTIONS: - for k, v in tokens.get(section, {}).items(): - lines.append(f"--{k}:{v};") + for k, v in iter_section_items(tokens, COLOR_SECTIONS): + lines.append(f"--{k}:{v};") for name, source in tokens.get("glow", {}).items(): lines.append(f"--{name}:{hex_to_triple(resolve_color(tokens, source))};") for k, v in tokens.get("font", {}).items(): @@ -100,9 +105,8 @@ def emit_waybar_gtk(tokens): " GTK CSS has no custom properties; reference these as @name, and use", " alpha(@name, 0.5) where the web build uses rgba(var(--name),.5). */", ] - for section in COLOR_SECTIONS: - for k, v in tokens.get(section, {}).items(): - lines.append(f"@define-color {gtk(k)} {v};") + for k, v in iter_section_items(tokens, COLOR_SECTIONS): + lines.append(f"@define-color {gtk(k)} {v};") for name, source in tokens.get("glow", {}).items(): lines.append(f"@define-color {gtk(name)} {resolve_color(tokens, source)};") return "\n".join(lines) @@ -111,9 +115,8 @@ def emit_waybar_gtk(tokens): def emit_elisp(tokens): """An alist of (name . hex) plus timing, for the future svg.el renderer.""" pairs = [] - for section in COLOR_SECTIONS: - for k, v in tokens.get(section, {}).items(): - pairs.append(f"({k} . {_el_str(v)})") + for k, v in iter_section_items(tokens, COLOR_SECTIONS): + pairs.append(f"({k} . {_el_str(v)})") for name, source in tokens.get("glow", {}).items(): pairs.append(f"({name} . {_el_str(resolve_color(tokens, source))})") for k, v in tokens.get("font", {}).items(): diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 9ff492d..f53b823 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -581,6 +581,13 @@ const INFO={ origin:'Hardware samplers.',difficulty:'Expert.', prefer:'The region IS the value and the data must stay visible.', period:'1980s sampler styling.'}, +'A1':{input:'Drag horizontally; the thumb catches at each marked detent and needs an extra push to break free. Drag-only.', + solves:'A continuous value with a few mechanically meaningful resting points.', + use:'Specialty. Shines for balances and thresholds where common values should be felt without losing the continuum.', + limits:'Detents need enough spacing to remain distinct; a dense set should be a stepped selector instead.', + origin:'Console faders with center and calibrated detents.',difficulty:'Easy.', + prefer:'Use when the stops should slow a sweep, not quantize it.', + period:'1960s onward studio and control-console hardware.'}, 'R20':{input:'Drag each drum vertically to roll it. Drag-only.', solves:'Stepped selection with a satisfying mechanical roll.', use:'Specialty. Shines for paired coarse selections (EQ turnovers).', @@ -1394,9 +1401,12 @@ card(C,'R18','Thumb-slide attenuator pair', card(C,'R19','Waveform region editor', (st,rd)=>DUPRE.waveRegion(st,{start:22,end:76,onChange:(v,t)=>rd(t)}), '<b>pick a region on the wave.</b> The monochrome LCD shows the sample; drag near either flag to move START or END, and the selection draws bright. After a sampler edit screen.'); +card(C,'A1','Detent fader', + (st,rd)=>DUPRE.detentFader(st,{value:50,detents:[25,50,75],onChange:(v,t)=>rd(t)}), + '<b>continuous travel with three speedbumps.</b> The cap catches at each calibrated tick and needs an extra push to leave it. Drag through the rail; the readout names a parked detent.'); card(C,'R20','Drum roller selector', - (st,rd)=>DUPRE.drumRoller(st,{onChange:(v,t)=>rd(t)}), - '<b>roll to the number.</b> The numbered drum shows through a tall window and the center value sits on an inverted chip. Drag either drum. After an equalizer tone selector.'); + (st,rd)=>DUPRE.drumRoller(st,{min:0,max:12,channels:[{name:'HIGH',v:6},{name:'MID',v:4},{name:'LOW',v:8}],onChange:(v,t)=>rd(t)}), + '<b>roll to the number.</b> Three independently sized numbered drums demonstrate the generalized channel bank and range; each center value sits on an inverted chip. Drag any drum. After an equalizer tone selector.'); card(C,'R21','LED program row', (st,rd)=>DUPRE.ledRow(st,{index:5,onChange:(v,t)=>rd(t)}), '<b>identical keys, one lit dot.</b> The LED above the button carries the selection, not the key itself — the classic program-select row. Click a key; readout names the program. After a digital reverb’s program bank.'); diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 57ad33e..508ec3c 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -1910,56 +1910,138 @@ DUPRE.waveRegion = function (host, opts = {}) { return { el: s, get: () => ({ s: S, e: E }), set }; }; -/* R20 drum roller selector — numbered paper drum in a window, center chip reads it. +/* A1 multi-detent fader — a horizontal attenuator with speedbump detents. + + Contract: + opts: value (0-100, default 68); detents (default [50]); magnet (capture + distance, default 3); escape (extra distance needed to leave a + detent, default 3); onChange(value, label). + handle: el, get(), set(value), parked() (detent value or null). */ +DUPRE.detentFader = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const detents = (opts.detents || [50]).slice().sort((a, b) => a - b); + const MAGNET = opts.magnet !== undefined ? opts.magnet : 3; + const ESCAPE = opts.escape !== undefined ? opts.escape : 3; + const el = document.createElement('span'); el.className = 'dupre-dfader'; + el.innerHTML = '<span class="df-slot"></span><span class="df-fill"></span>'; + const ticks = detents.map(d => { + const t = document.createElement('span'); t.className = 'df-tick'; + t.style.left = `calc(8px + ${d / 100} * (100% - 16px))`; + el.appendChild(t); return t; + }); + const cap = document.createElement('span'); cap.className = 'df-cap'; + el.appendChild(cap); host.appendChild(el); + + let v, park = null; + const paint = () => { + const left = `calc(8px + ${v / 100} * (100% - 16px))`; + cap.style.left = left; + el.querySelector('.df-fill').style.width = + `calc(${v / 100} * (100% - 18px))`; + el.classList.toggle('df-parked', park !== null); + ticks.forEach((t, i) => + t.classList.toggle('df-park', detents[i] === park)); + }; + const apply = raw => { + raw = Math.max(0, Math.min(100, raw)); + if (park !== null) { + if (Math.abs(raw - park) <= MAGNET + ESCAPE) raw = park; + else park = null; + } + if (park === null) { + const near = detents.find(d => Math.abs(raw - d) <= MAGNET); + if (near !== undefined) { park = near; raw = near; } + } + const nv = Math.round(raw); + if (nv === v && el.dataset.painted) return; + v = nv; el.dataset.painted = '1'; paint(); + onChange(v, park !== null ? `${v} · detent` : String(v)); + }; + el.addEventListener('pointerdown', e => { + el.setPointerCapture(e.pointerId); + const pct = m => { + const r = el.getBoundingClientRect(); + return ((m.clientX - r.left - 8) / (r.width - 16)) * 100; + }; + apply(pct(e)); + const move = m => apply(pct(m)); + const up = () => { + el.removeEventListener('pointermove', move); + el.removeEventListener('pointerup', up); + el.removeEventListener('pointercancel', up); + }; + el.addEventListener('pointermove', move); + el.addEventListener('pointerup', up); + el.addEventListener('pointercancel', up); + e.preventDefault(); + }); + v = -1; apply(opts.value !== undefined ? opts.value : 68); + return { el, get: () => v, set: apply, parked: () => park }; +}; + +/* R20 drum roller selector — numbered paper drums in a window. Contract (everything a consumer needs; no page globals touched): opts: title (engraved header, default 'EQUALIZER'); channels (array of - { name, v } drums, v 1-10, default HIGH/LOW pair — the layout is - sized for two); onChange(values array, 'NAME v · NAME v') fires on - every set, including the initial paint. - handle: el, get() (values array), set(i, v) — v clamps 1-10; each drum - drags vertically on its own hit strip. + { name, v } drums, any count >= 1, default HIGH/LOW pair); + min/max (default 1/10); height (default 140); onChange(values, + 'NAME v · ...') fires on every set, including initial paint. + handle: el, get() (values array), set(i, v); each drum 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.drumRoller = function (host, opts = {}) { const onChange = opts.onChange || noop; - const chans = (opts.channels || [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }]).map(c => ({ name: c.name, v: c.v })); - const s = stageSvg(host, 'rsvg', 130, 140), cy = 76, step = 17; + const MIN = opts.min !== undefined ? opts.min : 1; + const MAX = opts.max !== undefined ? opts.max : 10; + const chans = (opts.channels || + [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }]) + .map(c => ({ name: c.name, v: c.v })); + const N = chans.length; + const vw = Math.max(130, N * 50 + 40); + const vh = Math.max(100, opts.height !== undefined ? opts.height : 140); + const railY = 24, railH = vh - 36, winY = railY + 4, winH = railH - 8; + const s = stageSvg(host, 'rsvg', vw, vh); + const cy = winY + winH / 2, step = 17; gradDef('thRail', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]); gradDef('drPaper', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#cfc6a8'], ['.5', '#ece4c8'], ['1', '#c6bd9e']]); gradDef('drCurve', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'rgba(0,0,0,.4)'], ['.28', 'rgba(0,0,0,0)'], ['.72', 'rgba(0,0,0,0)'], ['1', 'rgba(0,0,0,.4)']]); const localDefs = svgEl(s, 'defs', {}); - svgEl(s, 'text', { x: 65, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.title || 'EQUALIZER'; + if (opts.title !== '') + svgEl(s, 'text', { x: vw / 2, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.title || 'EQUALIZER'; const grps = []; + const label = () => + chans.map(c => `${c.name} ${Math.round(c.v)}`).join(' · '); const set = (i, v) => { - v = Math.max(1, Math.min(10, v)); chans[i].v = v; + v = Math.max(MIN, Math.min(MAX, v)); chans[i].v = v; const p = grps[i]; p.nums.style.transform = `translateY(${(v - p.base) * step}px)`; p.chip.textContent = Math.round(v); - onChange(chans.map(c => c.v), `${chans[0].name} ${Math.round(chans[0].v)} · ${chans[1].name} ${Math.round(chans[1].v)}`); + onChange(chans.map(c => c.v), label()); }; + const x0 = (vw - (N - 1) * 50) / 2; chans.forEach((ch, i) => { - const x = 45 + i * 50, cid = uid('drClip'); - svgEl(s, 'text', { x: x + 18, y: 34, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '+'; - svgEl(s, 'text', { x: x + 18, y: 124, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '−'; - svgEl(s, 'rect', { x: x - 12, y: 24, width: 24, height: 104, rx: 4, fill: 'url(#thRail)', stroke: '#0a0908', 'stroke-width': 1.2 }); + const x = x0 + i * 50, cid = uid('drClip'); + svgEl(s, 'text', { x: x + 18, y: winY + 8, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '+'; + svgEl(s, 'text', { x: x + 18, y: winY + winH - 2, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '−'; + svgEl(s, 'rect', { x: x - 12, y: railY, width: 24, height: railH, rx: 4, fill: 'url(#thRail)', stroke: '#0a0908', 'stroke-width': 1.2 }); const clip = svgEl(localDefs, 'clipPath', { id: cid }); - svgEl(clip, 'rect', { x: x - 9, y: 28, width: 18, height: 96 }); - svgEl(s, 'rect', { x: x - 9, y: 28, width: 18, height: 96, fill: 'url(#drPaper)' }); + svgEl(clip, 'rect', { x: x - 9, y: winY, width: 18, height: winH }); + svgEl(s, 'rect', { x: x - 9, y: winY, width: 18, height: winH, fill: 'url(#drPaper)' }); const g = svgEl(s, 'g', { 'clip-path': `url(#${cid})` }); const nums = svgEl(g, 'g', {}); nums.style.transition = 'transform .12s'; - for (let n = 1; n <= 10; n++) + for (let n = MIN; n <= MAX; n++) svgEl(nums, 'text', { x, y: cy + 3 + (ch.v - n) * step, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#2b2418' }).textContent = n; - svgEl(g, 'rect', { x: x - 9, y: 28, width: 18, height: 96, fill: 'url(#drCurve)', 'pointer-events': 'none' }); + svgEl(g, 'rect', { x: x - 9, y: winY, width: 18, height: winH, fill: 'url(#drCurve)', 'pointer-events': 'none' }); svgEl(s, 'rect', { x: x - 8, y: cy - 6.5, width: 16, height: 13, rx: 1.5, fill: '#1a1613', opacity: .92 }); const chip = svgEl(s, 'text', { x, y: cy + 3, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: 'var(--cream)' }); - svgEl(s, 'text', { x, y: 136, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = ch.name; - const hit = svgEl(s, 'rect', { x: x - 14, y: 24, width: 28, height: 104, fill: 'transparent' }); + svgEl(s, 'text', { x, y: vh - 4, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = ch.name; + const hit = svgEl(s, 'rect', { x: x - 14, y: railY, width: 28, height: railH, fill: 'transparent' }); hit.style.cursor = 'ns-resize'; grps.push({ nums, chip, base: ch.v }); - dragDelta(hit, () => chans[i].v, v => set(i, v), { min: 1, max: 10, sens: .08 }); + dragDelta(hit, () => chans[i].v, v => set(i, v), + { min: MIN, max: MAX, sens: (MAX - MIN) / (winH * 1.1) }); }); - set(0, chans[0].v); set(1, chans[1].v); + chans.forEach((c, i) => set(i, c.v)); return { el: s, get: () => chans.map(c => c.v), set }; }; @@ -2251,11 +2333,13 @@ DUPRE.guardedToggle = function (host, opts = {}) { svgEl(lever, 'path', { d: `M ${cx - 3} ${cy} L ${cx - 2} 26 L ${cx + 2} 26 L ${cx + 3} ${cy} Z`, fill: 'url(#chromeG)', stroke: '#4e4a42', 'stroke-width': .5 }); svgEl(lever, 'rect', { x: cx - 4.5, y: 16, width: 9, height: 12, rx: 2, fill: 'url(#tipG)', stroke: '#5e5a52', 'stroke-width': .6 }); for (let r = 0; r < 4; r++) svgEl(lever, 'line', { x1: cx - 3.5, y1: 18.5 + r * 2.4, x2: cx + 3.5, y2: 18.5 + r * 2.4, stroke: 'rgba(0,0,0,.25)', 'stroke-width': .8 }); - lever.style.transformOrigin = `${cx}px ${cy}px`; lever.style.transition = 'transform .12s'; + lever.style.transformBox = 'view-box'; + lever.style.transformOrigin = `${cx}px ${cy}px`; + lever.style.transition = 'transform .16s ease-in-out'; let on; const set = v => { on = !!v; - lever.style.transform = on ? 'rotate(0deg)' : 'rotate(180deg)'; + lever.style.transform = on ? 'rotateX(0deg)' : 'rotateX(180deg)'; lblOn.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--dim)'); lblOff.setAttribute('fill', on ? 'var(--dim)' : 'var(--gold-hi)'); onChange(on, on ? 'ON' : 'OFF'); @@ -6089,6 +6173,23 @@ const DUPRE_CSS = ` .dupre-patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%} .dupre-patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round} +/* detent fader */ +.dupre-dfader{position:relative;display:inline-block;width:230px;height:36px;cursor:ew-resize;touch-action:none} +.dupre-dfader .df-slot{position:absolute;left:8px;right:8px;top:15px;height:6px;border-radius:4px; + background:var(--well,#0a0c0d);border:1px solid #0a0908;box-shadow:inset 0 1px 2px #000} +.dupre-dfader .df-fill{position:absolute;left:9px;top:16px;height:4px;border-radius:3px; + background:linear-gradient(180deg,var(--amber-grad-top,#f2c76a),var(--gold,#e2a038));pointer-events:none} +.dupre-dfader .df-tick{position:absolute;top:9px;width:2px;height:18px;border-radius:1px; + background:var(--steel,#969385);opacity:.55;transform:translateX(-50%);pointer-events:none} +.dupre-dfader .df-tick.df-park{background:var(--gold-hi,#ffbe54);opacity:1; + box-shadow:0 0 5px rgba(var(--glow-hi,255,190,84),.7)} +.dupre-dfader .df-cap{position:absolute;top:5px;width:12px;height:26px;border-radius:3px;transform:translateX(-50%); + background:linear-gradient(90deg,#7e7a70,#e8e5db 45%,#8a867c);border:1px solid #4e4a42; + box-shadow:0 2px 4px #000a;pointer-events:none} +.dupre-dfader .df-cap::after{content:"";position:absolute;left:50%;top:3px;bottom:3px;width:2px; + transform:translateX(-50%);background:rgba(0,0,0,.35);border-radius:1px} +.dupre-dfader.df-parked .df-cap{box-shadow:0 2px 4px #000a,0 0 7px rgba(var(--glow-hi,255,190,84),.55)} + /* ===== reference-batch (R) instruments — SVG-first, after period hardware ===== */ .rsvg{display:block} .rsvg.drag{cursor:ns-resize;touch-action:none} @@ -6136,6 +6237,15 @@ DUPRE.radarSweep.POLICY = { kind: 'screen', DUPRE.abcKeypad.POLICY = { kind: 'screen', why: 'The entry window is a screen like any other; its phosphor was made in several colours. (Mixed card: the keys are fixed-function and not a colour choice.)', authentic: 'The window’s screen family. The keycap colours are functional and stay put.' }; +DUPRE.detentFader.POLICY = { kind: 'relational', + why: 'The gold parked detent only means anything against the quieter ordinary scale ticks.', + authentic: 'The detent and scale remain a contrasting pair; their material finishes and spacing may vary.' }; +DUPRE.drumRoller.POLICY = { kind: 'material', + why: 'Its colours describe paper, ink and the dark mechanical housing rather than application state.', + authentic: 'Paper and metal finishes from physical numbered drum selectors; the channel count and numeric range may vary.' }; +DUPRE.guardedToggle.POLICY = { kind: 'coded', + why: 'The red guard and collar mark a critical protected throw; recolouring them would erase that warning grammar.', + authentic: 'Red warning hardware with neutral metal lever and posts. Labels and initial position may vary.' }; Object.assign(DUPRE, { SVGNS, svgEl, polar, dragX, dragY, dragDelta, SEG, seg7, buildBars, VUDB, vuDb, SCREEN_FAMS }); window.DUPRE = DUPRE; |
