From ad630ff4715cbee140053c82c8ff5d268ed39495 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 13 Feb 2026 06:46:46 -0700 Subject: [PATCH] Add Outer Limits easter egg to oscilloscope knobs Clicking the Vertical or Horizontal knobs triggers The Outer Limits (1963) opening narration as a typewriter overlay on the CRT display, with Web Speech API narration. Dismiss via click, Escape, or knob re-click. Works on both 465 and 545A skins. --- docs/src/components/OscilloscopeDisplay.astro | 103 +++++++++++++++++- docs/src/styles/oscilloscope.css | 46 ++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) 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 @@
- +
Volts/Div
- +
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;