Add signal attenuation knob to oscilloscope control panel
8-position volume control (5%-40%, default 20%) between Source and Vertical sections. Persists via localStorage, keyboard accessible, speech volume scales proportionally.
This commit is contained in:
parent
80a01a7326
commit
59677a6a7d
@ -58,6 +58,14 @@
|
||||
<span class="scope-knob-label scope-source-name" id="scope-source-name">Spirals</span>
|
||||
</div>
|
||||
|
||||
<!-- LEVEL / ATTENUATION section -->
|
||||
<div class="scope-section">
|
||||
<span class="scope-section-label">Level</span>
|
||||
<div class="scope-knob scope-volume-knob" id="scope-volume-knob"
|
||||
role="button" tabindex="0" aria-label="Signal attenuation level"></div>
|
||||
<span class="scope-knob-label scope-volume-label" id="scope-volume-label">20%</span>
|
||||
</div>
|
||||
|
||||
<!-- VERTICAL section -->
|
||||
<div class="scope-section">
|
||||
<span class="scope-section-label">Vertical</span>
|
||||
@ -126,8 +134,8 @@
|
||||
|
||||
// ── Skin configuration ────────────────────────────────────
|
||||
const SKINS: Record<string, { brand: string; model: string; sub: string; sections: string[] }> = {
|
||||
'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();
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user