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">
|
<div class="scope-frame">
|
||||||
<!-- Top brand bar -->
|
<!-- Top brand bar — model name doubles as skin picker -->
|
||||||
<div class="scope-brand">
|
<div class="scope-brand">
|
||||||
<span class="scope-brand-name">Tektronix</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Recessed CRT bay -->
|
<!-- Recessed CRT bay -->
|
||||||
@ -76,11 +79,88 @@
|
|||||||
· <a href="https://gist.github.com/rsp2k/ac68b1bb290b8124e162987ed1df8d53" target="_blank" rel="noopener">rsp2k</a>
|
· <a href="https://gist.github.com/rsp2k/ac68b1bb290b8124e162987ed1df8d53" target="_blank" rel="noopener">rsp2k</a>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<audio id="scope-audio" src="/spirals_shrt.mp3" loop preload="none"></audio>
|
<audio id="scope-audio" src="/spirals_shrt.mp3" loop preload="none"></audio>
|
||||||
|
|
||||||
<script>
|
<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 ──────────────────────────────
|
// ── Oscilloscope XY renderer ──────────────────────────────
|
||||||
// Uses AnalyserNode (modern Web Audio) instead of deprecated ScriptProcessor
|
// Uses AnalyserNode (modern Web Audio) instead of deprecated ScriptProcessor
|
||||||
function initScope() {
|
function initScope() {
|
||||||
@ -244,13 +324,18 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init when DOM ready (works with Astro's page lifecycle)
|
function initAll() {
|
||||||
if (document.readyState === 'loading') {
|
initSkin();
|
||||||
document.addEventListener('DOMContentLoaded', initScope);
|
|
||||||
} else {
|
|
||||||
initScope();
|
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
|
// Re-init on Astro page transitions
|
||||||
document.addEventListener('astro:page-load', initScope);
|
document.addEventListener('astro:page-load', initAll);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -341,3 +341,132 @@
|
|||||||
height: 5px;
|
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