aboutsummaryrefslogtreecommitdiff
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
parent066bdac1c195fa27d4b71f2ba53ee9265ee7c833 (diff)
downloadarchsetup-a95200c188327dc17da23710a65d2d19a3c6b485.tar.gz
archsetup-a95200c188327dc17da23710a65d2d19a3c6b485.zip
refactor(gallery): extract indicators R35, R36, R45-R47, R52 into GW builders
-rw-r--r--docs/prototypes/panel-widget-gallery.html188
-rw-r--r--docs/prototypes/widgets.js260
2 files changed, 272 insertions, 176 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index f81b60b..2eae646 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -1868,22 +1868,22 @@ card(I,'R31','Radar sweep',
(st,rd)=>GW.radarSweep(st,{onChange:(v,t)=>rd(t)}),
'<b>the rotating scan.</b> A sweep beam circles the bearing ring, contacts bloom as it passes and fade after. Click to mark the current bearing. After a radar PPI scope.');
card(I,'R35','Day-date disc calendar',
- `<svg id="daydate" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ (st,rd)=>GW.dayDateCal(st,{onChange:(v,t)=>rd(t)}),
'<b>two coaxial discs, one index.</b> The outer ring carries the date, the inner disc the weekday; both read where the yellow hand points. Starts on today; click to roll midnight forward and watch the discs step. After a watch day-date movement.');
card(I,'R36','LED dot matrix',
- `<svg id="dotmx" class="rsvg press" viewBox="0 0 108 108" width="108" height="108"></svg>`,
+ (st,rd)=>GW.dotMatrix(st,{onChange:(v,t)=>rd(t)}),
'<b>a bitmap, one red dot at a time.</b> 64 discrete LEDs behind a tinted window — segments draw digits, but a matrix draws anything. Click dots to paint the pattern; the readout counts what is lit. After the K4816 pattern generator faceplate.');
card(I,'R45','Flip-disc tile array',
- `<svg id="flipdisc" class="rsvg press" viewBox="0 0 120 92" width="120" height="92"></svg>`,
+ (st,rd)=>GW.flipDisc(st,{onChange:(v,t)=>rd(t)}),
'<b>the mechanical pixel.</b> Each disc flips between its yellow face and its black back and then STAYS — no power, no glow, state that persists. Click a disc and watch it flip. The dot matrix glows; this one clacks. After transit flip-dot signs.');
card(I,'R46','Dekatron counting ring',
- `<svg id="dekatron" class="rsvg press" viewBox="0 0 120 120" width="120" height="120"></svg>`,
+ (st,rd)=>GW.dekatron(st,{onChange:(v,t)=>rd(t)}),
'<b>a count as a position, and the counting is visible.</b> Each click is a pulse; the neon glow steps one cathode around the ring and carries on wrap. Nixies show the digit — the Dekatron shows the arithmetic. After glow-transfer counter tubes.');
card(I,'R47','Landing gear indicator',
- `<svg id="gearind" class="rsvg press" viewBox="0 0 140 110" width="140" height="110"></svg>`,
+ (st,rd)=>GW.gearIndicator(st,{onChange:(v,t)=>rd(t)}),
'<b>three greens or nothing.</b> A spatial lamp panel: nose and mains each get a light, green only when down and locked; the amber transit pulse means neither state. Click the wheel lever to cycle the gear. After a cockpit gear panel.');
card(I,'R52','Blinkenlights front panel',
- `<svg id="blinken" class="rsvg press" viewBox="0 0 170 100" width="170" height="100"></svg>`,
+ (st,rd)=>GW.blinkenlights(st,{onChange:(v,t)=>rd(t)}),
'<b>the machine thinking, out loud.</b> Address and data lamps ripple with the live computation; the switch register below is yours — flip bits and watch the pattern change. DIP switches hold config; this shows work happening. After a PDP-11/70 front panel.');
/* ============ CONTROL WIRING ============ */
@@ -1994,179 +1994,15 @@ if(!reduced){
document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused');
}
-/* R35 day-date disc calendar: coaxial printed discs rotate so today reads under the fixed hand */
-const DD_DAYS=['SUN','MON','TUE','WED','THU','FRI','SAT'];
-let ddDate=1,ddDay=0,ddDateG=null,ddDayG=null;
-(function(){const s=$('daydate'),cx=65,cy=65,HAND=135;
- const defs=svgEl(s,'defs',{});
- const fc=svgEl(defs,'radialGradient',{id:'ddFace',cx:'46%',cy:'40%',r:'75%'});
- svgEl(fc,'stop',{offset:'0','stop-color':'#efe9da'});
- svgEl(fc,'stop',{offset:'1','stop-color':'#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
- ddDateG=svgEl(s,'g',{});
- for(let d=1;d<=31;d++){const a=(d-1)*(360/31);
- const g=svgEl(ddDateG,'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});
- ddDayG=svgEl(s,'g',{});
- for(let i=0;i<14;i++){const a=i*(360/14);
- const g=svgEl(ddDayG,'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=DD_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';
- s.addEventListener('click',()=>{setDdcal(ddDate%31+1,(ddDay+1)%7);});})();
-function setDdcal(d,w){ddDate=d;ddDay=w;const HAND=135;
- ddDateG.setAttribute('transform',`rotate(${(HAND-(d-1)*(360/31)).toFixed(2)},65,65)`);
- ddDayG.setAttribute('transform',`rotate(${(HAND-w*(360/14)).toFixed(2)},65,65)`);
- setRd('R35',DD_DAYS[w]+' '+d);}
-(function(){const now=new Date();setDdcal(now.getDate(),now.getDay());})();
-
-/* R36 LED dot matrix: 8x8 paintable bitmap, starts on the reference K */
-(function(){const s=$('dotmx'),N=8,STEP=11,X0=10.5,Y0=10.5;
- const K8=['10000100','10001000','10010000','11100000','10100000','10010000','10001000','10000100'];
- 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});
- const dots=[];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};dots.push(d);
- paint(d,K8[r][c]==='1');if(d.on)lit++;
- el.addEventListener('click',()=>{paint(d,!d.on);lit+=d.on?1:-1;
- setRd('R36',lit+' / 64 lit');});}
- setRd('R36',lit+' / 64 lit');})();
+/* R35, R36 extracted to widgets.js (GW.*) */
+
/* R43, R44 extracted to widgets.js (GW.*) */
-/* R45 flip-disc: bistable mechanical pixels; scaleX flip with color swap at the midpoint */
-(function(){const s=$('flipdisc'),COLS=7,ROWS=5,STEP=14,X0=13,Y0=13;
- const GLYPH=['0000100','0000010','1111111','0000010','0000100']; // arrow right
- svgEl(s,'rect',{x:2,y:2,width:116,height:88,rx:7,fill:'#17140f',stroke:'#060505','stroke-width':1.5});
- let lit=0;
- const discs=[];
- 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);}
- setRd('R45',lit+' discs showing');});
- discs.push(d);}
- setRd('R45',lit+' discs showing');})();
-
-/* R46 dekatron: click pulses the neon glow one cathode around; wrap = carry */
-let dkPos=0,dkCarry=0;
-(function(){const s=$('dekatron'),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';
- const draw=()=>{dots.forEach((d,i)=>{const on=i===dkPos;
- d.setAttribute('fill',on?'#ff9a4c':'#3a2a20');
- d.setAttribute('filter',on?'drop-shadow(0 0 5px rgba(255,154,76,.9))':'none');});
- setRd('R46','COUNT '+dkPos+(dkCarry?' · CARRY x'+dkCarry:''));};
- s.style.cursor='pointer';
- s.addEventListener('click',()=>{dkPos=(dkPos+1)%10;
- if(dkPos===0){dkCarry++;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();})();
-
-/* R47 landing gear: three greens or nothing; lever cycles down -> transit -> up */
-(function(){const s=$('gearind');
- 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='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)':'');
- setRd('R47',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();})();
-
-/* R52 blinkenlights: live register lamps driven by a pseudo-PC that folds in the switch register */
-(function(){const s=$('blinken');
- 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=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';
- const flip=()=>{sr^=(1<<(7-i));draw();};
- hit.addEventListener('click',flip);
- 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));
- setRd('R52','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);})();
+/* R45-R47, R52 extracted to widgets.js (GW.*) */
+
+
+
/* R53-R56 extracted to widgets.js (GW.*) */
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() {