spicebook/frontend/src/styles/scope-skin.css
Ryan Malloy 22d3e903db Add Tektronix oscilloscope skin for waveform viewer
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)
2026-02-15 18:05:59 -07:00

485 lines
14 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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;
}
}