aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-12 23:54:01 -0500
committerCraig Jennings <c@cjennings.net>2026-07-12 23:54:01 -0500
commit5c3a4b70696929078f8b14b81c29f83264a9589b (patch)
treef45a467f9ef01318e773b51a6dc3d6cd9a9a6d27 /docs/prototypes/widgets.js
parentb2ad9becbcce0301578e62cab9be27099a3f612a (diff)
downloadarchsetup-main.tar.gz
archsetup-main.zip
feat(gallery): rebuild R05 filter bank with three skin axesHEADmain
The old card was a sketch: eight bare tracks and flat teal arrows floating on the card background. Real slider banks (the Vieltronix filter, the Pioneer SG-9500, the Technics SH-8065, the Zaxcom Oasis) have a faceplate, dual dB rails, dense bands, and caps that read as machined parts. The rebuilt bank draws twelve bands on a screwed faceplate with tick rails both sides. Skins ship as constructor opts backed by GW.filterBank.STYLES on three independent axes: panel (lab parchment / silver aluminum / studio black), cap shape (tall block fader / short ribbed / chrome T), and cap color (black white-index / red, green, blue, amber stripes / chrome / cream). Shape and color are separate axes so any finish applies to any cap. opts.style picks a native trio. setStyle(axis, name) restyles a live instance with values preserved. The card grows chip groups driving setStyle, the same demo-rig pattern as the slide toggle. Values shorter than a custom freqs list now pad instead of placing caps at NaN. I filed the Oasis reference alongside the others.
Diffstat (limited to 'docs/prototypes/widgets.js')
-rw-r--r--docs/prototypes/widgets.js145
1 files changed, 117 insertions, 28 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index c01972e..53217ec 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -676,41 +676,130 @@ GW.flutedKnob = function (host, opts = {}) {
return { el: s, get: () => val, set };
};
-/* R05 filter slider bank — one fader per band, teal arrow caps on black tracks */
+/* R05 filter slider bank — a dense fader wall on a real faceplate. Period skins
+ as constructor opts backed by GW.filterBank.STYLES, three independent axes:
+ panel (lab parchment / silver hi-fi aluminum / studio black), caps — the cap
+ SHAPE (chrome T / short ribbed / tall block fader), and capColor — the cap
+ FINISH (black white-index / red-index / chrome / cream). opts.style picks a
+ native trio (lab+block+black, silver+chrome+chrome, studio+ribbed+red);
+ opts.panel / opts.caps / opts.capColor override an axis; setStyle(axis, name)
+ restyles live (values kept). After the Vieltronix filter, the Pioneer
+ SG-9500, the Technics SH-8065, and the Zaxcom Oasis block faders. */
GW.filterBank = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const freqs = opts.freqs || [78, 136, 235, 406, 701, 1210, 2090, 3620];
- const vals = (opts.values || [18, 30, 42, 25, 35, 55, 20, 48]).slice();
- const fmtHz = f => f < 1000 ? f : (f / 1000) + 'k';
- const s = stageSvg(host, 'rsvg', 190, 96);
- const y0 = 14, y1 = 84, x0 = 30, dx = 19.5;
- for (const [lbl, y] of [['0', y0], ['20', y0 + (y1 - y0) / 3], ['40', y0 + 2 * (y1 - y0) / 3], ['60', y1]])
- svgEl(s, 'text', { x: 14, y: y + 2, 'text-anchor': 'end', 'font-size': 6, 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = lbl;
- svgEl(s, 'text', { x: 14, y: 94, 'text-anchor': 'end', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'dB';
+ const freqs = opts.freqs || [78, 113, 163, 235, 339, 487, 701, 1010, 1450, 2090, 3010, 4340];
+ const vals = (opts.values || [18, 30, 42, 25, 35, 55, 20, 48, 38, 26, 44, 32]).slice(0, freqs.length);
+ while (vals.length < freqs.length) vals.push(30);
+ const fmtHz = f => f < 1000 ? f : (f / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
+ const ST = GW.filterBank.STYLES;
+ const native = {
+ lab: { caps: 'block', capColor: 'black' },
+ silver: { caps: 'chrome', capColor: 'chrome' },
+ studio: { caps: 'ribbed', capColor: 'red' },
+ };
+ const nat = native[opts.style] || {};
+ const cur = {
+ panel: ST.panel[opts.panel] ? opts.panel : (ST.panel[opts.style] ? opts.style : 'lab'),
+ caps: ST.caps[opts.caps] ? opts.caps : (nat.caps || 'block'),
+ capColor: ST.capColor[opts.capColor] ? opts.capColor : (nat.capColor || 'black'),
+ };
+ gradDef('fbPlateLab', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f0ead6'], ['1', '#dcd5bc']]);
+ gradDef('fbPlateSilver', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#d8d8d4'], ['.5', '#c6c6c1'], ['1', '#adada7']]);
+ gradDef('fbPlateStudio', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#221f1c'], ['1', '#131110']]);
+ gradDef('sfChrome', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f0efec'], ['.45', '#c4c1b9'], ['1', '#75726a']]);
+ gradDef('fbCapBlack', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#2a2724'], ['1', '#171412']]);
+ gradDef('fbCapCream', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f2ecd8'], ['1', '#cfc8ae']]);
+ const s = stageSvg(host, 'rsvg', 250, 112);
+ const y0 = 20, y1 = 92, x0 = 36, dx = (214 - x0) / (freqs.length - 1);
+ const yOf = db => y0 + db / 60 * (y1 - y0);
const caps = [];
+ const place = i => caps[i].setAttribute('transform', `translate(0,${yOf(vals[i])})`);
+ const drawCap = (g, kind, x) => {
+ const c = ST.capColor[cur.capColor];
+ if (kind === 'chrome') {
+ svgEl(g, 'rect', { x: x - 6, y: -3.25, width: 12, height: 6.5, rx: .9, fill: c.body, stroke: c.edge, 'stroke-width': .6 });
+ svgEl(g, 'line', { x1: x - 6, y1: 0, x2: x + 6, y2: 0, stroke: c.index, 'stroke-width': 1 });
+ svgEl(g, 'line', { x1: x - 6, y1: 2, x2: x + 6, y2: 2, stroke: c.rib, 'stroke-width': .5 });
+ } else if (kind === 'ribbed') {
+ svgEl(g, 'rect', { x: x - 5, y: -4, width: 10, height: 8, rx: 1, fill: c.body, stroke: c.edge, 'stroke-width': .6 });
+ for (const dy of [-2.4, 2.4])
+ svgEl(g, 'line', { x1: x - 4, y1: dy, x2: x + 4, y2: dy, stroke: c.rib, 'stroke-width': .6 });
+ svgEl(g, 'line', { x1: x - 5, y1: 0, x2: x + 5, y2: 0, stroke: c.index, 'stroke-width': 1.1 });
+ } else { /* block — tall ribbed fader after the Zaxcom Oasis; fine rib
+ groups above and below a mid-cap index line that marks the value */
+ svgEl(g, 'rect', { x: x - 4.5, y: -8, width: 9, height: 16, rx: 1.1, fill: c.body, stroke: c.edge, 'stroke-width': .6 });
+ for (const dy of [-6.4, -4.8, -3.2, -1.6, 1.6, 3.2, 4.8, 6.4])
+ svgEl(g, 'line', { x1: x - 3.7, y1: dy, x2: x + 3.7, y2: dy, stroke: c.rib, 'stroke-width': .6 });
+ svgEl(g, 'line', { x1: x - 4.5, y1: 0, x2: x + 4.5, y2: 0, stroke: c.index, 'stroke-width': 1.2 });
+ }
+ };
const set = (i, db) => {
- db = Math.max(0, Math.min(60, db)); vals[i] = db;
- caps[i].setAttribute('transform', `translate(0,${14 + db / 60 * 70})`);
+ db = Math.max(0, Math.min(60, db)); vals[i] = db; place(i);
onChange({ band: i, hz: freqs[i], db }, `${fmtHz(freqs[i])} Hz · −${Math.round(db)} dB`);
};
- freqs.forEach((f, i) => {
- const x = x0 + i * dx;
- svgEl(s, 'text', { x, y: 8, 'text-anchor': 'middle', 'font-size': 5.2, 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = fmtHz(f);
- svgEl(s, 'line', { x1: x, y1: y0, x2: x, y2: y1, stroke: '#0a0908', 'stroke-width': 3.4, 'stroke-linecap': 'round' });
- svgEl(s, 'line', { x1: x - .7, y1: y0, x2: x - .7, y2: y1, stroke: 'rgba(255,255,255,.05)', 'stroke-width': .7 });
- const cap = svgEl(s, 'g', {});
- svgEl(cap, 'polygon', { points: `${x - 8.5},${-4.5} ${x - 2.5},0 ${x - 8.5},${4.5}`, fill: 'var(--vfd)', stroke: '#123028', 'stroke-width': .6 });
- svgEl(cap, 'polygon', { points: `${x + 8.5},${-4.5} ${x + 2.5},0 ${x + 8.5},${4.5}`, fill: 'var(--vfd)', stroke: '#123028', 'stroke-width': .6 });
- svgEl(cap, 'circle', { cx: x, cy: 0, r: 1.5, fill: '#123028' });
- caps.push(cap);
- /* paint the initial position without announcing it */
- cap.setAttribute('transform', `translate(0,${14 + vals[i] / 60 * 70})`);
- const hit = svgEl(s, 'rect', { x: x - 9.5, y: y0 - 6, width: 19, height: y1 - y0 + 12, fill: 'transparent' });
- hit.style.cursor = 'ns-resize';
- dragY(hit, pct => set(i, (100 - pct) / 100 * 60));
- });
+ const render = () => {
+ const st = ST.panel[cur.panel];
+ s.replaceChildren(); caps.length = 0;
+ svgEl(s, 'rect', { x: 1.5, y: 1.5, width: 247, height: 109, rx: 5, fill: st.plate, stroke: st.edge, 'stroke-width': 1 });
+ svgEl(s, 'rect', { x: 3, y: 3, width: 244, height: 106, rx: 4, fill: 'none', stroke: st.bevel, 'stroke-width': .7 });
+ [[9, 9], [241, 9], [9, 103], [241, 103]].forEach(([sx, sy], k) => {
+ svgEl(s, 'circle', { cx: sx, cy: sy, r: 2.7, fill: st.screw, stroke: 'rgba(0,0,0,.55)', 'stroke-width': .6 });
+ svgEl(s, 'line', { x1: sx - 1.9, y1: sy, x2: sx + 1.9, y2: sy, stroke: 'rgba(0,0,0,.6)', 'stroke-width': .7, transform: `rotate(${25 + k * 40},${sx},${sy})` });
+ });
+ for (let db = 0; db <= 60; db += 10) {
+ const y = yOf(db), major = db % 20 === 0, len = major ? 4 : 2.5;
+ svgEl(s, 'line', { x1: 24 - len, y1: y, x2: 24, y2: y, stroke: st.dim, 'stroke-width': major ? .9 : .6 });
+ svgEl(s, 'line', { x1: 226, y1: y, x2: 226 + len, y2: y, stroke: st.dim, 'stroke-width': major ? .9 : .6 });
+ if (major) {
+ svgEl(s, 'text', { x: 17, y: y + 2, 'text-anchor': 'end', 'font-size': 5.2, 'font-family': 'var(--mono)', fill: st.ink }).textContent = db;
+ svgEl(s, 'text', { x: 233, y: y + 2, 'text-anchor': 'start', 'font-size': 5.2, 'font-family': 'var(--mono)', fill: st.ink }).textContent = db;
+ }
+ }
+ svgEl(s, 'text', { x: 20, y: 100, 'text-anchor': 'end', 'font-size': 4.6, 'font-family': 'var(--mono)', fill: st.dim }).textContent = 'dB';
+ freqs.forEach((f, i) => {
+ const x = x0 + i * dx;
+ svgEl(s, 'text', { x, y: 13, 'text-anchor': 'middle', 'font-size': 4.6, 'font-family': 'var(--mono)', fill: st.ink }).textContent = fmtHz(f);
+ svgEl(s, 'line', { x1: x, y1: y0, x2: x, y2: y1, stroke: st.track, 'stroke-width': st.trackW, 'stroke-linecap': 'round' });
+ if (st.trackHi) svgEl(s, 'line', { x1: x - st.trackW / 2 - .35, y1: y0, x2: x - st.trackW / 2 - .35, y2: y1, stroke: st.trackHi, 'stroke-width': .7 });
+ const cap = svgEl(s, 'g', {});
+ drawCap(cap, cur.caps, x);
+ caps.push(cap); place(i);
+ const hit = svgEl(s, 'rect', { x: x - dx / 2, y: y0 - 6, width: dx, height: y1 - y0 + 12, fill: 'transparent' });
+ hit.style.cursor = 'ns-resize';
+ dragY(hit, pct => set(i, (100 - pct) / 100 * 60));
+ });
+ };
+ const setStyle = (axis, name) => {
+ if (!ST[axis] || !ST[axis][name]) return;
+ cur[axis] = name; render();
+ };
+ render();
onChange(null, 'drag a band');
- return { el: s, get: () => vals.slice(), set };
+ return { el: s, get: () => vals.slice(), set, setStyle };
+};
+/* named styles per axis: dot = swatch color for pickers. caps is the cap
+ shape; capColor is the cap finish (body + rib + index line), applicable to
+ any shape. */
+GW.filterBank.STYLES = {
+ panel: {
+ lab: { dot: '#e9e4cf', plate: 'url(#fbPlateLab)', edge: '#b7b29a', bevel: 'rgba(255,255,255,.35)', ink: '#3a3128', dim: '#6a6152', screw: '#a8a496', track: '#0c0b09', trackW: 2.4, trackHi: null },
+ silver: { dot: '#c9c9c5', plate: 'url(#fbPlateSilver)', edge: '#8e8e88', bevel: 'rgba(255,255,255,.4)', ink: '#33332f', dim: '#5c5c56', screw: '#9c9c96', track: '#121210', trackW: 2.0, trackHi: null },
+ studio: { dot: '#1c1a18', plate: 'url(#fbPlateStudio)', edge: '#000', bevel: 'rgba(255,255,255,.06)', ink: '#b9b4a6', dim: '#6f6a5e', screw: '#3c3832', track: '#050505', trackW: 2.2, trackHi: 'rgba(255,255,255,.05)' },
+ },
+ caps: {
+ block: { dot: '#2c2824' },
+ ribbed: { dot: '#6f6a5e' },
+ chrome: { dot: '#dfe6f2' },
+ },
+ capColor: {
+ black: { dot: '#211e1b', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: '#f2f4f2', edge: '#000' },
+ red: { dot: 'var(--sevred)', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: 'var(--sevred)', edge: '#000' },
+ green: { dot: 'var(--sevgrn)', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: 'var(--sevgrn)', edge: '#000' },
+ blue: { dot: '#4f9fe0', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: '#4f9fe0', edge: '#000' },
+ amber: { dot: 'var(--gold-hi)', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: 'var(--gold-hi)', edge: '#000' },
+ chrome: { dot: '#dfe6f2', body: 'url(#sfChrome)', rib: 'rgba(0,0,0,.28)', index: '#14110e', edge: '#3c3a34' },
+ cream: { dot: '#efe9d4', body: 'url(#fbCapCream)', rib: 'rgba(0,0,0,.18)', index: '#3a3128', edge: '#8a8474' },
+ },
};
/* R06 chicken-head selector — tapered lever aims at the position */