Add Tektronix 545A scope skin with switchable skin picker
Second oscilloscope skin: 1959 Type 545A with blue-green hammertone panel, cream silk-screened labels, Bakelite knobs, deeper CRT recess, and ventilation holes. Click the model name to cycle between 465 and 545A skins. Selection persists via localStorage.
This commit is contained in:
parent
b7a370c1f4
commit
08e0ee3cba
@ -13,10 +13,13 @@
|
||||
---
|
||||
|
||||
<div class="scope-frame">
|
||||
<!-- Top brand bar -->
|
||||
<!-- Top brand bar — model name doubles as skin picker -->
|
||||
<div class="scope-brand">
|
||||
<span class="scope-brand-name">Tektronix</span>
|
||||
<span class="scope-brand-model">mcltspice</span>
|
||||
<div class="scope-brand-right">
|
||||
<button class="scope-brand-model" id="scope-skin-picker" type="button" aria-label="Switch oscilloscope model">mcltspice</button>
|
||||
<span class="scope-brand-sub" id="scope-brand-sub"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recessed CRT bay -->
|
||||
@ -76,11 +79,88 @@
|
||||
· <a href="https://gist.github.com/rsp2k/ac68b1bb290b8124e162987ed1df8d53" target="_blank" rel="noopener">rsp2k</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ventilation holes (545A skin only — hidden by default) -->
|
||||
<div class="scope-vent-holes" aria-hidden="true">
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
<div class="scope-vent-hole"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="scope-audio" src="/spirals_shrt.mp3" loop preload="none"></audio>
|
||||
|
||||
<script>
|
||||
// ── Skin configuration ────────────────────────────────────
|
||||
const SKINS: Record<string, { brand: string; model: string; sub: string; sections: string[] }> = {
|
||||
'465': { brand: 'Tektronix', model: 'mcltspice', sub: '', sections: ['Vertical', 'Horizontal', 'Trigger'] },
|
||||
'545a': { brand: 'Tektronix', model: 'Type 545A', sub: 'Oscilloscope', sections: ['Vert Ampl', 'Sweep', 'Trigger'] },
|
||||
};
|
||||
const SKIN_ORDER = Object.keys(SKINS);
|
||||
|
||||
function initSkin() {
|
||||
const frame = document.querySelector('.scope-frame') as HTMLElement | null;
|
||||
const picker = document.getElementById('scope-skin-picker') as HTMLButtonElement | null;
|
||||
const brandName = document.querySelector('.scope-brand-name') as HTMLElement | null;
|
||||
const brandSub = document.getElementById('scope-brand-sub') as HTMLElement | null;
|
||||
const sectionLabels = document.querySelectorAll('.scope-section-label');
|
||||
|
||||
if (!frame || !picker) return;
|
||||
// Guard against double-init (DOMContentLoaded + astro:page-load both fire)
|
||||
if (picker.dataset.skinInit) return;
|
||||
picker.dataset.skinInit = 'true';
|
||||
|
||||
let currentSkin = localStorage.getItem('scope-skin') || '465';
|
||||
if (!SKINS[currentSkin]) currentSkin = '465';
|
||||
|
||||
function applySkin(skinId: string) {
|
||||
const skin = SKINS[skinId];
|
||||
if (!skin) return;
|
||||
|
||||
// Remove all skin classes, then apply the active one
|
||||
SKIN_ORDER.forEach(id => frame!.classList.remove(`scope-skin-${id}`));
|
||||
if (skinId !== '465') {
|
||||
frame!.classList.add(`scope-skin-${skinId}`);
|
||||
}
|
||||
frame!.dataset.skin = skinId;
|
||||
|
||||
// Update brand text
|
||||
if (brandName) brandName.textContent = skin.brand;
|
||||
picker!.textContent = skin.model;
|
||||
if (brandSub) {
|
||||
brandSub.textContent = skin.sub;
|
||||
brandSub.style.display = skin.sub ? '' : 'none';
|
||||
}
|
||||
|
||||
// Update section labels (Vertical/Vert Ampl, etc.)
|
||||
sectionLabels.forEach((label, i) => {
|
||||
if (skin.sections[i]) label.textContent = skin.sections[i];
|
||||
});
|
||||
|
||||
localStorage.setItem('scope-skin', skinId);
|
||||
currentSkin = skinId;
|
||||
}
|
||||
|
||||
// Apply saved or default skin
|
||||
applySkin(currentSkin);
|
||||
|
||||
// Cycle on click
|
||||
picker.addEventListener('click', () => {
|
||||
const idx = SKIN_ORDER.indexOf(currentSkin);
|
||||
const next = SKIN_ORDER[(idx + 1) % SKIN_ORDER.length];
|
||||
applySkin(next);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Oscilloscope XY renderer ──────────────────────────────
|
||||
// Uses AnalyserNode (modern Web Audio) instead of deprecated ScriptProcessor
|
||||
function initScope() {
|
||||
@ -244,13 +324,18 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Init when DOM ready (works with Astro's page lifecycle)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initScope);
|
||||
} else {
|
||||
function initAll() {
|
||||
initSkin();
|
||||
initScope();
|
||||
}
|
||||
|
||||
// Init when DOM ready (works with Astro's page lifecycle)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initAll);
|
||||
} else {
|
||||
initAll();
|
||||
}
|
||||
|
||||
// Re-init on Astro page transitions
|
||||
document.addEventListener('astro:page-load', initScope);
|
||||
document.addEventListener('astro:page-load', initAll);
|
||||
</script>
|
||||
|
||||
@ -341,3 +341,132 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user