diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 22:43:53 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 22:43:53 -0500 |
| commit | a95200c188327dc17da23710a65d2d19a3c6b485 (patch) | |
| tree | efbeda5f508936996c955843f412c7a1bb36ae01 /docs/prototypes/widgets.js | |
| parent | 066bdac1c195fa27d4b71f2ba53ee9265ee7c833 (diff) | |
| download | archsetup-a95200c188327dc17da23710a65d2d19a3c6b485.tar.gz archsetup-a95200c188327dc17da23710a65d2d19a3c6b485.zip | |
refactor(gallery): extract indicators R35, R36, R45-R47, R52 into GW builders
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 4cd25eb..2c57935 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -3521,6 +3521,266 @@ GW.radarSweep = function (host, opts = {}) { return { el: s, get: () => a, tick }; }; +/* R35 day-date disc calendar — coaxial printed discs rotate so today reads + under the fixed hand; starts on today; click rolls midnight forward */ +GW.dayDateCal = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']; + const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 65, HAND = 135; + gradDef('ddFace', 'radialGradient', { cx: '46%', cy: '40%', r: '75%' }, [['0', '#efe9da'], ['1', '#cfc8b6']]); + /* movement plate + white calendar disc */ + svgEl(s, 'circle', { cx, cy, r: 62, fill: '#17140f', stroke: '#060505', 'stroke-width': 2 }); + for (let k = 0; k < 5; k++) { + const [x, y] = polar(cx, cy, 58, 20 + k * 72); + svgEl(s, 'circle', { cx: x, cy: y, r: 1.8, fill: '#060505' }); + } + svgEl(s, 'circle', { cx, cy, r: 54, fill: 'url(#ddFace)', stroke: '#8f897b', 'stroke-width': .8 }); + /* date ring: 31 radial numerals on its own group */ + const dateG = svgEl(s, 'g', {}); + for (let d = 1; d <= 31; d++) { + const a = (d - 1) * (360 / 31); + const g = svgEl(dateG, 'g', { transform: `rotate(${a},${cx},${cy})` }); + svgEl(g, 'text', { + x: cx, y: cy - 46, 'text-anchor': 'middle', 'font-size': 7.5, 'font-weight': 700, + 'font-family': 'var(--mono)', fill: '#14110e' + }).textContent = String(d); + } + /* inner weekday disc: 7 names twice around, separated by a hairline ring */ + svgEl(s, 'circle', { cx, cy, r: 38, fill: 'none', stroke: '#8f897b', 'stroke-width': .7 }); + const dayG = svgEl(s, 'g', {}); + for (let i = 0; i < 14; i++) { + const a = i * (360 / 14); + const g = svgEl(dayG, 'g', { transform: `rotate(${a},${cx},${cy})` }); + svgEl(g, 'text', { + x: cx, y: cy - 28, 'text-anchor': 'middle', 'font-size': 6.8, 'font-weight': 700, + 'font-family': 'var(--mono)', fill: '#14110e' + }).textContent = DAYS[i % 7]; + } + /* fixed yellow hand from hub toward the read index, and the hub */ + const [hx, hy] = polar(cx, cy, 44, HAND); + svgEl(s, 'line', { x1: cx, y1: cy, x2: hx, y2: hy, stroke: '#e8cf4a', 'stroke-width': 3, 'stroke-linecap': 'round' }); + svgEl(s, 'line', { x1: cx, y1: cy, x2: hx, y2: hy, stroke: 'rgba(0,0,0,.25)', 'stroke-width': .8 }); + svgEl(s, 'circle', { cx, cy, r: 6, fill: '#b8b2a4', stroke: '#5c574c', 'stroke-width': 1.2 }); + svgEl(s, 'circle', { cx, cy, r: 2.4, fill: '#7a7466' }); + s.style.cursor = 'pointer'; + let date, day; + const set = (d, w) => { + date = d; day = w; + dateG.setAttribute('transform', `rotate(${(HAND - (d - 1) * (360 / 31)).toFixed(2)},${cx},${cy})`); + dayG.setAttribute('transform', `rotate(${(HAND - w * (360 / 14)).toFixed(2)},${cx},${cy})`); + onChange([d, w], DAYS[w] + ' ' + d); + }; + s.addEventListener('click', () => set(date % 31 + 1, (day + 1) % 7)); + const now = new Date(); + set(opts.date !== undefined ? opts.date : now.getDate(), + opts.day !== undefined ? opts.day : now.getDay()); + return { el: s, get: () => [date, day], set }; +}; + +/* R36 LED dot matrix — 8x8 paintable bitmap behind a tinted window; click dots + to paint; starts on the reference K */ +GW.dotMatrix = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const N = 8, STEP = 11, X0 = 10.5, Y0 = 10.5; + const glyph = opts.glyph || ['10000100', '10001000', '10010000', '11100000', '10100000', '10010000', '10001000', '10000100']; + const s = stageSvg(host, 'rsvg press', 108, 108); + svgEl(s, 'rect', { x: 2, y: 2, width: 104, height: 104, rx: 8, fill: '#17140f', stroke: '#060505', 'stroke-width': 1.5 }); + svgEl(s, 'rect', { x: 6, y: 6, width: 96, height: 96, rx: 5, fill: '#0d0505', stroke: '#241010', 'stroke-width': 1 }); + let lit = 0; + const paint = (d, on) => { + d.on = on; + d.el.setAttribute('fill', on ? '#ff4a30' : '#2a1210'); + d.el.setAttribute('filter', on ? 'drop-shadow(0 0 3.5px rgba(255,74,48,.85))' : 'none'); + }; + for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) { + const el = svgEl(s, 'circle', { cx: X0 + c * STEP + STEP / 2, cy: Y0 + r * STEP + STEP / 2, r: 4.1, fill: '#2a1210' }); + el.style.cursor = 'pointer'; + const d = { el, on: false }; + paint(d, glyph[r][c] === '1'); if (d.on) lit++; + el.addEventListener('click', () => { + paint(d, !d.on); lit += d.on ? 1 : -1; + onChange(lit, lit + ' / 64 lit'); + }); + } + onChange(lit, lit + ' / 64 lit'); + return { el: s, get: () => lit }; +}; + +/* R45 flip-disc tile array — bistable mechanical pixels; scaleX flip with the + color swap at the midpoint; click a disc to flip it */ +GW.flipDisc = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const COLS = 7, ROWS = 5, STEP = 14, X0 = 13, Y0 = 13; + const glyph = opts.glyph || ['0000100', '0000010', '1111111', '0000010', '0000100']; /* arrow right */ + const s = stageSvg(host, 'rsvg press', 120, 92); + svgEl(s, 'rect', { x: 2, y: 2, width: 116, height: 88, rx: 7, fill: '#17140f', stroke: '#060505', 'stroke-width': 1.5 }); + let lit = 0; + for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { + const el = svgEl(s, 'circle', { cx: X0 + c * STEP + STEP / 2 - 2, cy: Y0 + r * STEP + STEP / 2 - 2, r: 5.6, stroke: '#060505', 'stroke-width': .8 }); + el.style.transformBox = 'fill-box'; el.style.transformOrigin = 'center'; el.style.transition = 'transform .11s ease-in'; + el.style.cursor = 'pointer'; + const d = { el, on: glyph[r][c] === '1' }; + el.setAttribute('fill', d.on ? '#e3d44f' : '#141210'); if (d.on) lit++; + el.addEventListener('click', () => { + d.on = !d.on; lit += d.on ? 1 : -1; + if (matchMedia('(prefers-reduced-motion: reduce)').matches) { el.setAttribute('fill', d.on ? '#e3d44f' : '#141210'); } + else { + el.style.transform = 'scaleX(0)'; + setTimeout(() => { el.setAttribute('fill', d.on ? '#e3d44f' : '#141210'); el.style.transform = 'scaleX(1)'; }, 115); + } + onChange(lit, lit + ' discs showing'); + }); + } + onChange(lit, lit + ' discs showing'); + return { el: s, get: () => lit }; +}; + +/* R46 dekatron counting ring — each click pulses the neon glow one cathode + around; the wrap flashes the carry dot */ +GW.dekatron = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 120, 120), cx = 60, cy = 60; + svgEl(s, 'circle', { cx, cy, r: 52, fill: '#171310', stroke: '#060505', 'stroke-width': 2 }); + svgEl(s, 'circle', { cx, cy, r: 46, fill: '#0d0a08', stroke: '#2a221c', 'stroke-width': 1 }); + svgEl(s, 'ellipse', { cx: cx - 14, cy: cy - 18, rx: 20, ry: 11, fill: 'rgba(255,255,255,.05)', transform: `rotate(-24,${cx - 14},${cy - 18})` }); + const dots = []; + for (let i = 0; i < 10; i++) { + const [x, y] = polar(cx, cy, 34, i * 36); + svgEl(s, 'text', { + x: polar(cx, cy, 43, i * 36)[0], y: polar(cx, cy, 43, i * 36)[1] + 2, 'text-anchor': 'middle', 'font-size': 5.2, + 'font-family': 'var(--mono)', fill: 'var(--steel)' + }).textContent = String(i); + dots.push(svgEl(s, 'circle', { cx: x, cy: y, r: 3.4, fill: '#3a2a20' })); + } + const carry = svgEl(s, 'circle', { cx, cy: cy - 6, r: 3, fill: '#2a1c14' }); + svgEl(s, 'text', { + x: cx, y: cy + 8, 'text-anchor': 'middle', 'font-size': 4.6, 'letter-spacing': '.12em', + 'font-family': 'var(--mono)', fill: 'var(--steel)' + }).textContent = 'CARRY'; + let pos = opts.count || 0, carries = 0; + const draw = () => { + dots.forEach((d, i) => { + const on = i === pos; + d.setAttribute('fill', on ? '#ff9a4c' : '#3a2a20'); + d.setAttribute('filter', on ? 'drop-shadow(0 0 5px rgba(255,154,76,.9))' : 'none'); + }); + onChange(pos, 'COUNT ' + pos + (carries ? ' · CARRY x' + carries : '')); + }; + s.style.cursor = 'pointer'; + s.addEventListener('click', () => { + pos = (pos + 1) % 10; + if (pos === 0) { + carries++; carry.setAttribute('fill', '#ff9a4c'); + carry.setAttribute('filter', 'drop-shadow(0 0 4px rgba(255,154,76,.9))'); + setTimeout(() => { carry.setAttribute('fill', '#2a1c14'); carry.removeAttribute('filter'); }, 220); + } + draw(); + }); + draw(); + return { el: s, get: () => pos }; +}; + +/* R47 landing gear indicator — three greens or nothing; the lever cycles + down → transit (amber pulse) → up */ +GW.gearIndicator = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 140, 110); + svgEl(s, 'rect', { x: 2, y: 2, width: 136, height: 106, rx: 9, fill: '#1c1916', stroke: '#060505', 'stroke-width': 1.5 }); + const POS = [['NOSE', 48, 28], ['L', 24, 74], ['R', 72, 74]]; + const lamps = {}; + POS.forEach(([n, x, y]) => { + svgEl(s, 'circle', { cx: x, cy: y, r: 9, fill: '#0d0b09', stroke: '#2c2820', 'stroke-width': 1.5 }); + lamps[n] = svgEl(s, 'circle', { cx: x, cy: y, r: 6.2, fill: '#16240f' }); + svgEl(s, 'text', { + x, y: y + 18, 'text-anchor': 'middle', 'font-size': 5.2, 'letter-spacing': '.08em', + 'font-family': 'var(--mono)', fill: 'var(--steel)' + }).textContent = n; + }); + /* gear lever: slot + wheel-shaped knob */ + svgEl(s, 'rect', { x: 106, y: 22, width: 8, height: 66, rx: 4, fill: '#0d0b09', stroke: '#2c2820', 'stroke-width': 1 }); + const lever = svgEl(s, 'g', {}); + svgEl(lever, 'rect', { x: 107.5, y: 36, width: 5, height: 26, fill: '#8f8a7e' }); + svgEl(lever, 'circle', { cx: 110, cy: 32, r: 8, fill: '#26221c', stroke: '#4a463e', 'stroke-width': 2.4 }); + svgEl(lever, 'circle', { cx: 110, cy: 32, r: 3, fill: '#4a463e' }); + svgEl(s, 'text', { + x: 110, y: 100, 'text-anchor': 'middle', 'font-size': 5.2, 'letter-spacing': '.08em', + 'font-family': 'var(--mono)', fill: 'var(--steel)' + }).textContent = 'GEAR'; + let state = opts.state || 'down', busy = false; + const paint = () => { + const c = state === 'down' ? 'var(--pass)' : state === 'transit' ? 'var(--amber-warn)' : '#16240f'; + for (const n of Object.keys(lamps)) { + lamps[n].setAttribute('fill', c); + lamps[n].setAttribute('filter', state === 'down' ? 'drop-shadow(0 0 4px rgba(116,147,47,.8))' : + state === 'transit' ? 'drop-shadow(0 0 4px rgba(var(--glow-lo),.7))' : 'none'); + lamps[n].style.animation = state === 'transit' ? 'pulse var(--pulse-rate) ease-in-out infinite' : 'none'; + } + lever.setAttribute('transform', state === 'up' ? 'translate(0,-8)' : state === 'transit' ? 'translate(0,-4)' : ''); + onChange(state, state === 'down' ? 'GEAR DOWN · 3 GREEN' : state === 'up' ? 'GEAR UP' : 'IN TRANSIT'); + }; + s.style.cursor = 'pointer'; + s.addEventListener('click', () => { + if (busy) return; + const target = state === 'down' ? 'up' : 'down'; + if (matchMedia('(prefers-reduced-motion: reduce)').matches) { state = target; paint(); return; } + busy = true; state = 'transit'; paint(); + setTimeout(() => { state = target; busy = false; paint(); }, 1500); + }); + paint(); + return { el: s, get: () => state }; +}; + +/* R52 blinkenlights front panel — address/data lamps ripple with a live + pseudo-PC (builder-owned clock, reduced-motion gated) that folds in the + switch register; flip the SR bits and the pattern changes */ +GW.blinkenlights = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 170, 100); + svgEl(s, 'rect', { x: 2, y: 2, width: 166, height: 96, rx: 8, fill: '#3a1420', stroke: '#060505', 'stroke-width': 1.5 }); + svgEl(s, 'rect', { x: 8, y: 8, width: 154, height: 62, rx: 5, fill: '#14090e' }); + const mkRow = (y, lbl) => { + const row = []; + svgEl(s, 'text', { + x: 12, y: y + 2.6, 'font-size': 4.6, 'letter-spacing': '.08em', 'font-family': 'var(--mono)', + fill: '#b08a96' + }).textContent = lbl; + for (let i = 0; i < 12; i++) row.push(svgEl(s, 'circle', { cx: 46 + i * 10, cy: y, r: 3, fill: '#2a1210' })); + return row; + }; + const addr = mkRow(24, 'ADDR'), data = mkRow(46, 'DATA'); + const paint = (row, val) => row.forEach((d, i) => { + const on = (val >> (11 - i)) & 1; + d.setAttribute('fill', on ? '#ffb43a' : '#2a1210'); + d.setAttribute('filter', on ? 'drop-shadow(0 0 3px rgba(255,180,58,.8))' : 'none'); + }); + /* switch register: 8 mini toggles */ + let sr = opts.sr !== undefined ? opts.sr : 0b10100101; + const toggles = []; + for (let i = 0; i < 8; i++) { + const x = 46 + i * 14; + svgEl(s, 'rect', { x: x - 3, y: 76, width: 6, height: 16, rx: 3, fill: '#0d0b09', stroke: '#4a2432', 'stroke-width': 1 }); + const t = svgEl(s, 'circle', { cx: x, cy: 80, r: 4, fill: '#8f8a7e', stroke: '#3c382f', 'stroke-width': 1 }); + t.style.cursor = 'pointer'; + const hit = svgEl(s, 'rect', { x: x - 7, y: 72, width: 14, height: 24, fill: 'transparent' }); + hit.style.cursor = 'pointer'; + hit.addEventListener('click', () => { sr ^= (1 << (7 - i)); draw(); }); + toggles.push(t); + } + svgEl(s, 'text', { + x: 12, y: 83, 'font-size': 4.6, 'letter-spacing': '.08em', 'font-family': 'var(--mono)', + fill: '#b08a96' + }).textContent = 'SR'; + let pc = 0o1234; + const draw = () => { + toggles.forEach((t, i) => t.setAttribute('cy', ((sr >> (7 - i)) & 1) ? 76 : 84)); + onChange(sr, 'SR ' + sr.toString(8).padStart(3, '0') + ' (octal)'); + }; + const tick = () => { pc = ((pc * 5 + sr + 1) & 0xFFF); paint(addr, pc); paint(data, (pc ^ (pc << 3) ^ sr) & 0xFFF); }; + draw(); tick(); + if (!matchMedia('(prefers-reduced-motion: reduce)').matches) setInterval(tick, 250); + return { el: s, get: () => sr, tick }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
