mcltspice/docs/src/styles/oscilloscope.css
Ryan Malloy be88ea53b7 Add signal source selector to oscilloscope hero
Source knob cycles through 6 tracks: the original CC-licensed
Spirals intro plus 5 N-Spheres tracks (Function, Intersect,
Attractor, Flux, Core) by Fenderson & Hansi3D.

- 48kHz FLAC conversions served via git LFS (~189MB total)
- Rotary knob with CSS custom property composable transforms
- Dynamic attribution (CC link for Spirals, album credit for N-Spheres)
- Signal selection persisted in localStorage
- Loading state overlay while buffering larger tracks
- Skin-aware labels (465: "Source", 545A: "Input")
- Keyboard accessible (Enter/Space to cycle)
2026-02-13 06:16:47 -07:00

518 lines
14 KiB
CSS

/* Oscilloscope display -- Tektronix 465 inspired
* Tan/champagne panel, recessed CRT, teal phosphor
* Designed for mcltspice docs hero section */
/* ── Outer chassis ───────────────────────────────────────── */
.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 10px 30px rgba(0, 0, 0, 0.55),
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.15),
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
max-width: 340px;
width: 100%;
margin: 0 auto;
overflow: hidden;
}
/* ── Top brand bar ───────────────────────────────────────── */
.scope-brand {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 8px 14px 6px;
border-bottom: 1px solid var(--scope-section-line);
}
.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.75;
}
.scope-brand-model {
font-family: var(--sl-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.55;
}
/* ── CRT bay (recessed dark area) ────────────────────────── */
.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-screen {
position: relative;
background: var(--scope-crt-bg);
border-radius: 3px;
overflow: hidden;
aspect-ratio: 1;
box-shadow:
0 0 15px var(--scope-teal-glow),
inset 0 1px 4px rgba(0, 0, 0, 0.4);
}
/* ── Canvas ──────────────────────────────────────────────── */
.scope-canvas {
display: block;
width: 100%;
height: 100%;
}
/* ── Graticule overlay (8x10 grid, like the 465) ─────────── */
.scope-graticule {
position: absolute;
inset: 0;
pointer-events: none;
/* 10 horizontal, 8 vertical — classic Tek grid */
background-image:
repeating-linear-gradient(
90deg,
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)
),
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-graticule::before,
.scope-graticule::after {
content: '';
position: absolute;
}
.scope-graticule::before {
/* horizontal center tick */
top: 50%;
left: calc(50% - 8px);
width: 16px;
height: 1px;
background: rgba(45, 212, 191, 0.18);
}
.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-scanlines {
position: absolute;
inset: 0;
pointer-events: none;
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;
}
/* ── Control panel area ──────────────────────────────────── */
.scope-panel {
padding: 8px 12px 6px;
border-top: 1px solid var(--scope-section-line);
margin-top: 10px;
}
.scope-controls-row {
display: flex;
align-items: flex-start;
gap: 2px;
}
/* ── Control section (labeled group) ─────────────────────── */
.scope-section {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 4px 2px;
position: relative;
}
/* Section divider lines */
.scope-section + .scope-section::before {
content: '';
position: absolute;
left: -1px;
top: 0;
bottom: 0;
width: 1px;
background: var(--scope-section-line);
}
.scope-section-label {
font-family: var(--sl-font-mono, ui-monospace, monospace);
font-size: 0.45rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--scope-label);
opacity: 0.6;
line-height: 1;
}
/* ── Rotary knob ─────────────────────────────────────────── */
.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;
}
/* Knob indicator line */
.scope-knob::after {
content: '';
position: absolute;
top: 3px;
left: 50%;
width: 1.5px;
height: 7px;
background: #d4d0c8;
border-radius: 1px;
transform: translateX(-50%);
}
.scope-knob-label {
font-family: var(--sl-font-mono, ui-monospace, monospace);
font-size: 0.4rem;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--scope-label);
opacity: 0.5;
line-height: 1;
}
/* ── Source knob (interactive) ───────────────────────────── */
/* Uses --knob-rotation custom prop so JS rotation composes with CSS scale */
.scope-source-knob {
cursor: pointer;
transition: transform 0.15s;
transform: rotate(var(--knob-rotation, 0deg));
}
.scope-source-knob:hover {
transform: rotate(var(--knob-rotation, 0deg)) scale(1.08);
}
.scope-source-knob:active {
transform: rotate(var(--knob-rotation, 0deg)) scale(0.95);
}
.scope-source-knob:focus-visible {
outline: 2px solid var(--scope-teal);
outline-offset: 2px;
}
/* ── Signal name label ──────────────────────────────────── */
.scope-source-name {
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
/* ── Loading state overlay ──────────────────────────────── */
.scope-screen[data-loading="true"]::after {
content: '';
position: absolute;
inset: 0;
background: rgba(10, 10, 10, 0.7);
z-index: 5;
animation: scope-loading-pulse 1.2s ease-in-out infinite;
}
@keyframes scope-loading-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 0.9; }
}
/* ── Power toggle ────────────────────────────────────────── */
.scope-toggle {
appearance: none;
background: var(--scope-knob);
border: 2px solid var(--scope-knob-ring);
border-radius: 6px;
color: #8a8880;
font-family: var(--sl-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);
}
.scope-toggle:hover {
color: #c4c0b8;
}
.scope-toggle[data-active="true"] {
color: var(--scope-teal);
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.3),
0 0 6px var(--scope-teal-glow);
}
.scope-toggle:focus-visible {
outline: 2px solid var(--scope-teal);
outline-offset: 2px;
}
/* ── Power LED ───────────────────────────────────────────── */
.scope-led {
width: 5px;
height: 5px;
border-radius: 50%;
background: #3a3632;
border: 1px solid rgba(0, 0, 0, 0.3);
transition: background 0.3s, box-shadow 0.3s;
}
.scope-led[data-on="true"] {
background: var(--scope-teal);
box-shadow: 0 0 6px var(--scope-teal-glow);
}
/* ── Attribution bar ─────────────────────────────────────── */
.scope-attribution-bar {
padding: 5px 14px 7px;
border-top: 1px solid var(--scope-section-line);
}
.scope-attribution {
font-family: var(--sl-font-mono, ui-monospace, monospace);
font-size: 0.45rem;
color: var(--scope-panel-dark);
line-height: 1.4;
text-align: center;
}
.scope-attribution a {
color: var(--scope-label);
text-decoration: none;
transition: color 0.15s;
}
.scope-attribution a:hover {
color: #1a1610;
}
/* ── Idle state ──────────────────────────────────────────── */
.scope-screen[data-idle="true"] .scope-canvas {
opacity: 0.6;
}
/* ── Reduced motion ──────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.scope-scanlines {
display: none;
}
}
/* ── Responsive ──────────────────────────────────────────── */
@media (max-width: 50rem) {
.scope-frame {
max-width: 280px;
}
.scope-knob {
width: 24px;
height: 24px;
}
.scope-knob::after {
height: 5px;
}
}
/* ── Skin picker (model name → clickable) ──────────────── */
.scope-brand-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 1px;
}
button.scope-brand-model {
appearance: none;
background: none;
border: none;
padding: 0;
cursor: pointer;
line-height: inherit;
transition: opacity 0.15s;
}
button.scope-brand-model:hover {
opacity: 0.85;
}
button.scope-brand-model:focus-visible {
outline: 2px solid var(--scope-teal);
outline-offset: 2px;
border-radius: 2px;
}
.scope-brand-sub {
font-family: var(--sl-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;
}
/* ── Tektronix Type 545A skin (1959) ─────────────────────── */
/* Vacuum-tube era: blue-green hammertone, cream silk-screen,
Bakelite knobs, deeper CRT recess, ventilation holes */
.scope-skin-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 — larger dimpled dots, irregular placement */
.scope-skin-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 — tube-era instruments had heavier bezels */
.scope-skin-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-545a .scope-knob {
width: 32px;
height: 32px;
background: radial-gradient(circle at 40% 35%, #2e2218, var(--scope-knob));
}
.scope-skin-545a .scope-knob::after {
height: 8px;
background: #c8b890;
}
/* Cream-on-green attribution */
.scope-skin-545a .scope-attribution {
color: var(--scope-label);
opacity: 0.6;
}
.scope-skin-545a .scope-attribution a {
color: var(--scope-label);
}
.scope-skin-545a .scope-attribution a:hover {
color: #fff;
}
/* ── Ventilation holes (545A only) ──────────────────────── */
.scope-vent-holes {
display: none;
justify-content: center;
gap: 8px;
padding: 6px 14px 8px;
}
.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);
}
.scope-skin-545a .scope-vent-holes {
display: flex;
}
/* ── 545A responsive overrides ──────────────────────────── */
@media (max-width: 50rem) {
.scope-skin-545a .scope-knob {
width: 26px;
height: 26px;
}
.scope-skin-545a .scope-knob::after {
height: 6px;
}
.scope-vent-hole {
width: 5px;
height: 5px;
}
}