Port 465/545A visual chrome from mcltspice docs into SpiceBook as a toggleable waveform viewer skin. uPlot chart renders inside a CRT screen with teal phosphor traces, graticule overlay, and scanlines. Functional knobs control trace visibility (setSeries), X/Y zoom (setScale) in standard 1-2-5 div steps. Digital readout bar shows current trace, div values, and analysis type. Two switchable hardware skins — 465 tan and 545A hammertone — persisted in localStorage. New files: ScopeWaveformViewer.tsx, scope-skin.css Modified: WaveformViewer.tsx (toggle), waveform-utils.ts (scope palette, 1-2-5 sequence, stack-safe min/max), globals.css (scope vars)
485 lines
14 KiB
CSS
485 lines
14 KiB
CSS
/* Tektronix oscilloscope skin for SpiceBook waveform viewer
|
||
* Ported from mcltspice docs OscilloscopeDisplay.astro + oscilloscope.css
|
||
* Adapted: landscape CRT (5:3), 10×8 graticule, no audio/easter-egg styles,
|
||
* SpiceBook font vars, .scope-skin-active scoping, uPlot overrides */
|
||
|
||
/* ── Outer chassis ───────────────────────────────────────── */
|
||
.scope-skin-active .scope-frame {
|
||
--scope-teal: #2dd4bf;
|
||
--scope-teal-dim: rgba(45, 212, 191, 0.12);
|
||
--scope-teal-glow: rgba(45, 212, 191, 0.18);
|
||
--scope-panel: #b5a48a;
|
||
--scope-panel-light: #c7b89e;
|
||
--scope-panel-dark: #9e8f78;
|
||
--scope-crt-bg: #0a0a0a;
|
||
--scope-label: #3b3428;
|
||
--scope-knob: #2a2a2d;
|
||
--scope-knob-ring: #1e1e20;
|
||
--scope-section-line: rgba(59, 52, 40, 0.25);
|
||
|
||
position: relative;
|
||
background:
|
||
/* subtle metallic grain */
|
||
url("data:image/svg+xml,%3Csvg width='4' height='4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1' x='0' y='0' fill='rgba(0,0,0,0.03)'/%3E%3Crect width='1' height='1' x='2' y='2' fill='rgba(255,255,255,0.02)'/%3E%3C/svg%3E"),
|
||
linear-gradient(175deg, var(--scope-panel-light), var(--scope-panel), var(--scope-panel-dark));
|
||
border-radius: 6px;
|
||
padding: 0;
|
||
box-shadow:
|
||
0 4px 12px rgba(0, 0, 0, 0.35),
|
||
0 1px 3px rgba(0, 0, 0, 0.2),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ── Top brand bar ───────────────────────────────────────── */
|
||
.scope-skin-active .scope-brand {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
padding: 8px 14px 6px;
|
||
border-bottom: 1px solid var(--scope-section-line);
|
||
}
|
||
|
||
.scope-skin-active .scope-brand-name {
|
||
font-family: 'Georgia', 'Times New Roman', serif;
|
||
font-size: 0.65rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
color: var(--scope-label);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.scope-skin-active .scope-brand-model {
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 0.55rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--scope-label);
|
||
opacity: 0.78;
|
||
}
|
||
|
||
/* ── Skin picker (model name → clickable) ──────────────── */
|
||
.scope-skin-active .scope-brand-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 1px;
|
||
}
|
||
|
||
.scope-skin-active button.scope-brand-model {
|
||
appearance: none;
|
||
background: none;
|
||
border: none;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
line-height: inherit;
|
||
transition: opacity 0.15s;
|
||
}
|
||
|
||
.scope-skin-active button.scope-brand-model:hover {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.scope-skin-active button.scope-brand-model:focus-visible {
|
||
outline: 2px solid var(--scope-teal);
|
||
outline-offset: 2px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.scope-skin-active .scope-brand-sub {
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 0.4rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--scope-label);
|
||
opacity: 0.4;
|
||
}
|
||
|
||
/* ── CRT bay (recessed dark area) ────────────────────────── */
|
||
.scope-skin-active .scope-crt-bay {
|
||
margin: 10px 12px 0;
|
||
background: #1a1816;
|
||
border-radius: 4px;
|
||
padding: 6px;
|
||
box-shadow:
|
||
inset 0 2px 6px rgba(0, 0, 0, 0.5),
|
||
inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
/* ── CRT screen ──────────────────────────────────────────── */
|
||
.scope-skin-active .scope-screen {
|
||
position: relative;
|
||
background: var(--scope-crt-bg);
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
aspect-ratio: 5 / 3;
|
||
box-shadow:
|
||
0 0 15px var(--scope-teal-glow),
|
||
inset 0 1px 4px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
/* uPlot chart fills the CRT screen */
|
||
.scope-skin-active .scope-screen .uplot {
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
|
||
/* ── Graticule overlay (10 horizontal × 8 vertical) ──────── */
|
||
.scope-skin-active .scope-graticule {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
/* 10 vertical divisions (10% each), 8 horizontal divisions (12.5% each) */
|
||
background-image:
|
||
repeating-linear-gradient(
|
||
90deg,
|
||
transparent,
|
||
transparent calc(10% - 0.5px),
|
||
rgba(45, 212, 191, 0.07) calc(10% - 0.5px),
|
||
rgba(45, 212, 191, 0.07) calc(10% + 0.5px),
|
||
transparent calc(10% + 0.5px)
|
||
),
|
||
repeating-linear-gradient(
|
||
0deg,
|
||
transparent,
|
||
transparent calc(12.5% - 0.5px),
|
||
rgba(45, 212, 191, 0.07) calc(12.5% - 0.5px),
|
||
rgba(45, 212, 191, 0.07) calc(12.5% + 0.5px),
|
||
transparent calc(12.5% + 0.5px)
|
||
);
|
||
}
|
||
|
||
/* Center crosshair tick marks */
|
||
.scope-skin-active .scope-graticule::before,
|
||
.scope-skin-active .scope-graticule::after {
|
||
content: '';
|
||
position: absolute;
|
||
}
|
||
|
||
.scope-skin-active .scope-graticule::before {
|
||
/* horizontal center tick */
|
||
top: 50%;
|
||
left: calc(50% - 8px);
|
||
width: 16px;
|
||
height: 1px;
|
||
background: rgba(45, 212, 191, 0.18);
|
||
}
|
||
|
||
.scope-skin-active .scope-graticule::after {
|
||
/* vertical center tick */
|
||
left: 50%;
|
||
top: calc(50% - 8px);
|
||
width: 1px;
|
||
height: 16px;
|
||
background: rgba(45, 212, 191, 0.18);
|
||
}
|
||
|
||
/* ── Scanline overlay ────────────────────────────────────── */
|
||
.scope-skin-active .scope-scanlines {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 3;
|
||
background: repeating-linear-gradient(
|
||
0deg,
|
||
transparent,
|
||
transparent 2px,
|
||
rgba(0, 0, 0, 0.04) 2px,
|
||
rgba(0, 0, 0, 0.04) 4px
|
||
);
|
||
mix-blend-mode: multiply;
|
||
}
|
||
|
||
/* ── Digital readout bar ─────────────────────────────────── */
|
||
.scope-skin-active .scope-readout {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
padding: 5px 12px;
|
||
background: #0a0a0a;
|
||
margin: 0 12px;
|
||
border-radius: 0 0 3px 3px;
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 0.65rem;
|
||
letter-spacing: 0.04em;
|
||
color: var(--scope-teal);
|
||
text-shadow: 0 0 4px var(--scope-teal-glow);
|
||
}
|
||
|
||
.scope-skin-active .scope-readout-item {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.scope-skin-active .scope-readout-divider {
|
||
color: rgba(45, 212, 191, 0.25);
|
||
}
|
||
|
||
/* ── Control panel area ──────────────────────────────────── */
|
||
.scope-skin-active .scope-panel {
|
||
padding: 8px 12px 6px;
|
||
border-top: 1px solid var(--scope-section-line);
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.scope-skin-active .scope-controls-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 2px;
|
||
}
|
||
|
||
/* ── Control section (labeled group) ─────────────────────── */
|
||
.scope-skin-active .scope-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 2px;
|
||
position: relative;
|
||
}
|
||
|
||
/* Section divider lines */
|
||
.scope-skin-active .scope-section + .scope-section::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: -1px;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 1px;
|
||
background: var(--scope-section-line);
|
||
}
|
||
|
||
.scope-skin-active .scope-section-label {
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 0.5rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--scope-label);
|
||
opacity: 0.88;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* ── Rotary knob ─────────────────────────────────────────── */
|
||
/* Reset native button styling so knobs render as pure circles */
|
||
.scope-skin-active button.scope-knob {
|
||
appearance: none;
|
||
padding: 0;
|
||
font: inherit;
|
||
color: inherit;
|
||
line-height: 1;
|
||
}
|
||
|
||
.scope-skin-active .scope-knob {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at 40% 35%, #3a3a3e, var(--scope-knob));
|
||
border: 2px solid var(--scope-knob-ring);
|
||
box-shadow:
|
||
0 2px 4px rgba(0, 0, 0, 0.35),
|
||
inset 0 1px 1px rgba(255, 255, 255, 0.06);
|
||
position: relative;
|
||
cursor: pointer;
|
||
transition: transform 0.15s;
|
||
transform: rotate(var(--knob-rotation, 0deg));
|
||
}
|
||
|
||
/* Knob indicator line */
|
||
.scope-skin-active .scope-knob::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 3px;
|
||
left: 50%;
|
||
width: 1.5px;
|
||
height: 7px;
|
||
background: #d4d0c8;
|
||
border-radius: 1px;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
.scope-skin-active .scope-knob:hover {
|
||
transform: rotate(var(--knob-rotation, 0deg)) scale(1.08);
|
||
}
|
||
|
||
.scope-skin-active .scope-knob:active {
|
||
transform: rotate(var(--knob-rotation, 0deg)) scale(0.95);
|
||
}
|
||
|
||
.scope-skin-active .scope-knob:focus-visible {
|
||
outline: 2px solid var(--scope-teal);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.scope-skin-active .scope-knob-label {
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 0.45rem;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
color: var(--scope-label);
|
||
opacity: 0.78;
|
||
line-height: 1;
|
||
max-width: 60px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ── Mode badge (TRAN / AC) ──────────────────────────────── */
|
||
.scope-skin-active .scope-mode-badge {
|
||
appearance: none;
|
||
background: var(--scope-knob);
|
||
border: 2px solid var(--scope-knob-ring);
|
||
border-radius: 6px;
|
||
color: var(--scope-teal);
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 0.55rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.06em;
|
||
padding: 5px 8px;
|
||
cursor: pointer;
|
||
transition: color 0.2s, box-shadow 0.2s;
|
||
text-transform: uppercase;
|
||
line-height: 1;
|
||
box-shadow:
|
||
0 2px 4px rgba(0, 0, 0, 0.3),
|
||
0 0 6px var(--scope-teal-glow);
|
||
}
|
||
|
||
.scope-skin-active .scope-mode-badge:hover {
|
||
color: #5eead4;
|
||
}
|
||
|
||
.scope-skin-active .scope-mode-badge:focus-visible {
|
||
outline: 2px solid var(--scope-teal);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* ── Power LED ───────────────────────────────────────────── */
|
||
.scope-skin-active .scope-led {
|
||
width: 5px;
|
||
height: 5px;
|
||
border-radius: 50%;
|
||
background: var(--scope-teal);
|
||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||
box-shadow: 0 0 6px var(--scope-teal-glow);
|
||
}
|
||
|
||
/* ── Ventilation holes (545A only) ──────────────────────── */
|
||
.scope-skin-active .scope-vent-holes {
|
||
display: none;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 6px 14px 8px;
|
||
}
|
||
|
||
.scope-skin-active .scope-vent-hole {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.15) 100%);
|
||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* ── Tektronix Type 545A skin (1959) ─────────────────────── */
|
||
.scope-skin-active .scope-frame.scope-hw-545a {
|
||
--scope-panel: #4a6e64;
|
||
--scope-panel-light: #5a7e72;
|
||
--scope-panel-dark: #3a5a50;
|
||
--scope-label: #e0d8c8;
|
||
--scope-knob: #1c1814;
|
||
--scope-knob-ring: #141210;
|
||
--scope-section-line: rgba(224, 216, 200, 0.15);
|
||
}
|
||
|
||
/* Hammertone texture */
|
||
.scope-skin-active .scope-frame.scope-hw-545a {
|
||
background:
|
||
url("data:image/svg+xml,%3Csvg width='8' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='2' cy='2' r='1.2' fill='rgba(0,0,0,0.06)'/%3E%3Ccircle cx='6' cy='5' r='0.8' fill='rgba(255,255,255,0.04)'/%3E%3Ccircle cx='4' cy='7' r='1' fill='rgba(0,0,0,0.04)'/%3E%3Ccircle cx='7' cy='1' r='0.6' fill='rgba(255,255,255,0.03)'/%3E%3C/svg%3E"),
|
||
linear-gradient(175deg, var(--scope-panel-light), var(--scope-panel), var(--scope-panel-dark));
|
||
}
|
||
|
||
/* Deeper CRT bay recess */
|
||
.scope-skin-active .scope-frame.scope-hw-545a .scope-crt-bay {
|
||
box-shadow:
|
||
inset 0 3px 8px rgba(0, 0, 0, 0.6),
|
||
inset 0 0 0 1px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* Larger Bakelite-era knobs */
|
||
.scope-skin-active .scope-frame.scope-hw-545a .scope-knob {
|
||
width: 32px;
|
||
height: 32px;
|
||
background: radial-gradient(circle at 40% 35%, #2e2218, var(--scope-knob));
|
||
}
|
||
|
||
.scope-skin-active .scope-frame.scope-hw-545a .scope-knob::after {
|
||
height: 8px;
|
||
background: #c8b890;
|
||
}
|
||
|
||
/* 545A shows vent holes */
|
||
.scope-skin-active .scope-frame.scope-hw-545a .scope-vent-holes {
|
||
display: flex;
|
||
}
|
||
|
||
/* ── uPlot CRT overrides ─────────────────────────────────── */
|
||
.scope-skin-active .scope-screen .uplot canvas {
|
||
filter: drop-shadow(0 0 3px var(--scope-teal-glow));
|
||
}
|
||
|
||
.scope-skin-active .scope-screen .u-legend {
|
||
display: none; /* legend info is in the digital readout bar */
|
||
}
|
||
|
||
/* Hide uPlot axes — the graticule replaces them */
|
||
.scope-skin-active .scope-screen .u-axis {
|
||
display: none;
|
||
}
|
||
|
||
/* ── Reduced motion ──────────────────────────────────────── */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.scope-skin-active .scope-scanlines {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* ── Responsive ──────────────────────────────────────────── */
|
||
@media (max-width: 50rem) {
|
||
.scope-skin-active .scope-knob {
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
.scope-skin-active .scope-knob::after {
|
||
height: 5px;
|
||
}
|
||
|
||
.scope-skin-active .scope-readout {
|
||
font-size: 0.55rem;
|
||
gap: 8px;
|
||
}
|
||
|
||
.scope-skin-active .scope-frame.scope-hw-545a .scope-knob {
|
||
width: 26px;
|
||
height: 26px;
|
||
}
|
||
|
||
.scope-skin-active .scope-frame.scope-hw-545a .scope-knob::after {
|
||
height: 6px;
|
||
}
|
||
|
||
.scope-skin-active .scope-vent-hole {
|
||
width: 5px;
|
||
height: 5px;
|
||
}
|
||
}
|