diff --git a/docs/src/components/OscilloscopeDisplay.astro b/docs/src/components/OscilloscopeDisplay.astro index d49fb45..6562603 100644 --- a/docs/src/components/OscilloscopeDisplay.astro +++ b/docs/src/components/OscilloscopeDisplay.astro @@ -35,6 +35,10 @@ >
+ + @@ -52,14 +56,16 @@
Vertical - +
Volts/Div
Horizontal - +
Time/Div
@@ -429,9 +435,102 @@ ctx.fill(); } + // ── Outer Limits easter egg ────────────────────────────── + function initOuterLimits() { + const knobs = document.querySelectorAll('.scope-easter-knob'); + const overlay = document.getElementById('scope-outer-limits'); + const textEl = document.getElementById('scope-ol-text'); + if (!knobs.length || !overlay || !textEl) return; + + // Guard against double-init + if (overlay.dataset.olInit) return; + overlay.dataset.olInit = 'true'; + + const quote = 'There is nothing wrong with your television set. Do not attempt to adjust the picture. We are controlling transmission. We will control the horizontal. We will control the vertical. For the next hour, sit quietly and we will control all that you see and hear.'; + + let typeTimer: number | null = null; + let utterance: SpeechSynthesisUtterance | null = null; + + function show() { + overlay!.dataset.active = 'true'; + overlay!.setAttribute('aria-hidden', 'false'); + textEl!.textContent = ''; + typeText(quote, 0); + speak(quote); + } + + function dismiss() { + if (typeTimer) { clearTimeout(typeTimer); typeTimer = null; } + overlay!.dataset.active = 'false'; + overlay!.setAttribute('aria-hidden', 'true'); + if (utterance && window.speechSynthesis.speaking) { + window.speechSynthesis.cancel(); + } + } + + function typeText(text: string, i: number) { + if (i >= text.length) return; + textEl!.textContent += text[i]; + typeTimer = window.setTimeout(() => typeText(text, i + 1), 30); + } + + function speak(text: string) { + if (!('speechSynthesis' in window)) return; + window.speechSynthesis.cancel(); + utterance = new SpeechSynthesisUtterance(text); + utterance.rate = 0.85; + utterance.pitch = 0.7; + utterance.volume = 0.8; + + // Prefer a deep English voice if available + const voices = window.speechSynthesis.getVoices(); + const preferred = voices.find(v => + v.lang.startsWith('en') && /male|daniel|david|james|google uk/i.test(v.name) + ) || voices.find(v => v.lang.startsWith('en')); + if (preferred) utterance.voice = preferred; + + window.speechSynthesis.speak(utterance); + } + + // Ensure voices are loaded (some browsers load async) + if ('speechSynthesis' in window) { + window.speechSynthesis.getVoices(); + window.speechSynthesis.addEventListener('voiceschanged', () => {}); + } + + // Knob clicks toggle the overlay + knobs.forEach(knob => { + knob.addEventListener('click', () => { + if (overlay!.dataset.active === 'true') { + dismiss(); + } else { + show(); + } + }); + knob.addEventListener('keydown', (e: Event) => { + const ke = e as KeyboardEvent; + if (ke.key === 'Enter' || ke.key === ' ') { + ke.preventDefault(); + if (overlay!.dataset.active === 'true') { + dismiss(); + } else { + show(); + } + } + }); + }); + + // Dismiss on overlay click or Escape + overlay.addEventListener('click', dismiss); + document.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape' && overlay!.dataset.active === 'true') dismiss(); + }); + } + function initAll() { initSkin(); initScope(); + initOuterLimits(); } // Init when DOM ready (works with Astro's page lifecycle) diff --git a/docs/src/styles/oscilloscope.css b/docs/src/styles/oscilloscope.css index d4e9b0d..2505956 100644 --- a/docs/src/styles/oscilloscope.css +++ b/docs/src/styles/oscilloscope.css @@ -359,6 +359,52 @@ color: #1a1610; } +/* ── Outer Limits easter egg overlay ─────────────────────── */ +.scope-outer-limits { + position: absolute; + inset: 0; + background: rgba(10, 10, 10, 0.92); + z-index: 10; + display: none; + align-items: center; + justify-content: center; + padding: 12%; + cursor: pointer; +} + +.scope-outer-limits[data-active="true"] { + display: flex; +} + +.scope-ol-text { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 0.65rem; + line-height: 1.6; + color: var(--scope-teal); + text-align: center; + text-shadow: 0 0 8px var(--scope-teal-glow); + margin: 0; +} + +/* ── Easter egg knobs (Vertical / Horizontal) ────────────── */ +.scope-easter-knob { + cursor: pointer; + transition: transform 0.15s; +} + +.scope-easter-knob:hover { + transform: scale(1.08); +} + +.scope-easter-knob:active { + transform: scale(0.95); +} + +.scope-easter-knob:focus-visible { + outline: 2px solid var(--scope-teal); + outline-offset: 2px; +} + /* ── Idle state ──────────────────────────────────────────── */ .scope-screen[data-idle="true"] .scope-canvas { opacity: 0.6;