diff options
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 3399 |
1 files changed, 440 insertions, 2959 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index d67034b..9740c46 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -54,9 +54,28 @@ h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} .masthead p b{color:var(--silver)} .szbar{display:flex;align-items:center;gap:8px;margin-top:.8rem} .szbar .lab{color:var(--steel);font-size:.68rem;letter-spacing:.22em;text-transform:uppercase;margin-right:4px} + +/* masthead row: description left, index panel right */ +.mastrow{display:flex;gap:28px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap} +.toc{position:relative;flex:0 0 auto;min-width:220px;padding:14px 20px 13px;border-radius:12px; + background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320; + box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.4)} +.toc .tt{color:var(--steel);font-size:.66rem;letter-spacing:.26em;text-transform:uppercase; + margin-bottom:7px;display:flex;align-items:center;gap:10px} +.toc .tt::after{content:"";height:1px;background:var(--wash);flex:1;min-width:24px} +.toc a{display:block;color:var(--dim);text-decoration:none;font-size:.8rem;letter-spacing:.04em;padding:3.5px 0} +.toc a::before{content:"▸ ";color:var(--steel);font-size:.72em} +.toc a:hover{color:var(--gold-hi)} +.toc .scr{position:absolute;width:5px;height:5px;border-radius:50%; + background:radial-gradient(circle at 35% 30%,#524d43,#161310);box-shadow:0 .5px 0 rgba(255,255,255,.08)} +html{scroll-behavior:smooth} /* size toggle scales the widget stage only; card text stays at its own (slightly raised) size */ body[data-size="2"] .stagew{zoom:1.7} body[data-size="3"] .stagew{zoom:2.4} +/* intrinsically tiny widgets get an extra bump so they do not float in an empty stage */ +.stagew.boost{zoom:1.5} +body[data-size="2"] .stagew.boost{zoom:2.55} +body[data-size="3"] .stagew.boost{zoom:3.6} body[data-size="2"] .grid{grid-template-columns:repeat(auto-fill,minmax(380px,1fr))} body[data-size="3"] .grid{grid-template-columns:repeat(auto-fill,minmax(520px,1fr))} body[data-size="2"] .wrap,body[data-size="3"] .wrap{max-width:none} @@ -69,7 +88,10 @@ body[data-size="2"] .wrap,body[data-size="3"] .wrap{max-width:none} .swatch .sw.txt{display:flex;align-items:center;justify-content:center;color:var(--steel);font-size:.62rem; background:var(--well);padding:0 6px;text-align:center;overflow:hidden} .swatch .swn{color:var(--cream);font-size:.8rem} -.swatch .swv{color:var(--dim);font-size:.74rem;font-variant-numeric:tabular-nums;overflow-wrap:anywhere} +.swatch .swv{color:var(--dim);font-size:.72rem;line-height:1.35} +.palhead{grid-column:1/-1;color:var(--steel);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase; + margin-top:.8rem;display:flex;align-items:center;gap:12px} +.palhead::after{content:"";height:1px;background:var(--wash);flex:1} h2{color:var(--steel);font-size:.74rem;letter-spacing:.24em;text-transform:uppercase; margin:2.2rem 0 .2rem;display:flex;align-items:center;gap:12px} h2::after{content:"";height:1px;background:var(--wash);flex:1} @@ -80,12 +102,40 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 6px 14px rgba(0,0,0,.4)} .wname{color:var(--cream);font-size:.95rem;font-weight:700;display:flex;align-items:center;gap:8px} .wname .no{color:var(--panel);background:var(--gold);border-radius:4px;font-size:.7rem;padding:0 5px;font-weight:400} +/* validation lamp: off = not done, amber = in progress, green = done; click cycles, state persists */ +.vlamp{margin-left:auto;flex:0 0 auto;width:10px;height:10px;border-radius:50%;cursor:pointer; + background:#221f1b;border:1px solid #33302b} +.vlamp[data-v="amber"],.vtally .vdot[data-v="amber"],#vaudit .vdot[data-v="amber"]{background:var(--gold);border-color:#7d5c16;box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)} +.vlamp[data-v="green"],.vtally .vdot[data-v="green"],#vaudit .vdot[data-v="green"]{background:var(--pass);border-color:#4a5c22;box-shadow:0 0 6px 1px rgba(116,147,47,.6)} +/* audit stepper pill: docks bottom-right while stepping through one state's cards */ +#vaudit{position:fixed;right:18px;bottom:16px;z-index:60;display:flex;align-items:center;gap:7px; + padding:8px 13px;border-radius:20px;background:var(--raise);border:1px solid var(--wash); + color:var(--cream);font-size:.78rem;letter-spacing:.05em;cursor:pointer;user-select:none; + box-shadow:0 4px 14px rgba(0,0,0,.5)} +#vaudit .vdot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#221f1b;border:1px solid #33302b} +#vaudit .vx{margin-left:6px;color:var(--steel);padding:0 2px} +#vaudit .vx:hover{color:var(--fail)} +/* validation tally in the index: .vdot mirrors the lamp look but must NOT match + .vlamp — the counter counts .vlamp nodes */ +.vtally{margin-top:2px} +.vtally .vrow{display:flex;align-items:center;gap:7px;color:var(--dim);font-size:.75rem; + letter-spacing:.04em;padding:2.5px 0} +.vtally .vrow[data-v]{cursor:pointer} +.vtally .vrow[data-v]:hover{color:var(--gold-hi)} +.vtally .vdot{flex:0 0 auto;width:8px;height:8px;border-radius:50%; + background:#221f1b;border:1px solid #33302b} +.vtally .vn{margin-left:auto;color:var(--cream);font-variant-numeric:tabular-nums} +.vtally .vtot{border-top:1px solid var(--wash);margin-top:3px;padding-top:4.5px;color:var(--steel)} .stagew{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:14px 12px; min-height:78px;display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap} .wrd{color:var(--gold-hi);font-size:.8rem;letter-spacing:.06em;font-variant-numeric:tabular-nums; - min-height:.95rem;display:flex;align-items:center;gap:6px} + min-height:.95rem;display:flex;align-items:center;gap:6px; + border-top:1px solid var(--wash);padding-top:9px;margin-top:2px} .wrd::before{content:"▸";color:var(--steel);font-size:.7rem} -.wnote{color:var(--dim);font-size:.85rem;line-height:1.45} +.opts{display:flex;flex-direction:column;gap:5px} +.opts:empty{display:none} +.wnote{color:var(--dim);font-size:.85rem;line-height:1.45; + border-top:1px solid var(--wash);padding-top:9px;margin-top:2px} .wnote b{color:var(--steel);font-weight:400} /* per-card spec sheet (collapsible) */ @@ -99,506 +149,31 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .igrid .ik{color:var(--steel);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;padding-top:1px} .igrid .iv{color:var(--dim);font-size:.82rem;line-height:1.45} .igrid .iv b{color:var(--silver);font-weight:400} - -/* ---- shared primitives ---- */ -.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)} -.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)} -.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)} -.lamp.off{background:var(--wash);box-shadow:none} -.lamp.busy{background:var(--gold);animation:pulse var(--pulse-rate) ease-in-out infinite} -/* standard pulse: 1s ease-in-out — the norm for every pulsing / flashing element */ -@keyframes pulse{50%{opacity:.25}} - -.switch{width:40px;height:21px;border-radius:11px;background:var(--wash);border:1px solid var(--slate); - position:relative;cursor:pointer} -.switch::after{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%; - background:var(--dim);transition:left .15s} -.switch.on{background:var(--slate);border-color:var(--gold)} -.switch.on::after{left:21px;background:var(--gold)} -.switch.red{background:rgba(203,107,77,.2);border-color:var(--fail)} -.switch.red::after{background:var(--fail);left:2px} - -.key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer; - background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a; - border-radius:8px;padding:8px 12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} -.key:hover{color:var(--gold);border-color:var(--gold)} -.key:active{transform:translateY(1px)} -.key.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi);font-weight:700} -.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)} -.key.off{opacity:.4} - -.chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px} -.chip.on{color:var(--gold);border-color:var(--gold)} - -.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px} -.badge.red{background:var(--fail);color:var(--cream)} -.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)} - -/* fader */ -.fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none} -.fader .slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden} -.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold))} -.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px; - background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)} -/* vertical fader */ -.vfader{width:16px;height:64px;position:relative;cursor:pointer;touch-action:none} -.vfader .slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden} -.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,var(--amber-grad-bot),var(--gold))} -.vfader .cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px; - background:linear-gradient(90deg,var(--amber-grad-mid),var(--amber-grad-top));border:1px solid var(--amber-edge)} - -/* rotary knob */ -.knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none; - background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c; - box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)} -.knob .ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi); - margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)} - -/* needle gauge */ -.gauge{width:96px;cursor:ns-resize;touch-action:none} -.gauge .dial{position:relative;height:48px;overflow:hidden} -.gauge .arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%} -.gauge .tk{position:absolute;left:50%;top:1px;width:1.5px;height:8px;margin-left:-.75px;background:var(--steel);transform-origin:50% 47px} -.gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi); - transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(var(--glow-hi),.5); - transition:transform .12s cubic-bezier(.3,1.3,.5,1)} -.gauge .hub{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--gold)} -.gauge .gv{color:var(--cream);text-align:center;font-size:12px;font-weight:700;margin-top:5px;font-variant-numeric:tabular-nums} - -/* segmented VU / LED bar */ -.vu{width:170px;display:flex;flex-direction:column;gap:5px} -.vurow{display:flex;align-items:center;gap:7px} -.vurow .ch{color:var(--steel);font-size:.6rem;width:8px} -.vubar{flex:1;display:flex;gap:2px;height:9px} -.vubar i{flex:1;background:var(--wash);border-radius:1px;opacity:.3} -.vubar i.on{opacity:1;background:var(--pass)}.vubar i.hot{opacity:1;background:var(--gold)} -.vubar i.clip{opacity:1;background:var(--fail)}.vubar i.peak{outline:1px solid var(--gold-hi);outline-offset:-1px} - -/* mini 4-bar signal */ -.sig{display:flex;align-items:flex-end;gap:2px;height:18px} -.sig i{width:4px;background:var(--wash);border-radius:1px} -.sig i:nth-child(1){height:5px}.sig i:nth-child(2){height:9px}.sig i:nth-child(3){height:13px}.sig i:nth-child(4){height:17px} -.sig i.on{background:var(--pass)}.sig i.hot{background:var(--gold)}.sig i.clip{background:var(--fail)} - -/* signal ladder (wifi bars) */ -.ladder{display:inline-flex;gap:3px;align-items:flex-end;height:18px;cursor:pointer} -.ladder i{width:5px;background:var(--wash);border-radius:1px} -.ladder i:nth-child(1){height:6px}.ladder i:nth-child(2){height:10px} -.ladder i:nth-child(3){height:14px}.ladder i:nth-child(4){height:18px} - -/* linear progress / fuel bar */ -.bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative;cursor:pointer;touch-action:none} -.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold));border-radius:6px} -.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))} - -/* radial ring */ -.ring{width:60px;height:60px;border-radius:50%;cursor:ns-resize;touch-action:none; - background:conic-gradient(var(--gold) calc(var(--p)*1%),var(--wash) 0); - display:grid;place-items:center;position:relative} -.ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)} -.ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums} - -/* tabular readout */ -.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer} -.readout small{color:var(--dim);font-size:12px;font-weight:400} -.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px} - -/* sparkline */ -.spark{width:170px;height:44px} -.spark svg{display:block;width:100%;height:100%} - -/* lamp row (list item) */ -.lrow{width:190px;display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px;background:#141210;cursor:pointer;font-size:12.5px} -.lrow:hover{background:var(--wash)} -.lrow .who{color:var(--silver)}.lrow .who b{color:var(--cream)} -.lrow .what{margin-left:auto;color:var(--dim);font-size:11px} - -/* arm-to-fire */ -.arm{font:inherit;font-size:11.5px;color:var(--silver);cursor:pointer;background:#191715;border:1px solid #33302b; - border-radius:8px;padding:7px 12px} -.arm.armed{background:rgba(203,107,77,.12);border-color:var(--fail);color:var(--fail)} - -/* stepper / segmented selector */ -.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden} -.seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer} -.seg button:last-child{border-right:0} -.seg button.on{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700} - -/* engraved section label */ -.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer; - display:flex;align-items:center;gap:9px} -.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1} -.engrave::before{max-width:10px} -.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none} - -/* waveform strip */ -.wave{width:170px;height:38px} -.wave svg{width:100%;height:100%;display:block} - -/* toast */ -.toastw{font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;padding:5px 10px;cursor:pointer} - -/* output well (log step) */ -.owell{width:200px;background:var(--well);border:1px solid var(--wash);border-radius:8px;padding:7px 9px;font-size:11px;cursor:pointer} -.ostep{display:flex;gap:7px;align-items:flex-start;padding:2px 0} -.ostep .lamp{margin-top:3px;width:7px;height:7px} -.ostep b{color:var(--cream);font-weight:700}.ostep .ev{color:var(--steel);display:block;font-size:10.5px} - -/* rotary selector */ -.rotsel{position:relative;width:118px;height:74px} -.rotsel>.knob{position:absolute;left:50%;top:20px;margin-left:-26px;cursor:pointer} -.rotsel .pos{position:absolute;font-size:9px;color:var(--dim);transform:translate(-50%,-50%);letter-spacing:.02em} -.rotsel .pos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)} - -/* slide-rule tuner dial */ -.tuner{width:180px;height:46px;position:relative;border-radius:6px;overflow:hidden;cursor:pointer; - background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c; - box-shadow:inset 0 0 20px rgba(var(--glow-lo),.12),inset 0 1px 0 rgba(255,255,255,.03)} -.tuner .tick{position:absolute;top:6px;width:1px;height:11px;background:var(--steel);transform:translateX(-50%)} -.tuner .mk{position:absolute;bottom:8px;transform:translateX(-50%);color:var(--steel);font-size:10px} -.tuner .mk.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.6)} -.tuner:focus-visible{outline:2px solid var(--gold);outline-offset:2px} -.tuner .ndl{position:absolute;top:3px;bottom:3px;width:2px;margin-left:-1px;border-radius:1px; - background:var(--fail);box-shadow:0 0 7px rgba(203,107,77,.85);transition:left .25s} - -/* nixie tube */ -.nixie{display:inline-flex;gap:5px;cursor:pointer} -.nixie .tube{width:30px;height:44px;border-radius:5px;position:relative;overflow:hidden; - background:radial-gradient(circle at 50% 38%,#241a12,#0b0807);border:1px solid #2c261d; - display:grid;place-items:center;box-shadow:inset 0 0 12px rgba(0,0,0,.6)} -.nixie .tube b{font-size:26px;font-weight:400;color:#ff9a3c; - text-shadow:0 0 6px rgba(255,140,50,.85),0 0 15px rgba(255,120,40,.45)} -.nixie .tube.off b{color:#3a2a1c;text-shadow:none} -.nixie .tube::after{content:"";position:absolute;inset:0;pointer-events:none; - background:linear-gradient(180deg,rgba(255,255,255,.06),transparent 32%)} - -/* ===== candidate-unique CSS ===== */ -/* rocker */ -.rocker{width:66px;height:40px;border-radius:7px;cursor:pointer;position:relative; - background:linear-gradient(180deg,#25211c,#141210);border:1px solid #34302a; - box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5);overflow:hidden} -.rocker .half{position:absolute;left:0;right:0;height:50%;display:flex;align-items:center;justify-content:center; - font-size:12px;letter-spacing:.06em;color:var(--dim)} -.rocker .top{top:0;border-bottom:1px solid #0c0b0a;background:linear-gradient(180deg,#211d19,#181513)} -.rocker .bot{bottom:0;background:linear-gradient(180deg,#141210,#100e0c);box-shadow:inset 0 3px 5px rgba(0,0,0,.55)} -.rocker.on .top{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700;box-shadow:0 0 10px rgba(var(--glow-hi),.35)} -.rocker.on .bot{color:var(--steel)} -.rocker:not(.on) .top{color:var(--steel)} -.rocker:not(.on) .bot{background:linear-gradient(180deg,#2a1512,#1c0f0d);color:var(--fail);box-shadow:inset 0 3px 5px rgba(0,0,0,.55),0 0 8px rgba(203,107,77,.25)} - -/* transport */ -.reels{display:flex;gap:16px;align-items:center;justify-content:center;margin-bottom:2px} -.reel{width:26px;height:26px;border-radius:50%;border:2px solid #34302a;position:relative; - background:radial-gradient(circle at 45% 40%,#211d18,#0d0b09)} -.reel::before{content:"";position:absolute;inset:9px;border-radius:50%;border:1px solid #4a443a;background:#161310} -.reel i{position:absolute;left:50%;top:50%;width:2px;height:11px;background:var(--steel);margin:-5.5px 0 0 -1px;transform-origin:50% 5.5px} -.reel i:nth-child(1){background:var(--gold-hi);box-shadow:0 0 4px rgba(var(--glow-hi),.55)} -.reel i:nth-child(2){transform:rotate(120deg)}.reel i:nth-child(3){transform:rotate(240deg)} -.transport{display:flex;gap:5px} -.tbtn{font:inherit;font-size:12px;color:var(--silver);cursor:pointer;width:30px;height:26px;display:grid;place-items:center; - background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;border-radius:6px} -.tbtn:hover{color:var(--gold);border-color:var(--gold)} -.tbtn.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi)} -.tbtn.rec.on{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)} - -/* radio bank */ -.radiobank{display:flex;gap:0;border:1px solid #0c0b0a;border-radius:7px;overflow:hidden; - background:#0c0b0a;padding:3px;box-shadow:inset 0 2px 4px rgba(0,0,0,.5)} -.preset{font:inherit;font-size:11px;color:var(--dim);cursor:pointer;padding:8px 9px;border:0;border-radius:4px; - background:linear-gradient(180deg,#211d19,#161310);box-shadow:inset 0 1px 0 rgba(255,255,255,.04)} -.preset+.preset{margin-left:3px} -.preset:hover{color:var(--silver)} -.preset.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));font-weight:700; - box-shadow:inset 0 2px 4px rgba(0,0,0,.4)} - -/* concentric dual knob */ -.dualknob{width:64px;height:64px;position:relative} -.dualknob .outer{width:64px;height:64px;border-radius:50%;position:absolute;inset:0;cursor:ns-resize;touch-action:none; - background:radial-gradient(circle at 40% 35%,#2a2622,#100e0c);border:1px solid #3a352c; - box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)} -.dualknob .outer .tick{position:absolute;left:50%;top:3px;width:2px;height:9px;background:var(--steel);margin-left:-1px;transform-origin:50% 29px} -.dualknob .inner{width:34px;height:34px;border-radius:50%;position:absolute;left:15px;top:15px;cursor:ns-resize;touch-action:none; - background:radial-gradient(circle at 40% 35%,#37322a,#1a1713);border:1px solid #4a443a;box-shadow:0 2px 4px rgba(0,0,0,.5)} -.dualknob .inner .ind{position:absolute;left:50%;top:3px;width:2px;height:11px;background:var(--gold-hi);margin-left:-1px; - transform-origin:50% 14px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)} - -/* rotary encoder + LED ring */ -.encoder{position:relative;width:66px;height:66px;display:grid;place-items:center;cursor:ns-resize;touch-action:none} -.encoder .led{position:absolute;width:5px;height:5px;border-radius:50%;background:var(--wash); - left:50%;top:50%;margin:-2.5px} -.encoder .led.on{background:var(--gold);box-shadow:0 0 5px 1px rgba(var(--glow-lo),.7)} -.encoder .knob{width:40px;height:40px;cursor:ns-resize} -.encoder .knob .ind{transform-origin:50% 15px;top:4px;height:12px} - -/* keyed mode switch */ -.keylock{position:relative;width:110px;height:70px} -.keylock .body{position:absolute;left:50%;top:30px;margin-left:-21px;width:42px;height:42px;border-radius:50%; - background:radial-gradient(circle at 42% 36%,#2a2622,#0f0d0b);border:1px solid #3a352c;cursor:pointer; - box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)} -.keylock .barrel{position:absolute;left:50%;top:50%;width:5px;height:5px;margin:-2.5px;border-radius:1px;background:#0a0908; - box-shadow:0 0 2px rgba(0,0,0,.8)} -.keylock .bit{position:absolute;left:50%;top:50%;width:3px;height:15px;margin:-15px 0 0 -1.5px;background:var(--gold-hi); - border-radius:1px;transform-origin:50% 100%;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .2s} -.keylock .kpos{position:absolute;font-size:9px;color:var(--dim);letter-spacing:.05em;transform:translate(-50%,-50%)} -.keylock .kpos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)} - -/* crossfader */ -.xfader{width:160px;height:22px;position:relative;cursor:pointer;touch-action:none} -.xfader .slot{position:absolute;top:9px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18} -.xfader .detent{position:absolute;top:2px;left:50%;width:1px;height:18px;background:var(--steel);margin-left:-.5px;opacity:.7} -.xfader .end{position:absolute;top:11px;font-size:9px;color:var(--steel);transform:translateY(-50%)} -.xfader .cap{position:absolute;top:3px;width:9px;height:16px;border-radius:2px;margin-left:-4.5px;transition:left .05s; - background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)} - -/* thumbwheel */ -.thumbw{display:flex;align-items:center;gap:10px} -.thumbwheel{width:34px;height:52px;border-radius:6px;cursor:ns-resize;touch-action:none;position:relative;overflow:hidden; - border:1px solid #34302a;box-shadow:inset 0 0 8px rgba(0,0,0,.6); - background:repeating-linear-gradient(0deg,#0e0c0a 0 2px,#221e19 2px 3px,#2f2a23 3px 5px,#221e19 5px 6px)} -.thumbwheel::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.55),transparent 30%,transparent 70%,rgba(0,0,0,.55))} -.thumbw .win{color:var(--cream);font-size:15px;font-weight:700;font-variant-numeric:tabular-nums; - background:var(--well);border:1px solid #231f18;border-radius:4px;padding:3px 8px} - -/* DIP bank */ -.dip{display:flex;gap:3px;padding:6px 7px;background:#0f1a2a;border:1px solid #24344a;border-radius:5px; - box-shadow:inset 0 1px 3px rgba(0,0,0,.5)} -.dipsw{width:12px;height:26px;background:#0a1220;border-radius:2px;position:relative;cursor:pointer;border:1px solid #1c2c42} -.dipsw i{position:absolute;left:1px;right:1px;height:11px;border-radius:1px;background:linear-gradient(180deg,#eae4d2,#b4ad98); - bottom:1px;transition:bottom .12s,top .12s} -.dipsw.on i{bottom:auto;top:1px;background:linear-gradient(180deg,var(--amber-grad-top),var(--gold))} - -/* jog / shuttle */ -.jog{position:relative;width:74px;height:74px;cursor:ns-resize;touch-action:none} -.jog .shuttle{position:absolute;inset:0;border-radius:50%;border:6px solid #1a1713; - background:conic-gradient(from -90deg,#3a352c 0 var(--sh,40deg),#141210 0 360deg); - box-shadow:inset 0 0 8px rgba(0,0,0,.6)} -.jog .inner{position:absolute;inset:12px;border-radius:50%;transition:transform .05s linear; - background:radial-gradient(circle at 42% 36%,#2c2822,#100e0c);border:1px solid #3a352c;box-shadow:0 2px 5px rgba(0,0,0,.6)} -.jog .dimple{position:absolute;left:50%;top:7px;width:8px;height:8px;margin-left:-4px;border-radius:50%; - background:radial-gradient(circle at 40% 35%,#151210,#000);box-shadow:inset 0 1px 2px rgba(0,0,0,.9)} - -/* oscilloscope — screen-family vars with the original green as fallback */ -.scope{width:176px;height:78px;border-radius:6px;position:relative;overflow:hidden; - background:radial-gradient(circle at 50% 45%,var(--scr-bgc,#04140a),var(--scr-bge,#020a05)); - border:1px solid var(--scr-brd,#123018); - box-shadow:inset 0 0 16px rgba(0,0,0,.7)} -.scope .grat{position:absolute;inset:0;opacity:.5; - background-image:linear-gradient(var(--scr-grat,rgba(111,206,51,.18)) 1px,transparent 1px),linear-gradient(90deg,var(--scr-grat,rgba(111,206,51,.18)) 1px,transparent 1px); - background-size:22px 19.5px} -.scope .grat::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:var(--scr-gratc,rgba(111,206,51,.35))} -.scope svg{position:absolute;inset:0;width:100%;height:100%} -.scope polyline{fill:none;stroke:var(--scr-hi,var(--phos));stroke-width:1.6;filter:drop-shadow(0 0 3px var(--scr-glow,rgba(127,224,160,.8)))} - -/* voice-loop keyset */ -.vloop{display:grid;grid-template-columns:repeat(4,1fr);gap:4px} -.vk{position:relative;font-size:7px;letter-spacing:.05em;text-align:center;padding:7px 4px 9px;cursor:pointer; - color:var(--dim);background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-radius:4px; - font-family:var(--mono)} -.vk .bar{position:absolute;left:4px;right:4px;bottom:3px;height:3px;border-radius:2px;background:#16240f} -.vk.mon{color:var(--silver)} -.vk.mon .bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)} -.vk.mon.act .bar{background:#a8d84a;box-shadow:0 0 7px rgba(150,200,60,.9)} -.vk.tlk{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold)); - border-color:var(--gold-hi);font-weight:700} -.vk.tlk .bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)} - -/* screen-family chips (alt phosphor / tube colors per screen widget) */ -.famchips{display:flex;gap:7px;align-items:center;margin-top:1px} +.igrid .iv a{color:var(--gold);text-decoration:none;border-bottom:1px dotted var(--gold)} +.igrid .iv a:hover{color:var(--gold-hi);border-color:var(--gold-hi)} +.card a.xref{color:var(--gold);text-decoration:none;border-bottom:1px dotted var(--gold)} +.card a.xref:hover{color:var(--gold-hi);border-color:var(--gold-hi)} +.card:target{border-color:var(--gold);box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 0 0 1px var(--gold),0 6px 14px rgba(0,0,0,.4)} + +/* option chips: each group is an encircled capsule (label + choices); groups flow with spacing */ +.famchips{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:1px} +.fgroup{display:inline-flex;align-items:center;gap:6px;padding:3px 9px 3px 8px; + border:1px solid #2e2b26;border-radius:10px;background:rgba(255,255,255,.015)} .famchips .lab{color:var(--steel);font-size:.6rem;letter-spacing:.14em;text-transform:uppercase;margin-right:2px} .famchips .fc{width:13px;height:13px;border-radius:50%;cursor:pointer; border:1px solid rgba(255,255,255,.22);box-shadow:0 1px 3px rgba(0,0,0,.5)} .famchips .fc.on{outline:2px solid var(--silver);outline-offset:1px} -/* spectrum / EQ */ -.eq{display:flex;align-items:flex-end;gap:3px;height:60px} -.eq .band{width:9px;display:flex;flex-direction:column-reverse;gap:2px;height:100%} -.eq .band i{height:5px;border-radius:1px;background:var(--wash);opacity:.3} -.eq .band i.on{opacity:1;background:var(--pass)} -.eq .band i.hot{opacity:1;background:var(--gold)} -.eq .band i.clip{opacity:1;background:var(--fail)} - -/* crossed-needle */ -.crossm{width:120px} -.crossm .face{position:relative;height:56px;overflow:hidden;cursor:ns-resize;touch-action:none} -.crossm .arc{position:absolute;inset:2px 4px -56px;border:2px solid var(--wash);border-radius:50%} -.crossm .nA{position:absolute;left:14px;bottom:2px;width:2px;height:52px;background:var(--gold-hi);transform-origin:50% 100%; - border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} -.crossm .nB{position:absolute;right:14px;bottom:2px;width:2px;height:52px;background:var(--fail);transform-origin:50% 100%; - border-radius:2px;box-shadow:0 0 5px rgba(203,107,77,.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} -.crossm .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} - -/* thermometer */ -.thermo{display:flex;align-items:flex-end;gap:6px;height:74px;cursor:ns-resize;touch-action:none} -.thermo .tube{width:12px;height:64px;border-radius:6px 6px 0 0;position:relative;background:#0d0f10;border:1px solid #231f18;overflow:hidden} -.thermo .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#a35a3f,var(--fail))} -.thermo .bulb{width:20px;height:20px;border-radius:50%;background:var(--fail);position:absolute;left:-4px;bottom:-9px; - box-shadow:0 0 6px rgba(203,107,77,.5)} -.thermo .scale{display:flex;flex-direction:column;justify-content:space-between;height:64px;font-size:9px;color:var(--steel)} -.thermo .wrapcol{position:relative;padding-bottom:9px} - -/* bourdon */ -.bourdon{width:82px;height:82px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none; - background:radial-gradient(circle at 48% 42%,#1d1a16,#0c0b09);border:2px solid #2c261d; - box-shadow:inset 0 2px 5px rgba(0,0,0,.6)} -.bourdon svg{position:absolute;inset:0} -.bourdon .ndl{position:absolute;left:50%;top:50%;width:2px;height:32px;margin:-32px 0 0 -1px;background:var(--gold-hi); - transform-origin:50% 100%;border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} -.bourdon .hub{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;background:var(--gold)} -.bourdon .cap{position:absolute;left:0;right:0;bottom:12px;text-align:center;font-size:8px;letter-spacing:.16em;color:var(--steel)} - -/* strip-chart */ -.strip{width:176px;height:62px;border-radius:5px;position:relative;overflow:hidden; - background:#0c0e0f;border:1px solid #231f18} -.strip .rule{position:absolute;inset:0;opacity:.4; - background-image:linear-gradient(90deg,rgba(150,147,133,.14) 1px,transparent 1px);background-size:16px 100%} -.strip svg{position:absolute;inset:0;width:100%;height:100%} -.strip polyline{fill:none;stroke:var(--gold);stroke-width:1.4} -.strip .pen{position:absolute;right:2px;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold-hi); - box-shadow:0 0 6px rgba(var(--glow-hi),.8);transition:top .08s linear} - -/* correlation */ -.corr{width:150px;cursor:pointer;touch-action:none} -.corr .face{position:relative;height:44px;overflow:hidden} -.corr .arc{position:absolute;inset:0 0 -44px;border:2px solid var(--wash);border-radius:50%} -.corr .zero{position:absolute;left:50%;top:2px;width:1px;height:10px;background:var(--gold);margin-left:-.5px} -.corr .ndl{position:absolute;left:50%;bottom:0;width:2px;height:38px;background:var(--gold-hi);transform-origin:50% 100%; - border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .1s ease-out} -.corr .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} - -/* battery */ -.batt{display:flex;align-items:center;cursor:pointer;touch-action:none} -.batt .cells{display:flex;gap:2px;padding:3px;border:1px solid #34302a;border-radius:4px;background:#0d0f10} -.batt .cell{width:12px;height:26px;border-radius:1px;background:var(--wash);opacity:.35} -.batt .cell.on{opacity:1;background:linear-gradient(180deg,var(--pass),#5c7526)} -.batt .cell.warn.on{background:linear-gradient(180deg,var(--fail),#a04a34)} -.batt .nub{width:4px;height:12px;background:#34302a;border-radius:0 2px 2px 0} - -/* split-flap */ -.flap{display:inline-flex;gap:4px;cursor:pointer} -.flapd{width:30px;height:42px;border-radius:5px;position:relative;overflow:hidden; - background:linear-gradient(180deg,#211d18,#141210);border:1px solid #34302a;box-shadow:0 2px 4px rgba(0,0,0,.5)} -.flapd b{position:absolute;inset:0;display:grid;place-items:center;font-size:24px;color:var(--cream); - text-shadow:0 1px 2px rgba(0,0,0,.7)} -.flapd::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:rgba(0,0,0,.65);box-shadow:0 1px 0 rgba(255,255,255,.03)} -.flapd.flip b{animation:flipdrop .3s ease-in} -@keyframes flipdrop{0%{transform:translateY(-42%) scaleY(.4);opacity:.3}100%{transform:none;opacity:1}} - -/* seven-segment */ -.seven{display:inline-flex;gap:6px;padding:6px 9px;border-radius:6px;background:#0a0806;border:1px solid #231f18;cursor:pointer; - box-shadow:inset 0 0 10px rgba(0,0,0,.6)} -.seg7{width:22px;height:40px;filter:drop-shadow(0 0 3px rgba(87,211,87,.55))} -.seg7.red{filter:drop-shadow(0 0 3px rgba(226,84,63,.55))} -.seven .colon{align-self:center;display:flex;flex-direction:column;gap:9px} -.seven .colon i{width:4px;height:4px;border-radius:50%;background:var(--sevgrn);box-shadow:0 0 4px rgba(87,211,87,.7)} - -/* DSKY verb/noun panel */ -.dsky{display:flex;gap:9px;align-items:stretch;background:#1c1916;border:1px solid #060505;border-radius:8px;padding:8px} -.dsky .lampcol{display:grid;grid-template-rows:repeat(6,1fr);gap:3px;width:52px} -.dsky .sl{font-size:6.5px;letter-spacing:.04em;display:flex;align-items:center;justify-content:center;text-align:center; - background:#141210;color:#5c574c;border-radius:2px;border:1px solid #26221c} -.dsky .sl.on{background:linear-gradient(180deg,var(--amber-warn),var(--gold));color:var(--panel);font-weight:700; - box-shadow:0 0 6px rgba(var(--glow-lo),.5)} -.dsky .right{display:flex;flex-direction:column;gap:6px} -.dsky .wins{display:flex;gap:8px} -.dsky .win{display:flex;flex-direction:column;align-items:center;gap:2px} -.dsky .win .wl{font-size:6px;letter-spacing:.14em;color:var(--steel)} -.dsky .win .wd{display:flex;gap:2px;background:#0a0806;border:1px solid #231f18;border-radius:4px;padding:3px 4px} -.dsky .win .wd .seg7{width:13px;height:24px} -.dsky .win.hot .wd{border-color:var(--gold);box-shadow:0 0 5px rgba(var(--glow-lo),.4)} -.dsky .pad{display:grid;grid-template-columns:repeat(5,1fr);gap:4px} -.dsky .pad .key{padding:4px 0;font-size:8.5px;border-radius:5px;text-align:center;letter-spacing:.03em} - -/* VFD marquee */ -.vfdm{width:176px;height:34px;border-radius:5px;overflow:hidden;position:relative;cursor:pointer; - background:linear-gradient(180deg,#04100e,#020807);border:1px solid #123028;box-shadow:inset 0 0 12px rgba(0,0,0,.6)} -.vfdm .txt{position:absolute;top:50%;transform:translateY(-50%);white-space:nowrap;font-size:15px;letter-spacing:.22em; - color:var(--vfd);text-shadow:0 0 6px rgba(99,230,200,.65)} -.vfdm .mesh{position:absolute;inset:0;pointer-events:none;opacity:.35; - background-image:radial-gradient(rgba(0,0,0,.6) 40%,transparent 41%);background-size:3px 3px} - -/* annunciator */ -.annwrap{display:flex;flex-direction:column;gap:6px} -.annbar{display:flex;gap:5px;align-items:center} -.mc{font-size:7px;letter-spacing:.08em;padding:4px 8px;border-radius:3px;background:#141210;color:#5c574c; - border:1px solid #26221c;text-align:center} -.mc.on{background:linear-gradient(180deg,#d98a6f,var(--fail));color:var(--cream);font-weight:700; - box-shadow:0 0 8px rgba(203,107,77,.5)} -.mc.on.fl{animation:pulse var(--pulse-rate) ease-in-out infinite} -.annbar .key{padding:3px 7px;font-size:7.5px;border-radius:4px} -.annun{display:grid;grid-template-columns:repeat(3,1fr);gap:3px} -.acell{font-size:8.5px;letter-spacing:.08em;text-align:center;color:var(--dim);padding:6px 4px;border-radius:3px;cursor:pointer; - background:#141210;border:1px solid #262320;line-height:1.2} -.acell.warn{color:var(--panel);background:linear-gradient(180deg,var(--amber-warn),var(--gold));font-weight:700;box-shadow:0 0 8px rgba(var(--glow-lo),.4)} -.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse var(--pulse-rate) ease-in-out infinite} - -/* jewel */ -.jewel{width:24px;height:24px;border-radius:50%;position:relative;cursor:pointer; - background:radial-gradient(circle at 36% 30%,rgba(255,255,255,.75),var(--jc) 42%,#3a0d08 100%); - box-shadow:0 0 10px 1px var(--jc),inset 0 -2px 3px rgba(0,0,0,.5)} -.jewel::after{content:"";position:absolute;inset:0;border-radius:50%; - background:conic-gradient(from 0deg,rgba(255,255,255,.12) 0 22deg,transparent 22deg 45deg,rgba(0,0,0,.18) 45deg 67deg,transparent 67deg 90deg); - background-repeat:repeat} -.jewel.dim{background:radial-gradient(circle at 36% 30%,rgba(120,120,120,.3),#241f1b 55%,#0e0c0a);box-shadow:inset 0 -2px 3px rgba(0,0,0,.5)} - -/* tape counter */ -.counter{display:inline-flex;gap:2px;padding:4px;background:#0c0b0a;border:1px solid #2c261d;border-radius:4px; - box-shadow:inset 0 1px 3px rgba(0,0,0,.6)} -.cwheel{width:18px;height:34px;border-radius:2px;position:relative;overflow:hidden; - background:linear-gradient(180deg,#efe9d6,#c7c0ac);box-shadow:inset 0 0 3px rgba(0,0,0,.3)} -.cwheel .col{position:absolute;left:0;right:0;text-align:center;color:#1a1613;font-size:18px;font-weight:700;line-height:34px; - transition:top .35s cubic-bezier(.4,1.4,.5,1)} -.cwheel .col span{display:block;height:34px} -.cwheel::before,.cwheel::after{content:"";position:absolute;left:0;right:0;height:9px;z-index:2;pointer-events:none} -.cwheel::before{top:0;background:linear-gradient(180deg,rgba(0,0,0,.4),transparent)} -.cwheel::after{bottom:0;background:linear-gradient(0deg,rgba(0,0,0,.4),transparent)} -.counter .redw{background:linear-gradient(180deg,#e7b46a,#cf9440)} - -/* analog clock */ -.clock{width:78px;height:78px;border-radius:50%;position:relative; - background:radial-gradient(circle at 46% 40%,#1c1915,#0c0b09);border:2px solid #2c261d;box-shadow:inset 0 2px 5px rgba(0,0,0,.6)} -.clock .tk{position:absolute;left:50%;top:4px;width:1.5px;height:6px;background:var(--steel);margin-left:-.75px;transform-origin:50% 35px} -.clock .hh{position:absolute;left:50%;top:50%;width:3px;height:20px;margin:-20px 0 0 -1.5px;background:var(--cream); - transform-origin:50% 100%;border-radius:2px} -.clock .mh{position:absolute;left:50%;top:50%;width:2px;height:28px;margin:-28px 0 0 -1px;background:var(--silver); - transform-origin:50% 100%;border-radius:2px} -.clock .sh{position:absolute;left:50%;top:50%;width:1px;height:30px;margin:-30px 0 0 -.5px;background:var(--gold-hi); - transform-origin:50% 100%;box-shadow:0 0 4px rgba(var(--glow-hi),.5)} -.clock .pin{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold)} - -/* frequency-dial scale */ -.freqscale{width:184px;height:44px;position:relative;border-radius:5px;overflow:hidden;cursor:pointer;touch-action:none; - background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c; - box-shadow:inset 0 0 16px rgba(var(--glow-lo),.1)} -.freqscale .tick{position:absolute;top:5px;width:1px;background:var(--steel);transform:translateX(-50%)} -.freqscale .mk{position:absolute;bottom:6px;transform:translateX(-50%);color:var(--steel);font-size:9px} -.freqscale .band{position:absolute;bottom:2px;left:6px;color:var(--gold);font-size:8px;letter-spacing:.18em} -.freqscale .fptr{position:absolute;top:3px;bottom:14px;width:2px;margin-left:-1px;border-radius:1px; - background:var(--fail);box-shadow:0 0 7px rgba(203,107,77,.85)} - -/* patch-bay */ -.patch{padding:7px 9px;background:#141210;border:1px solid #2c261d;border-radius:5px;position:relative} -.patch .row{display:flex;gap:9px} -.patch .row+.row{margin-top:9px} -.patch .jack{width:13px;height:13px;border-radius:50%;cursor:pointer;background:radial-gradient(circle at 40% 35%,#3a352c,#0a0908 70%); - border:1px solid #4a443a;box-shadow:inset 0 1px 2px rgba(0,0,0,.8)} -.patch .jack.hot{background:radial-gradient(circle at 40% 35%,var(--amber-edge),#1a1408 70%)} -.patch .jack.sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(var(--glow-hi),.7)} -.patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%} -.patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round} - -/* ===== reference-batch (R) widgets — SVG-first, after period hardware ===== */ -.rsvg{display:block} -.rsvg.drag{cursor:ns-resize;touch-action:none} -.rsvg.press{cursor:pointer} - @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} </style> </head> -<body data-size="3"> +<body data-size="2"> <div class="wrap"> <header class="masthead"> <div class="eyebrow">archsetup · dupre panel family · live kit</div> <h1>Widget gallery — the instrument-console kit, fully driveable</h1> + <div class="mastrow"> + <div> <p>Every control + display idiom in the dupre faceplate language, merged into one live console — the base kit, the candidate batch, and the growing reference batch (R: modeled on period hardware), all rendering from the same tokens the net / bt / sound panels use. @@ -606,60 +181,117 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} readouts</b> show state or a number. Every widget is operable: drag the faders, knobs and gauges; click the switches, keys and lamps. Each card carries a live readout that tracks what you do. The trace displays run a live signal; the pointer types drive from your drag.</p> - <div class="szbar"><span class="lab">widget size</span> - <button class="key" data-sz="1">S</button> - <button class="key" data-sz="2">M</button> - <button class="key on" data-sz="3">L</button> + <div class="szbar"><span class="lab">widget size</span> + <button class="key" data-sz="1">S</button> + <button class="key on" data-sz="2">M</button> + <button class="key" data-sz="3">L</button> + </div> + </div> + <nav class="toc"> + <span class="scr" style="top:6px;left:6px"></span><span class="scr" style="top:6px;right:6px"></span> + <span class="scr" style="bottom:6px;left:6px"></span><span class="scr" style="bottom:6px;right:6px"></span> + <div class="tt">Index</div> + <a href="#sec-controls">Controls</a> + <a href="#sec-meters">Meters & gauges</a> + <a href="#sec-indicators">Indicators & readouts</a> + <a href="#sec-palette">Palette</a> + <div class="tt" style="margin-top:11px">Validation</div> + <div class="vtally" id="vtally"></div> + </nav> </div> </header> -<h2>Palette — design tokens</h2> -<p class="secnote">Read live from the generated :root block (tokens.json via gen_tokens.py), so this section always shows - exactly what the widgets render with. Hex tokens get a swatch; the glow entries are rgb triples used inside - rgba() glows; mono and pulse-rate are the non-color tokens.</p> -<div class="palgrid" id="palette"></div> - -<h2>Controls — take input</h2> +<h2 id="sec-controls">Controls — take input</h2> <div class="grid" id="controls"></div> -<h2>Meters & gauges — live analog value</h2> +<h2 id="sec-meters">Meters & gauges — live analog value</h2> <div class="grid" id="meters"></div> -<h2>Indicators & readouts — state or number</h2> +<h2 id="sec-indicators">Indicators & readouts — state or number</h2> <div class="grid" id="indicators"></div> +<h2 id="sec-palette">Palette — the instrument colors</h2> +<p class="secnote">Every named color the instruments are built from — markers, hands, faces, lamps, phosphors, + materials — with where it appears. Page chrome (background, body text) is deliberately absent.</p> +<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; + localStorage.setItem('gv-size',b.dataset.sz); document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k===b)); })); - -/* ---- palette section: read the generated :root block live ---- */ +(function(){const s=localStorage.getItem('gv-size'); + if(!s||!/^[123]$/.test(s))return; + document.body.dataset.size=s; + document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k.dataset.sz===s));})(); +/* boost the intrinsically tiny widgets (audit: content under 10% of the stage) */ +const BOOST=['01','07','08','12','13','N14','18']; + +/* ---- palette: the named instrument colors, grouped by role. No codes shown — names and uses. ---- */ +const PALETTE=[ + ['Materials',[ + ['Brass','#b8944a','comfort-meter bezel and knurl'], + ['Copper','#ad7a43','knife-switch blades, jaws and hinge'], + ['Chrome silver','var(--silver)','needles, ball lever, fader caps, chrome handles'], + ['Machined steel','var(--steel)','engraved captions, scale furniture, printed panel lines'], + ['Aluminum','#c9c4b8','timer hub, lever shafts, counter bezels'], + ['Rubber & bakelite','#2c2824','knob bodies, rocker pad, stomp switch'], + ]], + ['Faces & inks',[ + ['Cream dial','var(--cream)','tick marks, scale numerals, dial faces, flag windows'], + ['Chart paper','#efe9da','day-date disc, circular chart, comfort face, telegraph dial'], + ['Face ink','#14110e','numerals and printing on the paper faces'], + ['CRT face green','#b9d8c0','round-scope face tint'], + ['Sky slate','var(--slate-hi)','attitude-indicator sky'], + ['Earth brown','#5c4630','attitude-indicator ground'], + ['Instrument black','#17140f','bezels, plates and housings'], + ]], + ['Lamps, LEDs & jewels',[ + ['Lit amber','var(--gold-hi)','lit keys, sweep line, fixed indexes, blinkenlight lamps'], + ['Panel amber','var(--gold)','key gradients, bearing rings, seated pins, heading bug'], + ['Warning amber','var(--amber-warn)','warn cells, tripped collars, gear-transit lamps'], + ['Run green','var(--pass)','run lamps, gear greens, monitor bars, LIVE lamps'], + ['Fail terracotta','var(--fail)','muted keys, fault cells, TRIP button'], + ['LED green','var(--sevgrn)','seven-segment digits, battery cells'], + ['LED red','var(--sevred)','red seven-segment variant'], + ['Matrix red','#ff4a30','dot-matrix LEDs'], + ['Jewel red','var(--jewel-r)','pilot-lamp lens'], + ['Jewel amber','var(--jewel-a)','pilot-lamp lens, four-way corner lamps'], + ['Jewel green','var(--jewel-g)','pilot-lamp lens'], + ['Neon orange','#ff9a4c','nixie digits, dekatron glow, red screen family'], + ['Flip-disc yellow','#e3d44f','flip-dot faces'], + ['PDP magenta','#3a1420','blinkenlights panel skin'], + ]], + ['Screens & phosphors',[ + ['Phosphor green','var(--phos)','oscilloscope traces, green screen family'], + ['Phosphor dim','var(--phos-dim)','phosphor afterglow and dark screen grounds'], + ['VFD cyan','var(--vfd)','marquee, filter-bank arrows, vfd screen family'], + ['Graticule green','#3d5c46','CRT graticule rulings'], + ['LCD white','#f2f4f2','waveform-editor ink, white screen family'], + ['P4 blue-white','#cfe4ff','blue screen family'], + ]], + ['Needles & controls',[ + ['Needle red','#c23a28','comfort-meter needles, red hands'], + ['Control red','#e0523a','stop knobs, telegraph pointer, palm buttons, OFF marks'], + ['Pointer yellow','#e8cf4a','day-date read hand'], + ]], +]; (function buildPalette(){ const host=$('palette'); if(!host)return; - let rootRule=null; - for(const sh of document.styleSheets){ - for(const r of sh.cssRules){ if(r.selectorText===':root'){rootRule=r;break;} } - if(rootRule)break; - } - if(!rootRule)return; - const st=rootRule.style; - for(let i=0;i<st.length;i++){ - const name=st[i]; if(!name.startsWith('--'))continue; - const val=st.getPropertyValue(name).trim(); - const isHex=/^#[0-9a-f]{3,8}$/i.test(val); - const isTriple=/^\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}$/.test(val); - const esc=val.replace(/&/g,'&').replace(/</g,'<'); - const sw=isHex?`<div class="sw" style="background:${val}"></div>` - :isTriple?`<div class="sw" style="background:rgb(${val})"></div>` - :`<div class="sw txt">${esc}</div>`; - const tile=document.createElement('div'); tile.className='swatch'; - tile.innerHTML=`${sw}<div class="swn">${name}</div><div class="swv">${isTriple?esc+' (glow rgb)':esc}</div>`; - host.appendChild(tile); - } + for(const [group,colors] of PALETTE){ + const h=document.createElement('div'); h.className='palhead'; h.textContent=group; + host.appendChild(h); + for(const [name,val,used] of colors){ + const tile=document.createElement('div'); tile.className='swatch'; + tile.innerHTML=`<div class="sw" style="background:${val}"></div>`+ + `<div class="swn">${name}</div><div class="swv">${used}</div>`; + host.appendChild(tile);}} })(); /* ---- screen-color families: period-authentic alternates for the screen widgets ---- @@ -671,44 +303,20 @@ document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',( 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'; - const lab=document.createElement('span'); lab.className='lab'; lab.textContent='screen'; row.appendChild(lab); + const g=document.createElement('span'); g.className='fgroup'; row.appendChild(g); + const lab=document.createElement('span'); lab.className='lab'; lab.textContent='screen'; g.appendChild(lab); fams.forEach(f=>{const b=document.createElement('span'); b.className='fc'+(f===def?' on':''); b.style.background=SCREEN_FAMS[f]['--scr-hi']; b.title=f; b.addEventListener('click',()=>{ for(const [k,v] of Object.entries(SCREEN_FAMS[f])) el.style.setProperty(k,v); - row.querySelectorAll('.fc').forEach(x=>x.classList.toggle('on',x===b)); + g.querySelectorAll('.fc').forEach(x=>x.classList.toggle('on',x===b)); setRd(no,'screen '+f);}); - row.appendChild(b);}); - cardEl.insertBefore(row,cardEl.querySelector('.wrd')); + g.appendChild(b);}); + cardEl.querySelector('.opts').appendChild(row); } /* ---- per-card spec sheets ---- input how you drive it, incl. the model: click widgets port to every target (Emacs click-regions @@ -719,10 +327,12 @@ function screenChips(no,el,fams,def){ const INFO={ '01':{input:'Click to flip. Click-only, ports everywhere incl. Emacs.', solves:'One persistent on/off state, visible at a glance without reading text.', - use:'Common everywhere. Shines as a master control: power, mute, enable.', + use:'Common in every modern UI. Shines as a master control: power, mute, enable.', limits:'Two states only. Ambiguous if the on-direction is not marked.', - origin:'Electrical panels generally.',difficulty:'Intuitive.', - prefer:'The state persists and gets glanced at more than changed.'}, + origin:'Slide switches (transistor radios, Walkman hold) → the iPod hold switch with its orange reveal → the iOS toggle (2007). This card wears the hardware skin of that lineage.',difficulty:'Intuitive.', + prefer:'The state persists and gets glanced at more than changed. For period consoles use R03 or N01 instead.', + period:'The pill rendering is modern (iPod/iOS era); the slide-switch mechanism underneath is 1950s+.', + ref:'reference/slide-toggle.jpg'}, '02':{input:'Click one key to engage it; the others release. Click-only.', solves:'One-of-few mode selection where the active mode must glow.', use:'Common on consoles. Shines for 2-4 modes switched often.', @@ -777,7 +387,7 @@ const INFO={ limits:'Stepping is sequential — jumping to a far position takes several clicks.', origin:'Rotary switches on radios and instruments.',difficulty:'Intuitive.', prefer:'The selection should read like hardware, not a menu.'}, -'25':{input:'Drag to slide the cursor. Drag-only — needs a key idiom for Emacs.', +'25':{input:'Click a numeral, a mark, or between marks for the units; focused, arrows step one unit. Click + keys.', solves:'Reading a value against a long calibrated scale.', use:'Specialty. Shines where the scale itself carries meaning.', limits:'Scale literacy required; poor for quick setting.', @@ -1428,1797 +1038,505 @@ const INFO={ }; const INFO_FIELDS=[['input','input'],['solves','solves'],['use','use'],['limits','limits'], ['origin','origin'],['difficulty','difficulty'],['prefer','prefer when'],['period','period']]; +const VLAMP_TITLES={off:'validation: not done',amber:'validation: in progress',green:'validation: done'}; +/* index tally: recounts every card lamp; setV calls it on every state change. + Clicking a row jumps to the next card in that state (cycles), so a count + that disagrees with a visual scan can be audited card by card. */ +const VJUMP={green:0,amber:0,off:0}; +function updateVTally(){ + const el=$('vtally'); if(!el)return; + const lamps=[...document.querySelectorAll('.vlamp')]; + const n={off:0,amber:0,green:0}; + lamps.forEach(l=>{n[l.dataset.v]=(n[l.dataset.v]||0)+1;}); + el.innerHTML= + `<div class="vrow" data-v="green"><span class="vdot" data-v="green"></span>done<span class="vn">${n.green}</span></div>`+ + `<div class="vrow" data-v="amber"><span class="vdot" data-v="amber"></span>in progress<span class="vn">${n.amber}</span></div>`+ + `<div class="vrow" data-v="off"><span class="vdot"></span>not done<span class="vn">${n.off}</span></div>`+ + `<div class="vrow vtot">total<span class="vn">${lamps.length}</span></div>`; + el.querySelectorAll('.vrow[data-v]').forEach(row=>row.addEventListener('click',()=>{ + VJUMP[row.dataset.v]=0; auditNext(row.dataset.v); + })); +} +/* audit stepper: a row click jumps to the first card in that state and docks a + floating pill; clicking the pill advances through the rest without scrolling + back to the index. The card list is re-read on every step, so lamp changes + mid-audit are picked up. Esc or the x dismisses. */ +const VAUDIT_LBL={green:'done',amber:'in progress',off:'not done'}; +function auditNext(v){ + const cards=[...document.querySelectorAll('.vlamp')].filter(l=>l.dataset.v===v) + .map(l=>l.closest('.card')); + if(!cards.length){auditStop();return;} + const i=VJUMP[v]%cards.length; VJUMP[v]++; + location.hash='';location.hash=cards[i].id; /* re-fire :target ring on repeat visits */ + let pill=$('vaudit'); + if(!pill){pill=document.createElement('div');pill.id='vaudit';document.body.appendChild(pill); + pill.addEventListener('click',e=>{ + if(e.target.classList.contains('vx')){auditStop();return;} + auditNext(pill.dataset.v);});} + pill.dataset.v=v; + pill.innerHTML=`<span class="vdot" data-v="${v==='off'?'':v}"></span>`+ + `${VAUDIT_LBL[v]} ${i+1}/${cards.length} · next ▸<span class="vx" title="stop auditing">✕</span>`; +} +function auditStop(){const p=$('vaudit');if(p)p.remove();} +addEventListener('keydown',e=>{if(e.key==='Escape')auditStop();}); +/* card(host, no, name, htmlOrBuild, note) — the declarative card record. + htmlOrBuild: a legacy stage-HTML string, or a builder function (stage, rd) => handle + that instantiates a GW.* widget into the stage; rd(txt) writes the card readout. */ function card(host, no, name, html, note){ - const c=document.createElement('div'); c.className='card'; - c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}</div>`+ - `<div class="stagew">${html}</div><div class="wrd" id="rd-${no}">—</div><div class="wnote">${note}</div>`; + const isBuild=typeof html==='function'; + const c=document.createElement('div'); c.className='card'; c.id='card-'+no; + c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}<span class="vlamp" data-v="off"></span></div>`+ + `<div class="stagew">${isBuild?'':html}</div><div class="wrd" id="rd-${no}">—</div>`+ + `<div class="opts"></div><div class="wnote">${note}</div>`; + const lamp=c.querySelector('.vlamp'), VKEY='gv-'+no, VSTATES=['off','amber','green']; + const setV=v=>{lamp.dataset.v=v;lamp.title=VLAMP_TITLES[v];updateVTally();}; + setV(localStorage.getItem(VKEY)||'off'); + lamp.addEventListener('click',e=>{e.stopPropagation(); + const v=VSTATES[(VSTATES.indexOf(lamp.dataset.v)+1)%3]; + setV(v);localStorage.setItem(VKEY,v);}); const inf=(typeof INFO!=='undefined')&&INFO[no]; if(inf){const d=document.createElement('details'); d.className='winfo'; let rows=''; for(const [k,lbl] of INFO_FIELDS) if(inf[k]) rows+=`<div class="ik">${lbl}</div><div class="iv">${inf[k]}</div>`; + if(inf.ref) rows+=`<div class="ik">reference</div><div class="iv"><a href="${inf.ref}" target="_blank">photo</a></div>`; d.innerHTML=`<summary>spec sheet</summary><div class="igrid">${rows}</div>`; c.appendChild(d);} - host.appendChild(c); return c; + host.appendChild(c); + if(isBuild)c.gw=html(c.querySelector('.stagew'),txt=>setRd(no,txt)); + 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'); -card(C,'01','Toggle switch', - `<span id="toggle01"><span class="switch on"></span></span>`, - '<b>on / off.</b> The faceplate master control — power, mute, enable. Click to flip; readout shows the state.'); +card(C,'01','Slide toggle', + (st,rd)=>GW.slideToggle(st,{on:true,onChange:(v,t)=>rd(t)}), + '<b>on / off, the touchscreen way.</b> The one born-digital control in the kit — the smartphone pill toggle the live net/bt/audio panels use. Click to flip; readout shows the state. Physical cousins: the bat-handle (R03) and the rocker (N01).'); card(C,'02','Console key', - `<span id="keys02"><button class="key on">LIVE</button><button class="key">SCAN</button><button class="key red">MUTED</button></span>`, + (st,rd)=>GW.consoleKeys(st,{onChange:(i,t)=>rd(t)}), '<b>physical push button.</b> Mutually exclusive — click one to engage. Gold = live, terracotta = muted.'); card(C,'03','Horizontal fader', - `<div class="fader" id="f1"><div class="slot"><div class="fill" style="width:68%"></div></div><div class="cap" style="left:68%"></div></div>`, + (st,rd)=>GW.faderH(st,{value:68,onChange:(v,t)=>rd(t)}), '<b>continuous 0-100.</b> Per-device volume, brightness. Drag the gold cap; readout tracks.'); card(C,'04','Vertical fader', - `<div class="vfader" id="v1"><div class="slot"><div class="fill" style="height:60%"></div></div><div class="cap" style="bottom:60%"></div></div> - <div class="vfader" id="v2"><div class="slot"><div class="fill" style="height:35%"></div></div><div class="cap" style="bottom:35%"></div></div>`, + (st,rd)=>{let ready=false; + const upd=()=>{if(ready)rd('A '+Math.round(a.get())+' · B '+Math.round(b.get()));}; + const a=GW.faderV(st,{value:60,onChange:upd}); + const b=GW.faderV(st,{value:35,onChange:upd}); + ready=true;upd();return {a,b};}, '<b>channel-strip style.</b> A mixer column per device. Drag each fader up or down.'); card(C,'05','Rotary knob', - `<span class="knob" id="knob"><span class="ind" id="kind"></span></span>`, + (st,rd)=>GW.knob(st,{value:53,onChange:v=>rd('gain '+Math.round(v))}), '<b>dial in a value.</b> Volume/gain the analog way. Drag up/down to turn; readout shows the level.'); card(C,'06','Segmented selector', - `<div class="seg" id="seg06"><button class="on">TIMER</button><button>ALARM</button><button>POMO</button></div>`, - '<b>pick one of a few.</b> Timer type, layout mode. Click a segment; readout names the choice.'); + (st,rd)=>GW.segmented(st,{onChange:(i,t)=>rd(t)}), + '<b>pick one of a few.</b> Timer type, layout mode. Click a segment; readout names the choice. The lit segment ships in amber, green, or red — the accent chips below switch it.'); card(C,'07','Chip toggle', - `<span class="chip on" id="chip07">discoverable on</span>`, + (st,rd)=>GW.chipToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>inline binary.</b> A soft toggle inside a line of text. Click to flip; gold when on.'); card(C,'08','Arm-to-fire', - `<button class="arm" id="arm">forget</button>`, + (st,rd)=>GW.armButton(st,{onChange:(v,t)=>rd(t)}), '<b>two-stage confirm.</b> Destructive actions. First click arms (red), second fires. Readout shows the stage.'); card(C,'09','Lamp row', - `<div class="lrow" id="lrow9"><span class="lamp gold"></span><span class="who"><b>WH-1000XM4</b></span><span class="what">tap to connect</span></div>`, + (st,rd)=>GW.lampRow(st,{onChange:(i,t)=>rd(t)}), '<b>actionable list item.</b> Lamp + name + status. Click to cycle idle → connecting → connected.'); card(C,'24','Rotary selector', - `<span class="rotsel" id="rotsel24"> - <span class="pos" style="left:14%;top:53%">4</span><span class="pos" style="left:28%;top:18%">6</span><span class="pos on" style="left:50%;top:7%">8</span><span class="pos" style="left:72%;top:18%">10</span><span class="pos" style="left:86%;top:53%">12</span> - <span class="knob"><span class="ind"></span></span> - </span>`, + (st,rd)=>GW.rotarySelector(st,{onChange:(v,t)=>rd(t)}), '<b>pick one of N by position.</b> Printed detents, the pointer names the value. Click to turn; readout shows it.'); card(C,'25','Slide-rule dial', - `<span class="tuner" id="tuner" tabindex="0" role="slider" aria-label="slide-rule value"> - <span class="tick" style="left:12px"></span><span class="tick" style="left:51px"></span><span class="tick" style="left:90px"></span><span class="tick" style="left:129px"></span><span class="tick" style="left:168px"></span> - <span class="mk" style="left:12px">4</span><span class="mk" style="left:51px">6</span><span class="mk on" style="left:90px">8</span><span class="mk" style="left:129px">10</span><span class="mk" style="left:168px">12</span> - <span class="ndl" id="tundl" style="left:90px"></span> - </span>`, - '<b>value on a printed scale.</b> A lit pointer glides a warm-backlit strip. Click a mark to jump, or focus it and press ←/→ (↑/↓); readout shows it.'); + (st,rd)=>GW.slideRule(st,{onChange:(v,t)=>rd(t)}), + '<b>value on a printed scale.</b> A lit pointer glides a warm-backlit strip. Printed numerals are the majors; the units between them carry minor ticks. Click any of them to jump, or focus and press ←/→ (↑/↓) to step a unit; readout shows it. Four faces — warm backlit, chrome, 80s black glass, marantz blue — on the chips below.'); card(C,'N01','Rocker power switch', - `<span class="rocker on" id="rocker"><span class="half top">ON</span><span class="half bot">OFF</span></span>`, + (st,rd)=>GW.rocker(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>hard on / off, lit legend.</b> A master power paddle — the pressed half glows. Click to rock.'); card(C,'N02','Transport cluster', - `<div style="display:flex;flex-direction:column;gap:6px;align-items:center"> - <div class="reels"><span class="reel spin"><i></i><i></i><i></i></span><span class="reel spin"><i></i><i></i><i></i></span></div> - <div class="transport" id="tport"> - <button class="tbtn" data-t="rew" onclick="transport(this)">⏮</button> - <button class="tbtn on" data-t="play" onclick="transport(this)">▶</button> - <button class="tbtn" data-t="stop" onclick="transport(this)">⏹</button> - <button class="tbtn rec" data-t="rec" onclick="transport(this)">⏺</button> - </div></div>`, + (st,rd)=>GW.transport(st,{mode:'play',animate:!reduced,onChange:(m,t)=>rd(t)}), '<b>run / pause / capture.</b> One mode lit at a time, reels turn while it plays. Click a button.'); card(C,'N03','Radio preset bank', - `<div class="radiobank" id="rbank"> - <button class="preset on" onclick="preset(this)">WIFI</button> - <button class="preset" onclick="preset(this)">ETH</button> - <button class="preset" onclick="preset(this)">CELL</button> - <button class="preset" onclick="preset(this)">OFF</button> - </div>`, + (st,rd)=>GW.presetBank(st,{onChange:(i,t)=>rd(t)}), '<b>pick one, the rest release.</b> The car-radio preset row — mechanically exclusive. Click one.'); card(C,'N04','Concentric dual knob', - `<span class="dualknob"> - <span class="outer" id="dko"><span class="tick" style="transform:rotate(-120deg)"></span><span class="tick" style="transform:rotate(120deg)"></span></span> - <span class="inner" id="dki"><span class="ind"></span></span> - </span>`, + (st,rd)=>GW.dualKnob(st,{outer:50,inner:50,onChange:(o,i)=>rd('bass '+Math.round(o)+' · treble '+Math.round(i))}), '<b>two values on one spindle.</b> Bass / treble, coarse / fine. Drag the outer ring or inner cap — each is independent.'); card(C,'N05','Rotary encoder + LED ring', - `<span class="encoder" id="enc"></span>`, + (st,rd)=>GW.encoder(st,{value:7,onChange:(v,t)=>rd(t)}), '<b>endless dial, lit position.</b> A detentless encoder; the LED arc lights to the level and the value keeps accumulating. Drag to turn.'); card(C,'N06','Keyed mode switch', - `<span class="keylock" id="keylock6"> - <span class="kpos" style="left:16%;top:64%">OFF</span><span class="kpos on" style="left:50%;top:12%">ON</span><span class="kpos" style="left:84%;top:64%">RUN</span> - <span class="body" onclick="keyTurn()"><span class="bit" id="kbit"></span><span class="barrel"></span></span> - </span>`, + (st,rd)=>GW.keySwitch(st,{index:1,onChange:(i,t)=>rd(t)}), '<b>guarded three-position mode.</b> Off · on · run. The key-bit points at the live position. Click to turn.'); card(C,'N07','Center-detented crossfader', - `<div class="xfader" id="xf"> - <div class="slot"></div><div class="detent"></div> - <span class="end" style="left:0">A</span><span class="end" style="right:0">B</span> - <div class="cap" id="xfcap" style="left:50%"></div> - </div>`, + (st,rd)=>GW.crossfader(st,{onChange:(v,t)=>rd(t)}), '<b>throw to either side of zero.</b> A fader notched at center — balance, L/R mix. Drag; readout shows the signed value.'); card(C,'N08','Thumbwheel', - `<div class="thumbw"><span class="thumbwheel" id="thumbwheel"></span><span class="win" id="thval">42</span></div>`, + (st,rd)=>GW.thumbwheel(st,{value:42,onChange:(v,t)=>rd(t)}), '<b>ribbed edge-wheel, one value.</b> A knurled wheel with a windowed number. Drag up/down to roll.'); card(C,'N09','DIP-switch bank', - `<span class="dip" id="dips"> - <span class="dipsw on" onclick="dip(this)"><i></i></span><span class="dipsw" onclick="dip(this)"><i></i></span> - <span class="dipsw on" onclick="dip(this)"><i></i></span><span class="dipsw on" onclick="dip(this)"><i></i></span> - <span class="dipsw" onclick="dip(this)"><i></i></span><span class="dipsw" onclick="dip(this)"><i></i></span> - </span>`, + (st,rd)=>GW.dipBank(st,{onChange:(w,t)=>rd(t)}), '<b>a bank of hard flags.</b> Hardware slide switches — up is on. Click each to flip; readout is the binary word.'); card(C,'N10','Jog / shuttle wheel', - `<span class="jog" id="jog10"><span class="shuttle" style="--sh:40deg"></span><span class="inner" id="jogi"><span class="dimple"></span></span></span>`, + (st,rd)=>GW.jogWheel(st,{onChange:(v,t)=>rd(t)}), '<b>scrub fine, shuttle fast.</b> The edit-deck wheel. Drag to jog the inner wheel; readout shows the position.'); card(C,'R02','Calibrated vernier dial', - `<svg id="vern" class="rsvg drag" viewBox="0 0 150 150" width="150" height="150"></svg>`, + (st,rd)=>GW.vernierDial(st,{value:42.0,onChange:(v,t)=>rd(t)}), '<b>precision on a rotating disc.</b> The big lab dial — the calibrated disc turns under a fixed hairline. Drag up/down; readout reads to a tenth. After a 1950s lab-oscillator vernier.'); card(C,'R03','Bat-handle toggle', - `<svg id="batsw" class="rsvg press" viewBox="0 0 70 90" width="70" height="90"></svg>`, + (st,rd)=>GW.batToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>the period power switch.</b> Chrome bat lever on a hex bushing, snaps up ON / down OFF. Click to throw it. After a lab-instrument panel toggle.'); card(C,'R04','Bakelite fluted knob', - `<svg id="bakek" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.flutedKnob(st,{value:63,onChange:(v,t)=>rd(t)}), '<b>the skirted console knob.</b> Scalloped bakelite skirt, glossy dome, amber index over a printed 0-10 scale. Drag up/down to turn. After a vintage console mixer knob.'); card(C,'R05','Filter slider bank', - `<svg id="fbank" class="rsvg" viewBox="0 0 190 96" width="190" height="96"></svg>`, - '<b>a wall of band faders.</b> One slider per band, arrow-head caps on black tracks, dB rail at the side. Drag any cap; readout names band and level. After a variable multi-band filter.'); + (st,rd)=>GW.filterBank(st,{onChange:(v,t)=>rd(t)}), + '<b>a wall of band faders.</b> Twelve bands on a screwed faceplate, dB rails both sides. Drag any cap; readout names band and level. Skins split three independent axes — panel (silver hi-fi after the Pioneer SG-9500 / studio black after the Technics SH-8065), cap shape (tall block fader after the Zaxcom Oasis / short ribbed / chrome T), and cap color (black white-index / red / green / blue / amber stripes / chrome / cream). The chips below mix them freely.'); card(C,'R06','Chicken-head selector', - `<svg id="chick" class="rsvg press" viewBox="0 0 110 96" width="110" height="96"></svg>`, + (st,rd)=>GW.chickenHead(st,{index:2,onChange:(i,t)=>rd(t)}), '<b>the pointer-lever switch.</b> The tapered bakelite lever IS the indicator — it aims at the engraved position. Click to step through. After a modulator mode switch.'); card(C,'R12','Chrome slot fader', - `<svg id="slotf" class="rsvg drag" viewBox="0 0 90 150" width="90" height="150"></svg>`, + (st,rd)=>GW.slotFader(st,{value:-4,onChange:(v,t)=>rd(t)}), '<b>the console gain slide.</b> A chrome T-handle rides a recessed slot, the dB scale engraved on the plate — gain above zero, cut below. Drag; readout shows signed dB. After a vintage channel-strip output fader.'); card(C,'R14','Spade-pointer tuning knob', - `<svg id="spade" class="rsvg drag" viewBox="0 0 140 124" width="140" height="124"></svg>`, + (st,rd)=>GW.spadeKnob(st,{value:8.3,onChange:(v,t)=>rd(t)}), '<b>the pointer rides the knob.</b> A knurled bakelite knob carries a bright spade pointer over a scale engraved into the panel itself. Drag up/down to tune. After a 1920s radio receiver dial.'); card(C,'R15','Multi-band dial', - `<svg id="mband" class="rsvg" viewBox="0 0 190 110" width="190" height="110"></svg>`, + (st,rd)=>GW.multiBandDial(st,{onChange:(v,t)=>rd(t)}), '<b>four scales, one needle.</b> Nested band arcs on a cream face; the active ring reads bold. Drag the left dial to tune; click the bandspread dial to switch bands. After a shortwave receiver dial pair.'); card(C,'R16','Entry keypad', - `<svg id="keypad" class="rsvg" viewBox="0 0 150 190" width="150" height="190"></svg>`, + (st,rd)=>GW.entryKeypad(st,{onChange:(v,t)=>rd(t)}), '<b>a composed entry instrument.</b> Worn keycaps feed the amber display; the lamp watches the buffer; the amber keys confirm and clear. Click the keys. After an industrial keypad prop.'); card(C,'R18','Thumb-slide attenuator pair', - `<svg id="thumbsl" class="rsvg" viewBox="0 0 130 150" width="130" height="150"></svg>`, + (st,rd)=>GW.thumbSlide(st,{onChange:(v,t)=>rd(t)}), '<b>a lit column of numbers, a thumb tab.</b> The scale glows inside the rail; the cream tab rides beside it and the nearest figure lights. Drag either channel. After a compressor blend/mix pair.'); card(C,'R19','Waveform region editor', - `<svg id="wregion" class="rsvg" viewBox="0 0 190 100" width="190" height="100"></svg>`, + (st,rd)=>GW.waveRegion(st,{start:22,end:76,onChange:(v,t)=>rd(t)}), '<b>pick a region on the wave.</b> The monochrome LCD shows the sample; drag near either flag to move START or END, and the selection draws bright. After a sampler edit screen.'); card(C,'R20','Drum roller selector', - `<svg id="drumr" class="rsvg" viewBox="0 0 130 140" width="130" height="140"></svg>`, + (st,rd)=>GW.drumRoller(st,{onChange:(v,t)=>rd(t)}), '<b>roll to the number.</b> The numbered drum shows through a tall window and the center value sits on an inverted chip. Drag either drum. After an equalizer tone selector.'); card(C,'R21','LED program row', - `<svg id="ledrow" class="rsvg" viewBox="0 0 190 58" width="190" height="58"></svg>`, + (st,rd)=>GW.ledRow(st,{index:5,onChange:(v,t)=>rd(t)}), '<b>identical keys, one lit dot.</b> The LED above the button carries the selection, not the key itself — the classic program-select row. Click a key; readout names the program. After a digital reverb’s program bank.'); card(C,'R22','Three-position slide', - `<svg id="varslide" class="rsvg press" viewBox="0 0 110 64" width="110" height="64"></svg>`, + (st,rd)=>GW.pillSlide(st,{index:1,onChange:(v,t)=>rd(t)}), '<b>a detented pill, three stops.</b> The chrome pill parks at A, AB, or B, and the LED pair below tells the truth — both light on AB. Click a position. After a drum machine variation switch.'); card(C,'R23','Spun-aluminum knob', - `<svg id="spunk" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.spunKnob(st,{value:62,onChange:(v,t)=>rd(t)}), '<b>bright metal, machined rings.</b> A spun face with concentric tooling marks in a knurled grip ring, red index over dot ticks. Drag up/down. After a boutique pedal knob, confirmed by an avionics cluster.'); card(C,'R24','Stomp switch + jewel', - `<svg id="stomp" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.stompSwitch(st,{on:false,onChange:(v,t)=>rd(t)}), '<b>the engage pair.</b> A chrome dome stomp and the faceted jewel that reports it — press to engage, the jewel lights; press again to bypass. Click it. After a tremolo pedal footswitch.'); card(C,'R27','Winged gain selector', - `<svg id="wingk" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.wingSelector(st,{index:7,onChange:(v,t)=>rd(t)}), '<b>the red T-bar, stepped.</b> A bar-grip handle over a dot ring, snapping between labeled gain detents. Drag up/down to step it. After a classic mic-preamp gain switch.'); card(C,'R28','Rotary disc switch', - `<svg id="discsw" class="rsvg press" viewBox="0 0 110 116" width="110" height="116"></svg>`, + (st,rd)=>GW.discSwitch(st,{index:1,onChange:(v,t)=>rd(t)}), '<b>the master power disc.</b> The whole red disc turns, grip bar molded in — a heavy three-position selector for OFF, ON, and COMBINE. Click to step. After a marine battery switch.'); card(C,'R29','Guarded toggle', - `<svg id="guardsw" class="rsvg press" viewBox="0 0 90 100" width="90" height="100"></svg>`, + (st,rd)=>GW.guardedToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>friction sized to consequence.</b> Guard posts flank the lever and a red collar marks it critical — the avionics way to prevent an accidental throw. Click to throw it deliberately. After a flight-deck switch panel.'); card(C,'R32','Mechanical timer dial', - `<svg id="mtimer" class="rsvg" viewBox="0 0 158 132" width="158" height="132"></svg>`, + (st,rd)=>GW.timerDial(st,{minutes:0,onChange:(v,t)=>rd(t)}), '<b>turn to start, push to stop.</b> Drag the knurled dial to wind minutes onto the fixed red index; it winds back down to OFF on its own (demo runs a minute per second). Click the red knob to stop it dead. After a mechanical interval timer.'); card(C,'R33','Four-way rocker', - `<svg id="rocker4" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.rockerPad(st,{onChange:(v,t)=>rd(t)}), '<b>discrete direction, one press at a time.</b> Click an arrow (the whole quadrant is the target) to step that way; the readout tracks the cursor position it drives. After a remote-control navigation pad.'); card(C,'R34','Four-way toggle selector', - `<svg id="fourway" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.fourWayToggle(st,{position:'C',onChange:(v,t)=>rd(t)}), '<b>a lever that lives in one of four states.</b> Click a quadrant and the ball lever throws to A, B, C or D; the matching corner lamp takes the light. The rocker steps — this one selects. After a test-panel quadrant switch.'); card(C,'R37','Pin routing matrix', - `<svg id="pinmx" class="rsvg press" viewBox="0 0 160 110" width="160" height="110"></svg>`, + (st,rd)=>GW.pinMatrix(st,{onChange:(v,t)=>rd(t)}), '<b>many-to-many routing you can read.</b> Sources are rows, destinations are columns; click an intersection to seat a pin and the route exists. Every simultaneous route stays visible. After the EMS VCS3 pin matrix.'); card(C,'R38','Dead-man button', - `<svg id="deadman" class="rsvg" viewBox="0 0 120 110" width="120" height="110"></svg>`, + (st,rd)=>GW.deadMan(st,{onChange:(v,t)=>rd(t)}), '<b>active only while held.</b> Press and hold — the ring arms, the lamp runs, the dwell clock counts. Let go anywhere and it drops to SAFE. The kit has toggles and momentaries; this is the only control whose state IS your grip.'); card(C,'R39','Rotary telephone dial', - `<svg id="teldial" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.telephoneDial(st,{onChange:(v,t)=>rd(t)}), '<b>numeric entry embodied as time.</b> Click a hole: the finger wheel winds to the stop and spins back, and the digit lands in the number. Bigger digits take longer, exactly like the pulses did. After a Western Electric dial.'); card(C,'R40','Circuit breaker panel', - `<svg id="brkpanel" class="rsvg press" viewBox="0 0 160 110" width="160" height="110"></svg>`, + (st,rd)=>GW.breakerPanel(st,{onChange:(v,t)=>rd(t)}), '<b>the first control the system can throw.</b> Click a breaker to close or open it; hit TRIP and a fault pops one out to the tripped mid-position, amber collar showing. A tripped breaker must be pulled to reset before it closes again. After an aircraft breaker row.'); card(C,'R41','DSKY verb-noun panel', - `<div class="dsky" id="dsky"></div>`, + (st,rd)=>GW.dsky(st,{onChange:(v,t)=>rd(t)}), '<b>a command grammar, not a button per command.</b> VERB is the action, NOUN is the operand: press VERB 3 5 ENTR for the lamp test. Wrong grammar lights OPR ERR. Two keys plus digits reach every function — the Apollo answer to a panel with too few buttons.'); card(C,'R42','Cam-timer program drum', - `<svg id="camtimer" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.camTimer(st,{position:0,onChange:(v,t)=>rd(t)}), '<b>a procedure as a rotating position.</b> The ring is the whole program — fill, wash, rinse, spin — and the pointer is where you are in it. Click to advance (or start from OFF); it then walks itself through the steps. After a washing-machine cam timer.'); card(C,'R48','Knife switch', - `<svg id="knifesw" class="rsvg press" viewBox="0 0 130 110" width="130" height="110"></svg>`, + (st,rd)=>GW.knifeSwitch(st,{closed:true,onChange:(v,t)=>rd(t)}), '<b>the visible disconnect.</b> Copper blades either sit in their jaws or hang open in the air — you can SEE the circuit is broken, which is the entire safety argument. Click the handle to throw it. After early power-station switchboards.'); card(C,'R49','Decade switch box', - `<svg id="decbox" class="rsvg" viewBox="0 0 160 100" width="160" height="100"></svg>`, + (st,rd)=>GW.decadeBox(st,{digits:[3,5,0,0],onChange:(v,t)=>rd(t)}), '<b>a number composed a digit at a time.</b> Four skirted knobs, one per decade — drag each to its digit and the total just IS their sum. The thumbwheel does one digit; this is how a lab built a precise value. After a General Radio decade resistance box.'); card(C,'R50','Two-hand safety control', - `<svg id="twohand" class="rsvg press" viewBox="0 0 160 100" width="160" height="100"></svg>`, + (st,rd)=>GW.twoHandSafety(st,{onChange:(v,t)=>rd(t)}), '<b>both hands, deliberately.</b> Click one palm button to arm the window, the other within half a second to cycle the press. Too slow — TIE-DOWN fault, start over. The hardware keeps hands out of the die; the grammar survives on screen. After OSHA press controls.'); card(C,'R51','Voice-loop keyset', - `<div class="vloop" id="vloop"></div>`, + (st,rd)=>GW.voiceLoop(st,{onChange:(v,t)=>rd(t)}), '<b>attention management, not selection.</b> Each loop is independent: click to monitor (green bar, flickering with activity), again to talk (amber, exclusive — one voice out), again to drop. Flight controllers ran a dozen of these at once. After a mission-control comm keyset.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); card(M,'10','Needle gauge', - `<div class="gauge" id="gauge10"><div class="dial"><div class="arc"></div> - <div class="tk" style="transform:rotate(-60deg)"></div><div class="tk" style="transform:rotate(0)"></div><div class="tk" style="transform:rotate(60deg)"></div> - <div class="ndl" id="g1"></div><div class="hub"></div></div><div class="gv"><span id="g1v">0</span>%</div></div>`, + (st,rd)=>GW.needleGauge(st,{value:50,onChange:(v,t)=>rd(t)}), '<b>analog dial.</b> Throughput, battery, volume. Drag up/down to sweep the needle; readout tracks. <b>Smooth sweep wants a Cairo/GTK area.</b>'); card(M,'11','Stereo VU (LED bar)', - `<div class="vu"><div class="vurow"><span class="ch">L</span><span class="vubar" id="vuL"></span></div> - <div class="vurow"><span class="ch">R</span><span class="vubar" id="vuR"></span></div></div>`, + (st,rd)=>GW.vuPair(st,{onChange:(v,t)=>rd(t)}), '<b>live signal level.</b> The sound panel meter row, peak-hold outline. Runs a live signal; readout shows L/R.'); card(M,'12','Mini signal (4-bar)', - `<span class="sig" id="mini"></span>`, + (st,rd)=>GW.miniSig(st,{onChange:(v,t)=>rd(t)}), '<b>compact activity.</b> Per-row "is this device playing". Live signal; readout shows activity %.'); card(M,'13','Signal ladder', - `<span class="ladder" id="ladder"><i></i><i></i><i></i><i></i></span>`, + (st,rd)=>GW.signalLadder(st,{value:3,onChange:(v,t)=>rd(t)}), '<b>discrete strength.</b> Wifi bars, bt RSSI — a stepped 0-4. Click to cycle strength; readout shows bars.'); card(M,'14','Linear fuel bar', - `<div class="bar" id="bar14"><span style="width:72%"></span></div><div class="bar" id="bar14b"><span style="width:40%"></span></div>`, + (st,rd)=>{const a=GW.fuelBar(st,{value:72,onChange:(v,t)=>rd(t)}); + const b=GW.fuelBar(st,{value:40,onChange:(v,t)=>rd(t)}); + a.set(72);return {a,b};}, '<b>a single 0-100.</b> Battery, disk, download. Drag either bar; warn tint under threshold; readout tracks.'); card(M,'15','Radial ring', - `<span class="ring" id="ring15" style="--p:68"><b>68</b></span>`, + (st,rd)=>GW.radialRing(st,{value:68,onChange:(v,t)=>rd(t)}), '<b>percentage as a donut.</b> CPU, battery. Drag up/down to set; conic-gradient tracks the readout.'); card(M,'16','Sparkline', - `<span class="spark" id="spark"><svg viewBox="0 0 170 44" preserveAspectRatio="none"><polyline id="sparkp" fill="none" stroke="var(--gold-hi)" stroke-width="1.5"/></svg></span>`, + (st,rd)=>GW.sparkline(st,{onChange:(v,t)=>rd(t)}), '<b>recent history.</b> Throughput/CPU over the last minute. Live trace; readout shows the current value.'); card(M,'17','Waveform strip', - `<span class="wave" id="wave"><svg viewBox="0 0 170 38" preserveAspectRatio="none"><path id="wavep" fill="none" stroke="var(--gold)" stroke-width="1.2"/></svg></span>`, + (st,rd)=>GW.waveStrip(st,{onChange:(v,t)=>rd(t)}), '<b>audio waveform / scope.</b> A richer signal view. Live trace; readout shows amplitude. <b>Needs a drawing surface.</b>'); card(M,'N11','Oscilloscope', - `<span class="scope"><span class="grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline id="scopep"/></svg></span>`, + (st,rd)=>GW.scope(st,{onChange:(v,t)=>rd(t)}), '<b>a live phosphor trace.</b> A waveform swept over a graticule. Live; readout shows Vpp. <b>Needs a Cairo/GTK area for a smooth trace.</b>'); card(M,'N12','Spectrum / EQ bars', - `<span class="eq" id="eq"></span>`, + (st,rd)=>GW.eqBars(st,{onChange:(v,t)=>rd(t)}), '<b>level across bands.</b> A live multi-band bar graph — spectrum, per-core CPU. Live; readout shows the peak band.'); card(M,'N13','Crossed-needle meter', - `<div class="crossm"><div class="face" id="crossm13"><div class="arc"></div><div class="nA" id="cnA"></div><div class="nB" id="cnB"></div></div> - <div class="lbl"><span>FWD</span><span>RFL</span></div></div>`, + (st,rd)=>GW.crossNeedle(st,{value:55,onChange:(v,t)=>rd(t)}), '<b>two readings, one face.</b> Forward + reflected needles cross. Drag up/down to drive power; readout shows both. <b>Round build wants a drawing surface.</b>'); card(M,'N14','Thermometer column', - `<div class="thermo" id="thermo14"><div class="scale"><span>90</span><span>60</span><span>30</span></div> - <div class="wrapcol"><div class="tube"><div class="fill" id="thermf" style="height:58%"></div></div><div class="bulb"></div></div></div>`, + (st,rd)=>GW.thermometer(st,{value:58,onChange:(v,t)=>rd(t)}), '<b>a rising column.</b> The classic mercury tube — temperature, a climbing load. Drag up/down; readout shows the value.'); card(M,'N15','Bourdon pressure gauge', - `<div class="bourdon" id="bourdon15"> - <svg viewBox="0 0 82 82"><path d="M14 62 A34 34 0 0 1 68 62" fill="none" stroke="#2c2f32" stroke-width="3"/> - <path d="M52 22 A34 34 0 0 1 68 62" fill="none" stroke="#cb6b4d" stroke-width="3"/></svg> - <div class="ndl" id="bourd" style="transform:rotate(30deg)"></div><div class="hub"></div><div class="cap">PSI</div></div>`, + (st,rd)=>GW.bourdon(st,{value:45,onChange:(v,t)=>rd(t)}), '<b>a round dial with a red zone.</b> Needle over a printed arc, redline in terracotta. Drag to set PSI; readout tracks. <b>Round sweep is a drawing-area job.</b>'); card(M,'N16','Strip-chart recorder', - `<span class="strip"><span class="rule"></span><svg viewBox="0 0 176 62" preserveAspectRatio="none"><polyline id="stripp"/></svg><span class="pen" id="strippen" style="top:31px"></span></span>`, + (st,rd)=>GW.stripChart(st,{onChange:(v,t)=>rd(t)}), '<b>a scrolling pen trace.</b> History scrolls left, the pen writes at the right edge. Live; readout shows the current value. <b>Real trace wants a drawing surface.</b>'); card(M,'N17','Correlation meter (±)', - `<div class="corr" id="corr17"><div class="face"><div class="arc"></div><div class="zero"></div><div class="ndl" id="corrn" style="transform:rotate(8deg)"></div></div> - <div class="lbl"><span>−1</span><span>0</span><span>+1</span></div></div>`, + (st,rd)=>GW.corrMeter(st,{value:58,onChange:(v,t)=>rd(t)}), '<b>deviation from center.</b> The needle rests at 0 and swings ±. Drag left/right; readout shows the signed value.'); card(M,'N18','Battery-cell gauge', - `<div class="batt" id="batt18"><div class="cells" id="battc"></div><span class="nub"></span></div>`, + (st,rd)=>GW.battCells(st,{value:62,onChange:(v,t)=>rd(t)}), '<b>charge as discrete cells.</b> Filled cells count up, the last warn in terracotta. Drag left/right to set; readout shows %.'); card(M,'R01','Moving-coil VU meter', - `<svg id="mcvu" class="rsvg" viewBox="0 0 150 96" width="150" height="96"></svg>`, + (st,rd)=>GW.mcVu(st,{onChange:(v,t)=>rd(t)}), '<b>the classic cream-faced VU.</b> Printed dB arc, red zone past 0, ballistic needle. Runs the live signal; readout shows VU. After a vintage console meter pair.'); card(M,'R07','Round panel meter', - `<svg id="rndm" class="rsvg drag" viewBox="0 0 110 104" width="110" height="104"></svg>`, + (st,rd)=>GW.roundMeter(st,{value:50,onChange:(v,t)=>rd(t)}), '<b>the porthole lab meter.</b> Cream dial in a round black bezel, printed dB arc, black needle. Drag up/down to drive it; readout shows dB. After a modulator front meter.'); card(M,'R08','Chrome MIN/MAX indicator', - `<svg id="chrmm" class="rsvg drag" viewBox="0 0 110 104" width="110" height="104"></svg>`, + (st,rd)=>GW.chromeMinMax(st,{value:50,onChange:(v,t)=>rd(t)}), '<b>the hi-fi trim window.</b> Polished chrome ring, brushed dome, a dark pointer sweeping MIN to MAX over the metal. Drag up/down; readout shows the position. After a hi-fi amplifier level window.'); card(M,'R09','Black-face aviation gauge', - `<svg id="avgauge" class="rsvg drag" viewBox="0 0 110 104" width="110" height="104"></svg>`, + (st,rd)=>GW.aviationGauge(st,{value:52,onChange:(v,t)=>rd(t)}), '<b>amber on black, zone-marked.</b> The cockpit round: amber ticks and numerals on a black face, painted caution and limit sectors, amber needle. Drag up/down; readout tracks. After a flight-instrument cluster.'); card(M,'R13','Edgewise strip meter', - `<svg id="edgem" class="rsvg drag" viewBox="0 0 70 130" width="70" height="130"></svg>`, + (st,rd)=>GW.edgeMeter(st,{value:.62,onChange:(v,t)=>rd(t)}), '<b>a needle on a vertical scale.</b> The parchment strip reads attenuation down a compressed log scale; the bar rides its edge. Drag up/down; readout shows dB. After a channel-strip gain-reduction meter.'); card(M,'R43','Attitude indicator', - `<svg id="attind" class="rsvg" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.attitudeIndicator(st,{onChange:(v,t)=>rd(t)}), '<b>orientation, not a number.</b> The horizon rolls and shifts behind the fixed amber airplane — two axes in one read. Drag it: left/right banks, up/down pitches. The first two-axis instrument in the kit. After a cockpit artificial horizon.'); card(M,'R44','Heading bug + servo needle', - `<svg id="hdgbug" class="rsvg" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.headingBug(st,{value:90,onChange:(v,t)=>rd(t)}), '<b>commanded versus actual on one dial.</b> Drag to park the amber bug where you want to go; the needle chases it with honest servo lag. The gap between them IS the information. After an HSI heading bug and synchro repeaters.'); card(M,'R54','Vertical tape instrument', - `<svg id="vtape" class="rsvg" viewBox="0 0 70 130" width="70" height="130"></svg>`, + (st,rd)=>GW.verticalTape(st,{value:24,onChange:(v,t)=>rd(t)}), '<b>the scale moves, the index does not.</b> A scrolling tape behind a fixed amber index — your value is always at eye level and the neighborhood above and below stays visible. Drag to spin the tape. After the Ki-57 remote tachometer; modern cockpits tape everything.'); card(M,'R55','Twin-needle gauge', - `<svg id="twinng" class="rsvg" viewBox="0 0 120 110" width="120" height="110"></svg>`, + (st,rd)=>GW.twinNeedle(st,{fuel:2.4,oil:3.1,onChange:(v,t)=>rd(t)}), '<b>two values, one housing, mirrored scales.</b> Fuel presses up the left arc, oil up the right, each with its own needle from the shared hub. Drag either half to set that side. Not the crossed-needle (N13) — these are read separately, not at their intersection. After the Ki-57 fuel/oil pressure gauge.'); card(M,'R56','Comfort-zone crossed needles', - `<svg id="comfort" class="rsvg" viewBox="0 0 130 122" width="130" height="122"></svg>`, + (st,rd)=>GW.comfortMeter(st,{temp:72,humidity:45,onChange:(v,t)=>rd(t)}), '<b>two values, one verdict.</b> Temperature and humidity needles cross over a face printed with judgments — the crossing point lands in TOO WARM, TOO DRY, or JUST RIGHT. N13 derives a number from the crossing; this one derives an opinion. Drag each half. After a brass weather-station comfort meter.'); card(M,'R53','Circular chart recorder', - `<svg id="circhart" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (st,rd)=>GW.chartRecorder(st,{onChange:(v,t)=>rd(t)}), '<b>cyclic time on round paper.</b> The pen sweeps a full day per revolution, so "same time yesterday" is the same angle and daily rhythm draws itself as a flower. Click for fresh paper. The strip chart shows a window; this shows the cycle. After a Partlow round-chart recorder.'); card(M,'R17','Round CRT scope', - `<svg id="rcrt" class="rsvg" viewBox="0 0 110 104" width="110" height="104"></svg>`, + (st,rd)=>GW.roundCrt(st,{onChange:(v,t)=>rd(t)}), '<b>the round-tube scope.</b> Pale phosphor face in a screwed porthole bezel, dark graticule, a bright live trace. Runs live; readout shows Vpp. After a 1950s bench oscilloscope.'); /* ============ INDICATORS & READOUTS ============ */ const I=$('indicators'); card(I,'18','Status lamp', - `<span id="lamp18" style="display:inline-flex;gap:9px;align-items:center"><span class="lamp"></span><span class="lamp gold"></span><span class="lamp red"></span><span class="lamp off"></span><span class="lamp busy"></span></span>`, + (st,rd)=>GW.statusLamps(st,{onChange:(v,t)=>rd(t)}), '<b>one-glance health.</b> Green ok · gold engaged · red fail · dim off · pulsing busy. Click any lamp to cycle its state.'); card(I,'19','Badge / tag', - `<span id="badge19"><span class="badge">TUNNEL</span> <span class="badge red">LOW BATT</span> <span class="badge ghost">2.4G</span></span>`, + (st,rd)=>GW.badges(st,{onChange:(v,t)=>rd(t)}), '<b>a labelled flag.</b> MUTED, AIRPLANE, a band tag. Click a badge to cycle its variant.'); card(I,'20','Tabular readout', - `<div style="text-align:center"><div class="readout" id="treadout">24:10</div><span class="u">timer</span></div>`, + (st,rd)=>GW.tabularReadout(st,{onChange:(v,t)=>rd(t)}), '<b>a precise number.</b> Clock, countdown, volume %. Runs a live countdown; click to pause/resume.'); card(I,'21','Engraved label', - `<span class="engrave" id="engrave">outputs<span class="cnt" id="engc">· 3</span></span>`, + (st,rd)=>GW.engravedLabel(st,{onChange:(v,t)=>rd(t)}), '<b>section divider.</b> The hairline-flanked caps label with a count. Click to bump the count.'); card(I,'22','Output well', - `<div class="owell" id="owell"><div class="ostep"><span class="lamp"></span><span><b>Link</b><span class="ev">wlp170s0 · @Hyatt</span></span></div> - <div class="ostep"><span class="lamp gold"></span><span><b>DNS</b><span class="ev">resolving…</span></span></div></div>`, + (st,rd)=>GW.outputWell(st,{onChange:(v,t)=>rd(t)}), '<b>streaming step log.</b> Lamp-per-step with evidence. Click to stream the next step.'); card(I,'23','Toast / status line', - `<span class="toastw" id="toast">joined @Hyatt_WiFi — saved</span>`, + (st,rd)=>GW.toast(st,{onChange:(v,t)=>rd(t)}), '<b>transient confirmation.</b> The one-line result after an action. Click to fire the next toast.'); card(I,'26','Nixie tube', - `<span class="nixie" id="nixie"><span class="tube off"><b>0</b></span><span class="tube"><b>8</b></span></span>`, + (st,rd)=>GW.nixie(st,{onChange:(v,t)=>rd(t)}), '<b>a single warm-glowing numeral.</b> One lit digit per tube, leading zeros dark. Click to increment the count.'); card(I,'N20','Split-flap display', - `<span class="flap" id="flap"><span class="flapd"><b>D</b></span><span class="flapd"><b>N</b></span><span class="flapd"><b>S</b></span></span>`, + (st,rd)=>GW.splitFlap(st,{animate:!reduced,onChange:(v,t)=>rd(t)}), '<b>flips to the new value.</b> Each card drops to the next glyph with the mechanical clack. Auto-flips; click to flip now.'); card(I,'N21','Seven-segment display', - `<span class="seven" id="seven"></span>`, + (st,rd)=>GW.sevenSeg(st,{onChange:(v,t)=>rd(t)}), '<b>a lit-segment number.</b> Green segments, distinct from the nixie glow. Live countdown; click to add a minute.'); card(I,'N22','VFD marquee', - `<span class="vfdm" id="vfdm"><span class="txt" id="vfdt">ARCHSETUP · NET OK · BT 2 · SND 62%</span><span class="mesh"></span></span>`, + (st,rd)=>GW.vfdMarquee(st,{onChange:(v,t)=>rd(t)}), '<b>scrolling status line.</b> Teal glow, dot-matrix mesh, text tracks across. Scrolls live; click to change the message.'); card(I,'N23','Annunciator panel', - `<div class="annwrap"><div class="annun" id="annun"> - <span class="acell">SYNC</span><span class="acell warn">LOW BATT</span><span class="acell">LINK</span> - <span class="acell fault">NO DNS</span><span class="acell">VPN</span><span class="acell">DISK</span> - </div><div class="annbar" id="annbar"><span class="mc" id="anmc">MSTR CAUTION</span> - <button class="key" data-k="ACK">ACK</button><button class="key" data-k="TEST">TEST</button> - <button class="key" data-k="RESET">RESET</button></div></div>`, + (st,rd)=>GW.annunciator(st,{onChange:(v,t)=>rd(t)}), '<b>a grid of named alarms with the alarm lifecycle.</b> Cells light amber to warn, red to fault; any active alarm flashes MSTR CAUTION until ACK steadies it, a new alarm re-flashes it, RESET clears the board, TEST proves the bulbs. Click cells to raise alarms.'); card(I,'N24','Jewel pilot lamps', - `<span id="jewels" style="display:inline-flex;gap:10px;align-items:center"><span class="jewel" style="--jc:var(--jewel-r)"></span><span class="jewel" style="--jc:var(--jewel-a)"></span> - <span class="jewel" style="--jc:var(--jewel-g)"></span><span class="jewel dim"></span></span>`, + (st,rd)=>GW.jewels(st,{onChange:(v,t)=>rd(t)}), '<b>a faceted glass indicator.</b> The chunky bezel pilot lamp with a real bloom. Click a jewel to cycle red · amber · green · dark.'); card(I,'N25','Tape counter', - `<span class="counter" id="counter"></span>`, + (st,rd)=>GW.tapeCounter(st,{onChange:(v,t)=>rd(t)}), '<b>rolling odometer digits.</b> Numbered wheels roll to the next figure. Rolls live; readout shows the total.'); card(I,'N26','Analog clock', - `<span class="clock" id="clock"><span class="hh" id="clkh"></span><span class="mh" id="clkm"></span><span class="sh" id="clks"></span><span class="pin"></span></span>`, + (st,rd)=>GW.analogClock(st,{onChange:(v,t)=>rd(t)}), '<b>time on a real face.</b> Engraved ticks, three hands, live seconds sweep. Runs live; readout shows the time.'); card(I,'N27','Frequency-dial scale', - `<span class="freqscale" id="fscale"><span class="band">MHz</span></span>`, + (st,rd)=>GW.freqScale(st,{onChange:(v,t)=>rd(t)}), '<b>a printed log scale.</b> Marks crowd low, spread high — a real logarithmic axis. Drag the pointer; readout shows the frequency.'); card(I,'N28','Patch-bay jack field', - `<div class="patch" id="patch"></div>`, + (st,rd)=>GW.patchBay(st,{onChange:(v,t)=>rd(t)}), '<b>routing shown as patches.</b> A grid of jacks with cables between connected pairs. Click one jack then another to add/remove a cable.'); card(I,'R10','Data matrix readout', - `<svg id="dmx" class="rsvg press" viewBox="0 0 190 74" width="190" height="74"></svg>`, + (st,rd)=>GW.dataMatrix(st,{onChange:(v,t)=>rd(t)}), '<b>a page of amber fields.</b> The console data block — several labeled values on one lit matrix. Click to cycle pages; readout names the page. After a cockpit data readout.'); card(I,'R11','Warning flag window', - `<svg id="wflag" class="rsvg press" viewBox="0 0 120 64" width="120" height="64"></svg>`, + (st,rd)=>GW.warningFlag(st,{onChange:(v,t)=>rd(t)}), '<b>the mechanical alarm flag.</b> A striped barber-pole slides into a window when the condition trips — not a lamp, a flag. Click to trip and clear it. After the VIB flag on an altimeter.'); card(I,'R25','Fourteen-segment display', - `<svg id="seg14" class="rsvg press" viewBox="0 0 130 54" width="130" height="54"></svg>`, + (st,rd)=>GW.seg14(st,{onChange:(v,t)=>rd(t)}), '<b>segments that spell.</b> The starburst alphanumeric — diagonals and split bars let it write words, not just digits. Click to cycle the word. After a pedal preset display.'); card(I,'R26','Response graph', - `<svg id="rgraph" class="rsvg" viewBox="0 0 190 110" width="190" height="110"></svg>`, + (st,rd)=>GW.responseGraph(st,{onChange:(v,t)=>rd(t)}), '<b>a curve on labeled axes.</b> Log frequency across, dB up the side, and the amber peak is yours to place — drag it anywhere on the plot. After the response display of a mastering processor.'); card(I,'R30','Telegraph indicator', - `<svg id="telind" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`, + (st,rd)=>GW.telegraphIndicator(st,{onChange:(v,t)=>rd(t)}), '<b>state on a pie of sectors.</b> The pointer and its little flag name the active state, engine-telegraph style. Click to step the state. After an electric indicator dial.'); card(I,'R31','Radar sweep', - `<svg id="radar" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`, + (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 ============ */ /* 01 toggle */ -(function(){const sw=$('toggle01').querySelector('.switch'); - function upd(){setRd('01',sw.classList.contains('on')?'ON':'OFF');} - sw.onclick=()=>{sw.classList.toggle('on');upd();};upd();})(); -/* 02 console key */ -$('keys02').querySelectorAll('.key').forEach(k=>k.onclick=()=>{ - $('keys02').querySelectorAll('.key').forEach(x=>x.classList.remove('on','red')); - k.classList.add(k.textContent==='MUTED'?'red':'on');setRd('02',k.textContent);}); -setRd('02','LIVE'); -/* 03 horizontal fader */ -function setF1(p){const f=$('f1');f.querySelector('.fill').style.width=p+'%';f.querySelector('.cap').style.left=p+'%';setRd('03','level '+Math.round(p));} -dragX($('f1'),setF1);setF1(68); -/* 04 vertical faders */ -let vfA=60,vfB=35; -function vfUpd(){setRd('04','A '+Math.round(vfA)+' · B '+Math.round(vfB));} -function setVf(el,p){el.querySelector('.fill').style.height=p+'%';el.querySelector('.cap').style.bottom=p+'%';} -dragY($('v1'),p=>{vfA=p;setVf($('v1'),p);vfUpd();}); -dragY($('v2'),p=>{vfB=p;setVf($('v2'),p);vfUpd();}); -setVf($('v1'),60);setVf($('v2'),35);vfUpd(); -/* 05 knob */ -let knobV=53; -function setKnob(v){knobV=v;$('kind').style.transform=`rotate(${-150+v/100*300}deg)`;setRd('05','gain '+Math.round(v));} -dragDelta($('knob'),()=>knobV,setKnob,{min:0,max:100});setKnob(53); -/* 06 segmented */ -$('seg06').querySelectorAll('button').forEach(b=>b.onclick=()=>{ - $('seg06').querySelectorAll('button').forEach(x=>x.classList.remove('on'));b.classList.add('on');setRd('06',b.textContent);}); -setRd('06','TIMER'); -/* 07 chip */ -$('chip07').onclick=()=>{$('chip07').classList.toggle('on');setRd('07',$('chip07').classList.contains('on')?'ON':'OFF');}; -setRd('07','ON'); -/* 08 arm */ -let armed=false; -$('arm').onclick=()=>{const a=$('arm');if(!armed){armed=true;a.classList.add('armed');a.textContent='forget? again';setRd('08','ARMED');} - else{armed=false;a.classList.remove('armed');a.textContent='forget';setRd('08','FIRED · reset');}}; -setRd('08','SAFE'); -/* 09 lamp row */ -const lrStates=[['gold','tap to connect'],['busy','connecting…'],['','connected']];let lrI=0; -$('lrow9').onclick=()=>{lrI=(lrI+1)%3;const s=lrStates[lrI];$('lrow9').querySelector('.lamp').className='lamp '+s[0]; - $('lrow9').querySelector('.what').textContent=s[1];setRd('09',s[1]);}; -setRd('09','tap to connect'); -/* 24 rotary selector */ -const rotVals=[4,6,8,10,12];let rotIdx=2;const rotAng=[-70,-35,0,35,70]; -$('rotsel24').querySelector('.knob').onclick=()=>{rotIdx=(rotIdx+1)%5; - $('rotsel24').querySelector('.ind').style.transform=`rotate(${rotAng[rotIdx]}deg)`; - $('rotsel24').querySelectorAll('.pos').forEach((p,i)=>p.classList.toggle('on',i===rotIdx));setRd('24','size '+rotVals[rotIdx]);}; -setRd('24','size 8'); -/* 25 slide-rule */ -const tunVals=[4,6,8,10,12];let tunIdx=2;const tunX=[12,51,90,129,168]; -function setTun(i){tunIdx=Math.max(0,Math.min(tunVals.length-1,i));$('tundl').style.left=tunX[tunIdx]+'px'; - $('tuner').querySelectorAll('.mk').forEach((m,j)=>m.classList.toggle('on',j===tunIdx));setRd('25','pos '+tunVals[tunIdx]);} -$('tuner').onclick=(e)=>{const r=$('tuner').getBoundingClientRect(),x=e.clientX-r.left; - let best=0,bd=1e9;tunX.forEach((tx,j)=>{const d=Math.abs(tx-x);if(d<bd){bd=d;best=j;}});setTun(best);$('tuner').focus();}; -$('tuner').onkeydown=(e)=>{if(e.key==='ArrowLeft'||e.key==='ArrowDown'){e.preventDefault();setTun(tunIdx-1);} - else if(e.key==='ArrowRight'||e.key==='ArrowUp'){e.preventDefault();setTun(tunIdx+1);}}; -setRd('25','pos 8'); -/* N01 rocker */ -$('rocker').onclick=()=>{$('rocker').classList.toggle('on');setRd('N01',$('rocker').classList.contains('on')?'ON':'OFF');}; -setRd('N01','ON'); -/* N02 transport */ -function transport(b){b.parentElement.querySelectorAll('.tbtn').forEach(x=>x.classList.remove('on'));b.classList.add('on'); - const playing=b.dataset.t==='play';document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState=playing&&!reduced?'running':'paused'); - setRd('N02',{rew:'REW',play:'PLAY',stop:'STOP',rec:'REC'}[b.dataset.t]);} -setRd('N02','PLAY'); -/* N03 radio preset */ -function preset(b){b.parentElement.querySelectorAll('.preset').forEach(x=>x.classList.remove('on'));b.classList.add('on');setRd('N03',b.textContent);} -setRd('N03','WIFI'); -/* N04 concentric dual knob */ -let dkoV=50,dkiV=50; -function setDual(){$('dko').style.transform=`rotate(${-150+dkoV/100*300}deg)`;$('dki').style.transform=`rotate(${-150+dkiV/100*300}deg)`; - setRd('N04','bass '+Math.round(dkoV)+' · treble '+Math.round(dkiV));} -dragDelta($('dko'),()=>dkoV,v=>{dkoV=v;setDual();},{min:0,max:100}); -dragDelta($('dki'),()=>dkiV,v=>{dkiV=v;setDual();},{min:0,max:100,stop:true}); -setDual(); -/* N05 encoder */ -let encV=7; -function paintEnc(){const leds=$('enc').querySelectorAll('.led');const lit=((Math.floor(encV)%12)+12)%12; - leds.forEach((l,i)=>l.classList.toggle('on',i<=lit));} -function setEnc(v){encV=v;paintEnc();const ind=$('enc').querySelector('.knob .ind');if(ind)ind.parentElement.style.transform=`rotate(${encV*30}deg)`; - setRd('N05','pos '+Math.round(encV));} -(function(){const enc=$('enc');const R=27,cx=33,cy=33; - for(let i=0;i<12;i++){const a=(i*30-90)*Math.PI/180;const d=document.createElement('span');d.className='led'; - d.style.left=(cx+R*Math.cos(a))+'px';d.style.top=(cy+R*Math.sin(a))+'px';enc.appendChild(d);} - const k=document.createElement('span');k.className='knob';k.innerHTML='<span class="ind"></span>';enc.appendChild(k);})(); -dragDelta($('enc'),()=>encV,setEnc,{min:0,max:100000,sens:0.25});setEnc(7); -/* N06 keylock */ -let keyIdx=1;const keyAng=[-70,0,70];const keyNm=['OFF','ON','RUN']; -function keyTurn(){keyIdx=(keyIdx+1)%3;$('kbit').style.transform=`rotate(${keyAng[keyIdx]}deg)`; - $('keylock6').querySelectorAll('.kpos').forEach((p,i)=>p.classList.toggle('on',i===keyIdx));setRd('N06',keyNm[keyIdx]);} -setRd('N06','ON'); -/* N07 crossfader */ -function setXf(p){$('xfcap').style.left=p+'%';const v=Math.round(p-50);setRd('N07',(v>0?'+':'')+v);} -dragX($('xf'),setXf);setXf(50); -/* N08 thumbwheel */ -let thumbV=42; -function setThumb(v){thumbV=Math.round(v);const s=(((thumbV%100)+100)%100);$('thval').textContent=String(s).padStart(2,'0');setRd('N08','value '+s);} -dragDelta($('thumbwheel'),()=>thumbV,setThumb,{min:0,max:99,sens:0.2});setThumb(42); -/* N09 DIP */ -function dipUpd(){const bits=[...$('dips').querySelectorAll('.dipsw')].map(x=>x.classList.contains('on')?'1':'0').join('');setRd('N09',bits);} -function dip(s){s.classList.toggle('on');dipUpd();} -dipUpd(); -/* N10 jog */ -let jogV=0; -function setJog(v){jogV=v;$('jogi').style.transform=`rotate(${jogV*4}deg)`;setRd('N10','pos '+Math.round(jogV));} -dragDelta($('jog10'),()=>jogV,setJog,{min:-100000,max:100000,sens:0.5});setJog(0); +/* 01-09, 24, 25, N01-N10 extracted to widgets.js (GW.*) — wired at their card records */ /* ============ METER WIRING (drag-driven) ============ */ -/* 10 needle gauge */ -let gV=50; -function setGauge(v){gV=v;$('g1').style.transform=`rotate(${-60+v/100*120}deg)`;$('g1v').textContent=Math.round(v);setRd('10','value '+Math.round(v)+'%');} -dragDelta($('gauge10'),()=>gV,setGauge,{min:0,max:100});setGauge(50); -/* 13 signal ladder */ -let ladV=3; -function setLadder(v){ladV=v;const bars=$('ladder').children; - for(let i=0;i<bars.length;i++)bars[i].style.background=i<v?'var(--gold)':'var(--wash)';setRd('13',v+'/4');} -$('ladder').onclick=()=>setLadder((ladV+1)%5);setLadder(3); -/* 14 fuel bars */ -function setBar(id,no,p){const b=$(id);b.querySelector('span').style.width=p+'%';b.classList.toggle('warn',p<20);setRd(no,Math.round(p)+'%');} -dragX($('bar14'),p=>setBar('bar14','14',p));dragX($('bar14b'),p=>setBar('bar14b','14',p)); -setBar('bar14','14',72); -/* 15 radial ring */ -let ringV=68; -function setRing(v){ringV=v;const r=$('ring15');r.style.setProperty('--p',v);r.querySelector('b').textContent=Math.round(v);setRd('15',Math.round(v)+'%');} -dragDelta($('ring15'),()=>ringV,setRing,{min:0,max:100});setRing(68); -/* N13 crossed-needle */ -let cnPow=55; -function setCross(v){cnPow=v;const fwd=v,rfl=v*0.68;$('cnA').style.transform=`rotate(${-42+fwd/100*84}deg)`; - $('cnB').style.transform=`rotate(${42-rfl/100*84}deg)`;setRd('N13','FWD '+Math.round(fwd)+' · RFL '+Math.round(rfl));} -dragDelta($('crossm13'),()=>cnPow,setCross,{min:0,max:100});setCross(55); -/* N14 thermometer */ -let thV=58; -function setThermo(v){thV=v;$('thermf').style.height=v+'%';setRd('N14',Math.round(30+v/100*60)+'°');} -dragDelta($('thermo14'),()=>thV,setThermo,{min:0,max:100});setThermo(58); -/* N15 bourdon */ -let bV=45; -function setBour(v){bV=v;$('bourd').style.transform=`rotate(${-60+v/100*120}deg)`;setRd('N15',Math.round(v/100*160)+' PSI');} -dragDelta($('bourdon15'),()=>bV,setBour,{min:0,max:100});setBour(45); -/* N17 correlation */ -function setCorr(p){const v=(p-50)/50;$('corrn').style.transform=`rotate(${v*38}deg)`;setRd('N17',(v>=0?'+':'')+v.toFixed(2));} -dragX($('corr17'),setCorr);setCorr(58); -/* N18 battery */ -(function(){const bc=$('battc');for(let i=0;i<8;i++){const c=document.createElement('span');c.className='cell';bc.appendChild(c);}})(); -function setBatt(p){const cells=$('battc').children,lit=Math.round(p/100*cells.length); - for(let i=0;i<cells.length;i++)cells[i].className='cell'+(i<lit?' on':'')+((p<=25&&i<lit)?' warn':'');setRd('N18',Math.round(p)+'%');} -dragX($('batt18'),setBatt);setBatt(62); +/* 10-17, N11-N18 extracted to widgets.js (GW.*) — wired at their card records */ /* ============ INDICATOR WIRING ============ */ -/* 18 status lamp */ -(function(){const lst=['lamp','lamp gold','lamp red','lamp off','lamp busy'];const nm=['ok','engaged','fault','off','busy']; - $('lamp18').querySelectorAll('.lamp').forEach(l=>{l.style.cursor='pointer';l.onclick=()=>{ - let i=lst.indexOf(l.className);if(i<0)i=0;i=(i+1)%lst.length;l.className=lst[i];setRd('18',nm[i]);};});})(); -setRd('18','five states'); -/* 19 badge */ -(function(){const bv=['badge','badge red','badge ghost']; - $('badge19').querySelectorAll('.badge').forEach(b=>{b.style.cursor='pointer';b.onclick=()=>{ - let i=bv.indexOf(b.className);if(i<0)i=0;i=(i+1)%bv.length;b.className=bv[i];setRd('19',b.textContent);};});})(); -setRd('19','three variants'); -/* 21 engraved */ -let engN=3;$('engrave').onclick=()=>{engN=engN%9+1;$('engc').textContent='· '+engN;setRd('21','count '+engN);}; -setRd('21','count 3'); -/* 22 output well */ -const owSteps=[['gold','Probe','8.8.8.8 …'],['','Gateway','10.0.0.1 ok'],['','DNS','1.1.1.1 ok'],['red','Retry','timeout']];let owI=0; -$('owell').onclick=()=>{owI=(owI+1)%owSteps.length;const s=owSteps[owI];const d=document.createElement('div');d.className='ostep'; - d.innerHTML=`<span class="lamp ${s[0]}"></span><span><b>${s[1]}</b><span class="ev">${s[2]}</span></span>`; - $('owell').appendChild(d);while($('owell').children.length>5)$('owell').removeChild($('owell').firstChild);setRd('22','+ '+s[1]);}; -setRd('22','click to stream'); -/* 23 toast */ -const toastMsgs=['link up · 300 Mbps','bt paired — WH-1000XM4','profile switched','joined @Hyatt_WiFi — saved'];let toastN=0; -$('toast').onclick=()=>{toastN++;$('toast').textContent=toastMsgs[toastN%toastMsgs.length]; - $('toast').style.opacity='0';requestAnimationFrame(()=>{$('toast').style.transition='opacity .3s';$('toast').style.opacity='1';});setRd('23','fired '+toastN);}; -setRd('23','click to fire'); -/* 26 nixie */ -let nixV=8; -function setNix(v){nixV=v;const t=$('nixie').children,s=String(v).padStart(2,'0'); - t[0].classList.toggle('off',v<10);t[0].querySelector('b').textContent=s[0];t[1].querySelector('b').textContent=s[1];setRd('26',s);} -$('nixie').onclick=()=>setNix((nixV+1)%100);setNix(8); -/* N23 annunciator with alarm lifecycle: raise -> MSTR CAUTION flashes -> ACK steadies -> RESET clears */ -(function(){const st=['acell','acell warn','acell fault'];const nm=['ok','warn','fault']; - const mc=$('anmc');let acked=false; - const active=()=>$('annun').querySelectorAll('.warn,.fault').length; - const refresh=()=>{const n=active(); - mc.classList.toggle('on',n>0); - mc.classList.toggle('fl',n>0&&!acked); - setRd('N23',n===0?'clear':n+' active · '+(acked?'ACK':'UNACK'));}; - $('annun').querySelectorAll('.acell').forEach(cell=>{const label=cell.textContent;cell.onclick=()=>{ - let cur=cell.className.trim();let i=st.indexOf(cur);if(i<0)i=0;i=(i+1)%st.length;cell.className=st[i]; - if(i>0)acked=false; // a new alarm re-arms the flasher - refresh();setRd('N23',label+' → '+nm[i]);};}); - $('annbar').querySelectorAll('.key').forEach(k=>k.addEventListener('click',()=>{ - const a=k.dataset.k; - if(a==='ACK'){if(active()>0)acked=true;refresh();} - else if(a==='RESET'){$('annun').querySelectorAll('.acell').forEach(c=>c.className='acell');acked=false;refresh();} - else if(a==='TEST'){const prev=[...$('annun').querySelectorAll('.acell')].map(c=>c.className); - $('annun').querySelectorAll('.acell').forEach(c=>c.className='acell fault'); - mc.classList.add('on','fl');setRd('N23','lamp test'); - setTimeout(()=>{[...$('annun').querySelectorAll('.acell')].forEach((c,i)=>c.className=prev[i]);refresh();},1100);}})); - refresh();})(); -/* N24 jewel */ -(function(){const cols=['var(--jewel-r)','var(--jewel-a)','var(--jewel-g)'];const nm=['red','amber','green']; - $('jewels').querySelectorAll('.jewel').forEach(j=>{j.onclick=()=>{ - if(j.classList.contains('dim')){j.classList.remove('dim');j.style.setProperty('--jc',cols[0]);setRd('N24','red');return;} - let cur=j.style.getPropertyValue('--jc').trim();let i=cols.indexOf(cur); - if(i>=cols.length-1||i<0){j.classList.add('dim');setRd('N24','dark');}else{j.style.setProperty('--jc',cols[i+1]);setRd('N24',nm[i+1]);}};});})(); -setRd('N24','click to cycle'); -/* N27 frequency-dial + pointer */ -(function(){const fs=$('fscale');const W=184;const marks=[0.5,1,2,3,5,7,10,14,20];const lo=Math.log10(0.5),hi=Math.log10(20); - marks.forEach(m=>{const x=6+((Math.log10(m)-lo)/(hi-lo))*(W-12); - const tk=document.createElement('span');tk.className='tick';tk.style.left=x+'px';tk.style.height='11px';fs.appendChild(tk); - const mk=document.createElement('span');mk.className='mk';mk.style.left=x+'px';mk.textContent=m;fs.appendChild(mk);}); - for(let m=0.5;m<=20;m+=(m<2?0.25:m<10?1:2)){const x=6+((Math.log10(m)-lo)/(hi-lo))*(W-12); - const tk=document.createElement('span');tk.className='tick';tk.style.left=x+'px';tk.style.height='6px';tk.style.opacity='.5';fs.appendChild(tk);} - const ptr=document.createElement('span');ptr.className='fptr';fs.appendChild(ptr); - function freqAt(p){return Math.pow(10,lo+(p/100)*(hi-lo));} - window.setFreq=function(p){ptr.style.left=(6+p/100*(W-12))+'px';const f=freqAt(p);setRd('N27',(f<10?f.toFixed(1):Math.round(f))+' MHz');}; - dragX(fs,window.setFreq);window.setFreq(45);})(); -/* N28 patch-bay */ -(function(){const patch=$('patch');const rows=2,cols=4;const jacks=[]; - for(let r=0;r<rows;r++){const rowEl=document.createElement('div');rowEl.className='row'; - for(let c=0;c<cols;c++){const j=document.createElement('span');j.className='jack';j.dataset.id=(r*cols+c);rowEl.appendChild(j);jacks.push(j);} - patch.appendChild(rowEl);} - const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');patch.appendChild(svg); - let conns=[];let pending=null; - function key(a,b){return Math.min(a,b)+'-'+Math.max(a,b);} - function draw(){while(svg.firstChild)svg.removeChild(svg.firstChild); - const pr=patch.getBoundingClientRect(); - jacks.forEach(j=>j.classList.remove('hot')); - conns.forEach(pair=>{const[a,b]=pair.split('-').map(Number); - const ra=jacks[a].getBoundingClientRect(),rb=jacks[b].getBoundingClientRect(); - const x1=ra.left-pr.left+ra.width/2,y1=ra.top-pr.top+ra.height/2; - const x2=rb.left-pr.left+rb.width/2,y2=rb.top-pr.top+rb.height/2; - const p=document.createElementNS('http://www.w3.org/2000/svg','path'); - const my=Math.max(y1,y2)+16;p.setAttribute('d',`M${x1} ${y1} C${x1} ${my} ${x2} ${my} ${x2} ${y2}`);svg.appendChild(p); - jacks[a].classList.add('hot');jacks[b].classList.add('hot');}); - setRd('N28',conns.length?conns.map(c=>{const[a,b]=c.split('-');return a+'↔'+b;}).join(' '):'no cables');} - jacks.forEach(j=>{j.onclick=()=>{const id=+j.dataset.id; - if(pending==null){pending=id;j.classList.add('sel');return;} - if(pending===id){pending=null;j.classList.remove('sel');return;} - const k=key(pending,id);const i=conns.indexOf(k);if(i>=0)conns.splice(i,1);else conns.push(k); - jacks[pending].classList.remove('sel');pending=null;draw();};}); - conns=['0-5','2-7'];draw();window.addEventListener('resize',draw);})(); +/* 18-23, 26 extracted to widgets.js (GW.*) — wired at their card records */ +/* N20-N28 extracted to widgets.js (GW.*) — wired at their card records */ /* ============ 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',{}); - const g1=svgEl(defs,'linearGradient',{id:'vuGlass',x1:0,y1:0,x2:0,y2:1}); - svgEl(g1,'stop',{offset:'0','stop-color':'rgba(255,255,255,.13)'}); - svgEl(g1,'stop',{offset:'.35','stop-color':'rgba(255,255,255,0)'}); - const clip=svgEl(defs,'clipPath',{id:'vuFace'}); - svgEl(clip,'rect',{x:8,y:8,width:134,height:74,rx:3}); - svgEl(s,'rect',{x:.5,y:.5,width:149,height:95,rx:7,fill:'#16130f',stroke:'#060505'}); - svgEl(s,'rect',{x:8,y:8,width:134,height:74,rx:3,fill:'var(--cream)'}); - const face=svgEl(s,'g',{'clip-path':'url(#vuFace)'}); - const arcPt=t=>polar(cx,cy,58,sweep(t)); - const [ax1,ay1]=arcPt(0),[ax2,ay2]=arcPt(1); - svgEl(face,'path',{d:`M ${ax1} ${ay1} A 58 58 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'#3a3128','stroke-width':1.2}); - const [rx1,ry1]=arcPt(.75); - svgEl(face,'path',{d:`M ${rx1} ${ry1} A 58 58 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'var(--fail)','stroke-width':3.5}); - for(const [db,t] of VUDB){const a=sweep(t),[x1,y1]=polar(cx,cy,55,a),[x2,y2]=polar(cx,cy,63,a); - svgEl(face,'line',{x1,y1,x2,y2,stroke:db>0?'var(--fail)':'#3a3128','stroke-width':1.1});} - for(const [db,t] of VUDB){if(![-20,-10,-5,-3,0,1,2,3].includes(db))continue; - const [x,y]=polar(cx,cy,69,sweep(t)); - svgEl(face,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.8,'font-family':'var(--mono)', - fill:db>0?'var(--fail)':'#3a3128'}).textContent=Math.abs(db);} - svgEl(face,'text',{x:75,y:78,'text-anchor':'middle','font-size':11,'font-weight':700, - 'font-family':'var(--mono)',fill:'#3a3128'}).textContent='VU'; - mcvu.needle=svgEl(face,'line',{x1:cx,y1:cy,x2:cx,y2:cy-62,stroke:'#1a1613','stroke-width':1.6, - transform:`rotate(${sweep(.35)},${cx},${cy})`}); - svgEl(face,'circle',{cx,cy:88,r:9,fill:'#16130f'}); - svgEl(s,'rect',{x:8,y:8,width:134,height:74,rx:3,fill:'url(#vuGlass)'}); - mcvu.sweep=sweep;})(); -function setMcVu(t){t=Math.max(0,Math.min(1.02,t));mcvu.t=t; - mcvu.needle.setAttribute('transform',`rotate(${mcvu.sweep(Math.min(1,t))},75,112)`); - const db=vuDb(Math.min(1,t)); - setRd('R01',(db>0?'+':'')+db.toFixed(1)+' VU');} +/* R01 ballistics: the page owns the signal chase; the meter just takes set(t) */ function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12); - setMcVu(mcvu.t+(target-mcvu.t)*(target>mcvu.t?0.32:0.10));} - -/* R02 calibrated vernier dial: the disc turns under a fixed hairline */ -let vernV=42.0,vernDisc=null; -(function(){const s=$('vern'),cx=75,cy=75; - const defs=svgEl(s,'defs',{}); - const fg=svgEl(defs,'radialGradient',{id:'vernFace',cx:'50%',cy:'45%',r:'60%'}); - svgEl(fg,'stop',{offset:'0','stop-color':'#f7f2da'}); - svgEl(fg,'stop',{offset:'1','stop-color':'#e4dfc2'}); - const bk=svgEl(defs,'radialGradient',{id:'bakeKnob',cx:'42%',cy:'36%',r:'75%'}); - svgEl(bk,'stop',{offset:'0','stop-color':'#37332c'}); - svgEl(bk,'stop',{offset:'1','stop-color':'#060505'}); - svgEl(s,'circle',{cx,cy,r:72,fill:'#0c0b0a',stroke:'#262320'}); - vernDisc=svgEl(s,'g',{}); - svgEl(vernDisc,'circle',{cx,cy,r:66,fill:'url(#vernFace)',stroke:'#b7b29a','stroke-width':1}); - for(let u=0;u<=100;u++){const a=u*3,major=u%10===0,half=u%5===0; - const len=major?10:half?7:4.5,[x1,y1]=polar(cx,cy,63,a),[x2,y2]=polar(cx,cy,63-len,a); - svgEl(vernDisc,'line',{x1,y1,x2,y2,stroke:'#3a3128','stroke-width':major?1.3:half?1:.7}); - if(major){const g=svgEl(vernDisc,'g',{transform:`rotate(${a},${cx},${cy})`}); - svgEl(g,'text',{x:cx,y:cy-44,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)', - fill:'#3a3128'}).textContent=u;}} - for(let k=0;k<12;k++){const [x,y]=polar(cx,cy,24,k*30); - svgEl(s,'circle',{cx:x,cy:y,r:4.8,fill:'url(#bakeKnob)'});} - svgEl(s,'circle',{cx,cy,r:24,fill:'url(#bakeKnob)',stroke:'#000','stroke-width':.6}); - svgEl(s,'circle',{cx,cy,r:16,fill:'url(#bakeKnob)'}); - svgEl(s,'ellipse',{cx:69,cy:66,rx:7,ry:4,fill:'rgba(255,255,255,.10)',transform:'rotate(-32,69,66)'}); - svgEl(s,'line',{x1:cx,y1:6,x2:cx,y2:22,stroke:'var(--fail)','stroke-width':2.2,'stroke-linecap':'round'});})(); -function setVern(v){vernV=v;vernDisc.setAttribute('transform',`rotate(${-v*3},75,75)`);setRd('R02',v.toFixed(1));} -dragDelta($('vern'),()=>vernV,setVern,{min:0,max:100,sens:0.15});setVern(42.0); - -/* R03 bat-handle toggle */ -let batOn=true,batLever=null,batLblOn=null,batLblOff=null; -(function(){const s=$('batsw'),cx=35,cy=44; - const defs=svgEl(s,'defs',{}); - const ng=svgEl(defs,'linearGradient',{id:'nutG',x1:0,y1:0,x2:0,y2:1}); - svgEl(ng,'stop',{offset:'0','stop-color':'#8a8578'}); - svgEl(ng,'stop',{offset:'1','stop-color':'#4e4a42'}); - const cg=svgEl(defs,'linearGradient',{id:'chromeG',x1:0,y1:0,x2:1,y2:0}); - svgEl(cg,'stop',{offset:'0','stop-color':'#7e7a70'}); - svgEl(cg,'stop',{offset:'.45','stop-color':'#e8e5db'}); - svgEl(cg,'stop',{offset:'1','stop-color':'#8a867c'}); - const tg=svgEl(defs,'radialGradient',{id:'tipG',cx:'40%',cy:'32%',r:'75%'}); - svgEl(tg,'stop',{offset:'0','stop-color':'#f2efe8'}); - svgEl(tg,'stop',{offset:'1','stop-color':'#7e7a70'}); - batLblOn=svgEl(s,'text',{x:cx,y:9,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--gold-hi)'});batLblOn.textContent='ON'; - batLblOff=svgEl(s,'text',{x:cx,y:87,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--dim)'});batLblOff.textContent='OFF'; - const pts=[];for(let k=0;k<6;k++){const a=(k*60+30)*Math.PI/180;pts.push((cx+17*Math.cos(a))+','+(cy+17*Math.sin(a)));} - svgEl(s,'polygon',{points:pts.join(' '),fill:'url(#nutG)',stroke:'#2b2822'}); - svgEl(s,'circle',{cx,cy,r:9,fill:'#14110e',stroke:'#000'}); - batLever=svgEl(s,'g',{}); - svgEl(batLever,'path',{d:`M ${cx-3.2} ${cy} L ${cx-1.8} 21 L ${cx+1.8} 21 L ${cx+3.2} ${cy} Z`,fill:'url(#chromeG)',stroke:'#4e4a42','stroke-width':.5}); - svgEl(batLever,'ellipse',{cx,cy:17,rx:6.5,ry:8,fill:'url(#tipG)',stroke:'#5e5a52','stroke-width':.6}); - batLever.style.transformOrigin=`${cx}px ${cy}px`;batLever.style.transition='transform .12s'; - $('batsw').onclick=()=>setBat(!batOn);})(); -function setBat(on){batOn=on;batLever.style.transform=on?'rotate(0deg)':'rotate(180deg)'; - batLblOn.setAttribute('fill',on?'var(--gold-hi)':'var(--dim)'); - batLblOff.setAttribute('fill',on?'var(--dim)':'var(--gold-hi)');setRd('R03',on?'ON':'OFF');} -setBat(true); - -/* R04 bakelite fluted knob over a printed 0-10 scale */ -let bakeV=63,bakeIdx=null; -(function(){const s=$('bakek'),cx=55,cy=54; - const defs=svgEl(s,'defs',{}); - const sk=svgEl(defs,'radialGradient',{id:'bakeSk',cx:'42%',cy:'36%',r:'80%'}); - svgEl(sk,'stop',{offset:'0','stop-color':'#312d27'}); - svgEl(sk,'stop',{offset:'1','stop-color':'#050404'}); - const dm=svgEl(defs,'radialGradient',{id:'bakeDome',cx:'40%',cy:'32%',r:'80%'}); - svgEl(dm,'stop',{offset:'0','stop-color':'#3a362f'}); - svgEl(dm,'stop',{offset:'1','stop-color':'#0a0908'}); - for(let i=0;i<=10;i++){const a=-135+i*27,major=i%5===0; - const [x1,y1]=polar(cx,cy,38,a),[x2,y2]=polar(cx,cy,major?45:43.5,a); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--steel)','stroke-width':major?1.5:1}); - if(major){const [x,y]=polar(cx,cy,50.5,a); - svgEl(s,'text',{x,y:y+2.6,'text-anchor':'middle','font-size':8,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=i;}} - for(let k=0;k<18;k++){const [x,y]=polar(cx,cy,31,k*20); - svgEl(s,'circle',{cx:x,cy:y,r:3.4,fill:'url(#bakeSk)'});} - svgEl(s,'circle',{cx,cy,r:31,fill:'url(#bakeSk)',stroke:'#000','stroke-width':.6}); - bakeIdx=svgEl(s,'line',{x1:cx,y1:cy-30,x2:cx,y2:cy-21,stroke:'var(--gold-hi)','stroke-width':2.4, - 'stroke-linecap':'round'}); - svgEl(s,'circle',{cx,cy,r:19,fill:'url(#bakeDome)'}); - svgEl(s,'ellipse',{cx:49,cy:46,rx:6,ry:3.5,fill:'rgba(255,255,255,.14)',transform:'rotate(-28,49,46)'});})(); -function setBake(v){bakeV=v;bakeIdx.setAttribute('transform',`rotate(${v*2.7-135},55,54)`); - 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)));} - -/* R05 filter slider bank: one fader per band, arrow caps on black tracks */ -const FB_FREQ=[78,136,235,406,701,1210,2090,3620]; -const fbank={vals:[18,30,42,25,35,55,20,48],caps:[]}; -(function(){const s=$('fbank'),y0=14,y1=84,x0=30,dx=19.5; - const yOf=v=>y0+v/60*(y1-y0); - 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'; - FB_FREQ.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=f<1000?f:(f/1000)+'k'; - 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'}); - fbank.caps.push(cap); - 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=>setFbank(i,(100-pct)/100*60));}); - })(); -function setFbank(i,db){db=Math.max(0,Math.min(60,db));fbank.vals[i]=db; - fbank.caps[i].setAttribute('transform',`translate(0,${14+db/60*70})`); - setRd('R05',`${FB_FREQ[i]<1000?FB_FREQ[i]:(FB_FREQ[i]/1000)+'k'} Hz · −${Math.round(db)} dB`);} -fbank.vals.forEach((v,i)=>setFbank(i,v));setRd('R05','drag a band'); - -/* R06 chicken-head selector: the tapered lever aims at the position */ -const CHICK_POS=[['OFF',-60],['LO',-20],['MID',20],['HI',60]]; -let chickI=2,chickLever=null,chickLbls=[]; -(function(){const s=$('chick'),cx=55,cy=58; - const defs=svgEl(s,'defs',{}); - const bg=svgEl(defs,'radialGradient',{id:'chickG',cx:'42%',cy:'34%',r:'80%'}); - svgEl(bg,'stop',{offset:'0','stop-color':'#35312b'}); - svgEl(bg,'stop',{offset:'1','stop-color':'#070606'}); - CHICK_POS.forEach(([lbl,a],i)=>{const [tx,ty]=polar(cx,cy,42,a); - const t=svgEl(s,'text',{x:tx,y:ty+2.5,'text-anchor':'middle','font-size':6.5,'letter-spacing':'.06em', - 'font-family':'var(--mono)',fill:'var(--dim)'});t.textContent=lbl;chickLbls.push(t); - const [mx1,my1]=polar(cx,cy,33,a),[mx2,my2]=polar(cx,cy,29,a); - svgEl(s,'line',{x1:mx1,y1:my1,x2:mx2,y2:my2,stroke:'var(--steel)','stroke-width':1.2});}); - svgEl(s,'circle',{cx,cy,r:12,fill:'#0c0b0a'}); - chickLever=svgEl(s,'g',{}); - svgEl(chickLever,'path',{d:`M ${cx} ${cy-29} L ${cx+8.5} ${cy-4} Q ${cx+9} ${cy+8} ${cx+5} ${cy+11} A 7 7 0 0 1 ${cx-5} ${cy+11} Q ${cx-9} ${cy+8} ${cx-8.5} ${cy-4} Z`, - fill:'url(#chickG)',stroke:'#000','stroke-width':.7}); - svgEl(chickLever,'line',{x1:cx,y1:cy-26,x2:cx,y2:cy+6,stroke:'#4a463e','stroke-width':1.4,'stroke-linecap':'round'}); - chickLever.style.transformOrigin=`${cx}px ${cy}px`;chickLever.style.transition='transform .12s'; - $('chick').onclick=()=>setChick((chickI+1)%CHICK_POS.length);})(); -function setChick(i){chickI=i;chickLever.style.transform=`rotate(${CHICK_POS[i][1]}deg)`; - chickLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)')); - setRd('R06',CHICK_POS[i][0]);} -setChick(2); - -/* R07 round panel meter: porthole bezel, same VU law as R01 */ -let rndT=.5,rndNeedle=null; -(function(){const s=$('rndm'),cx=55,fy=54,py=74,sweep=t=>-40+t*80; - const defs=svgEl(s,'defs',{}); - const clip=svgEl(defs,'clipPath',{id:'rndFace'}); - svgEl(clip,'circle',{cx,cy:fy,r:40}); - svgEl(s,'circle',{cx,cy:fy,r:48,fill:'#141110',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy:fy,r:40,fill:'var(--cream)'}); - const g=svgEl(s,'g',{'clip-path':'url(#rndFace)'}); - const pt=(r,t)=>polar(cx,py,r,sweep(t)); - const [ax1,ay1]=pt(44,0),[ax2,ay2]=pt(44,1); - svgEl(g,'path',{d:`M ${ax1} ${ay1} A 44 44 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'#3a3128','stroke-width':1.1}); - const [rzx,rzy]=pt(44,.75); - svgEl(g,'path',{d:`M ${rzx} ${rzy} A 44 44 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'var(--fail)','stroke-width':3}); - for(const [db,t] of VUDB){const [x1,y1]=pt(41.5,t),[x2,y2]=pt(47,t); - svgEl(g,'line',{x1,y1,x2,y2,stroke:db>0?'var(--fail)':'#3a3128','stroke-width':1});} - for(const [db,t] of VUDB){if(![-20,-10,-5,-3,0,3].includes(db))continue; - const [x,y]=pt(52,t); - svgEl(g,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)', - fill:db>0?'var(--fail)':'#3a3128'}).textContent=Math.abs(db);} - svgEl(g,'text',{x:cx,y:fy+14,'text-anchor':'middle','font-size':8,'font-weight':700, - 'font-family':'var(--mono)',fill:'#3a3128'}).textContent='dB'; - rndNeedle=svgEl(g,'line',{x1:cx,y1:py,x2:cx,y2:py-46,stroke:'#1a1613','stroke-width':1.5, - transform:`rotate(0,${cx},${py})`}); - svgEl(g,'circle',{cx,cy:py,r:5,fill:'#141110'}); - svgEl(s,'ellipse',{cx:cx-8,cy:fy-22,rx:20,ry:9,fill:'rgba(255,255,255,.07)',transform:`rotate(-18,${cx-8},${fy-22})`});})(); -function setRnd(v){const t=Math.max(0,Math.min(1,v/100));rndT=t; - rndNeedle.setAttribute('transform',`rotate(${-40+t*80},55,74)`); - const db=vuDb(t);setRd('R07',(db>0?'+':'')+db.toFixed(1)+' dB');} -dragDelta($('rndm'),()=>rndT*100,setRnd,{min:0,max:100});setRnd(50); + const t=MH.mcvu.get();MH.mcvu.set(t+(target-t)*(target>t?0.32:0.10));} -/* R08 chrome MIN/MAX indicator */ -let chrV=50,chrPtr=null; -(function(){const s=$('chrmm'),cx=55,cy=58; - const defs=svgEl(s,'defs',{}); - const ring=svgEl(defs,'linearGradient',{id:'chrRing',x1:0,y1:0,x2:0,y2:1}); - svgEl(ring,'stop',{offset:'0','stop-color':'#eceef0'}); - svgEl(ring,'stop',{offset:'.5','stop-color':'#9aa0a8'}); - svgEl(ring,'stop',{offset:'1','stop-color':'#54585e'}); - const dome=svgEl(defs,'radialGradient',{id:'chrDome',cx:'48%',cy:'20%',r:'85%'}); - svgEl(dome,'stop',{offset:'0','stop-color':'#e6e8ec'}); - svgEl(dome,'stop',{offset:'1','stop-color':'#82878f'}); - svgEl(s,'circle',{cx,cy:54,r:47,fill:'url(#chrRing)'}); - svgEl(s,'circle',{cx,cy:54,r:40,fill:'#101214',stroke:'#2c2f33','stroke-width':1}); - svgEl(s,'path',{d:`M 21 60 A 34 34 0 0 1 89 60 Z`,fill:'url(#chrDome)',stroke:'#55595f','stroke-width':.5}); - for(const r of [14,22,30]) - svgEl(s,'path',{d:`M ${cx-r} 60 A ${r} ${r} 0 0 1 ${cx+r} 60`,fill:'none',stroke:'rgba(255,255,255,.13)','stroke-width':.7}); - chrPtr=svgEl(s,'line',{x1:cx,y1:cy-10,x2:cx,y2:cy-31,stroke:'#17191b','stroke-width':2.2,'stroke-linecap':'round'}); - svgEl(s,'circle',{cx,cy,r:6,fill:'#23262a'}); - for(let k=0;k<8;k++){const [x,y]=polar(cx,cy,6,k*45); - svgEl(s,'circle',{cx:x,cy:y,r:1.3,fill:'#23262a'});} - svgEl(s,'circle',{cx,cy,r:1.6,fill:'#0c0d0e'}); - svgEl(s,'text',{x:24,y:66,'text-anchor':'middle','font-size':6,'letter-spacing':'.05em', - 'font-family':'var(--mono)',fill:'#d7dade'}).textContent='MIN'; - svgEl(s,'text',{x:86,y:66,'text-anchor':'middle','font-size':6,'letter-spacing':'.05em', - 'font-family':'var(--mono)',fill:'#d7dade'}).textContent='MAX'; - svgEl(s,'text',{x:cx,y:84,'text-anchor':'middle','font-size':7,'font-style':'italic', - 'font-family':'var(--mono)',fill:'#787c82'}).textContent='dupre.'; - svgEl(s,'ellipse',{cx:47,cy:34,rx:22,ry:9,fill:'rgba(255,255,255,.10)',transform:'rotate(-16,47,34)'});})(); -function setChr(v){chrV=Math.max(0,Math.min(100,v)); - chrPtr.setAttribute('transform',`rotate(${-55+chrV/100*110},55,58)`); - setRd('R08',chrV<=2?'MIN':chrV>=98?'MAX':Math.round(chrV)+'%');} -dragDelta($('chrmm'),()=>chrV,setChr,{min:0,max:100});setChr(50); +/* R02-R06, R12, R14, R15 extracted to widgets.js (GW.*) */ -/* R09 black-face aviation gauge: zone arcs per the airspeed-indicator scheme */ -let avV=52,avNeedle=null; -(function(){const s=$('avgauge'),cx=55,cy=52,sweep=t=>-135+t*270; - const defs=svgEl(s,'defs',{}); - const fl=svgEl(defs,'filter',{id:'avGlow',x:'-60%',y:'-60%',width:'220%',height:'220%'}); - svgEl(fl,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:1.4}); - svgEl(s,'circle',{cx,cy,r:47,fill:'#1b1917',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:41,fill:'#0a0908',stroke:'#3a352c','stroke-width':1}); - const zone=(t1,t2,color)=>{const [x1,y1]=polar(cx,cy,37,sweep(t1)),[x2,y2]=polar(cx,cy,37,sweep(t2)); - svgEl(s,'path',{d:`M ${x1} ${y1} A 37 37 0 ${(t2-t1)>.5?1:0} 1 ${x2} ${y2}`,fill:'none',stroke:color,'stroke-width':3,opacity:.85});}; - zone(.15,.6,'var(--pass)');zone(.6,.85,'var(--gold)');zone(.85,1,'var(--fail)'); - for(let i=0;i<=40;i++){const t=i/40,major=i%5===0,a=sweep(t); - const [x1,y1]=polar(cx,cy,40,a),[x2,y2]=polar(cx,cy,major?32:36,a); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--gold)','stroke-width':major?1.6:.8,opacity:major?1:.7}); - if(major){const [nx,ny]=polar(cx,cy,25,a); - svgEl(s,'text',{x:nx,y:ny+2.4,'text-anchor':'middle','font-size':6.5,'font-family':'var(--mono)', - fill:'var(--gold-hi)'}).textContent=i/5;}} - svgEl(s,'text',{x:cx,y:cy+16,'text-anchor':'middle','font-size':5.5,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--gold)',opacity:.8}).textContent='×100 RPM'; - avNeedle=svgEl(s,'g',{}); - svgEl(avNeedle,'line',{x1:cx,y1:cy+8,x2:cx,y2:cy-30,stroke:'var(--gold-hi)','stroke-width':4, - 'stroke-linecap':'round',opacity:.5,filter:'url(#avGlow)'}); - svgEl(avNeedle,'line',{x1:cx,y1:cy+8,x2:cx,y2:cy-30,stroke:'var(--gold-hi)','stroke-width':2.2, - 'stroke-linecap':'round'}); - svgEl(s,'circle',{cx,cy,r:5,fill:'#1b1917',stroke:'#3a352c'}); - svgEl(s,'ellipse',{cx:cx-10,cy:cy-24,rx:18,ry:8,fill:'rgba(255,255,255,.05)',transform:`rotate(-18,${cx-10},${cy-24})`});})(); -function setAv(v){avV=Math.max(0,Math.min(100,v)); - avNeedle.setAttribute('transform',`rotate(${-135+avV/100*270},55,52)`); - setRd('R09',(avV/100*8).toFixed(1)+' ×100 rpm');} -dragDelta($('avgauge'),()=>avV,setAv,{min:0,max:100});setAv(52); -/* R10 data matrix readout */ -const DMX_PAGES=[ - ['SYS',()=>['CPU 42 MEM 61','DSK 58 TMP 47C','ARCH 6.18 LTS']], - ['NET',()=>['WIFI @HYATT 300M','VPN UP DNS OK','BT 2 PAIRED']], - ['TIME',()=>{const d=new Date(),p=n=>String(n).padStart(2,'0'); - return [`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`, - `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`, - 'UP 14D 06:12'];}]]; -let dmxI=0,dmxLines=[]; -(function(){const s=$('dmx'); - const defs=svgEl(s,'defs',{}); - const fl=svgEl(defs,'filter',{id:'dmxGlow',x:'-30%',y:'-30%',width:'160%',height:'160%'}); - svgEl(fl,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:1.1}); - const bg=svgEl(defs,'linearGradient',{id:'dmxBg',x1:0,y1:0,x2:0,y2:1}); - svgEl(bg,'stop',{offset:'0','stop-color':'var(--scr-bg1,#140d06)'}); - svgEl(bg,'stop',{offset:'1','stop-color':'var(--scr-bg2,#0a0705)'}); - svgEl(s,'rect',{x:1,y:1,width:188,height:72,rx:5,fill:'url(#dmxBg)',stroke:'#2c261d'}); - for(let r=0;r<3;r++){ - const glow=svgEl(s,'text',{x:12,y:24+r*18,'font-size':9,'letter-spacing':'.15em', - 'font-family':'var(--mono)',fill:'var(--scr-hi,var(--gold-hi))',opacity:.6,filter:'url(#dmxGlow)'}); - const crisp=svgEl(s,'text',{x:12,y:24+r*18,'font-size':9,'letter-spacing':'.15em', - 'font-family':'var(--mono)',fill:'var(--scr-hi,var(--gold-hi))'}); - dmxLines.push([glow,crisp]);} - $('dmx').onclick=()=>setDmx((dmxI+1)%DMX_PAGES.length);})(); -function setDmx(i){dmxI=i;const [name,rowsFn]=DMX_PAGES[i]; - rowsFn().forEach((txt,r)=>{dmxLines[r][0].textContent=txt;dmxLines[r][1].textContent=txt;}); - setRd('R10','page '+name);} -setDmx(0); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{if(DMX_PAGES[dmxI][0]==='TIME')setDmx(dmxI);},1000); -/* R11 warning flag window */ -let flagOn=false,flagStripes=null; -(function(){const s=$('wflag'); - const defs=svgEl(s,'defs',{}); - const pat=svgEl(defs,'pattern',{id:'barber',width:10,height:10,patternUnits:'userSpaceOnUse', - patternTransform:'rotate(45)'}); - svgEl(pat,'rect',{x:0,y:0,width:10,height:10,fill:'#17130c'}); - svgEl(pat,'rect',{x:0,y:0,width:5,height:10,fill:'var(--gold)'}); - const clip=svgEl(defs,'clipPath',{id:'flagWin'}); - svgEl(clip,'rect',{x:14,y:20,width:56,height:24}); - svgEl(s,'rect',{x:11,y:17,width:62,height:30,rx:3,fill:'#0a0908',stroke:'#2c261d','stroke-width':2}); - const g=svgEl(s,'g',{'clip-path':'url(#flagWin)'}); - flagStripes=svgEl(g,'rect',{x:14,y:20,width:56,height:24,fill:'url(#barber)'}); - flagStripes.style.transition='transform .18s';flagStripes.style.transform='translateX(-58px)'; - svgEl(s,'text',{x:92,y:35,'text-anchor':'middle','font-size':7,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='VIB'; - $('wflag').onclick=()=>setFlag(!flagOn);})(); -function setFlag(on){flagOn=on;flagStripes.style.transform=on?'translateX(0)':'translateX(-58px)'; - setRd('R11',on?'FLAG':'clear');} -setFlag(false); -/* R12 chrome slot fader: engraved dB scale, chrome T-handle */ -let slotDb=-4,slotHandle=null; -(function(){const s=$('slotf'),cx=45,yTop=30,yBot=120; - const yOf=db=>yTop+(12-db)/36*(yBot-yTop); - const defs=svgEl(s,'defs',{}); - const pl=svgEl(defs,'linearGradient',{id:'sfPlate',x1:0,y1:0,x2:1,y2:1}); - svgEl(pl,'stop',{offset:'0','stop-color':'#242019'}); - svgEl(pl,'stop',{offset:'1','stop-color':'#131110'}); - const ch=svgEl(defs,'linearGradient',{id:'sfChrome',x1:0,y1:0,x2:0,y2:1}); - svgEl(ch,'stop',{offset:'0','stop-color':'#f0efec'}); - svgEl(ch,'stop',{offset:'.45','stop-color':'#c4c1b9'}); - svgEl(ch,'stop',{offset:'1','stop-color':'#75726a'}); - const sc=svgEl(defs,'radialGradient',{id:'sfScrew',cx:'40%',cy:'35%',r:'75%'}); - svgEl(sc,'stop',{offset:'0','stop-color':'#9b968a'}); - svgEl(sc,'stop',{offset:'1','stop-color':'#4a463e'}); - svgEl(s,'rect',{x:14,y:8,width:62,height:134,rx:6,fill:'url(#sfPlate)',stroke:'#0a0908','stroke-width':1.5}); - svgEl(s,'rect',{x:15.5,y:9.5,width:59,height:131,rx:5,fill:'none',stroke:'rgba(255,255,255,.05)','stroke-width':1}); - for(const sy of [16,134]){ - svgEl(s,'circle',{cx,cy:sy,r:3.2,fill:'url(#sfScrew)',stroke:'#14110e','stroke-width':.6}); - const [x1,y1]=polar(cx,sy,3,112),[x2,y2]=polar(cx,sy,3,292); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'#221f1a','stroke-width':1});} - svgEl(s,'rect',{x:40.5,y:yTop-4,width:9,height:yBot-yTop+8,rx:4,fill:'#0b0a09',stroke:'#050404'}); - for(const db of [12,8,4,0,-4,-8,-12,-16,-20,-24]){const y=yOf(db); - svgEl(s,'line',{x1:26,y1:y,x2:39,y2:y,stroke:'var(--steel)','stroke-width':db===0?1.3:.8,opacity:db===0?1:.75}); - svgEl(s,'line',{x1:51,y1:y,x2:64,y2:y,stroke:'var(--steel)','stroke-width':db===0?1.3:.8,opacity:db===0?1:.75}); - svgEl(s,'text',{x:23,y:y+2.2,'text-anchor':'end','font-size':6,'font-family':'var(--mono)', - fill:db===0?'var(--cream)':'var(--steel)'}).textContent=Math.abs(db);} - slotHandle=svgEl(s,'g',{}); - svgEl(slotHandle,'polygon',{points:'45,0 58,-5 58,5',fill:'#b9b6ae',stroke:'#5e5a52','stroke-width':.5}); - svgEl(slotHandle,'rect',{x:58,y:-7,width:22,height:14,rx:3.5,fill:'url(#sfChrome)',stroke:'#4e4a42','stroke-width':.6}); - svgEl(slotHandle,'rect',{x:60,y:-5.2,width:18,height:2.2,rx:1,fill:'rgba(255,255,255,.4)'}); - dragY(s,pct=>setSlot(-24+pct/100*36));})(); -function setSlot(db){slotDb=Math.max(-24,Math.min(12,db)); - slotHandle.setAttribute('transform',`translate(0,${30+(12-slotDb)/36*90})`); - setRd('R12',(slotDb>0?'+':slotDb<0?'−':'')+Math.abs(slotDb).toFixed(1)+' dB');} -setSlot(-4); -/* R13 edgewise strip meter: compressed log scale, bar rides the edge */ -const EDGE_SCALE=[[0,.08],[3,.26],[6,.44],[12,.62],[20,.78],[40,.94]]; -let edgeFrac=.62,edgeBar=null; -(function(){const s=$('edgem'),wy0=12,wy1=118; - const defs=svgEl(s,'defs',{}); - const pg=svgEl(defs,'linearGradient',{id:'edgeFace',x1:0,y1:0,x2:1,y2:0}); - svgEl(pg,'stop',{offset:'0','stop-color':'#efe6ca'}); - svgEl(pg,'stop',{offset:'1','stop-color':'#d9cfae'}); - svgEl(s,'rect',{x:8,y:6,width:54,height:118,rx:8,fill:'#171412',stroke:'#060505','stroke-width':1.5}); - svgEl(s,'rect',{x:14,y:wy0,width:42,height:wy1-wy0,fill:'url(#edgeFace)',stroke:'#0a0908'}); - const yOf=f=>wy0+f*(wy1-wy0); - for(const [db,f] of EDGE_SCALE){const y=yOf(f); - svgEl(s,'line',{x1:19,y1:y,x2:40,y2:y,stroke:'#3a3128','stroke-width':1}); - svgEl(s,'text',{x:52,y:y+2.4,'text-anchor':'end','font-size':7,'font-family':'var(--mono)', - fill:'#3a3128'}).textContent=db;} - for(let i=0;i<EDGE_SCALE.length-1;i++){const y=yOf((EDGE_SCALE[i][1]+EDGE_SCALE[i+1][1])/2); - svgEl(s,'line',{x1:19,y1:y,x2:32,y2:y,stroke:'#3a3128','stroke-width':.6,opacity:.7});} - edgeBar=svgEl(s,'polygon',{points:'56,0 24,-1.4 24,1.4',fill:'#1a1613'}); - svgEl(s,'rect',{x:14,y:wy0,width:42,height:24,fill:'rgba(255,255,255,.08)'}); - dragY(s,pct=>setEdge(.08+(100-pct)/100*.86));})(); -function setEdge(f){edgeFrac=Math.max(.08,Math.min(.94,f)); - edgeBar.setAttribute('transform',`translate(0,${12+edgeFrac*106})`); - let i=0;while(i<EDGE_SCALE.length-2&&EDGE_SCALE[i+1][1]<edgeFrac)i++; - const [d1,f1]=EDGE_SCALE[i],[d2,f2]=EDGE_SCALE[i+1]; - const db=d1+(d2-d1)*Math.max(0,Math.min(1,(edgeFrac-f1)/(f2-f1))); - setRd('R13','−'+db.toFixed(1)+' dB');} -setEdge(.62); -/* R14 spade-pointer tuning knob: engraved panel arc, pointer rides the knob */ -let spadeV=8.3,spadeGrp=null; -(function(){const s=$('spade'),cx=70,cy=82,sweep=v=>-80+v/10*160; - const defs=svgEl(s,'defs',{}); - const bk=svgEl(defs,'radialGradient',{id:'spadeKnob',cx:'42%',cy:'34%',r:'80%'}); - svgEl(bk,'stop',{offset:'0','stop-color':'#33302a'}); - svgEl(bk,'stop',{offset:'1','stop-color':'#080706'}); - const mt=svgEl(defs,'linearGradient',{id:'spadeMetal',x1:0,y1:0,x2:1,y2:1}); - svgEl(mt,'stop',{offset:'0','stop-color':'#e9e7e0'}); - svgEl(mt,'stop',{offset:'1','stop-color':'#8d897f'}); - // engraved arc: every stroke carries a faint light twin offset below (incised relief) - const engrave=(x1,y1,x2,y2,w)=>{ - svgEl(s,'line',{x1,y1:y1+.8,x2,y2:y2+.8,stroke:'rgba(255,255,255,.13)','stroke-width':w}); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'#050404','stroke-width':w});}; - for(let i=0;i<=40;i++){const v=i/4,a=sweep(v),major=i%4===0; - const [x1,y1]=polar(cx,cy,major?50:52,a),[x2,y2]=polar(cx,cy,58,a); - engrave(x1,y1,x2,y2,major?1.6:.9); - if(major){const [nx,ny]=polar(cx,cy,65,a); - svgEl(s,'text',{x:nx,y:ny+3.2,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)', - fill:'rgba(255,255,255,.13)'}).textContent=v; - svgEl(s,'text',{x:nx,y:ny+2.4,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)', - fill:'#0a0908'}).textContent=v;}} - spadeGrp=svgEl(s,'g',{}); - svgEl(spadeGrp,'path',{d:`M ${cx-2} ${cy-28} L ${cx-4.5} ${cy-42} L ${cx} ${cy-54} L ${cx+4.5} ${cy-42} L ${cx+2} ${cy-28} Z`, - fill:'url(#spadeMetal)',stroke:'#55524a','stroke-width':.6}); - svgEl(spadeGrp,'circle',{cx,cy,r:30,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.8}); - svgEl(spadeGrp,'circle',{cx,cy,r:28.5,fill:'none',stroke:'#0a0908','stroke-width':3,'stroke-dasharray':'2.2 2.2'}); - svgEl(spadeGrp,'circle',{cx,cy,r:22,fill:'url(#spadeKnob)'}); - svgEl(spadeGrp,'ellipse',{cx:cx-8,cy:cy-9,rx:8,ry:5,fill:'rgba(255,255,255,.08)',transform:`rotate(-30,${cx-8},${cy-9})`});})(); -function setSpade(v){spadeV=Math.max(0,Math.min(10,v)); - spadeGrp.setAttribute('transform',`rotate(${-80+spadeV/10*160},70,82)`); - setRd('R14',spadeV.toFixed(1));} -dragDelta($('spade'),()=>spadeV,setSpade,{min:0,max:10,sens:.05});setSpade(8.3); -/* R15 multi-band dial: nested arcs, one needle; bandspread selects the ring */ -const MBAND_RANGES=[[0.54,1.6],[1.6,5.1],[5.1,15.5],[15.5,30.5]]; -let mbandV=45,mbandB=2,mbNeedle=null,mbRings=[],mbSpread=null; -(function(){const s=$('mband'),cx=62,cy=62,sweep=t=>-70+t*140; - const defs=svgEl(s,'defs',{}); - const pf=svgEl(defs,'linearGradient',{id:'mbFace',x1:0,y1:0,x2:1,y2:1}); - svgEl(pf,'stop',{offset:'0','stop-color':'#eee5c8'}); - svgEl(pf,'stop',{offset:'1','stop-color':'#dcd2ae'}); - svgEl(s,'rect',{x:4,y:4,width:182,height:102,rx:12,fill:'#171412',stroke:'#060505'}); - svgEl(s,'rect',{x:9,y:9,width:172,height:92,rx:9,fill:'url(#mbFace)',stroke:'#0a0908'}); - const INK='#2c2318'; - [18,26,34,42].forEach((r,bi)=>{const ring=svgEl(s,'g',{}); - const [x1,y1]=polar(cx,cy,r,-70),[x2,y2]=polar(cx,cy,r,70); - svgEl(ring,'path',{d:`M ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',stroke:INK,'stroke-width':.9}); - for(let i=0;i<=8;i++){const a=sweep(i/8),[tx1,ty1]=polar(cx,cy,r,a),[tx2,ty2]=polar(cx,cy,r+3,a); - svgEl(ring,'line',{x1:tx1,y1:ty1,x2:tx2,y2:ty2,stroke:INK,'stroke-width':.8});} - mbRings.push(ring);}); - mbNeedle=svgEl(s,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'#1a1613','stroke-width':1.6,'stroke-linecap':'round'}); - svgEl(s,'circle',{cx,cy,r:7,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.6}); - svgEl(s,'text',{x:14,y:100,'font-size':5,'letter-spacing':'.12em','font-family':'var(--mono)', - fill:INK,opacity:.8}).textContent='MEGACYCLES'; - // bandspread dial (click = band select) - const bx=142,by=56; - for(let i=0;i<12;i++){const a=i*30,[x1,y1]=polar(bx,by,24,a),[x2,y2]=polar(bx,by,21,a); - svgEl(s,'line',{x1,y1,x2,y2,stroke:INK,'stroke-width':.8});} - svgEl(s,'circle',{cx:bx,cy:by,r:17,fill:'none',stroke:INK,'stroke-width':.7,opacity:.5}); - mbSpread=svgEl(s,'line',{x1:bx,y1:by,x2:bx,y2:by-22,stroke:'#1a1613','stroke-width':1.4,'stroke-linecap':'round'}); - svgEl(s,'circle',{cx:bx,cy:by,r:6,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.6}); - svgEl(s,'text',{x:bx,y:96,'text-anchor':'middle','font-size':5,'letter-spacing':'.12em', - 'font-family':'var(--mono)',fill:INK,opacity:.8}).textContent='BANDSPREAD'; - const dragHit=svgEl(s,'rect',{x:9,y:9,width:100,height:92,fill:'transparent'}); - dragHit.style.cursor='ns-resize'; - dragDelta(dragHit,()=>mbandV,v=>setMband(v,mbandB),{min:0,max:100}); - const clickHit=svgEl(s,'rect',{x:112,y:9,width:69,height:92,fill:'transparent'}); - clickHit.style.cursor='pointer'; - clickHit.addEventListener('click',()=>setMband(mbandV,(mbandB+1)%4));})(); -function setMband(v,b){mbandV=Math.max(0,Math.min(100,v));mbandB=b; - mbNeedle.setAttribute('transform',`rotate(${-70+mbandV/100*140},62,62)`); - mbSpread.setAttribute('transform',`rotate(${-45+b*30},142,56)`); - mbRings.forEach((g,i)=>g.setAttribute('opacity',i===b?'1':'.4')); - const [lo,hi]=MBAND_RANGES[b];const mc=lo+mbandV/100*(hi-lo); - setRd('R15',`B${b+1} · ${mc.toFixed(2)} Mc`);} -setMband(45,2); -/* R16 entry keypad: worn keys feed the amber display; lamps watch state */ -let kpBuf='',kpDisp=null,kpLampTop=null,kpLampBot=null; -(function(){const s=$('keypad'); - const defs=svgEl(s,'defs',{}); - const kg=svgEl(defs,'linearGradient',{id:'kpKey',x1:0,y1:0,x2:0,y2:1}); - svgEl(kg,'stop',{offset:'0','stop-color':'#dbd8cf'}); - svgEl(kg,'stop',{offset:'1','stop-color':'#a29d92'}); - const ag=svgEl(defs,'linearGradient',{id:'kpAmber',x1:0,y1:0,x2:0,y2:1}); - svgEl(ag,'stop',{offset:'0','stop-color':'var(--amber-grad-top)'}); - svgEl(ag,'stop',{offset:'1','stop-color':'var(--amber-grad-mid)'}); - // lamp pod - const lamp=cy=>{const l=svgEl(s,'circle',{cx:13,cy,r:6.5,fill:'#3a0f0a',stroke:'#14100e','stroke-width':1.5});return l;}; - kpLampTop=lamp(20);kpLampBot=lamp(38); - // display window - svgEl(s,'rect',{x:26,y:8,width:116,height:32,rx:5,fill:'#0a0806',stroke:'#2c261d','stroke-width':2}); - kpDisp=svgEl(s,'text',{x:84,y:31,'text-anchor':'middle','font-size':16,'letter-spacing':'.22em', - 'font-family':'var(--mono)',fill:'var(--gold-hi)'}); - // key plate - svgEl(s,'rect',{x:24,y:48,width:118,height:138,rx:8,fill:'#1a1714',stroke:'#0a0908','stroke-width':1.5}); - const KEYS=[['1','2','3'],['4','5','6'],['7','8','9'],['✓','0','✗']]; - const jitter=[-2,1,-1,2,0,-2,1,-1,2,0,-1,1]; - KEYS.forEach((row,r)=>row.forEach((k,c)=>{ - const x=47+c*36,y=68+r*32,amber=(k==='✓'||k==='✗'); - const g=svgEl(s,'g',{});g.style.cursor='pointer';g.style.transition='transform .07s'; - svgEl(g,'rect',{x:x-14,y:y-11,width:28,height:24,rx:4,fill:amber?'url(#kpAmber)':'url(#kpKey)', - stroke:'#4e4a42','stroke-width':1,'stroke-opacity':.8}); - svgEl(g,'text',{x,y:y+6,'text-anchor':'middle','font-size':13,'font-weight':700, - 'font-family':'var(--mono)',fill:k==='✓'?'#3f5a1f':k==='✗'?'#7a2a1a':'#2b2721', - transform:`rotate(${jitter[r*3+c]},${x},${y})`}).textContent=k; - g.addEventListener('click',()=>{g.style.transform='translateY(1.5px)'; - setTimeout(()=>g.style.transform='',80);kpPress(k);});})); - })(); -function kpRender(){kpDisp.textContent=kpBuf.padEnd(6,'–'); - kpLampTop.setAttribute('fill',kpBuf?'var(--jewel-r)':'#3a0f0a');} -function kpPress(k){ - if(k==='✓'){setRd('R16',kpBuf?'OK · '+kpBuf:'empty');kpBuf='';kpRender();return;} - if(k==='✗'){kpBuf='';kpRender();setRd('R16','cleared'); - kpLampBot.setAttribute('fill','var(--jewel-r)'); - setTimeout(()=>kpLampBot.setAttribute('fill','#3a0f0a'),350);return;} - if(kpBuf.length<6){kpBuf+=k;kpRender();setRd('R16',kpBuf);}} -kpRender();setRd('R16','enter a code'); -/* R17 round CRT scope: pale phosphor face, live trace */ -let rcrtPh=0,rcrtTrace=null; -(function(){const s=$('rcrt'),cx=55,cy=52; - const defs=svgEl(s,'defs',{}); - const ph=svgEl(defs,'radialGradient',{id:'rcrtFace',cx:'50%',cy:'44%',r:'70%'}); - svgEl(ph,'stop',{offset:'0','stop-color':'var(--crt-face1,#b9d8c0)'}); - svgEl(ph,'stop',{offset:'1','stop-color':'var(--crt-face2,#8bb296)'}); - const clip=svgEl(defs,'clipPath',{id:'rcrtClip'}); - svgEl(clip,'circle',{cx,cy,r:38}); - svgEl(s,'circle',{cx,cy,r:48,fill:'#141110',stroke:'#060505','stroke-width':2}); - for(let k=0;k<4;k++){const [x,y]=polar(cx,cy,43,45+k*90); - svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5});} - svgEl(s,'circle',{cx,cy,r:38,fill:'url(#rcrtFace)'}); - const g=svgEl(s,'g',{'clip-path':'url(#rcrtClip)'}); - for(let i=-4;i<=4;i++){ - svgEl(g,'line',{x1:cx+i*9,y1:cy-38,x2:cx+i*9,y2:cy+38,stroke:'var(--scr-dim,#3d5c46)','stroke-width':i===0?.9:.5,opacity:.8}); - svgEl(g,'line',{x1:cx-38,y1:cy+i*9,x2:cx+38,y2:cy+i*9,stroke:'var(--scr-dim,#3d5c46)','stroke-width':i===0?.9:.5,opacity:.8});} - svgEl(g,'polyline',{id:'rcrtGlow',points:'',fill:'none',stroke:'var(--crt-glow,#eef7ee)','stroke-width':3,opacity:.35,filter:'url(#avGlow)'}); - rcrtTrace=svgEl(g,'polyline',{points:'',fill:'none',stroke:'#f4fcf4','stroke-width':1.3}); - svgEl(s,'ellipse',{cx:cx-10,cy:cy-16,rx:20,ry:10,fill:'rgba(255,255,255,.10)',transform:`rotate(-20,${cx-10},${cy-16})`});})(); -function tickRcrt(){rcrtPh+=0.12;const amp=8+3*Math.sin(rcrtPh*0.35);let pts=''; - for(let x=-38;x<=38;x+=2){const y=Math.sin(x*0.55+rcrtPh*2.5)*amp; - pts+=`${55+x},${(52+y).toFixed(1)} `;} - rcrtTrace.setAttribute('points',pts.trim()); - document.getElementById('rcrtGlow').setAttribute('points',pts.trim()); - setRd('R17','Vpp '+Math.round(amp*2*4.5));} -tickRcrt(); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tickRcrt,80); +/* R07-R09 extracted to widgets.js (GW.*) */ -/* R18 thumb-slide attenuator pair: lit numeral strip, side thumb tab */ -const THSL=[{name:'BLEND',v:74},{name:'MIX',v:92}]; -const thslParts=[]; -(function(){const s=$('thumbsl'),y0=22,y1=122; - const defs=svgEl(s,'defs',{}); - const rg=svgEl(defs,'linearGradient',{id:'thRail',x1:0,y1:0,x2:1,y2:0}); - svgEl(rg,'stop',{offset:'0','stop-color':'#3a3733'}); - svgEl(rg,'stop',{offset:'.5','stop-color':'#211f1c'}); - svgEl(rg,'stop',{offset:'1','stop-color':'#161412'}); - const tg=svgEl(defs,'linearGradient',{id:'thTab',x1:0,y1:0,x2:0,y2:1}); - svgEl(tg,'stop',{offset:'0','stop-color':'#f2efe6'}); - svgEl(tg,'stop',{offset:'1','stop-color':'#c6c1b3'}); - THSL.forEach((ch,i)=>{const x=40+i*50; - svgEl(s,'rect',{x:x-9,y:y0-10,width:18,height:y1-y0+20,rx:3,fill:'url(#thRail)',stroke:'#0a0908','stroke-width':1.2}); - for(const sy of [y0-6,y1+6]){svgEl(s,'circle',{cx:x,cy:sy,r:2.4,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5}); - svgEl(s,'line',{x1:x-1.8,y1:sy-1,x2:x+1.8,y2:sy+1,stroke:'#14110e','stroke-width':.8});} - svgEl(s,'rect',{x:x-5,y:y0,width:10,height:y1-y0,fill:'#0d0a08',stroke:'#050404'}); - for(let n=0;n<=10;n++){const val=100-n*10,y=y0+6+n*(y1-y0-12)/10; - svgEl(s,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)', - fill:'var(--gold)',opacity:.55}).textContent=val;} - const tab=svgEl(s,'g',{}); - svgEl(tab,'rect',{x:x+7,y:-6,width:8,height:12,rx:2,fill:'url(#thTab)',stroke:'#7a766a','stroke-width':.6}); - svgEl(tab,'rect',{x:x+8.2,y:-1,width:5.6,height:1.6,fill:'rgba(0,0,0,.25)'}); - svgEl(s,'text',{x,y:145,'text-anchor':'middle','font-size':6,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=ch.name; - const hit=svgEl(s,'rect',{x:x-12,y:y0-10,width:30,height:y1-y0+20,fill:'transparent'}); - hit.style.cursor='ns-resize'; - thslParts.push({tab,nums:[...s.querySelectorAll('text')].filter(t=>t.getAttribute('x')==String(x)&&t.textContent!==ch.name)}); - dragY(hit,pct=>setThsl(i,pct));}); - })(); -function setThsl(i,v){v=Math.max(0,Math.min(100,Math.round(v)));THSL[i].v=v; - const p=thslParts[i]; - p.tab.setAttribute('transform',`translate(0,${28+(100-v)*0.88})`); - p.nums.forEach(t=>{const on=Math.abs(parseInt(t.textContent)-v)<=5; - t.setAttribute('fill',on?'var(--gold-hi)':'var(--gold)');t.setAttribute('opacity',on?'1':'.55');}); - setRd('R18',`${THSL[0].name} ${THSL[0].v} · ${THSL[1].name} ${THSL[1].v}`);} -setThsl(0,74);setThsl(1,92); +/* R10, R11 extracted to widgets.js (GW.*) */ -/* R19 waveform region editor: monochrome LCD, draggable S/E flags */ -let wrS=22,wrE=76,wrBars=[],wrFlagS=null,wrFlagE=null; -(function(){const s=$('wregion'),x0=10,x1=180,yMid=48,N=85; - svgEl(s,'rect',{x:1,y:1,width:188,height:98,rx:6,fill:'var(--scr-bg1,#0b0c0b)',stroke:'var(--scr-brd,#2a2c2a)','stroke-width':2}); - svgEl(s,'text',{x:12,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C1:START'; - svgEl(s,'text',{x:78,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C2:OFF'; - svgEl(s,'text',{x:136,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C3:END'; - svgEl(s,'line',{x1:x0,y1:yMid,x2:x1,y2:yMid,stroke:'#3a3c3a','stroke-width':.6,'stroke-dasharray':'1.5 2'}); - for(let i=0;i<N;i++){const x=x0+(i/(N-1))*(x1-x0); - const env=Math.abs(Math.sin(i*0.19))*Math.abs(Math.sin(i*0.045))*(i%17<11?1:.25); - const h=Math.max(1.2,env*26); - wrBars.push(svgEl(s,'rect',{x:x-0.8,y:yMid-h,width:1.6,height:h*2,fill:'var(--scr-dim,#5a5c5a)'}));} - const flag=(lbl)=>{const g=svgEl(s,'g',{}); - svgEl(g,'line',{x1:0,y1:18,x2:0,y2:78,stroke:'var(--scr-hi,#f2f4f2)','stroke-width':1}); - svgEl(g,'rect',{x:lbl==='S'?0:-8,y:70,width:8,height:8,fill:'var(--scr-hi,#f2f4f2)'}); - svgEl(g,'text',{x:lbl==='S'?4:-4,y:76.5,'text-anchor':'middle','font-size':6.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent=lbl;return g;}; - wrFlagS=flag('S');wrFlagE=flag('E'); - svgEl(s,'rect',{x:10,y:84,width:78,height:11,fill:'var(--scr-hi,#e8eae8)'}); - svgEl(s,'text',{x:14,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent='ENC:ZOOM(1x)'; - svgEl(s,'text',{x:96,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='A-3 M:[S]'; - svgEl(s,'rect',{x:148,y:84,width:32,height:11,fill:'var(--scr-hi,#e8eae8)'}); - svgEl(s,'text',{x:164,y:92.5,'text-anchor':'middle','font-size':6.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent='MENU'; - s.style.cursor='ew-resize'; - dragX(s,pct=>{ - // move whichever flag is nearer the pointer - if(Math.abs(pct-wrS)<=Math.abs(pct-wrE))setWregion(Math.min(pct,wrE-4),wrE); - else setWregion(wrS,Math.max(pct,wrS+4));});})(); -function setWregion(sv,ev){wrS=Math.max(0,Math.min(96,sv));wrE=Math.max(4,Math.min(100,ev)); - const x0=10,x1=180,xOf=p=>x0+p/100*(x1-x0); - wrFlagS.setAttribute('transform',`translate(${xOf(wrS)},0)`); - wrFlagE.setAttribute('transform',`translate(${xOf(wrE)},0)`); - wrBars.forEach((b,i)=>{const p=i/(wrBars.length-1)*100; - b.setAttribute('fill',(p>=wrS&&p<=wrE)?'var(--scr-hi,#f2f4f2)':'var(--scr-dim,#5a5c5a)');}); - setRd('R19',`S ${Math.round(wrS)}% · E ${Math.round(wrE)}%`);} -setWregion(22,76); -/* R20 drum roller selector: numbered drum in a window, center chip reads it */ -const DRUMR=[{name:'HIGH',v:6},{name:'LOW',v:8}]; -const drumrGrps=[]; -(function(){const s=$('drumr'),cy=76,step=17; - const defs=svgEl(s,'defs',{}); - const paper=svgEl(defs,'linearGradient',{id:'drPaper',x1:0,y1:0,x2:1,y2:0}); - svgEl(paper,'stop',{offset:'0','stop-color':'#cfc6a8'}); - svgEl(paper,'stop',{offset:'.5','stop-color':'#ece4c8'}); - svgEl(paper,'stop',{offset:'1','stop-color':'#c6bd9e'}); - const curve=svgEl(defs,'linearGradient',{id:'drCurve',x1:0,y1:0,x2:0,y2:1}); - svgEl(curve,'stop',{offset:'0','stop-color':'rgba(0,0,0,.4)'}); - svgEl(curve,'stop',{offset:'.28','stop-color':'rgba(0,0,0,0)'}); - svgEl(curve,'stop',{offset:'.72','stop-color':'rgba(0,0,0,0)'}); - svgEl(curve,'stop',{offset:'1','stop-color':'rgba(0,0,0,.4)'}); - svgEl(s,'text',{x:65,y:12,'text-anchor':'middle','font-size':7,'letter-spacing':'.18em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='EQUALIZER'; - DRUMR.forEach((ch,i)=>{const x=45+i*50; - svgEl(s,'text',{x:x+18,y:34,'text-anchor':'middle','font-size':8,fill:'var(--steel)'}).textContent='+'; - svgEl(s,'text',{x:x+18,y:124,'text-anchor':'middle','font-size':8,fill:'var(--steel)'}).textContent='−'; - svgEl(s,'rect',{x:x-12,y:24,width:24,height:104,rx:4,fill:'url(#thRail)',stroke:'#0a0908','stroke-width':1.2}); - const clip=svgEl(defs,'clipPath',{id:'drClip'+i}); - svgEl(clip,'rect',{x:x-9,y:28,width:18,height:96}); - svgEl(s,'rect',{x:x-9,y:28,width:18,height:96,fill:'url(#drPaper)'}); - const g=svgEl(s,'g',{'clip-path':`url(#drClip${i})`}); - const nums=svgEl(g,'g',{});nums.style.transition='transform .12s'; - for(let n=1;n<=10;n++) - svgEl(nums,'text',{x,y:cy+3+(ch.v-n)*step,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'#2b2418'}).textContent=n; - svgEl(g,'rect',{x:x-9,y:28,width:18,height:96,fill:'url(#drCurve)','pointer-events':'none'}); - svgEl(s,'rect',{x:x-8,y:cy-6.5,width:16,height:13,rx:1.5,fill:'#1a1613',opacity:.92}); - const chip=svgEl(s,'text',{x,y:cy+3,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}); - svgEl(s,'text',{x,y:136,'text-anchor':'middle','font-size':6,'letter-spacing':'.12em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=ch.name; - const hit=svgEl(s,'rect',{x:x-14,y:24,width:28,height:104,fill:'transparent'}); - hit.style.cursor='ns-resize'; - drumrGrps.push({nums,chip,base:ch.v}); - dragDelta(hit,()=>DRUMR[i].v,v=>setDrumr(i,v),{min:1,max:10,sens:.08});}); - })(); -function setDrumr(i,v){v=Math.max(1,Math.min(10,v));DRUMR[i].v=v; - const p=drumrGrps[i]; - p.nums.style.transform=`translateY(${(v-p.base)*17}px)`; - p.chip.textContent=Math.round(v); - setRd('R20',`${DRUMR[0].name} ${Math.round(DRUMR[0].v)} · ${DRUMR[1].name} ${Math.round(DRUMR[1].v)}`);} -setDrumr(0,6);setDrumr(1,8); -/* R21 LED program row: exclusive select, the LED carries the state */ -const LEDROW_PROGS=['Sm Hall B','VocPlate','Lg Hall B','Chamber','ParcPlate','Sm Hall A','Room A','Const Plate']; -let ledrowI=5,ledrowLeds=[]; -(function(){const s=$('ledrow'); - const defs=svgEl(s,'defs',{}); - const kb=svgEl(defs,'linearGradient',{id:'lrKey',x1:0,y1:0,x2:0,y2:1}); - svgEl(kb,'stop',{offset:'0','stop-color':'#2b2823'}); - svgEl(kb,'stop',{offset:'1','stop-color':'#171512'}); - LEDROW_PROGS.forEach((name,i)=>{const x=16+i*22.6; - svgEl(s,'text',{x,y:9,'text-anchor':'middle','font-size':6,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=i+1; - ledrowLeds.push(svgEl(s,'circle',{cx:x,cy:17,r:2.4,fill:'#3a0f0a'})); - const g=svgEl(s,'g',{});g.style.cursor='pointer';g.style.transition='transform .07s'; - svgEl(g,'rect',{x:x-8.5,y:24,width:17,height:22,rx:2.5,fill:'url(#lrKey)',stroke:'#0a0908','stroke-width':1}); - svgEl(g,'rect',{x:x-6.5,y:26.5,width:13,height:3,rx:1.5,fill:'rgba(255,255,255,.06)'}); - g.addEventListener('click',()=>{g.style.transform='translateY(1.5px)'; - setTimeout(()=>g.style.transform='',80);setLedrow(i);});}); - svgEl(s,'text',{x:95,y:56,'text-anchor':'middle','font-size':6,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='PROGRAM';})(); -function setLedrow(i){ledrowI=i; - ledrowLeds.forEach((l,k)=>{const on=k===i; - l.setAttribute('fill',on?'var(--jewel-r)':'#3a0f0a'); - l.setAttribute('style',on?'filter:drop-shadow(0 0 3px rgba(255,91,69,.8))':'');}); - setRd('R21',`${ledrowI+1} · ${LEDROW_PROGS[ledrowI]}`);} -setLedrow(5); -/* R22 three-position slide: chrome pill, detents, honest LED pair */ -const VARSL_POS=['A','AB','B']; -let varslI=1,varslPill=null,varslLbls=[],varslLeds=[]; -(function(){const s=$('varslide');const detX=[34,55,76]; - svgEl(s,'text',{x:55,y:10,'text-anchor':'middle','font-size':5.5,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='BASIC · VARIATION'; - svgEl(s,'rect',{x:20,y:16,width:70,height:14,rx:7,fill:'#0b0a09',stroke:'#000','stroke-width':1}); - svgEl(s,'rect',{x:21,y:17,width:68,height:5,rx:2.5,fill:'rgba(255,255,255,.04)'}); - varslPill=svgEl(s,'rect',{x:-13,y:18.5,width:26,height:9,rx:4.5,fill:'url(#sfChrome)',stroke:'#4e4a42','stroke-width':.6}); - varslPill.style.transition='transform .12s'; - VARSL_POS.forEach((lbl,i)=>{ - varslLbls.push(svgEl(s,'text',{x:detX[i],y:40,'text-anchor':'middle','font-size':6.5, - 'font-family':'var(--mono)',fill:'var(--dim)'})); - varslLbls[i].textContent=lbl;}); - svgEl(s,'rect',{x:32,y:45,width:46,height:11,rx:2,fill:'#141210',stroke:'#060505'}); - [44,66].forEach(x=>varslLeds.push(svgEl(s,'circle',{cx:x,cy:50.5,r:2.4,fill:'#3a0f0a'}))); - s.addEventListener('click',e=>{const r=s.getBoundingClientRect(); - const x=(e.clientX-r.left)/r.width*110; - setVarsl(x<45?0:x<66?1:2);});})(); -function setVarsl(i){varslI=i;const detX=[34,55,76]; - varslPill.setAttribute('transform',`translate(${detX[i]},0)`); - varslLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)')); - const lit=[i===0||i===1,i===2||i===1]; - varslLeds.forEach((l,k)=>l.setAttribute('fill',lit[k]?'var(--jewel-r)':'#3a0f0a')); - setRd('R22',VARSL_POS[i]);} -setVarsl(1); +/* R13, R17 extracted to widgets.js (GW.*) */ -/* R23 spun-aluminum knob: machined rings in a knurled grip */ -let spunV=62,spunIdx=null; -(function(){const s=$('spunk'),cx=55,cy=52; - const defs=svgEl(s,'defs',{}); - const al=svgEl(defs,'radialGradient',{id:'spunFace',cx:'42%',cy:'34%',r:'85%'}); - svgEl(al,'stop',{offset:'0','stop-color':'#e8e6e0'}); - svgEl(al,'stop',{offset:'.7','stop-color':'#b3b0a8'}); - svgEl(al,'stop',{offset:'1','stop-color':'#8a877e'}); - for(let k=0;k<8;k++){const [x,y]=polar(cx,cy,45,-135+k*38.6); - svgEl(s,'circle',{cx:x,cy:y,r:1.3,fill:'var(--steel)'});} - for(let k=0;k<22;k++){const [x,y]=polar(cx,cy,36,k*16.36); - svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'#14110e'});} - svgEl(s,'circle',{cx,cy,r:36,fill:'#1a1714',stroke:'#000','stroke-width':.8}); - svgEl(s,'circle',{cx,cy,r:30,fill:'url(#spunFace)',stroke:'#6e6b63','stroke-width':.8}); - for(const r of [24,18,12,6]) - svgEl(s,'circle',{cx,cy,r,fill:'none',stroke:r%12===0?'rgba(0,0,0,.12)':'rgba(255,255,255,.3)','stroke-width':.6}); - spunIdx=svgEl(s,'line',{x1:cx,y1:cy-28,x2:cx,y2:cy-10,stroke:'var(--fail)','stroke-width':2.6,'stroke-linecap':'round'}); - svgEl(s,'ellipse',{cx:cx-9,cy:cy-11,rx:9,ry:5,fill:'rgba(255,255,255,.28)',transform:`rotate(-32,${cx-9},${cy-11})`}); - svgEl(s,'text',{x:cx,y:103,'text-anchor':'middle','font-size':6,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='SPEED';})(); -function setSpun(v){spunV=Math.max(0,Math.min(100,v)); - spunIdx.setAttribute('transform',`rotate(${-135+spunV/100*270},55,52)`); - setRd('R23',Math.round(spunV)+'%');} -dragDelta($('spunk'),()=>spunV,setSpun,{min:0,max:100});setSpun(62); +/* R25, R26, R30, R31 extracted to widgets.js (GW.*) */ -/* R24 stomp switch + jewel: press to engage, the jewel reports */ -let stompOn=false,stompDome=null,stompJewel=null,stompGlow=null; -(function(){const s=$('stomp'),cx=55; - const defs=svgEl(s,'defs',{}); - const dg=svgEl(defs,'radialGradient',{id:'stompDome',cx:'40%',cy:'30%',r:'80%'}); - svgEl(dg,'stop',{offset:'0','stop-color':'#f4f2ec'}); - svgEl(dg,'stop',{offset:'.55','stop-color':'#b9b6ae'}); - svgEl(dg,'stop',{offset:'1','stop-color':'#6e6b63'}); - const jg=svgEl(defs,'radialGradient',{id:'stompJewel',cx:'38%',cy:'30%',r:'80%'}); - svgEl(jg,'stop',{offset:'0','stop-color':'#ffe9b0'}); - svgEl(jg,'stop',{offset:'.45','stop-color':'var(--jewel-a)'}); - svgEl(jg,'stop',{offset:'1','stop-color':'#5c3d0c'}); - stompGlow=svgEl(s,'circle',{cx,cy:22,r:14,fill:'rgba(255,180,58,.35)',filter:'url(#avGlow)',opacity:0}); - stompJewel=svgEl(s,'circle',{cx,cy:22,r:9,fill:'#241f1b',stroke:'#0a0908','stroke-width':1.5}); - for(let k=0;k<6;k++){const a1=k*60,a2=k*60+30; - const [x1,y1]=polar(cx,22,9,a1),[x2,y2]=polar(cx,22,9,a2); - svgEl(s,'path',{d:`M ${cx} 22 L ${x1} ${y1} A 9 9 0 0 1 ${x2} ${y2} Z`, - fill:k%2?'rgba(255,255,255,.09)':'rgba(0,0,0,.10)','pointer-events':'none'});} - for(let k=0;k<20;k++){const [x,y]=polar(cx,72,25,k*18); - svgEl(s,'circle',{cx:x,cy:y,r:2.4,fill:'#14110e'});} - svgEl(s,'circle',{cx,cy:72,r:25,fill:'#1a1714',stroke:'#000','stroke-width':.8}); - stompDome=svgEl(s,'g',{});stompDome.style.transition='transform .08s'; - svgEl(stompDome,'circle',{cx,cy:72,r:19,fill:'url(#stompDome)',stroke:'#55524a','stroke-width':.8}); - svgEl(stompDome,'ellipse',{cx:cx-6,cy:64,rx:8,ry:4.5,fill:'rgba(255,255,255,.5)',transform:`rotate(-24,${cx-6},64)`}); - $('stomp').onclick=()=>{stompDome.style.transform='translateY(1.5px)'; - setTimeout(()=>stompDome.style.transform='',90);setStomp(!stompOn);};})(); -function setStomp(on){stompOn=on; - stompJewel.setAttribute('fill',on?'url(#stompJewel)':'#241f1b'); - stompGlow.setAttribute('opacity',on?'1':'0'); - setRd('R24',on?'ENGAGED':'bypass');} -setStomp(false); -/* R27 winged gain selector: red T-bar over a dot ring, stepped detents */ -const WINGK_STEPS=[-80,-70,-60,-50,-40,-30,-20,-10,-5,0,5,10]; -let wingI=7,wingGrp=null; -(function(){const s=$('wingk'),cx=55,cy=52; - const defs=svgEl(s,'defs',{}); - const rg2=svgEl(defs,'radialGradient',{id:'wingRed',cx:'40%',cy:'32%',r:'80%'}); - svgEl(rg2,'stop',{offset:'0','stop-color':'#d98a6f'}); - svgEl(rg2,'stop',{offset:'.6','stop-color':'var(--fail)'}); - svgEl(rg2,'stop',{offset:'1','stop-color':'#6e2415'}); - WINGK_STEPS.forEach((db,i)=>{const a=-135+i/(WINGK_STEPS.length-1)*270; - const [dx,dy]=polar(cx,cy,38,a); - svgEl(s,'circle',{cx:dx,cy:dy,r:1.5,fill:'var(--steel)'}); - if(i%2===1||db===0||db===10||db===-80){const [tx,ty]=polar(cx,cy,46.5,a); - svgEl(s,'text',{x:tx,y:ty+2.2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=db>0?'+'+db:db;}}); - wingGrp=svgEl(s,'g',{});wingGrp.style.transition='transform .09s'; - svgEl(wingGrp,'rect',{x:cx-6.5,y:cy-30,width:13,height:60,rx:6,fill:'url(#wingRed)',stroke:'#4e150a','stroke-width':.8}); - svgEl(wingGrp,'circle',{cx,cy,r:15,fill:'url(#wingRed)',stroke:'#4e150a','stroke-width':.8}); - svgEl(wingGrp,'line',{x1:cx,y1:cy-28,x2:cx,y2:cy-18,stroke:'var(--cream)','stroke-width':2,'stroke-linecap':'round'}); - svgEl(wingGrp,'ellipse',{cx:cx-4,cy:cy-8,rx:5,ry:3,fill:'rgba(255,255,255,.18)',transform:`rotate(-30,${cx-4},${cy-8})`});})(); -function setWing(v){const i=Math.max(0,Math.min(WINGK_STEPS.length-1,Math.round(v)));wingI=i; - wingGrp.style.transform=`rotate(${-135+i/(WINGK_STEPS.length-1)*270}deg)`; - wingGrp.style.transformOrigin='55px 52px'; - const db=WINGK_STEPS[i]; - setRd('R27',(db>0?'+':'')+db+' dB');} -dragDelta($('wingk'),()=>wingI,setWing,{min:0,max:WINGK_STEPS.length-1,sens:.05});setWing(7); -/* R28 rotary disc switch: the whole disc turns between heavy positions */ -const DISCSW_POS=[['OFF',-45],['ON',45],['COMBINE',135]]; -let discI=1,discGrp=null,discLbls=[]; -(function(){const s=$('discsw'),cx=55,cy=58; - const defs=svgEl(s,'defs',{}); - const rr=svgEl(defs,'radialGradient',{id:'discRed',cx:'42%',cy:'34%',r:'85%'}); - svgEl(rr,'stop',{offset:'0','stop-color':'#d98a6f'}); - svgEl(rr,'stop',{offset:'.65','stop-color':'var(--fail)'}); - svgEl(rr,'stop',{offset:'1','stop-color':'#7a2a1a'}); - svgEl(s,'circle',{cx,cy,r:48,fill:'#1a1714',stroke:'#060505','stroke-width':1.5}); - for(let k=0;k<4;k++){const [x,y]=polar(cx,cy,44,45+k*90); - svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5});} - DISCSW_POS.forEach(([lbl,a],i)=>{const [tx,ty]=polar(cx,cy,42,a); - const t=svgEl(s,'text',{x:tx,y:ty+2.4,'text-anchor':'middle','font-size':5.8,'letter-spacing':'.06em', - 'font-family':'var(--mono)',fill:'var(--dim)'});t.textContent=lbl;discLbls.push(t);}); - svgEl(s,'circle',{cx,cy,r:36,fill:'url(#discRed)',stroke:'#4e150a','stroke-width':1}); - discGrp=svgEl(s,'g',{});discGrp.style.transition='transform .15s';discGrp.style.transformOrigin=`${cx}px ${cy}px`; - svgEl(discGrp,'rect',{x:cx-27,y:cy-8,width:54,height:16,rx:8,fill:'url(#discRed)',stroke:'#8f3520','stroke-width':1}); - svgEl(discGrp,'rect',{x:cx-25,y:cy-6.5,width:50,height:4,rx:2,fill:'rgba(255,255,255,.16)'}); - svgEl(discGrp,'line',{x1:cx+20,y1:cy,x2:cx+26,y2:cy,stroke:'var(--cream)','stroke-width':2.5,'stroke-linecap':'round'}); - svgEl(s,'ellipse',{cx:cx-12,cy:cy-14,rx:12,ry:6,fill:'rgba(255,255,255,.10)',transform:`rotate(-28,${cx-12},${cy-14})`}); - $('discsw').onclick=()=>setDisc((discI+1)%DISCSW_POS.length);})(); -function setDisc(i){discI=i; - discGrp.style.transform=`rotate(${DISCSW_POS[i][1]}deg)`; - discLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)')); - setRd('R28',DISCSW_POS[i][0]);} -setDisc(1); - -/* R29 guarded toggle: guard posts + red collar mark the critical throw */ -let guardOn=true,guardLever=null,guardLblOn=null,guardLblOff=null; -(function(){const s=$('guardsw'),cx=45,cy=52; - guardLblOn=svgEl(s,'text',{x:cx,y:12,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--gold-hi)'});guardLblOn.textContent='ON'; - guardLblOff=svgEl(s,'text',{x:cx,y:96,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--dim)'});guardLblOff.textContent='OFF'; - svgEl(s,'circle',{cx,cy,r:13,fill:'url(#discRed)',stroke:'#4e150a','stroke-width':1}); - const pts=[];for(let k=0;k<6;k++){const a=(k*60+30)*Math.PI/180;pts.push((cx+11*Math.cos(a))+','+(cy+11*Math.sin(a)));} - svgEl(s,'polygon',{points:pts.join(' '),fill:'url(#nutG)',stroke:'#2b2822'}); - svgEl(s,'circle',{cx,cy,r:6,fill:'#14110e',stroke:'#000'}); - for(const gx of [16,74]){ - svgEl(s,'rect',{x:gx-6,y:cy-24,width:12,height:48,rx:6,fill:'url(#nutG)',stroke:'#2b2822','stroke-width':1}); - svgEl(s,'rect',{x:gx-4,y:cy-21,width:3.5,height:42,rx:2,fill:'rgba(255,255,255,.14)'});} - guardLever=svgEl(s,'g',{}); - svgEl(guardLever,'path',{d:`M ${cx-3} ${cy} L ${cx-2} 26 L ${cx+2} 26 L ${cx+3} ${cy} Z`,fill:'url(#chromeG)',stroke:'#4e4a42','stroke-width':.5}); - svgEl(guardLever,'rect',{x:cx-4.5,y:16,width:9,height:12,rx:2,fill:'url(#tipG)',stroke:'#5e5a52','stroke-width':.6}); - for(let r=0;r<4;r++)svgEl(guardLever,'line',{x1:cx-3.5,y1:18.5+r*2.4,x2:cx+3.5,y2:18.5+r*2.4,stroke:'rgba(0,0,0,.25)','stroke-width':.8}); - guardLever.style.transformOrigin=`${cx}px ${cy}px`;guardLever.style.transition='transform .12s'; - $('guardsw').onclick=()=>setGuard(!guardOn);})(); -function setGuard(on){guardOn=on;guardLever.style.transform=on?'rotate(0deg)':'rotate(180deg)'; - guardLblOn.setAttribute('fill',on?'var(--gold-hi)':'var(--dim)'); - guardLblOff.setAttribute('fill',on?'var(--dim)':'var(--gold-hi)'); - setRd('R29',on?'ON':'OFF');} -setGuard(true); - -/* R30 telegraph indicator: the pointer names the state on labeled sectors */ -const TELIND_STATES=['OK','TEST','BUSY','STOP','ATTN','FAULT']; -let telI=0,telPtr=null; -(function(){const s=$('telind'),cx=55,cy=55,N=TELIND_STATES.length; - svgEl(s,'circle',{cx,cy,r:50,fill:'#171412',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:43,fill:'var(--cream)',stroke:'#b7b29a','stroke-width':1}); - TELIND_STATES.forEach((st,i)=>{ - const a=i*360/N-90+180/N; - const [lx1,ly1]=polar(cx,cy,9,i*360/N),[lx2,ly2]=polar(cx,cy,43,i*360/N); - svgEl(s,'line',{x1:lx1,y1:ly1,x2:lx2,y2:ly2,stroke:'#2b2418','stroke-width':1}); - const g=svgEl(s,'g',{transform:`rotate(${a+90},${cx},${cy})`}); - svgEl(g,'text',{x:cx,y:cy-28,'text-anchor':'middle','font-size':7,'font-weight':700, - 'font-family':'var(--mono)',fill:'#2b2418'}).textContent=st;}); - telPtr=svgEl(s,'g',{});telPtr.style.transition='transform .18s';telPtr.style.transformOrigin=`${cx}px ${cy}px`; - svgEl(telPtr,'polygon',{points:`${cx-2.5},${cy} ${cx},${cy-38} ${cx+2.5},${cy}`,fill:'var(--fail)',stroke:'#7a2a1a','stroke-width':.5}); - svgEl(telPtr,'rect',{x:cx+1,y:cy-16,width:10,height:7,rx:1,fill:'var(--gold)',stroke:'#7d5c16','stroke-width':.6}); - svgEl(s,'circle',{cx,cy,r:8,fill:'#171412',stroke:'#000'}); - $('telind').onclick=()=>setTelind((telI+1)%TELIND_STATES.length);})(); -function setTelind(i){telI=i;const N=TELIND_STATES.length; - telPtr.style.transform=`rotate(${i*360/N+180/N}deg)`; - setRd('R30',TELIND_STATES[i]);} -setTelind(0); - -/* R31 radar sweep: rotating beam, blooming contacts, click to mark */ -let radarA=210,radarSweep=null,radarBlips=[],radarMark=null; -const RADAR_CONTACTS=[[60,.55],[205,.7],[318,.4]]; -(function(){const s=$('radar'),cx=65,cy=65; - const defs=svgEl(s,'defs',{}); - const clip=svgEl(defs,'clipPath',{id:'radarClip'}); - svgEl(clip,'circle',{cx,cy,r:46}); - svgEl(s,'circle',{cx,cy,r:62,fill:'#1a1714',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:52,fill:'var(--scr-bg1,#120b05)',stroke:'var(--scr-brd,#3a2a12)','stroke-width':1}); - for(let d=0;d<360;d+=30){const a=d; - const [x1,y1]=polar(cx,cy,51,a),[x2,y2]=polar(cx,cy,47,a); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--scr-ink,var(--gold))','stroke-width':1,opacity:.8}); - const g=svgEl(s,'g',{transform:`rotate(${a},${cx},${cy})`}); - svgEl(g,'text',{x:cx,y:cy-47.5,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)', - fill:'var(--scr-ink,var(--gold))',opacity:.9}).textContent=String(d).padStart(3,'0');} - for(let d=0;d<360;d+=10){if(d%30===0)continue; - const [x1,y1]=polar(cx,cy,51,d),[x2,y2]=polar(cx,cy,49,d); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--scr-ink,var(--gold))','stroke-width':.6,opacity:.5});} - const g=svgEl(s,'g',{'clip-path':'url(#radarClip)'}); - svgEl(g,'circle',{cx,cy,r:46,fill:'var(--scr-bg2,#0d0803)'}); - for(const r of [15,30,45]) - svgEl(g,'circle',{cx,cy,r,fill:'none',stroke:'var(--scr-ink,var(--gold))','stroke-opacity':.18,'stroke-width':.7}); - radarSweep=svgEl(g,'g',{}); - for(let t=0;t<5;t++){const a0=-14+t*2.8,a1=a0+2.8; - const [x1,y1]=polar(cx,cy,46,a0),[x2,y2]=polar(cx,cy,46,a1); - svgEl(radarSweep,'path',{d:`M ${cx} ${cy} L ${x1} ${y1} A 46 46 0 0 1 ${x2} ${y2} Z`, - fill:'var(--scr-ink,var(--gold))','fill-opacity':(0.04+t*0.045).toFixed(3)});} - svgEl(radarSweep,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'var(--scr-hi,var(--gold-hi))','stroke-width':1.4, - transform:`rotate(0,${cx},${cy})`,opacity:.95}); - RADAR_CONTACTS.forEach(([bd,rf])=>{const [x,y]=polar(cx,cy,46*rf,bd); - radarBlips.push(svgEl(g,'circle',{cx:x,cy:y,r:2.2,fill:'var(--scr-hi,var(--gold-hi))',opacity:0}));}); - svgEl(s,'circle',{cx,cy,r:3,fill:'var(--scr-ink,var(--gold))',opacity:.8}); - radarMark=svgEl(s,'text',{x:cx,y:124,'text-anchor':'middle','font-size':6,'letter-spacing':'.12em', - 'font-family':'var(--mono)',fill:'var(--steel)'}); - radarMark.textContent='BEARING —'; - $('radar').onclick=()=>{radarMark.textContent='MARK '+String(Math.round(radarA)%360).padStart(3,'0'); - setRd('R31','mark '+String(Math.round(radarA)%360).padStart(3,'0'));};})(); -function tickRadar(){radarA=(radarA+2.6)%360; - radarSweep.setAttribute('transform',`rotate(${radarA},65,65)`); - RADAR_CONTACTS.forEach(([bd],i)=>{ - let d=(radarA-bd+360)%360; // degrees since beam passed - radarBlips[i].setAttribute('opacity',d<200?Math.max(0,.95-d/200):0);}); - radarMark.textContent=radarMark.textContent.startsWith('MARK')?radarMark.textContent: - 'BEARING '+String(Math.round(radarA)%360).padStart(3,'0');} -tickRadar();setRd('R31','sweeping'); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tickRadar,50); - -/* R25 fourteen-segment display: the starburst that spells */ -const SEG14_WORDS=['ZOOM','ECHO','TAPE','MOOD','HALL','COMP']; -const SEG14_MAP={A:['a','b','c','e','f','g1','g2'],C:['a','d','e','f'], - D:['a','b','c','d','i','l'],E:['a','d','e','f','g1','g2'],H:['b','c','e','f','g1','g2'], - L:['d','e','f'],M:['b','c','e','f','h','j'],O:['a','b','c','d','e','f'], - P:['a','b','e','f','g1','g2'],T:['a','i','l'],Z:['a','d','j','k']}; -let seg14I=0,seg14Cells=[]; -(function(){const s=$('seg14'); - svgEl(s,'rect',{x:1,y:1,width:128,height:52,rx:5,fill:'#0a0f08',stroke:'#1c2a16','stroke-width':2}); - // segment shapes in a 22x36 local box (x0,y0 top-left) - const TH=2.6,W=22,H=36,mid=H/2; - const segs={ - a:[[2,0],[W-2,0],[W-4,TH],[4,TH]], - d:[[2,H],[W-2,H],[W-4,H-TH],[4,H-TH]], - f:[[0,2],[TH,4],[TH,mid-2],[0,mid-1]], - e:[[0,H-2],[TH,H-4],[TH,mid+2],[0,mid+1]], - b:[[W,2],[W-TH,4],[W-TH,mid-2],[W,mid-1]], - c:[[W,H-2],[W-TH,H-4],[W-TH,mid+2],[W,mid+1]], - g1:[[2,mid],[4,mid-TH/2],[W/2-1,mid-TH/2],[W/2-1,mid+TH/2],[4,mid+TH/2]], - g2:[[W-2,mid],[W-4,mid-TH/2],[W/2+1,mid-TH/2],[W/2+1,mid+TH/2],[W-4,mid+TH/2]], - i:[[W/2-TH/2,3],[W/2+TH/2,3],[W/2+TH/2,mid-2],[W/2-TH/2,mid-2]], - l:[[W/2-TH/2,H-3],[W/2+TH/2,H-3],[W/2+TH/2,mid+2],[W/2-TH/2,mid+2]], - h:[[3,3],[5.5,3],[W/2-2,mid-3],[W/2-4.5,mid-3]], - j:[[W-3,3],[W-5.5,3],[W/2+2,mid-3],[W/2+4.5,mid-3]], - k:[[3,H-3],[5.5,H-3],[W/2-2,mid+3],[W/2-4.5,mid+3]], - m:[[W-3,H-3],[W-5.5,H-3],[W/2+2,mid+3],[W/2+4.5,mid+3]]}; - for(let d=0;d<4;d++){const x0=13+d*28,y0=9,cell={}; - for(const k in segs){ - cell[k]=svgEl(s,'polygon',{points:segs[k].map(p=>`${x0+p[0]},${y0+p[1]}`).join(' '), - fill:'var(--sevoff)'});} - seg14Cells.push(cell);} - $('seg14').onclick=()=>setSeg14((seg14I+1)%SEG14_WORDS.length);})(); -function setSeg14(i){seg14I=i;const word=SEG14_WORDS[i].padEnd(4,' '); - seg14Cells.forEach((cell,d)=>{const lit=SEG14_MAP[word[d]]||[]; - for(const k in cell){const on=lit.includes(k); - cell[k].setAttribute('fill',on?'var(--sevgrn)':'var(--sevoff)'); - cell[k].setAttribute('style',on?'filter:drop-shadow(0 0 2.5px rgba(87,211,87,.6))':'');}}); - setRd('R25',SEG14_WORDS[i]);} -setSeg14(0); - -/* R26 response graph: log-frequency axes, a draggable amber peak */ -const RG={x0:30,x1:182,y0:8,y1:86,fLo:Math.log10(32),fHi:Math.log10(16000),db:12}; -let rgFc=1200,rgGain=6.5,rgCurve=null,rgGlowLine=null,rgHandle=null; -(function(){const s=$('rgraph'); - svgEl(s,'rect',{x:1,y:1,width:188,height:108,rx:5,fill:'#0d1410',stroke:'#22301f','stroke-width':2}); - const FREQS=[32,64,128,250,500,1000,2000,4000,8000,16000]; - FREQS.forEach(f=>{const x=RG.x0+(Math.log10(f)-RG.fLo)/(RG.fHi-RG.fLo)*(RG.x1-RG.x0); - svgEl(s,'line',{x1:x,y1:RG.y0,x2:x,y2:RG.y1,stroke:'#22301f','stroke-width':.6}); - svgEl(s,'text',{x,y:RG.y1+9,'text-anchor':'middle','font-size':4.8,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=f<1000?f:(f/1000)+'k';}); - for(const db of [12,6,0,-6,-12]){const y=RG.y0+(RG.db-db)/(2*RG.db)*(RG.y1-RG.y0); - svgEl(s,'line',{x1:RG.x0,y1:y,x2:RG.x1,y2:y,stroke:'#22301f','stroke-width':db===0?1:.6}); - svgEl(s,'text',{x:RG.x0-4,y:y+2,'text-anchor':'end','font-size':4.8,'font-family':'var(--mono)', - fill:'var(--steel)'}).textContent=(db>0?'+':'')+db;} - rgGlowLine=svgEl(s,'polyline',{points:'',fill:'none',stroke:'var(--gold-hi)','stroke-width':3, - opacity:.35,filter:'url(#avGlow)'}); - rgCurve=svgEl(s,'polyline',{points:'',fill:'none',stroke:'var(--gold)','stroke-width':1.6}); - rgHandle=svgEl(s,'circle',{r:4,fill:'var(--gold-hi)',stroke:'#7d5c16','stroke-width':1}); - rgHandle.style.cursor='move'; - // 2D drag: the handle (and the plot) place the peak in both axes at once - s.style.touchAction='none'; - s.addEventListener('pointerdown',e=>{s.setPointerCapture(e.pointerId); - const move=ev=>{const r=s.getBoundingClientRect(),sx=190/r.width,sy=110/r.height; - const px=(ev.clientX-r.left)*sx,py=(ev.clientY-r.top)*sy; - const lf=RG.fLo+Math.max(0,Math.min(1,(px-RG.x0)/(RG.x1-RG.x0)))*(RG.fHi-RG.fLo); - const g=RG.db-Math.max(0,Math.min(1,(py-RG.y0)/(RG.y1-RG.y0)))*2*RG.db; - setRgraph(Math.pow(10,lf),g);}; - move(e); - const up=()=>{s.removeEventListener('pointermove',move);s.removeEventListener('pointerup',up);s.removeEventListener('pointercancel',up);}; - s.addEventListener('pointermove',move);s.addEventListener('pointerup',up);s.addEventListener('pointercancel',up); - e.preventDefault();});})(); -function setRgraph(fc,gain){rgFc=Math.max(32,Math.min(16000,fc));rgGain=Math.max(-12,Math.min(12,gain)); - const xOf=f=>RG.x0+(Math.log10(f)-RG.fLo)/(RG.fHi-RG.fLo)*(RG.x1-RG.x0); - const yOf=db=>RG.y0+(RG.db-db)/(2*RG.db)*(RG.y1-RG.y0); - const sigma=0.28;let pts=''; - for(let i=0;i<=76;i++){const lf=RG.fLo+i/76*(RG.fHi-RG.fLo); - const db=rgGain*Math.exp(-Math.pow(lf-Math.log10(rgFc),2)/(2*sigma*sigma)); - pts+=`${xOf(Math.pow(10,lf)).toFixed(1)},${yOf(db).toFixed(1)} `;} - rgCurve.setAttribute('points',pts.trim());rgGlowLine.setAttribute('points',pts.trim()); - rgHandle.setAttribute('cx',xOf(rgFc));rgHandle.setAttribute('cy',yOf(rgGain)); - const fLbl=rgFc<1000?Math.round(rgFc)+' Hz':(rgFc/1000).toFixed(1)+' kHz'; - setRd('R26',`${fLbl} · ${rgGain>=0?'+':''}${rgGain.toFixed(1)} dB`);} -setRgraph(1200,6.5); /* ============ LIVE SIGNAL LOOPS ============ */ -buildBars($('vuL'),16);buildBars($('vuR'),16);$('mini').innerHTML='<i></i><i></i><i></i><i></i>'; -(function(){const eq=$('eq');for(let b=0;b<11;b++){const band=document.createElement('span');band.className='band'; - for(let s=0;s<9;s++)band.appendChild(document.createElement('i'));eq.appendChild(band);}})(); -(function(){const cl=$('clock');for(let i=0;i<12;i++){const t=document.createElement('span');t.className='tk'; - t.style.transform=`rotate(${i*30}deg)`;cl.insertBefore(t,cl.firstChild);}})(); -const counterDigits=[4,7,1,3,0,0]; -(function(){const ct=$('counter');counterDigits.forEach((d,idx)=>{const w=document.createElement('span'); - w.className='cwheel'+(idx>=4?' redw':'');const col=document.createElement('span');col.className='col'; - for(let n=-1;n<=10;n++){const s=document.createElement('span');s.textContent=((n+10)%10);col.appendChild(s);} - w.appendChild(col);ct.appendChild(w);setWheel(w,d);});})(); -function setWheel(w,d){w.querySelector('.col').style.top=(-(d+1)*34)+'px';} +/* extracted meters: the page keeps the clock + demo signal, drives card handles */ +const MH={vu:$('card-11').gw,mini:$('card-12').gw,spark:$('card-16').gw,wave:$('card-17').gw, + scope:$('card-N11').gw,eq:$('card-N12').gw,strip:$('card-N16').gw,mcvu:$('card-R01').gw}; +const IH={flap:$('card-N20').gw,seven:$('card-N21').gw,vfd:$('card-N22').gw, + counter:$('card-N25').gw,clock:$('card-N26').gw}; let ph=0; -const hist=Array.from({length:40},()=>0.5); const eqBands=Array.from({length:11},(_,i)=>({v:0.4,ph:i*0.6})); -const stripHist=Array.from({length:60},()=>31); -const pkL={v:0},pkR={v:0}; -function paintVU(el,l,pk){const b=el.children,n=b.length,lit=Math.round(l*n); - pk.v=Math.max(lit,(pk.v||0)-0.4);const p=Math.round(pk.v); - for(let k=0;k<n;k++){let c=k<lit?(k>=n-2?'clip':k>=n-4?'hot':'on'):'';if(p>0&&k===p-1)c=(c?c+' ':'')+'peak';b[k].className=c;}} -function paintMini(el,l){const b=el.children,lit=Math.round(l*4);for(let k=0;k<4;k++)b[k].className=k<lit?(k>=3?'clip':k>=2?'hot':'on'):'';} -function paintEq(){const bands=$('eq').children;let peak=0; - for(let b=0;b<bands.length;b++){const cells=bands[b].children,n=cells.length,val=eqBands[b].v;const lit=Math.round(val*n);peak=Math.max(peak,val); - for(let k=0;k<n;k++){let c='';if(k<lit)c=(k>=n-1?'clip':k>=n-3?'hot':'on');cells[k].className=c?c:'';}}return peak;} function lvl(){return Math.max(0,Math.min(1,0.5+0.4*Math.sin(ph*1.3)+(Math.random()<0.15?Math.random()*0.4:0)-Math.random()*0.08));} function fastTick(){ ph+=0.09; const a=lvl(),b=lvl(); - paintVU($('vuL'),a,pkL);paintVU($('vuR'),b,pkR);paintMini($('mini'),a); - setRd('11','L '+Math.round(a*100)+' · R '+Math.round(b*100)); - setRd('12',Math.round(a*100)+'%'); - hist.push(0.5+0.42*Math.sin(ph*0.9)+(Math.random()-0.5)*0.25);hist.shift(); - $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-Math.max(0,Math.min(1,v))*40-2}`).join(' ')); - setRd('16',Math.round(Math.max(0,Math.min(1,hist[hist.length-1]))*100)); - let d='M0 19';const env=Math.min(1,0.6+0.3*Math.sin(ph*0.4)); - for(let x=0;x<=170;x+=3){const y=19+Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)*14;d+=` L${x} ${y.toFixed(1)}`;} - $('wavep').setAttribute('d',d);setRd('17','amp '+Math.round(env*100)+'%'); - let sp='';for(let x=0;x<=176;x+=3){const y=39+Math.sin(x*0.16+ph*3.2)*Math.min(1,0.6+0.3*Math.sin(ph*0.4))*22;sp+=`${x},${y.toFixed(1)} `;} - $('scopep').setAttribute('points',sp.trim());setRd('N11','Vpp '+Math.round((0.6+0.3*Math.sin(ph*0.4))*100)); + MH.vu.set(a,b);MH.mini.set(a); + MH.spark.push(0.5+0.42*Math.sin(ph*0.9)+(Math.random()-0.5)*0.25); + const env=Math.min(1,0.6+0.3*Math.sin(ph*0.4)); + const smp=[];for(let x=0;x<=170;x+=3)smp.push(Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)); + MH.wave.set(smp,env); + const senv=Math.min(1,0.6+0.3*Math.sin(ph*0.4)); + const ssmp=[];for(let x=0;x<=176;x+=3)ssmp.push(Math.sin(x*0.16+ph*3.2)*senv); + MH.scope.set(ssmp,0.6+0.3*Math.sin(ph*0.4)); eqBands.forEach(bd=>{bd.ph+=0.16;bd.v=Math.max(0.05,Math.min(1,0.5+0.42*Math.sin(bd.ph)+(Math.random()<0.2?Math.random()*0.3:0)-Math.random()*0.06));}); - const peak=paintEq();setRd('N12','peak '+Math.round(peak*100)+'%'); - stripHist.push(31-24*Math.sin(ph*0.7)*(0.6+0.4*Math.sin(ph*0.13))+(Math.random()-0.5)*4);stripHist.shift(); - const last=Math.max(3,Math.min(59,stripHist[stripHist.length-1])); - $('stripp').setAttribute('points',stripHist.map((v,i)=>`${i/(stripHist.length-1)*176},${Math.max(3,Math.min(59,v)).toFixed(1)}`).join(' ')); - $('strippen').style.top=last+'px';setRd('N16',Math.round((59-last)/56*100)); + MH.eq.set(eqBands.map(bd=>bd.v)); + MH.strip.push((28+24*Math.sin(ph*0.7)*(0.6+0.4*Math.sin(ph*0.13))-(Math.random()-0.5)*4)/56); } -/* clock */ -function tickClock(){const d=new Date();const s=d.getSeconds(),m=d.getMinutes(),h=d.getHours()%12; - $('clks').style.transform=`rotate(${s*6}deg)`;$('clkm').style.transform=`rotate(${m*6+s*0.1}deg)`;$('clkh').style.transform=`rotate(${h*30+m*0.5}deg)`; - setRd('N26',String(d.getHours()).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0'));} -/* split-flap */ -const flapWords=['DNS ','LINK','SYNC','WIFI','SCAN'];let flapI=0; -function tickFlap(){flapI=(flapI+1)%flapWords.length;const w=flapWords[flapI].padEnd(4,' '); - $('flap').querySelectorAll('.flapd').forEach((dd,i)=>{const ch=w[i]||' ';const bel=dd.querySelector('b'); - if(bel.textContent!==ch){bel.textContent=ch;if(!reduced){dd.classList.remove('flip');void dd.offsetWidth;dd.classList.add('flip');}}}); - setRd('N20',flapWords[flapI].trim());} -$('flap').onclick=tickFlap; -/* seven-segment countdown */ -let sevenSecs=24*60+10; -function renderSeven(){const mm=String(Math.floor(sevenSecs/60)).padStart(2,'0'),ss=String(sevenSecs%60).padStart(2,'0'); - $('seven').innerHTML=seg7(mm[0])+seg7(mm[1])+'<span class="colon"><i></i><i></i></span>'+seg7(ss[0])+seg7(ss[1]);setRd('N21',mm+':'+ss);} -function tickSeven(){sevenSecs=(sevenSecs-1+3600)%3600;renderSeven();} -$('seven').onclick=()=>{sevenSecs=(sevenSecs+60)%3600;renderSeven();}; -renderSeven(); -/* tabular timer */ -let tSecs=24*60+10,tRun=true; -function renderT(){$('treadout').textContent=String(Math.floor(tSecs/60)).padStart(2,'0')+':'+String(tSecs%60).padStart(2,'0'); - setRd('20',tRun?'running':'paused');} -function tickT(){if(tRun){tSecs=(tSecs-1+3600)%3600;renderT();}} -$('treadout').onclick=()=>{tRun=!tRun;renderT();};renderT(); -/* VFD marquee */ -const vfdMsgs=['ARCHSETUP · NET OK · BT 2 · SND 62%','WIFI @Hyatt · 300 Mbps · VPN UP','BATTERY 84% · DISK 61% · TEMP 47C'];let vfdMi=0,vfdX=176; -function tickVfd(){const t=$('vfdt');vfdX-=1.1;if(vfdX< -t.offsetWidth)vfdX=176;t.style.left=vfdX+'px';} -$('vfdm').onclick=()=>{vfdMi=(vfdMi+1)%vfdMsgs.length;$('vfdt').textContent=vfdMsgs[vfdMi];vfdX=176;setRd('N22','msg '+(vfdMi+1)+'/'+vfdMsgs.length);}; -setRd('N22','scrolling'); -/* tape counter */ -let ctrVal=471300; -function tickCounter(){ctrVal+=137;const s=String(ctrVal).padStart(6,'0'); - $('counter').querySelectorAll('.cwheel').forEach((w,i)=>setWheel(w,+s[i]));setRd('N25',s);} -setRd('N25',String(ctrVal).padStart(6,'0')); - -/* reels spin keyframe */ -(function(){const st=document.createElement('style');st.textContent='@keyframes reelspin{to{transform:rotate(360deg)}}';document.head.appendChild(st);})(); -document.querySelectorAll('.reel.spin').forEach(r=>{r.style.animation='reelspin 2.6s linear infinite';}); +/* indicator loops: builders own state + clicks; the page keeps the clocks and + the time source (tick contract) */ +function tickClock(){const d=new Date();IH.clock.set(d.getHours(),d.getMinutes(),d.getSeconds());} +function tickFlap(){IH.flap.next();} +function tickSeven(){IH.seven.tick();} +function tickT(){$('card-20').gw.tick();} +function tickVfd(){IH.vfd.tick();} +function tickCounter(){IH.counter.set(IH.counter.get()+137);} /* static paint for reduced motion */ function paintStatic(){ - paintVU($('vuL'),0.55,pkL);paintVU($('vuR'),0.5,pkR);paintMini($('mini'),0.55); - setRd('11','L 55 · R 50');setRd('12','55%'); - $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-0.5*40-2}`).join(' '));setRd('16','50'); - $('wavep').setAttribute('d','M0 19 L170 19');setRd('17','amp 60%'); - eqBands.forEach((bd,i)=>bd.v=0.3+0.5*Math.abs(Math.sin(i*0.9)));setRd('N12','peak '+Math.round(paintEq()*100)+'%'); - $('scopep').setAttribute('points',Array.from({length:60},(_,i)=>`${i*3},${39+Math.sin(i*0.5)*18}`).join(' '));setRd('N11','Vpp 74'); - $('stripp').setAttribute('points',stripHist.map((v,i)=>`${i/(stripHist.length-1)*176},${31-Math.sin(i*0.3)*16}`).join(' '));setRd('N16','50'); - tickClock();renderSeven();setRd('N20','DNS');setRd('N22','scrolling');setRd('N25',String(ctrVal).padStart(6,'0')); - setMcVu(.5); + MH.vu.set(0.55,0.5);MH.mini.set(0.55); + MH.spark.fill(0.5); + MH.wave.set([],0.6); + eqBands.forEach((bd,i)=>bd.v=0.3+0.5*Math.abs(Math.sin(i*0.9))); + MH.eq.set(eqBands.map(bd=>bd.v)); + MH.scope.set(Array.from({length:60},(_,i)=>Math.sin(i*0.5)*18/22),0.74); + MH.strip.set(Array.from({length:60},(_,i)=>0.5+Math.sin(i*0.3)*16/56),0.5); + tickClock();IH.flap.set(0); + MH.mcvu.set(.5); } if(!reduced){ @@ -3236,900 +1554,63 @@ if(!reduced){ document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused'); } -/* R32 mechanical timer dial: dial rotates under a fixed index; wind by drag, stop by the red knob */ -let mtMin=0,mtRot=null; -(function(){const s=$('mtimer'),cx=62,cy=58; - const defs=svgEl(s,'defs',{}); - const fc=svgEl(defs,'radialGradient',{id:'mtFace',cx:'50%',cy:'42%',r:'75%'}); - svgEl(fc,'stop',{offset:'0','stop-color':'#282320'}); - svgEl(fc,'stop',{offset:'1','stop-color':'#14110e'}); - const rd=svgEl(defs,'radialGradient',{id:'mtRed',cx:'38%',cy:'32%',r:'80%'}); - svgEl(rd,'stop',{offset:'0','stop-color':'#e0523a'}); - svgEl(rd,'stop',{offset:'1','stop-color':'#8f2416'}); - const hb=svgEl(defs,'linearGradient',{id:'mtHub',x1:0,y1:0,x2:0,y2:1}); - svgEl(hb,'stop',{offset:'0','stop-color':'#c9c4b8'}); - svgEl(hb,'stop',{offset:'1','stop-color':'#6e685c'}); - // bezel + knurled coin edge - svgEl(s,'circle',{cx,cy,r:50,fill:'#17140f',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:47.5,fill:'none',stroke:'#3a352c','stroke-width':4,'stroke-dasharray':'2 1.6'}); - svgEl(s,'circle',{cx,cy,r:44,fill:'url(#mtFace)',stroke:'#0a0908','stroke-width':1}); - // rotating scale: OFF at 0, minutes climb clockwise at 4.5 deg/min (60 at 270) - mtRot=svgEl(s,'g',{}); - for(let m=0;m<=60;m+=5){const major=m%20===0; - const [x1,y1]=polar(cx,cy,40,m*4.5),[x2,y2]=polar(cx,cy,major?34:37,m*4.5); - svgEl(mtRot,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':major?1.2:.55,opacity:major?.9:.55});} - for(const m of [20,40,60]){const [x,y]=polar(cx,cy,27,m*4.5); - svgEl(mtRot,'text',{x,y:y+3,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=String(m);} - const [ox,oy]=polar(cx,cy,27,0); - svgEl(mtRot,'text',{x:ox,y:oy+3,'text-anchor':'middle','font-size':7.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'#e0523a'}).textContent='OFF'; - const [mx,my]=polar(cx,cy,16,180); - svgEl(mtRot,'text',{x:mx,y:my+2.5,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.14em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='MINUTES'; - // hub with screw slot - svgEl(s,'circle',{cx,cy,r:9,fill:'url(#mtHub)',stroke:'#3c382f','stroke-width':1}); - svgEl(s,'line',{x1:cx-5.5,y1:cy,x2:cx+5.5,y2:cy,stroke:'#4a4438','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:2.2,fill:'#8f897b'}); - // fixed red index at top + winding-direction arrow - svgEl(s,'line',{x1:cx,y1:cy-44,x2:cx,y2:cy-32,stroke:'#e0523a','stroke-width':1.6}); - svgEl(s,'path',{d:'M 122 14 A 26 26 0 0 1 136 26',fill:'none',stroke:'var(--steel)','stroke-width':1.6}); - svgEl(s,'polygon',{points:'136,26 130.5,24.5 134.5,19.5',fill:'var(--steel)'}); - // engraved plate captions - const cap=(t,y)=>svgEl(s,'text',{x:4,y,'font-size':6.2,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=t; - cap('PUSH TO STOP',118); cap('TURN TO START',128); - // dial drag surface (under the knob in DOM so the knob wins the overlap) - const hit=svgEl(s,'circle',{cx,cy,r:50,fill:'transparent'}); - hit.style.cursor='grab'; - dragDelta(hit,()=>mtMin,v=>setMtimer(v),{min:0,max:60}); - // red stop knob: fluted edge, domed center - const knob=svgEl(s,'g',{}); - svgEl(knob,'circle',{cx:126,cy:104,r:20,fill:'url(#mtRed)',stroke:'#5c150c','stroke-width':1.5}); - svgEl(knob,'circle',{cx:126,cy:104,r:18,fill:'none',stroke:'rgba(0,0,0,.4)','stroke-width':3.4,'stroke-dasharray':'3 2.4'}); - svgEl(knob,'circle',{cx:126,cy:104,r:8.5,fill:'url(#mtRed)',stroke:'rgba(0,0,0,.3)','stroke-width':.8}); - svgEl(knob,'ellipse',{cx:121,cy:98,rx:6,ry:3.4,fill:'rgba(255,255,255,.18)',transform:'rotate(-28,121,98)'}); - knob.style.cursor='pointer'; - knob.addEventListener('click',()=>{setMtimer(0);setRd('R32','STOP · OFF');});})(); -function setMtimer(m){mtMin=Math.max(0,Math.min(60,m)); - mtRot.setAttribute('transform',`rotate(${(-mtMin*4.5).toFixed(2)},62,58)`); - setRd('R32',mtMin>0.5?'T-'+Math.round(mtMin)+' MIN':'OFF');} -setMtimer(0); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{if(mtMin>0){setMtimer(mtMin-1);if(mtMin===0)setRd('R32','DING · OFF');}},1000); - -/* R33 four-way rocker: quadrant clicks step a tracked cursor; arrows flash on press */ -(function(){const s=$('rocker4'),cx=55,cy=55; - const defs=svgEl(s,'defs',{}); - const pd=svgEl(defs,'radialGradient',{id:'rk4Pad',cx:'42%',cy:'34%',r:'85%'}); - svgEl(pd,'stop',{offset:'0','stop-color':'#33302b'}); - svgEl(pd,'stop',{offset:'1','stop-color':'#141210'}); - const ar=svgEl(defs,'linearGradient',{id:'rk4Arr',x1:0,y1:0,x2:0,y2:1}); - svgEl(ar,'stop',{offset:'0','stop-color':'#e9e4d6'}); - svgEl(ar,'stop',{offset:'1','stop-color':'#9d988b'}); - const nb=svgEl(defs,'radialGradient',{id:'rk4Nub',cx:'38%',cy:'30%',r:'85%'}); - svgEl(nb,'stop',{offset:'0','stop-color':'#4a453e'}); - svgEl(nb,'stop',{offset:'1','stop-color':'#0d0c0a'}); - // recessed plate + rubber pad - svgEl(s,'rect',{x:3,y:3,width:104,height:104,rx:18,fill:'#211e1a',stroke:'#0c0b09','stroke-width':1.5}); - svgEl(s,'circle',{cx,cy,r:45,fill:'#0f0d0b'}); - svgEl(s,'circle',{cx,cy,r:41,fill:'url(#rk4Pad)',stroke:'#060505','stroke-width':1}); - // four arrows, pointing outward - const ARR={up:'55,22 45,36 65,36',down:'55,88 45,74 65,74',left:'22,55 36,45 36,65',right:'88,55 74,45 74,65'}; - const arrows={}; - for(const [dir,pts] of Object.entries(ARR)) - arrows[dir]=svgEl(s,'polygon',{points:pts,fill:'url(#rk4Arr)',stroke:'#3c382f','stroke-width':.7}); - // center pivot nub - svgEl(s,'circle',{cx,cy,r:7,fill:'url(#rk4Nub)',stroke:'#060505','stroke-width':.8}); - svgEl(s,'ellipse',{cx:cx-2,cy:cy-2.5,rx:2.6,ry:1.7,fill:'rgba(255,255,255,.25)'}); - // quadrant hit zones (drawn last so they win), press flash + tracked position - let rx=0,ry=0; - const press=(dir,dx,dy)=>{rx+=dx;ry+=dy; - setRd('R33',dir.toUpperCase()+' · x '+rx+' y '+ry); - arrows[dir].setAttribute('fill','var(--gold-hi)'); - setTimeout(()=>arrows[dir].setAttribute('fill','url(#rk4Arr)'),160);}; - const zone=(dir,pts,dx,dy)=>{const z=svgEl(s,'polygon',{points:pts,fill:'transparent'}); - z.style.cursor='pointer';z.addEventListener('click',()=>press(dir,dx,dy));}; - zone('up',`${cx},${cy} 20,20 90,20`,0,1); - zone('down',`${cx},${cy} 20,90 90,90`,0,-1); - zone('left',`${cx},${cy} 20,20 20,90`,-1,0); - zone('right',`${cx},${cy} 90,20 90,90`,1,0); - setRd('R33','x 0 y 0');})(); - -/* R34 four-way toggle selector: ball lever throws to a diagonal; corner lamps show the state */ -(function(){const s=$('fourway'),cx=65,cy=65; - const defs=svgEl(s,'defs',{}); - const ch=svgEl(defs,'linearGradient',{id:'fw4Chrome',x1:0,y1:0,x2:1,y2:1}); - svgEl(ch,'stop',{offset:'0','stop-color':'#e8e6e0'}); - svgEl(ch,'stop',{offset:'.5','stop-color':'#9a968c'}); - svgEl(ch,'stop',{offset:'1','stop-color':'#55524a'}); - const bl=svgEl(defs,'radialGradient',{id:'fw4Ball',cx:'36%',cy:'30%',r:'80%'}); - svgEl(bl,'stop',{offset:'0','stop-color':'#f2f0ea'}); - svgEl(bl,'stop',{offset:'.55','stop-color':'#8e8a80'}); - svgEl(bl,'stop',{offset:'1','stop-color':'#3c3a34'}); - // printed panel graphics: axes, circle, diagonal square, labels - svgEl(s,'line',{x1:8,y1:cy,x2:122,y2:cy,stroke:'var(--steel)','stroke-width':.7,opacity:.7}); - svgEl(s,'line',{x1:cx,y1:8,x2:cx,y2:122,stroke:'var(--steel)','stroke-width':.7,opacity:.7}); - svgEl(s,'text',{x:cx+2,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='Y'; - svgEl(s,'text',{x:116,y:cy-4,'font-size':7,'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='X'; - svgEl(s,'circle',{cx,cy,r:42,fill:'none',stroke:'var(--steel)','stroke-width':.9,opacity:.8}); - const QUAD={A:315,B:45,C:135,D:225}; - const diag=Object.values(QUAD).map(a=>polar(cx,cy,42,a)); - svgEl(s,'polygon',{points:diag.map(p=>p.join(',')).join(' '),fill:'none',stroke:'var(--steel)','stroke-width':.8,opacity:.8}); - const LAMP_AT={A:[14,14],B:[116,14],C:[116,116],D:[14,116]}; - const lamps={}; - for(const [q,a] of Object.entries(QUAD)){ - const [lx,ly]=polar(cx,cy,42,a); - const [px,py]=LAMP_AT[q]; - svgEl(s,'line',{x1:lx,y1:ly,x2:px,y2:py,stroke:'var(--steel)','stroke-width':.7,opacity:.5}); - svgEl(s,'circle',{cx:lx,cy:ly,r:6.5,fill:'#14110e',stroke:'var(--steel)','stroke-width':.9}); - svgEl(s,'text',{x:lx,y:ly+2.8,'text-anchor':'middle','font-size':7.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=q; - svgEl(s,'circle',{cx:px,cy:py,r:5.5,fill:'#0d0b09',stroke:'#2c2820','stroke-width':1}); - lamps[q]=svgEl(s,'circle',{cx:px,cy:py,r:3.6,fill:'#3a3426'});} - // bezel + boot - svgEl(s,'circle',{cx,cy,r:20,fill:'#0d0c0a',stroke:'#2a2722','stroke-width':1.5}); - svgEl(s,'circle',{cx,cy,r:15,fill:'#17140f',stroke:'#060505','stroke-width':1}); - // lever: shaft + grip rotate to the selected diagonal; ball pivot stays centered - const lever=svgEl(s,'g',{}); - svgEl(lever,'polygon',{points:`${cx-3},${cy} ${cx+3},${cy} ${cx+2},${cy-26} ${cx-2},${cy-26}`,fill:'url(#fw4Chrome)',stroke:'#3c3a34','stroke-width':.6}); - svgEl(lever,'rect',{x:cx-3.4,y:cy-40,width:6.8,height:16,rx:3,fill:'url(#fw4Chrome)',stroke:'#3c3a34','stroke-width':.6}); - svgEl(s,'circle',{cx,cy,r:8.5,fill:'url(#fw4Ball)',stroke:'#26241f','stroke-width':.8}); - svgEl(s,'ellipse',{cx:cx-2.6,cy:cy-3,rx:2.8,ry:1.9,fill:'rgba(255,255,255,.5)'}); - let fwPos=null; - const setFw=q=>{fwPos=q; - lever.setAttribute('transform',`rotate(${QUAD[q]},${cx},${cy})`); - for(const k of Object.keys(QUAD)){ - lamps[k].setAttribute('fill',k===q?'var(--jewel-a)':'#3a3426'); - lamps[k].setAttribute('filter',k===q?'drop-shadow(0 0 4px rgba(255,180,58,.8))':'none');} - setRd('R34','POS '+q);}; - // quadrant click zones (diagonal quadrants, drawn last) - const zone=(q,pts)=>{const z=svgEl(s,'polygon',{points:pts,fill:'transparent'}); - z.style.cursor='pointer';z.addEventListener('click',()=>setFw(q));}; - zone('A',`${cx},${cy} ${cx},5 5,5 5,${cy}`); - zone('B',`${cx},${cy} ${cx},5 125,5 125,${cy}`); - zone('C',`${cx},${cy} 125,${cy} 125,125 ${cx},125`); - zone('D',`${cx},${cy} ${cx},125 5,125 5,${cy}`); - setFw('C');})(); - -/* 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');})(); - -/* R37 pin routing matrix: click an intersection to seat/pull a pin; many-to-many */ -(function(){const s=$('pinmx'); - const ROWS=['OSC1','OSC2','LFO','NOIS','ENV'],COLS=['VCF','VCA','PAN','DLY','OUT','MOD']; - const X0=42,Y0=30,DX=19,DY=15.5; - svgEl(s,'rect',{x:2,y:2,width:156,height:106,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5}); - COLS.forEach((c,j)=>svgEl(s,'text',{x:X0+j*DX,y:16,'text-anchor':'middle','font-size':5.4, - 'letter-spacing':'.05em','font-family':'var(--mono)',fill:'var(--steel)'}).textContent=c); - ROWS.forEach((r,i)=>svgEl(s,'text',{x:34,y:Y0+i*DY+2,'text-anchor':'end','font-size':5.4, - 'letter-spacing':'.05em','font-family':'var(--mono)',fill:'var(--steel)'}).textContent=r); - const pins=new Set(['OSC1>VCF','LFO>PAN','ENV>VCA']); // a legible default patch - ROWS.forEach((r,i)=>COLS.forEach((c,j)=>{ - const cx=X0+j*DX,cy=Y0+i*DY,key=r+'>'+c; - svgEl(s,'circle',{cx,cy,r:3.4,fill:'#0a0908',stroke:'#2c2820','stroke-width':1}); - const cap=svgEl(s,'circle',{cx,cy,r:4.4,fill:'var(--gold)',stroke:'#7d5c16','stroke-width':1, - opacity:pins.has(key)?1:0,filter:'drop-shadow(0 1px 2px rgba(0,0,0,.6))'}); - const hit=svgEl(s,'circle',{cx,cy,r:8,fill:'transparent'}); - hit.style.cursor='pointer'; - hit.addEventListener('click',()=>{const on=!pins.has(key); - if(on)pins.add(key);else pins.delete(key); - cap.setAttribute('opacity',on?1:0); - setRd('R37',(on?r+' → '+c:'pulled '+r+' → '+c)+' · '+pins.size+' routes');});})); - setRd('R37',pins.size+' routes');})(); - -/* R38 dead-man button: state exists only while the pointer holds it down */ -(function(){const s=$('deadman'),cx=60,cy=58; - const defs=svgEl(s,'defs',{}); - const rb=svgEl(defs,'radialGradient',{id:'dmBtn',cx:'40%',cy:'32%',r:'85%'}); - svgEl(rb,'stop',{offset:'0','stop-color':'#3a3631'}); - svgEl(rb,'stop',{offset:'1','stop-color':'#16130f'}); - svgEl(s,'rect',{x:4,y:4,width:112,height:102,rx:12,fill:'#211e1a',stroke:'#0c0b09','stroke-width':1.5}); - svgEl(s,'text',{x:cx,y:18,'text-anchor':'middle','font-size':6.2,'letter-spacing':'.16em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='HOLD TO RUN'; - const lamp=svgEl(s,'circle',{cx:104,cy:16,r:4.5,fill:'#1e2a12'}); - const ring=svgEl(s,'circle',{cx,cy,r:33,fill:'none',stroke:'#2c2820','stroke-width':3}); - const btn=svgEl(s,'g',{}); - svgEl(btn,'circle',{cx,cy,r:28,fill:'url(#dmBtn)',stroke:'#060505','stroke-width':1.5}); - svgEl(btn,'circle',{cx,cy,r:22,fill:'none',stroke:'rgba(255,255,255,.07)','stroke-width':1}); - svgEl(btn,'text',{x:cx,y:cy+2.5,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--silver)'}).textContent='RUN'; - btn.style.cursor='pointer'; - let dmT0=null,dmIv=null; - const stop=()=>{if(dmT0===null)return; - const held=((Date.now()-dmT0)/1000).toFixed(1); - dmT0=null;clearInterval(dmIv); - btn.removeAttribute('transform'); - ring.setAttribute('stroke','#2c2820');ring.removeAttribute('filter'); - lamp.setAttribute('fill','#1e2a12');lamp.removeAttribute('filter'); - setRd('R38','SAFE (held '+held+'s)');}; - btn.addEventListener('pointerdown',e=>{btn.setPointerCapture(e.pointerId); - dmT0=Date.now(); - btn.setAttribute('transform','translate(0,1.5)'); - ring.setAttribute('stroke','var(--gold)');ring.setAttribute('filter','drop-shadow(0 0 5px rgba(var(--glow-lo),.7))'); - lamp.setAttribute('fill','var(--pass)');lamp.setAttribute('filter','drop-shadow(0 0 4px rgba(116,147,47,.8))'); - setRd('R38','RUNNING 0.0s'); - dmIv=setInterval(()=>{if(dmT0!==null)setRd('R38','RUNNING '+((Date.now()-dmT0)/1000).toFixed(1)+'s');},100); - e.preventDefault();}); - btn.addEventListener('pointerup',stop); - btn.addEventListener('pointercancel',stop); - setRd('R38','SAFE');})(); - -/* R39 rotary telephone dial: click a hole; the wheel winds to the stop and returns */ -(function(){const s=$('teldial'),cx=65,cy=63; - const DIGITS=['1','2','3','4','5','6','7','8','9','0']; - const angOf=i=>(60-i*30+360)%360; // 1 at 60cw, counterclockwise to 0 at 150 - const STOP=105; - const defs=svgEl(s,'defs',{}); - const mt=svgEl(defs,'radialGradient',{id:'tdWheel',cx:'42%',cy:'36%',r:'85%'}); - svgEl(mt,'stop',{offset:'0','stop-color':'#8f8a7e'}); - svgEl(mt,'stop',{offset:'1','stop-color':'#4a463e'}); - svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:50,fill:'#1e1a16',stroke:'#0a0908','stroke-width':1}); - // rotating finger wheel: ring + hole openings (dark wells punched in the metal) - const wheel=svgEl(s,'g',{}); - svgEl(wheel,'circle',{cx,cy,r:46,fill:'none',stroke:'url(#tdWheel)','stroke-width':17,opacity:.95}); - DIGITS.forEach((d,i)=>{const [x,y]=polar(cx,cy,38,angOf(i)); - svgEl(wheel,'circle',{cx:x,cy:y,r:7.2,fill:'#1e1a16',stroke:'#26221c','stroke-width':1.4});}); - // fixed digits ON TOP so they read through the holes (they stay put while the wheel spins) - DIGITS.forEach((d,i)=>{const [x,y]=polar(cx,cy,38,angOf(i)); - svgEl(s,'text',{x,y:y+3,'text-anchor':'middle','font-size':8.5,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=d;}); - svgEl(s,'circle',{cx,cy,r:16,fill:'url(#tdWheel)',stroke:'#26221c','stroke-width':1}); - // finger stop - const [fx,fy]=polar(cx,cy,52,STOP); - svgEl(s,'rect',{x:fx-2.5,y:fy-8,width:5,height:16,rx:2,fill:'#b8b2a4',stroke:'#3c382f','stroke-width':1, - transform:`rotate(${STOP},${fx},${fy})`}); - let dialed='',spinning=false; - const setW=a=>wheel.setAttribute('transform',`rotate(${a},${cx},${cy})`); - const finish=d=>{dialed=(dialed+d).slice(-10);setRd('R39',dialed);spinning=false;}; - DIGITS.forEach((d,i)=>{const [x,y]=polar(cx,cy,38,angOf(i)); - const hit=svgEl(s,'circle',{cx:x,cy:y,r:8.5,fill:'transparent'}); - hit.style.cursor='pointer'; - hit.addEventListener('click',()=>{if(spinning)return;spinning=true; - const R=(STOP-angOf(i)+360)%360; - if(matchMedia('(prefers-reduced-motion: reduce)').matches){setW(R);setTimeout(()=>{setW(0);finish(d);},160);return;} - const t0=performance.now(),Tf=R/300*1000,Tb=R/450*1000; - const step=t=>{const el=t-t0; - if(el<Tf){setW(R*el/Tf);requestAnimationFrame(step);} - else if(el<Tf+80){setW(R);requestAnimationFrame(step);} - else if(el<Tf+80+Tb){setW(R*(1-(el-Tf-80)/Tb));requestAnimationFrame(step);} - else{setW(0);finish(d);}}; - requestAnimationFrame(step);});}); - setRd('R39','dial a number');})(); +/* R35, R36 extracted to widgets.js (GW.*) */ -/* R40 circuit breaker panel: on/off by click, TRIP pops one to the amber mid-state, reset is two-step */ -(function(){const s=$('brkpanel'); - const NAMES=['MAIN','PUMP','LAMP','AUX']; - svgEl(s,'rect',{x:2,y:2,width:156,height:106,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5}); - const brk=[]; - const draw=()=>{let closed=0,trip=null; - brk.forEach(b=>{if(b.state==='on')closed++;if(b.state==='tripped')trip=b.name; - b.lamp.setAttribute('fill',b.state==='on'?'var(--pass)':'#1e2a12'); - b.lamp.setAttribute('filter',b.state==='on'?'drop-shadow(0 0 3px rgba(116,147,47,.8))':'none'); - b.handle.setAttribute('y',b.state==='on'?40:b.state==='tripped'?57:74); - b.collar.setAttribute('opacity',b.state==='tripped'?1:0);}); - setRd('R40',closed+'/4 closed'+(trip?' · '+trip+' TRIPPED':''));}; - NAMES.forEach((name,i)=>{const x=14+i*30; - svgEl(s,'rect',{x,y:36,width:20,height:52,rx:4,fill:'#14110e',stroke:'#2c2820','stroke-width':1}); - const lamp=svgEl(s,'circle',{cx:x+10,cy:26,r:4,fill:'#1e2a12'}); - const collar=svgEl(s,'rect',{x:x+4,y:69,width:12,height:5,fill:'var(--amber-warn)',opacity:0}); - const handle=svgEl(s,'rect',{x:x+4,y:40,width:12,height:14,rx:2,fill:'#3a3631',stroke:'#060505','stroke-width':1}); - svgEl(s,'text',{x:x+10,y:98,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.05em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=name; - const hit=svgEl(s,'rect',{x,y:20,width:20,height:80,fill:'transparent'}); - hit.style.cursor='pointer'; - const b={name,state:i<3?'on':'off',lamp,collar,handle}; - hit.addEventListener('click',()=>{b.state=b.state==='on'?'off':b.state==='off'?'on':'off';draw();}); - brk.push(b);}); - const tb=svgEl(s,'g',{}); - svgEl(tb,'rect',{x:132,y:40,width:22,height:16,rx:4,fill:'var(--fail)',stroke:'#5c150c','stroke-width':1}); - svgEl(tb,'text',{x:143,y:50.5,'text-anchor':'middle','font-size':6,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent='TRIP'; - tb.style.cursor='pointer'; - tb.addEventListener('click',()=>{for(let i=brk.length-1;i>=0;i--)if(brk[i].state==='on'){brk[i].state='tripped';break;}draw();}); - draw();})(); -/* R41 DSKY: verb/noun command grammar with status lamps and a lamp-test verb */ -(function(){const host=$('dsky'); - const LAMPS=['UPLINK','TEMP','GIMBAL','PROG','RESTART','OPR ERR']; - host.innerHTML= - `<div class="lampcol">${LAMPS.map(l=>`<div class="sl" data-l="${l}">${l}</div>`).join('')}</div>`+ - `<div class="right"><div class="wins">`+ - `<div class="win" data-w="prog"><span class="wl">PROG</span><span class="wd"></span></div>`+ - `<div class="win" data-w="verb"><span class="wl">VERB</span><span class="wd"></span></div>`+ - `<div class="win" data-w="noun"><span class="wl">NOUN</span><span class="wd"></span></div>`+ - `</div><div class="pad"></div></div>`; - const win=w=>host.querySelector(`[data-w="${w}"]`); - const setWin=(w,txt)=>{win(w).querySelector('.wd').innerHTML=seg7(txt[0]||' ')+seg7(txt[1]||' ');}; - const lampEl=l=>host.querySelector(`[data-l="${l}"]`); - let mode=null,vals={prog:' ',verb:' ',noun:' '},entry=''; - const KEYS=['VERB','NOUN','CLR','ENTR','RSET','7','8','9','4','5','6','1','2','3','0']; - const pad=host.querySelector('.pad'); - const hot=()=>{host.querySelectorAll('.win').forEach(w=>w.classList.toggle('hot',w.dataset.w===mode));}; - const oprErr=()=>{lampEl('OPR ERR').classList.add('on');setTimeout(()=>lampEl('OPR ERR').classList.remove('on'),1200);setRd('R41','OPR ERR');}; - const commit=()=>{ - if(vals.verb.trim().length<2){oprErr();return;} - const v=vals.verb,n=vals.noun.trim(); - if(v==='35'){LAMPS.forEach(l=>lampEl(l).classList.add('on')); - setTimeout(()=>LAMPS.forEach(l=>lampEl(l).classList.remove('on')),1400); - setRd('R41','V35 · lamp test');} - else if(v==='16'&&n==='36'){vals.prog='16';setWin('prog',vals.prog);setRd('R41','V16 N36 · monitor clock');} - else setRd('R41','V'+v+(n?' N'+n:'')+' ENTR'); - vals.verb=' ';vals.noun=' ';entry='';mode=null;hot();}; - KEYS.forEach(k=>{const b=document.createElement('button');b.className='key';b.textContent=k; - b.addEventListener('click',()=>{ - if(k==='VERB'||k==='NOUN'){mode=k.toLowerCase();entry='';vals[mode]=' ';setWin(mode,' ');hot();} - else if(k==='CLR'){if(mode){vals[mode]=' ';entry='';setWin(mode,' ');}} - else if(k==='RSET'){LAMPS.forEach(l=>lampEl(l).classList.remove('on'));setRd('R41','RSET');} - else if(k==='ENTR')commit(); - else if(/\d/.test(k)){if(!mode){oprErr();return;} - entry=(entry+k).slice(0,2);vals[mode]=entry.padEnd(2,' ');setWin(mode,vals[mode]);}}); - pad.appendChild(b);}); - vals.prog='00';setWin('prog',vals.prog);setWin('verb',' ');setWin('noun',' '); - setRd('R41','VERB ## ENTR');})(); - -/* R42 cam-timer program drum: the program is a ring; the pointer self-advances through it */ -const CAM_STEPS=['OFF','FILL','WASH','WASH','WASH','RINSE','RINSE','DRAIN','SPIN','SPIN','FLUFF','COOL']; -const CAM_COLS={OFF:'#3a3426',FILL:'#54677d',WASH:'#d29638',RINSE:'#46b89e',DRAIN:'#969385',SPIN:'#cb6b4d',FLUFF:'#c9b98a',COOL:'#7c99b0'}; -let camPos=0,camPtr=null; -(function(){const s=$('camtimer'),cx=65,cy=65; - svgEl(s,'circle',{cx,cy,r:60,fill:'#17140f',stroke:'#060505','stroke-width':2}); - CAM_STEPS.forEach((st,i)=>{const a0=i*30-90,a1=a0+30; - const p0o=polar(cx,cy,52,a0+90),p1o=polar(cx,cy,52,a1+90),p0i=polar(cx,cy,38,a0+90),p1i=polar(cx,cy,38,a1+90); - svgEl(s,'path',{d:`M ${p0i[0]} ${p0i[1]} L ${p0o[0]} ${p0o[1]} A 52 52 0 0 1 ${p1o[0]} ${p1o[1]} L ${p1i[0]} ${p1i[1]} A 38 38 0 0 0 ${p0i[0]} ${p0i[1]} Z`, - fill:CAM_COLS[st],opacity:.75,stroke:'#0a0908','stroke-width':.8}); - const [tx,ty]=polar(cx,cy,45,i*30+15); - svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.4,'font-weight':700, - 'font-family':'var(--mono)',fill:st==='OFF'?'var(--cream)':'#14110e'}).textContent=st==='OFF'?'OFF':st[0];}); - svgEl(s,'circle',{cx,cy,r:34,fill:'#211e1a',stroke:'#0a0908','stroke-width':1.5}); - CAM_STEPS.forEach((st,i)=>{if(i===0)return;const [lx,ly]=polar(cx,cy,27,i*30+15); - svgEl(s,'text',{x:lx,y:ly+1.8,'text-anchor':'middle','font-size':4.2, - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=st;}); - svgEl(s,'circle',{cx,cy,r:17,fill:'#14110e',stroke:'#2c2820','stroke-width':1.5}); - camPtr=svgEl(s,'polygon',{points:`${cx-2.5},${cy} ${cx+2.5},${cy} ${cx},${cy-31}`,fill:'var(--gold-hi)', - filter:'drop-shadow(0 0 3px rgba(var(--glow-lo),.7))'}); - svgEl(s,'circle',{cx,cy,r:5,fill:'#3a3631',stroke:'#060505','stroke-width':1}); - s.style.cursor='pointer'; - s.addEventListener('click',()=>setCam((camPos+1)%12));})(); -function setCam(i){camPos=i; - camPtr.setAttribute('transform',`rotate(${i*30+15},65,65)`); - setRd('R42',camPos===0?'OFF':'STEP '+camPos+' · '+CAM_STEPS[camPos]);} -setCam(0); -if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{if(camPos>0){const n=(camPos+1)%12;setCam(n);if(n===0)setRd('R42','CYCLE DONE · OFF');}},2000); - -/* R43 attitude indicator: sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */ -(function(){const s=$('attind'),cx=65,cy=65; - const defs=svgEl(s,'defs',{}); - const clip=svgEl(defs,'clipPath',{id:'aiClip'}); - svgEl(clip,'circle',{cx,cy,r:48}); - const sky=svgEl(defs,'linearGradient',{id:'aiSky',x1:0,y1:0,x2:0,y2:1}); - svgEl(sky,'stop',{offset:'0','stop-color':'#54677d'}); - svgEl(sky,'stop',{offset:'1','stop-color':'#3a4c60'}); - const gnd=svgEl(defs,'linearGradient',{id:'aiGnd',x1:0,y1:0,x2:0,y2:1}); - svgEl(gnd,'stop',{offset:'0','stop-color':'#5c4630'}); - svgEl(gnd,'stop',{offset:'1','stop-color':'#3c2e1e'}); - svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2}); - const g=svgEl(s,'g',{'clip-path':'url(#aiClip)'}); - const ball=svgEl(g,'g',{}); - svgEl(ball,'rect',{x:cx-90,y:cy-180,width:180,height:180,fill:'url(#aiSky)'}); - svgEl(ball,'rect',{x:cx-90,y:cy,width:180,height:180,fill:'url(#aiGnd)'}); - svgEl(ball,'line',{x1:cx-90,y1:cy,x2:cx+90,y2:cy,stroke:'#f3e7c5','stroke-width':1.4}); - for(const p of [-20,-10,10,20]){const y=cy-p*1.6,w=p%20===0?16:10; - svgEl(ball,'line',{x1:cx-w,y1:y,x2:cx+w,y2:y,stroke:'#f3e7c5','stroke-width':.8,opacity:.85}); - svgEl(ball,'text',{x:cx+w+3,y:y+2,'font-size':4.6,'font-family':'var(--mono)', - fill:'#f3e7c5',opacity:.85}).textContent=String(Math.abs(p));} - // fixed bank scale + pointer + miniature aircraft - for(const b of [-60,-45,-30,-20,-10,0,10,20,30,45,60]){ - const [x1,y1]=polar(cx,cy,47,b),[x2,y2]=polar(cx,cy,b%30===0?42:44,b); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':b===0?1.6:.9,opacity:.9});} - svgEl(s,'polygon',{points:`${cx-3.5},${cy-38} ${cx+3.5},${cy-38} ${cx},${cy-44}`,fill:'var(--gold-hi)'}); - svgEl(s,'path',{d:`M ${cx-22} ${cy} L ${cx-7} ${cy} L ${cx-4} ${cy+4} M ${cx+22} ${cy} L ${cx+7} ${cy} L ${cx+4} ${cy+4}`, - fill:'none',stroke:'var(--gold-hi)','stroke-width':2.4,'stroke-linecap':'round'}); - svgEl(s,'circle',{cx,cy,r:2,fill:'var(--gold-hi)'}); - let aiBank=0,aiPitch=0; - const setAi=(b,p)=>{aiBank=Math.max(-60,Math.min(60,b));aiPitch=Math.max(-20,Math.min(20,p)); - ball.setAttribute('transform',`rotate(${(-aiBank).toFixed(1)},${cx},${cy}) translate(0,${(aiPitch*1.6).toFixed(1)})`); - setRd('R43','BANK '+(aiBank<0?'L':aiBank>0?'R':'')+Math.abs(Math.round(aiBank))+' · PITCH '+(aiPitch>=0?'+':'-')+String(Math.abs(Math.round(aiPitch))).padStart(2,'0'));}; - s.style.touchAction='none';s.style.cursor='move'; - s.addEventListener('pointerdown',e=>{s.setPointerCapture(e.pointerId); - const x0=e.clientX,y0=e.clientY,b0=aiBank,p0=aiPitch; - const mv=ev=>setAi(b0+(ev.clientX-x0)*0.4,p0+(ev.clientY-y0)*0.25); - const up=()=>{s.removeEventListener('pointermove',mv);s.removeEventListener('pointerup',up);s.removeEventListener('pointercancel',up);}; - s.addEventListener('pointermove',mv);s.addEventListener('pointerup',up);s.addEventListener('pointercancel',up);e.preventDefault();}); - setAi(0,0);})(); - -/* R44 heading bug + servo needle: drag sets the command; the needle chases with lag */ -let hbCmd=90,hbAct=90,hbBug=null,hbNeedle=null; -(function(){const s=$('hdgbug'),cx=65,cy=65; - svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:50,fill:'#1a1714',stroke:'#2c2820','stroke-width':1}); - for(let d=0;d<360;d+=10){const maj=d%30===0; - const [x1,y1]=polar(cx,cy,48,d),[x2,y2]=polar(cx,cy,maj?42:45,d); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':maj?1:.5,opacity:maj?.9:.5});} - for(let d=0;d<360;d+=30){const lbl=d===0?'N':d===90?'E':d===180?'S':d===270?'W':String(d/10); - const [x,y]=polar(cx,cy,35,d); - svgEl(s,'text',{x,y:y+2.6,'text-anchor':'middle','font-size':d%90===0?8:6.4,'font-weight':700, - 'font-family':'var(--mono)',fill:d%90===0?'var(--cream)':'var(--steel)'}).textContent=lbl;} - svgEl(s,'line',{x1:cx,y1:cy-56,x2:cx,y2:cy-48,stroke:'var(--fail)','stroke-width':2}); - hbBug=svgEl(s,'path',{d:`M ${cx-6} ${cy-49} L ${cx-6} ${cy-44} L ${cx-2.5} ${cy-44} L ${cx} ${cy-47} L ${cx+2.5} ${cy-44} L ${cx+6} ${cy-44} L ${cx+6} ${cy-49} Z`, - fill:'var(--gold-hi)',stroke:'#7d5c16','stroke-width':.7}); - hbNeedle=svgEl(s,'g',{}); - svgEl(hbNeedle,'polygon',{points:`${cx-2},${cy+10} ${cx+2},${cy+10} ${cx+1.2},${cy-40} ${cx},${cy-44} ${cx-1.2},${cy-40}`, - fill:'#bfc4d0',stroke:'#4a4e58','stroke-width':.6}); - svgEl(s,'circle',{cx,cy,r:4.5,fill:'#3a3631',stroke:'#060505','stroke-width':1}); - const draw=()=>{hbBug.setAttribute('transform',`rotate(${hbCmd.toFixed(1)},${cx},${cy})`); - hbNeedle.setAttribute('transform',`rotate(${hbAct.toFixed(1)},${cx},${cy})`); - setRd('R44','CMD '+String(Math.round((hbCmd%360+360)%360)).padStart(3,'0')+' · ACT '+String(Math.round((hbAct%360+360)%360)).padStart(3,'0'));}; - dragDelta(s,()=>hbCmd,v=>{hbCmd=(v%360+360)%360;draw();},{min:-100000,max:100000,sens:1.2}); - draw(); - if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{const diff=((hbCmd-hbAct+540)%360)-180; - if(Math.abs(diff)<0.4){if(hbAct!==hbCmd){hbAct=hbCmd;draw();}return;} - hbAct=(hbAct+diff*0.07+360)%360;draw();},80); - else{hbAct=hbCmd;}})(); - -/* 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();})(); +/* R43, R44 extracted to widgets.js (GW.*) */ -/* R48 knife switch (side view): the blade hinges at the left post and lands in the right jaw */ -(function(){const s=$('knifesw'); - const defs=svgEl(s,'defs',{}); - const cu=svgEl(defs,'linearGradient',{id:'ksCu',x1:0,y1:0,x2:0,y2:1}); - svgEl(cu,'stop',{offset:'0','stop-color':'#d09a5c'}); - svgEl(cu,'stop',{offset:'1','stop-color':'#8a5a2a'}); - const HX=32,HY=74; // hinge pivot - svgEl(s,'rect',{x:6,y:56,width:118,height:44,rx:6,fill:'#22262a',stroke:'#0c0d0e','stroke-width':1.5}); - const lamp=svgEl(s,'circle',{cx:116,cy:22,r:4.5,fill:'#1e2a12'}); - svgEl(s,'text',{x:116,y:36,'text-anchor':'middle','font-size':4.8,'letter-spacing':'.08em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='LIVE'; - // hinge post (left) and jaw clip (right), both on the base - svgEl(s,'rect',{x:HX-6,y:HY-6,width:12,height:16,rx:2,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.8}); - svgEl(s,'rect',{x:96,y:HY-9,width:5,height:18,rx:1.5,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.7}); - svgEl(s,'rect',{x:104,y:HY-9,width:5,height:18,rx:1.5,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.7}); - // blade + upright handle, hinged at (HX,HY) - const blade=svgEl(s,'g',{}); - svgEl(blade,'rect',{x:HX,y:HY-3,width:71,height:6,rx:2.5,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.8}); - svgEl(blade,'rect',{x:88,y:HY-26,width:7,height:26,rx:3,fill:'#14110e',stroke:'#000','stroke-width':1}); - svgEl(blade,'circle',{cx:91.5,cy:HY-26,r:5,fill:'#14110e',stroke:'#000','stroke-width':1}); - svgEl(s,'circle',{cx:HX,cy:HY,r:3,fill:'#5c3a18'}); - blade.style.transformBox='view-box';blade.style.transformOrigin=`${HX}px ${HY}px`; - blade.style.transition='transform .28s ease'; - let closed=true; - const draw=()=>{blade.style.transform=closed?'rotate(0deg)':'rotate(-46deg)'; - lamp.setAttribute('fill',closed?'var(--pass)':'#1e2a12'); - lamp.setAttribute('filter',closed?'drop-shadow(0 0 4px rgba(116,147,47,.8))':'none'); - setRd('R48',closed?'CLOSED · live':'OPEN · visibly dead');}; - s.style.cursor='pointer'; - s.addEventListener('click',()=>{closed=!closed;draw();}); - draw();})(); +/* R45-R47, R52 extracted to widgets.js (GW.*) */ -/* R49 decade box: four skirted knobs, one digit each; the value is their sum */ -(function(){const s=$('decbox'); - const MUL=[1000,100,10,1],LBL=['x1000','x100','x10','x1']; - const digs=[3,5,0,0]; - svgEl(s,'rect',{x:2,y:2,width:156,height:96,rx:8,fill:'#17140f',stroke:'#060505','stroke-width':1.5}); - const wins=[],knobs=[]; - const total=()=>digs.reduce((a,d,i)=>a+d*MUL[i],0); - const draw=()=>{wins.forEach((w,i)=>w.textContent=String(digs[i])); - knobs.forEach((k,i)=>k.setAttribute('transform',`rotate(${digs[i]*36},${28+i*35},58)`)); - setRd('R49',total().toLocaleString('en-US')+' Ω');}; - MUL.forEach((m,i)=>{const cx=28+i*35,cy=58; - svgEl(s,'rect',{x:cx-8,y:16,width:16,height:12,rx:2,fill:'#0a0806',stroke:'#2c2820','stroke-width':1}); - wins.push(svgEl(s,'text',{x:cx,y:25.5,'text-anchor':'middle','font-size':8,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'})); - svgEl(s,'circle',{cx,cy,r:15,fill:'#211e1a',stroke:'#0a0908','stroke-width':1.5}); // skirt - for(let t=0;t<10;t++){const [x1,y1]=polar(cx,cy,14,t*36),[x2,y2]=polar(cx,cy,12,t*36); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--steel)','stroke-width':.6,opacity:.7});} - const k=svgEl(s,'g',{}); - svgEl(k,'circle',{cx,cy,r:10,fill:'#2c2824',stroke:'#060505','stroke-width':1}); - svgEl(k,'line',{x1:cx,y1:cy-9,x2:cx,y2:cy-4,stroke:'var(--gold-hi)','stroke-width':1.8}); - knobs.push(k); - svgEl(s,'text',{x:cx,y:88,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.06em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=LBL[i]; - const hit=svgEl(s,'circle',{cx,cy,r:16,fill:'transparent'}); - hit.style.cursor='ns-resize'; - dragDelta(hit,()=>digs[i],v=>{digs[i]=Math.round(Math.max(0,Math.min(9,v)));draw();},{min:0,max:9,sens:9/70});}); - draw();})(); -/* R50 two-hand safety: one palm button arms a 500ms window; the other completes the cycle */ -(function(){const s=$('twohand'); - svgEl(s,'rect',{x:2,y:2,width:156,height:96,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5}); - const runLamp=svgEl(s,'circle',{cx:80,cy:24,r:5.5,fill:'#1e2a12'}); - svgEl(s,'text',{x:80,y:40,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.12em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='CYCLE'; - const mkBtn=(cx,lbl)=>{const g=svgEl(s,'g',{}); - svgEl(g,'ellipse',{cx,cy:70,rx:19,ry:6,fill:'#0d0b09'}); - const cap=svgEl(g,'circle',{cx,cy:62,r:16,fill:'#8f2416',stroke:'#5c150c','stroke-width':1.5}); - svgEl(g,'ellipse',{cx:cx-5,cy:56,rx:6,ry:3.4,fill:'rgba(255,255,255,.22)'}); - svgEl(s,'text',{x:cx,y:92,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=lbl; - g.style.cursor='pointer';return {g,cap};}; - const L=mkBtn(32,'LEFT PALM'),R=mkBtn(128,'RIGHT PALM'); - let armed=null,armT=null,busy=false; - const flash=(cap)=>{cap.setAttribute('fill','#e0523a');setTimeout(()=>cap.setAttribute('fill','#8f2416'),200);}; - const fault=()=>{armed=null;clearTimeout(armT); - setRd('R50','TIE-DOWN FAULT · release and retry');}; - const press=side=>{if(busy)return; - if(armed===null){armed=side;flash(side==='L'?L.cap:R.cap); - setRd('R50',side+' armed · other hand within 0.5s'); - armT=setTimeout(fault,500);return;} - if(armed===side){fault();return;} // same hand twice = tie-down - clearTimeout(armT);armed=null;busy=true; - flash(side==='L'?L.cap:R.cap); - runLamp.setAttribute('fill','var(--pass)'); - runLamp.setAttribute('filter','drop-shadow(0 0 5px rgba(116,147,47,.8))'); - setRd('R50','CYCLE RUNNING'); - setTimeout(()=>{runLamp.setAttribute('fill','#1e2a12');runLamp.removeAttribute('filter'); - busy=false;setRd('R50','ready');},1500);}; - L.g.addEventListener('click',()=>press('L')); - R.g.addEventListener('click',()=>press('R')); - setRd('R50','ready');})(); -/* R51 voice-loop keyset: independent monitor states, exclusive talk, activity flicker */ -(function(){const host=$('vloop'); - const LOOPS=['FD','GNC','ECOM','SURG','A/G','NET1','NET2','PAO']; - const keys=LOOPS.map((l,i)=>{const k=document.createElement('div');k.className='vk'; - k.innerHTML=l+'<span class="bar"></span>';k.dataset.state='0';host.appendChild(k);return k;}); - // a legible default: FD + A/G monitored, GNC talking - keys[0].dataset.state='1';keys[0].classList.add('mon'); - keys[4].dataset.state='1';keys[4].classList.add('mon'); - keys[1].dataset.state='2';keys[1].classList.add('mon','tlk'); - const refresh=()=>{const mon=keys.filter(k=>k.dataset.state!=='0').length; - const tlk=keys.findIndex(k=>k.dataset.state==='2'); - setRd('R51',mon===0?'all loops dropped':mon+' monitored'+(tlk>=0?' · talk '+LOOPS[tlk]:''));}; - keys.forEach((k,i)=>k.addEventListener('click',()=>{ - const st=k.dataset.state; - if(st==='0'){k.dataset.state='1';k.classList.add('mon');} - else if(st==='1'){keys.forEach(o=>{if(o.dataset.state==='2'){o.dataset.state='1';o.classList.remove('tlk');}}); - k.dataset.state='2';k.classList.add('tlk');} - else{k.dataset.state='0';k.classList.remove('mon','tlk','act');} - refresh();})); - if(!matchMedia('(prefers-reduced-motion: reduce)').matches) - setInterval(()=>{const t=performance.now()/1000; - keys.forEach((k,i)=>{if(k.dataset.state==='0'){k.classList.remove('act');return;} - k.classList.toggle('act',Math.sin(t*(1.1+i*0.37)+i*2.1)>0.55);});},300); - refresh();})(); -/* 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);})(); - -/* R53 circular chart recorder: a day per revolution; the pen draws the cycle */ -(function(){const s=$('circhart'),cx=65,cy=65; - svgEl(s,'circle',{cx,cy,r:60,fill:'#17140f',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:54,fill:'#efe9da',stroke:'#8f897b','stroke-width':.8}); - for(const r of [20,31,42]) - svgEl(s,'circle',{cx,cy,r,fill:'none',stroke:'#b8ae98','stroke-width':.5}); - for(let h=0;h<24;h++){const a=h*15; - const [x1,y1]=polar(cx,cy,54,a),[x2,y2]=polar(cx,cy,h%6===0?12:48,a); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'#b8ae98','stroke-width':h%6===0?.6:.35,opacity:.8}); - if(h%6===0){const [tx,ty]=polar(cx,cy,50,a+5); - svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)', - fill:'#6e6552'}).textContent=String(h).padStart(2,'0');}} - const trace=svgEl(s,'path',{d:'',fill:'none',stroke:'#8f2416','stroke-width':1.1,'stroke-linejoin':'round'}); - const pen=svgEl(s,'line',{x1:cx,y1:cy,x2:cx,y2:cy-54,stroke:'#4a463e','stroke-width':1}); - const penDot=svgEl(s,'circle',{cx,cy:cy-30,r:2,fill:'#8f2416'}); - svgEl(s,'circle',{cx,cy,r:4,fill:'#4a463e'}); - const val=h=>52+30*Math.sin(h/24*2*Math.PI-2.1)+12*Math.sin(h/24*6*Math.PI)+4*Math.sin(h*2.7); - const rOf=v=>12+Math.max(0,Math.min(100,v))*0.36; - let pts=[],hour=0; - const draw=()=>{const a=hour*15,r=rOf(val(hour)); - pts.push(polar(cx,cy,r,a)); - if(pts.length>1)trace.setAttribute('d','M '+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ')); - pen.setAttribute('transform',`rotate(${a},${cx},${cy})`); - const [px,py]=polar(cx,cy,r,a); - penDot.setAttribute('cx',px);penDot.setAttribute('cy',py); - setRd('R53',String(Math.floor(hour)).padStart(2,'0')+':'+(hour%1>=0.5?'30':'00')+' · '+Math.round(val(hour))+'%');}; - const reset=()=>{pts=[];hour=0;trace.setAttribute('d','');draw();}; - s.style.cursor='pointer'; - s.addEventListener('click',reset); - if(matchMedia('(prefers-reduced-motion: reduce)').matches){ - for(hour=0;hour<24;hour+=0.25)draw();hour=23.75;} - else{reset();setInterval(()=>{hour+=0.25;if(hour>=24){pts=[];hour=0;}draw();},375);}})(); - -/* R54 vertical tape: the scale scrolls behind a fixed index; drag to drive the value */ -(function(){const s=$('vtape'),W=70,H=130,CX=38,CY=65; - const MIN=5,MAX=35,PPU=5.5; // rpm x100, px per unit - const defs=svgEl(s,'defs',{}); - const clip=svgEl(defs,'clipPath',{id:'vtClip'}); - svgEl(clip,'rect',{x:14,y:8,width:48,height:114,rx:4}); - svgEl(s,'rect',{x:2,y:2,width:66,height:126,rx:7,fill:'#1c1916',stroke:'#060505','stroke-width':1.5}); - svgEl(s,'rect',{x:14,y:8,width:48,height:114,rx:4,fill:'#0d0c0a',stroke:'#2c2820','stroke-width':1}); - const tapeG=svgEl(s,'g',{'clip-path':'url(#vtClip)'}); - const tape=svgEl(tapeG,'g',{}); - for(let u=MIN;u<=MAX;u++){const y=-u*PPU; // higher value further up the tape - const maj=u%5===0; - svgEl(tape,'line',{x1:maj?20:26,y1:y,x2:34,y2:y,stroke:'var(--cream)','stroke-width':maj?1.1:.55,opacity:maj?.95:.6}); - if(maj)svgEl(tape,'text',{x:48,y:y+3,'text-anchor':'middle','font-size':9,'font-weight':700, - 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=String(u);} - // fixed index: side pointer + line across the window at center height - svgEl(s,'polygon',{points:`8,${CY} 15,${CY-4} 15,${CY+4}`,fill:'var(--gold-hi)'}); - svgEl(s,'line',{x1:15,y1:CY,x2:62,y2:CY,stroke:'var(--gold-hi)','stroke-width':1,opacity:.85}); - svgEl(s,'text',{x:CX,y:126,'text-anchor':'middle','font-size':5,'letter-spacing':'.1em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='RPM x100'; - let vt=24; - const draw=()=>{tape.setAttribute('transform',`translate(0,${(CY+vt*PPU).toFixed(1)})`); - setRd('R54','RPM '+Math.round(vt*100));}; - s.style.cursor='ns-resize'; - dragDelta(s,()=>vt,v=>{vt=Math.max(MIN,Math.min(MAX,v));draw();},{min:MIN,max:MAX,sens:(MAX-MIN)/140}); - draw();})(); - -/* R55 twin-needle gauge: mirrored half-scales, one hub, two independent needles */ -(function(){const s=$('twinng'),cx=60,cy=62,R=44; - svgEl(s,'circle',{cx,cy,r:52,fill:'#14110e',stroke:'#060505','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:46,fill:'#1a1714',stroke:'#2c2820','stroke-width':1}); - // mirrored arcs: left = FUEL (0 at bottom-left up to 4 at top), right = OIL - const angL=v=>-170+v/4*160; // 0..4 -> -170..-10 (left side, cw from top) - const angR=v=>170-v/4*160; // 0..4 -> 170..10 (right side) - for(let v=0;v<=4;v++){ - for(const [ang,anchor] of [[angL(v),'end'],[angR(v),'start']]){ - const [x1,y1]=polar(cx,cy,R,ang),[x2,y2]=polar(cx,cy,R-5,ang); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':1,opacity:.9}); - const [tx,ty]=polar(cx,cy,R-11,ang); - svgEl(s,'text',{x:tx,y:ty+2.4,'text-anchor':'middle','font-size':6,'font-family':'var(--mono)', - fill:'var(--cream)'}).textContent=String(v);}} - for(let v=0.5;v<4;v+=0.5){if(v%1===0)continue; - for(const ang of [angL(v),angR(v)]){ - const [x1,y1]=polar(cx,cy,R,ang),[x2,y2]=polar(cx,cy,R-3,ang); - svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':.5,opacity:.55});}} - svgEl(s,'text',{x:cx-20,y:cy+34,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='FUEL'; - svgEl(s,'text',{x:cx+20,y:cy+34,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='OIL'; - svgEl(s,'text',{x:cx,y:cy-26,'text-anchor':'middle','font-size':4.6,'letter-spacing':'.06em', - 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='kg/cm2'; - const needle=color=>{const g=svgEl(s,'g',{}); - svgEl(g,'polygon',{points:`${cx-1.6},${cy+8} ${cx+1.6},${cy+8} ${cx+1},${cy-R+8} ${cx},${cy-R+4} ${cx-1},${cy-R+8}`, - fill:color,stroke:'rgba(0,0,0,.4)','stroke-width':.5});return g;}; - const nF=needle('#e0523a'),nO=needle('#bfc4d0'); - svgEl(s,'circle',{cx,cy,r:5,fill:'#3a3631',stroke:'#060505','stroke-width':1}); - let vF=2.4,vO=3.1; - const draw=()=>{nF.setAttribute('transform',`rotate(${angL(vF).toFixed(1)},${cx},${cy})`); - nO.setAttribute('transform',`rotate(${angR(vO).toFixed(1)},${cx},${cy})`); - setRd('R55','FUEL '+vF.toFixed(1)+' · OIL '+vO.toFixed(1));}; - // each half is its own drag surface - const half=(x,get,set)=>{const hit=svgEl(s,'rect',{x,y:10,width:60,height:104,fill:'transparent'}); - hit.style.cursor='ns-resize'; - dragDelta(hit,get,v=>{set(Math.max(0,Math.min(4,v)));draw();},{min:0,max:4,sens:4/110});}; - half(0,()=>vF,v=>vF=v); - half(60,()=>vO,v=>vO=v); - draw();})(); - -/* R56 comfort meter: temp and humidity needles cross over printed zone verdicts */ -(function(){const s=$('comfort'),cx=65,cy=60; - const defs=svgEl(s,'defs',{}); - const br=svgEl(defs,'linearGradient',{id:'cmBrass',x1:0,y1:0,x2:0,y2:1}); - svgEl(br,'stop',{offset:'0','stop-color':'#d8b25c'}); - svgEl(br,'stop',{offset:'.5','stop-color':'#a07c30'}); - svgEl(br,'stop',{offset:'1','stop-color':'#6e5218'}); - svgEl(s,'circle',{cx,cy,r:56,fill:'url(#cmBrass)',stroke:'#4a3610','stroke-width':2}); - svgEl(s,'circle',{cx,cy,r:53,fill:'none',stroke:'#7a5c1e','stroke-width':2.5,'stroke-dasharray':'2 1.4'}); - svgEl(s,'circle',{cx,cy,r:49,fill:'#efe9da',stroke:'#8f897b','stroke-width':1}); - const PT=[38,96],PH=[92,96]; // temp pivot (BL), humidity pivot (BR) - const angT=t=>105-(t-40)/60*70; // 40..100F -> bearing 105..35 from PT - const angH=h=>-105+h/100*70; // 0..100% -> bearing -105..-35 from PH - // scale ticks along each sweep, near the rim - for(let t=40;t<=100;t+=10){const a=angT(t); - const [x1,y1]=polar(PT[0],PT[1],86,a),[x2,y2]=polar(PT[0],PT[1],82,a); - if(Math.hypot(x1-cx,y1-cy)<48){svgEl(s,'line',{x1,y1,x2,y2,stroke:'#3c382f','stroke-width':t%20===0?1:.5}); - if(t%20===0){const [tx,ty]=polar(PT[0],PT[1],78,a); - svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)', - fill:'#3c382f'}).textContent=String(t);}}} - for(let h=0;h<=100;h+=10){const a=angH(h); - const [x1,y1]=polar(PH[0],PH[1],86,a),[x2,y2]=polar(PH[0],PH[1],82,a); - if(Math.hypot(x1-cx,y1-cy)<48){svgEl(s,'line',{x1,y1,x2,y2,stroke:'#3c382f','stroke-width':h%20===0?1:.5}); - if(h%20===0){const [tx,ty]=polar(PH[0],PH[1],78,a); - svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)', - fill:'#3c382f'}).textContent=String(h);}}} - // printed zone verdicts; the active one goes red - const zones={}; - const zone=(key,txt,x,y,rot,size)=>{zones[key]=svgEl(s,'text',{x,y,'text-anchor':'middle', - 'font-size':size||5.2,'font-weight':700,'letter-spacing':'.06em','font-family':'var(--mono)', - fill:'#8f897b',transform:`rotate(${rot},${x},${y})`});zones[key].textContent=txt;}; - zone('humid','TOO HUMID',42,34,-32); - zone('warm','TOO WARM',89,34,32); - zone('right','JUST RIGHT',65,58,0,6.2); - zone('dry','TOO DRY',38,74,28); - zone('cold','TOO COLD',93,74,-28); - svgEl(s,'text',{x:cx,y:92,'text-anchor':'middle','font-size':5.6,'letter-spacing':'.18em', - 'font-family':'var(--mono)',fill:'#3c382f'}).textContent='WEATHER STATION'; - // needles: temp from bottom-left, humidity from bottom-right — clipped to the face - const nclip=svgEl(defs,'clipPath',{id:'cmClip'}); - svgEl(nclip,'circle',{cx,cy,r:49}); - const ng=svgEl(s,'g',{'clip-path':'url(#cmClip)'}); - const needle=()=>{const g=svgEl(ng,'g',{}); - svgEl(g,'line',{x1:0,y1:0,x2:0,y2:-84,stroke:'#c23a28','stroke-width':1.6,'stroke-linecap':'round'}); - svgEl(g,'circle',{cx:0,cy:0,r:2.6,fill:'#8f2416'});return g;}; - const nT=needle(),nH=needle(); - nT.setAttribute('transform',`translate(${PT[0]},${PT[1]})`); - nH.setAttribute('transform',`translate(${PH[0]},${PH[1]})`); - let vT=72,vH=45; - const verdict=()=>vT>78?'warm':vT<62?'cold':vH>60?'humid':vH<30?'dry':'right'; - const draw=()=>{ - nT.setAttribute('transform',`translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`); - nH.setAttribute('transform',`translate(${PH[0]},${PH[1]}) rotate(${angH(vH).toFixed(1)})`); - const v=verdict(); - for(const k of Object.keys(zones))zones[k].setAttribute('fill',k===v?'#c23a28':'#8f897b'); - setRd('R56',Math.round(vT)+'F · '+Math.round(vH)+'% RH · '+zones[v].textContent);}; - const half=(x,get,set,min,max)=>{const hit=svgEl(s,'rect',{x,y:8,width:65,height:106,fill:'transparent'}); - hit.style.cursor='ns-resize'; - dragDelta(hit,get,v=>{set(Math.max(min,Math.min(max,v)));draw();},{min,max,sens:(max-min)/110});}; - half(0,()=>vT,v=>vT=v,40,100); - half(65,()=>vH,v=>vH=v,0,100); - draw();})(); +/* R53-R56 extracted to widgets.js (GW.*) */ /* ---- screen-family chips: first round, the clearly-screen widgets ---- Defaults match each widget's shipped color (var fallbacks), so no family is applied until a chip is clicked. The nixie stays chipless on purpose: neon is only ever orange, so alternates would be historically false. */ -screenChips('R10',$('dmx'),['amber','green','red','blue','vfd'],'amber'); -screenChips('R17',$('rcrt'),['green','amber','red','blue','vfd'],'green'); -screenChips('R19',$('wregion'),['white','green','amber','blue','vfd'],'white'); -screenChips('R31',$('radar'),['amber','green','red','blue','vfd'],'amber'); -screenChips('N11',$('scopep').closest('.scope'),['green','amber','red','blue','vfd'],'green'); +screenChips('R10',$('card-R10').gw.el,['amber','green','red','blue','vfd'],'amber'); +screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'green'); +screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white'); +screenChips('R31',$('card-R31').gw.el,['amber','green','red','blue','vfd'],'amber'); +screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green'); +BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no); + if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');}); + +/* cross-reference links: card IDs mentioned in notes and spec sheets jump to that card */ +(function(){const ids=new Set([...document.querySelectorAll('.card .no')].map(n=>n.textContent)); + document.querySelectorAll('.wnote,.igrid .iv').forEach(el=>{ + if(el.querySelector('a[href^="reference/"]'))return; // leave the photo-link rows alone + el.innerHTML=el.innerHTML.replace(/\b(R\d{2}|N\d{2})\b/g, + m=>ids.has(m)?`<a class="xref" href="#card-${m}">${m}</a>`:m);});})(); + +/* style chips: shared demo rig for builders with constructor style opts — + reads a builder's STYLES table and drives setStyle on the live card instance */ +function styleChips(no,STYLES,AXES){ + const h=$('card-'+no)?.gw; if(!h)return; + const cardEl=h.el.closest('.card'); + const row=document.createElement('div'); row.className='famchips'; + for(const [label,axis,def] of AXES){ + const g=document.createElement('span'); g.className='fgroup'; + const lab=document.createElement('span'); lab.className='lab'; lab.textContent=label; g.appendChild(lab); + const chips=[]; + for(const [name,o] of Object.entries(STYLES[axis])){ + const b=document.createElement('span'); b.className='fc'+(name===def?' on':''); + b.style.background=o.dot; b.title=name; chips.push(b); + b.addEventListener('click',()=>{h.setStyle(axis,name); + chips.forEach(x=>x.classList.toggle('on',x===b));}); + g.appendChild(b);} + row.appendChild(g);} + cardEl.querySelector('.opts').appendChild(row); +} +styleChips('01',GW.slideToggle.STYLES,[['on','on','amber'],['off','off','dark'],['off text','offText','white'],['thumb','thumb','light']]); +styleChips('R05',GW.filterBank.STYLES,[['panel','panel','silver'],['shape','caps','block'],['color','capColor','black']]); +styleChips('06',GW.segmented.STYLES,[['accent','accent','amber']]); +styleChips('25',GW.slideRule.STYLES,[['face','skin','warm']]); + +/* final tally pass: setV fires per card during build, but each card is still + detached at that moment, so the running counts lag by one — recount now */ +updateVTally(); </script> </body> </html> |
