diff --git a/docs/src/components/OscilloscopeDisplay.astro b/docs/src/components/OscilloscopeDisplay.astro
index 0ad105f..f019abe 100644
--- a/docs/src/components/OscilloscopeDisplay.astro
+++ b/docs/src/components/OscilloscopeDisplay.astro
@@ -58,6 +58,14 @@
Spirals
+
+
+
Vertical
@@ -126,8 +134,8 @@
// ── Skin configuration ────────────────────────────────────
const SKINS: Record = {
- '465': { brand: 'Tektronix', model: 'mcltspice', sub: '', sections: ['Source', 'Vertical', 'Horizontal', 'Trigger'] },
- '545a': { brand: 'Tektronix', model: 'Type 545A', sub: 'Oscilloscope', sections: ['Input', 'Vert Ampl', 'Sweep', 'Trigger'] },
+ '465': { brand: 'Tektronix', model: 'mcltspice', sub: '', sections: ['Source', 'Level', 'Vertical', 'Horizontal', 'Trigger'] },
+ '545a': { brand: 'Tektronix', model: 'Type 545A', sub: 'Oscilloscope', sections: ['Input', 'Atten', 'Vert Ampl', 'Sweep', 'Trigger'] },
};
const SKIN_ORDER = Object.keys(SKINS);
@@ -441,6 +449,45 @@
ctx.fill();
}
+ // ── Volume / attenuation control ─────────────────────────
+ function initVolume() {
+ const knob = document.getElementById('scope-volume-knob');
+ const label = document.getElementById('scope-volume-label');
+ const audio = document.getElementById('scope-audio') as HTMLAudioElement | null;
+ if (!knob || !label || !audio) return;
+ if (knob.dataset.volumeInit) return;
+ knob.dataset.volumeInit = 'true';
+
+ const LEVELS = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40];
+
+ let levelIndex = parseInt(localStorage.getItem('scope-volume') || '3', 10);
+ if (levelIndex < 0 || levelIndex >= LEVELS.length) levelIndex = 3;
+
+ function applyLevel(idx: number) {
+ levelIndex = idx;
+ const vol = LEVELS[idx];
+ audio!.volume = vol;
+ label!.textContent = `${Math.round(vol * 100)}%`;
+ const rotation = (idx / LEVELS.length) * 270 - 135;
+ knob!.style.setProperty('--knob-rotation', `${rotation}deg`);
+ localStorage.setItem('scope-volume', String(idx));
+ }
+
+ knob.addEventListener('click', () => {
+ applyLevel((levelIndex + 1) % LEVELS.length);
+ });
+
+ knob.addEventListener('keydown', (e: Event) => {
+ const ke = e as KeyboardEvent;
+ if (ke.key === 'Enter' || ke.key === ' ') {
+ ke.preventDefault();
+ applyLevel((levelIndex + 1) % LEVELS.length);
+ }
+ });
+
+ applyLevel(levelIndex);
+ }
+
// ── Outer Limits easter egg ──────────────────────────────
function initOuterLimits() {
const knobs = document.querySelectorAll('.scope-easter-knob');
@@ -493,7 +540,8 @@
utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 0.85;
utterance.pitch = 0.7;
- utterance.volume = 0.8;
+ const audio = document.getElementById('scope-audio') as HTMLAudioElement | null;
+ utterance.volume = Math.min(0.8, (audio?.volume ?? 0.2) * 2);
// Prefer a deep English voice if available
const voices = window.speechSynthesis.getVoices();
@@ -546,6 +594,7 @@
function initAll() {
initSkin();
initScope();
+ initVolume();
initOuterLimits();
}
diff --git a/docs/src/styles/oscilloscope.css b/docs/src/styles/oscilloscope.css
index 8ed9766..f06caa0 100644
--- a/docs/src/styles/oscilloscope.css
+++ b/docs/src/styles/oscilloscope.css
@@ -261,6 +261,35 @@
outline-offset: 2px;
}
+/* ── Volume / attenuation knob (interactive) ──────────── */
+.scope-volume-knob {
+ cursor: pointer;
+ transition: transform 0.15s;
+ transform: rotate(var(--knob-rotation, 0deg));
+}
+
+.scope-volume-knob:hover {
+ transform: rotate(var(--knob-rotation, 0deg)) scale(1.08);
+}
+
+.scope-volume-knob:active {
+ transform: rotate(var(--knob-rotation, 0deg)) scale(0.95);
+}
+
+.scope-volume-knob:focus-visible {
+ outline: 2px solid var(--scope-teal);
+ outline-offset: 2px;
+}
+
+/* ── Volume label ─────────────────────────────────────── */
+.scope-volume-label {
+ max-width: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: center;
+}
+
/* ── Signal name label ──────────────────────────────────── */
.scope-source-name {
max-width: 60px;