diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 21:08:41 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 21:08:41 -0500 |
| commit | acee657f802bd71c2a0cc999c0f1eeaa82c12651 (patch) | |
| tree | a83365227542c30dce5363462939dc58acb84c85 | |
| parent | 3ae44b636f5f55e9fe50e15fdfe7fe6ddfb6d1a9 (diff) | |
| download | archsetup-acee657f802bd71c2a0cc999c0f1eeaa82c12651.tar.gz archsetup-acee657f802bd71c2a0cc999c0f1eeaa82c12651.zip | |
refactor(gallery): extract the shared engine into widgets.js
First componentization batch per the component-generation spec: widgets.js is a classic script exposing the GW namespace (svgEl, polar, drag helpers, seg7, buildBars, VU law, SCREEN_FAMS) plus a css-injection scaffold that grows as builders move in. The gallery loads it via a relative script src and destructures the helpers, so file:// keeps working. Probes green.
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 77 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 122 |
2 files changed, 128 insertions, 71 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 1ffd302..d6e847b 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -692,8 +692,10 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} <div class="palgrid" id="palette"></div> </div> +<script src="widgets.js"></script> <script> const $ = id => document.getElementById(id); +const {svgEl,polar,dragX,dragY,dragDelta,seg7,buildBars,VUDB,vuDb,SCREEN_FAMS}=GW; const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',()=>{ document.body.dataset.size=b.dataset.sz; @@ -777,32 +779,7 @@ const PALETTE=[ white = monochrome LCD/CRT white Exploration data, deliberately NOT in tokens.json yet — families get tokenized once picked, the same way amber earned its tokens. Values keyed by the CSS custom property they drive. */ -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'}, -}; +/* SCREEN_FAMS lives in widgets.js (GW.SCREEN_FAMS) */ function screenChips(no,el,fams,def){ const cardEl=el.closest('.card'); if(!cardEl)return; const row=document.createElement('div'); row.className='famchips'; @@ -1559,42 +1536,7 @@ function card(host, no, name, html, note){ host.appendChild(c); return c; } function setRd(no,txt){const e=$('rd-'+no);if(e)e.textContent=txt;} -function buildBars(el,n){el.innerHTML='';for(let k=0;k<n;k++)el.appendChild(document.createElement('i'));} - -/* ---- pointer-drag helpers ---- */ -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>'; -} +/* drag helpers, seg7, buildBars live in widgets.js (GW) */ /* ============ CONTROLS ============ */ const C=$('controls'); @@ -2224,13 +2166,9 @@ setRd('N24','click to cycle'); conns=['0-5','2-7'];draw();window.addEventListener('resize',draw);})(); /* ============ REFERENCE-BATCH (R) WIDGETS — SVG builders ============ */ -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)];} +/* svgEl, polar, VUDB live in widgets.js (GW) */ /* R01 moving-coil VU: pivot below the face, authentic nonlinear dB scale */ -const VUDB=[[-20,0],[-10,.20],[-7,.31],[-5,.40],[-3,.50],[-2,.58],[-1,.66],[0,.75],[1,.84],[2,.91],[3,1]]; const mcvu={t:.35,needle:null}; (function(){const s=$('mcvu'),cx=75,cy=112,sweep=t=>-43+t*86; const defs=svgEl(s,'defs',{}); @@ -2353,10 +2291,7 @@ function setBake(v){bakeV=v;bakeIdx.setAttribute('transform',`rotate(${v*2.7-135 setRd('R04',(v/10).toFixed(1)+' / 10');} dragDelta($('bakek'),()=>bakeV,setBake,{min:0,max:100,sens:0.25});setBake(63); -/* shared: interpolate a dB reading from a 0..1 sweep position (VU law) */ -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)));} +/* vuDb (VU law) lives in widgets.js (GW) */ /* R05 filter slider bank: one fader per band, arrow caps on black tracks */ const FB_FREQ=[78,136,235,406,701,1210,2090,3620]; diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js new file mode 100644 index 0000000..b88981e --- /dev/null +++ b/docs/prototypes/widgets.js @@ -0,0 +1,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; +})(); |
