aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-12 22:43:53 -0500
committerCraig Jennings <c@cjennings.net>2026-07-12 22:43:53 -0500
commita95200c188327dc17da23710a65d2d19a3c6b485 (patch)
treeefbeda5f508936996c955843f412c7a1bb36ae01 /docs/prototypes/widgets.js
parent066bdac1c195fa27d4b71f2ba53ee9265ee7c833 (diff)
downloadarchsetup-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.js260
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() {