Last active
October 2, 2025 08:21
-
-
Save reynish/2f2bd1b6a6c776cfbb83fadc96b2e12f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Reptronome - Arcade Edition</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Retro 80s Arcade Styles */ | |
| body { | |
| font-family: 'Inter', monospace; /* Use monospace for a digital/retro feel */ | |
| background-color: #000000; /* Pure black background */ | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| padding: 1rem; | |
| /* Simple CRT scanline simulation */ | |
| background-image: linear-gradient(rgba(0, 0, 0, 0.4) 1px, transparent 1px); | |
| background-size: 100% 2px; | |
| } | |
| .container { | |
| max-width: 480px; | |
| width: 100%; | |
| background-color: #1a1a1a; /* Dark console casing */ | |
| border: 4px solid #00FFFF; /* Electric Cyan Frame */ | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.7); /* Neon cyan glow for the screen */ | |
| border-radius: 12px; | |
| } | |
| /* Custom Neon Glow Text Effect */ | |
| .neon-glow { | |
| color: #00FFFF; /* Cyan */ | |
| text-shadow: | |
| 0 0 5px #00FFFF, | |
| 0 0 15px #00FFFF, | |
| 0 0 25px rgba(0, 255, 255, 0.5); | |
| } | |
| /* Magenta Glow for Down/Stop */ | |
| .magenta-glow { | |
| color: #FF00FF; /* Magenta */ | |
| text-shadow: | |
| 0 0 5px #FF00FF, | |
| 0 0 15px #FF00FF, | |
| 0 0 25px rgba(255, 0, 255, 0.5); | |
| } | |
| /* Yellow Glow for Up */ | |
| .yellow-glow { | |
| color: #FFFF00; /* Yellow */ | |
| text-shadow: | |
| 0 0 5px #FFFF00, | |
| 0 0 15px #FFFF00, | |
| 0 0 25px rgba(255, 255, 0, 0.5); | |
| } | |
| /* Arcade Button Base Styling */ | |
| .arcade-btn { | |
| border: 3px solid #00FFFF; | |
| box-shadow: | |
| 0 0 10px #00FFFF, | |
| inset 0 0 5px #00FFFF; | |
| transition: all 0.1s ease-out; | |
| transform: translate(0, 0); | |
| } | |
| .arcade-btn:active { | |
| box-shadow: inset 0 0 15px #00FFFF, 0 0 5px #00FFFF; | |
| transform: translate(0, 2px); /* Simulate button press depth */ | |
| } | |
| /* Custom animation for the beat display */ | |
| @keyframes beatPulse { | |
| 0% { transform: scale(1); opacity: 0.8; } | |
| 50% { transform: scale(1.03); opacity: 1; } | |
| 100% { transform: scale(1); opacity: 0.8; } | |
| } | |
| .pulsing { | |
| animation: beatPulse 0.1s ease-out forwards; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container p-8 rounded-xl"> | |
| <h1 class="text-5xl font-extrabold text-center uppercase mb-2 neon-glow">Reptronome</h1> | |
| <p class="text-center text-gray-200 mb-8 font-mono text-sm neon-glow">GET READY TO REPEAT! (BETA 3.0)</p> | |
| <!-- Current BPM Display --> | |
| <div class="text-center mb-10 border-b border-gray-700 pb-4"> | |
| <span id="bpm-display" class="text-5xl font-extrabold yellow-glow">30</span> | |
| <span class="text-2xl ml-2 text-cyan-400">BPM</span> | |
| </div> | |
| <!-- Beat Status Display - Console Screen --> | |
| <div id="beat-status" class="w-full h-32 bg-gray-900 rounded-lg flex items-center justify-center mb-8 border-4 border-gray-700 shadow-inner shadow-black/50"> | |
| <span id="beat-text" class="text-6xl font-black uppercase text-gray-500 font-mono transition-colors">INSERT COIN</span> | |
| </div> | |
| <!-- Control Button (Start/Stop Toggle) --> | |
| <div class="flex justify-center"> | |
| <button id="toggle-btn" onclick="toggleMetronome()" class="arcade-btn text-white font-bold py-3 px-12 rounded-lg text-lg uppercase bg-cyan-800 hover:bg-cyan-700"> | |
| Start | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| // Global State Variables | |
| let metronomeIntervalId = null; | |
| const FIXED_BPM = 30; // Tempo fixed at 30 BPM | |
| let currentBPM = FIXED_BPM; | |
| let beatCount = 0; | |
| let isRunning = false; | |
| // DOM elements | |
| const beatText = document.getElementById('beat-text'); | |
| const toggleBtn = document.getElementById('toggle-btn'); // Single toggle button | |
| const beatStatusDiv = document.getElementById('beat-status'); | |
| // Constants for speech synthesis | |
| const UP_WORD = "Up"; | |
| const DOWN_WORD = "Down"; | |
| const SPEECH_RATE = 1.8; // Faster rate for metronome function | |
| /** | |
| * Converts text to speech using the Web Speech API. | |
| * @param {string} text - The word to speak ("Up" or "Down"). | |
| */ | |
| function speak(text) { | |
| if ('speechSynthesis' in window) { | |
| // Stop any previous speech to ensure accurate timing | |
| window.speechSynthesis.cancel(); | |
| const utterance = new SpeechSynthesisUtterance(text); | |
| utterance.rate = SPEECH_RATE; | |
| // For a reliable and consistent TTS voice across systems, we rely on the default. | |
| window.speechSynthesis.speak(utterance); | |
| } else { | |
| console.warn("Speech Synthesis API not supported in this browser."); | |
| } | |
| } | |
| /** | |
| * Main function executed on every beat. | |
| */ | |
| function playBeat() { | |
| beatCount++; | |
| // Determine the word to speak: Up on beat 1, Down on beat 2, then cycle. | |
| const currentWord = (beatCount % 2 === 1) ? UP_WORD : DOWN_WORD; | |
| const isUp = currentWord === UP_WORD; | |
| speak(currentWord); | |
| // Visual Update: Apply retro glow and text | |
| beatText.textContent = currentWord.toUpperCase(); | |
| // 1. Reset color classes | |
| beatText.classList.remove('yellow-glow', 'magenta-glow', 'text-gray-500'); | |
| // 2. Apply correct glow color | |
| if (isUp) { | |
| beatText.classList.add('yellow-glow'); | |
| beatStatusDiv.style.backgroundColor = 'rgba(255, 255, 0, 0.1)'; /* subtle yellow background pulse */ | |
| } else { | |
| beatText.classList.add('magenta-glow'); | |
| beatStatusDiv.style.backgroundColor = 'rgba(255, 0, 255, 0.1)'; /* subtle magenta background pulse */ | |
| } | |
| // 3. Apply quick pulse animation class | |
| beatStatusDiv.classList.add('pulsing'); | |
| // Remove the pulse class and reset background color shortly after | |
| setTimeout(() => { | |
| beatStatusDiv.classList.remove('pulsing'); | |
| beatStatusDiv.style.backgroundColor = '#1a1a1a'; /* Reset to dark background */ | |
| }, 150); | |
| } | |
| /** | |
| * Starts the metronome interval. | |
| */ | |
| function startMetronome() { | |
| if (isRunning) return; | |
| isRunning = true; | |
| beatCount = 0; | |
| // Calculate interval based on the fixed 30 BPM (2000ms per beat) | |
| const intervalMs = Math.round(60000 / currentBPM); | |
| // Execute the first beat immediately | |
| playBeat(); | |
| // Set up the interval for subsequent beats | |
| metronomeIntervalId = setInterval(playBeat, intervalMs); | |
| updateControls(); | |
| } | |
| /** | |
| * Stops the metronome interval. | |
| */ | |
| function stopMetronome() { | |
| if (!isRunning) return; | |
| clearInterval(metronomeIntervalId); | |
| metronomeIntervalId = null; | |
| isRunning = false; | |
| beatCount = 0; | |
| window.speechSynthesis.cancel(); | |
| // Visual Reset | |
| beatText.textContent = "GAME OVER"; | |
| beatText.classList.remove('yellow-glow', 'magenta-glow'); | |
| beatText.classList.add('text-gray-500'); | |
| beatStatusDiv.style.backgroundColor = '#1a1a1a'; | |
| updateControls(); | |
| } | |
| /** | |
| * Toggles the metronome state. | |
| */ | |
| function toggleMetronome() { | |
| if (isRunning) { | |
| stopMetronome(); | |
| } else { | |
| startMetronome(); | |
| } | |
| } | |
| /** | |
| * Updates the state and appearance of the single control button. | |
| */ | |
| function updateControls() { | |
| if (isRunning) { | |
| // Change to STOP state (Red/Magenta Arcade Button) | |
| toggleBtn.textContent = "STOP"; | |
| toggleBtn.classList.remove('bg-cyan-800', 'hover:bg-cyan-700', 'neon-glow', 'border-[#00FFFF]'); | |
| toggleBtn.classList.add('bg-fuchsia-800', 'hover:bg-fuchsia-700', 'magenta-glow', 'arcade-btn-stop'); | |
| toggleBtn.style.borderColor = '#FF00FF'; | |
| toggleBtn.style.boxShadow = '0 0 10px #FF00FF, inset 0 0 5px #FF00FF'; | |
| } else { | |
| // Change to START state (Blue/Cyan Arcade Button) | |
| toggleBtn.textContent = "START"; | |
| toggleBtn.classList.remove('bg-fuchsia-800', 'hover:bg-fuchsia-700', 'magenta-glow', 'arcade-btn-stop'); | |
| toggleBtn.classList.add('bg-cyan-800', 'hover:bg-cyan-700', 'neon-glow'); | |
| toggleBtn.style.borderColor = '#00FFFF'; | |
| toggleBtn.style.boxShadow = '0 0 10px #00FFFF, inset 0 0 5px #00FFFF'; | |
| // Initial "Insert Coin" state when stopped | |
| if (beatText.textContent !== "GAME OVER") { | |
| beatText.textContent = "INSERT COIN"; | |
| } | |
| } | |
| } | |
| // Initialize button states on load | |
| window.onload = updateControls; | |
| // --- Service Worker Implementation for Offline Access (PWA) --- | |
| // Service Worker script logic as a string | |
| const swScript = ` | |
| const CACHE_NAME = 'reptronome-cache-v1'; | |
| // Cache the essential files: The HTML file itself (represented by the root path) and the external Tailwind CSS dependency. | |
| const urlsToCache = [ | |
| '/', // Caches the main HTML file (or root context) | |
| 'https://cdn.tailwindcss.com', | |
| ]; | |
| self.addEventListener('install', (event) => { | |
| event.waitUntil( | |
| caches.open(CACHE_NAME) | |
| .then((cache) => { | |
| console.log('Service Worker: Caching critical resources.'); | |
| return cache.addAll(urlsToCache).catch(err => { | |
| console.error('Service Worker: Failed to cache resources', err); | |
| }); | |
| }) | |
| ); | |
| self.skipWaiting(); // Immediately activate the new service worker | |
| }); | |
| self.addEventListener('activate', (event) => { | |
| // Clean up old caches | |
| const cacheWhitelist = [CACHE_NAME]; | |
| event.waitUntil( | |
| caches.keys().then(cacheNames => { | |
| return Promise.all( | |
| cacheNames.map(cacheName => { | |
| if (cacheWhitelist.indexOf(cacheName) === -1) { | |
| console.log('Service Worker: Deleting old cache:', cacheName); | |
| return caches.delete(cacheName); | |
| } | |
| }) | |
| ); | |
| }) | |
| ); | |
| event.waitUntil(clients.claim()); // Take control of clients | |
| }); | |
| self.addEventListener('fetch', (event) => { | |
| // Serve cached content first | |
| event.respondWith( | |
| caches.match(event.request) | |
| .then((response) => { | |
| // Cache hit - return response | |
| if (response) { | |
| return response; | |
| } | |
| // Fallback to network request (e.g., for speech API) | |
| return fetch(event.request); | |
| } | |
| ) | |
| ); | |
| }); | |
| `; | |
| // 1. Create a Blob from the worker script string | |
| const swBlob = new Blob([swScript], { type: 'application/javascript' }); | |
| // 2. Create a URL for the Blob | |
| const swUrl = URL.createObjectURL(swBlob); | |
| // 3. Register the Service Worker | |
| if ('serviceWorker' in navigator) { | |
| window.addEventListener('load', () => { | |
| navigator.serviceWorker.register(swUrl, { scope: './' }) | |
| .then((registration) => { | |
| console.log('Service Worker registered successfully for offline use.'); | |
| }) | |
| .catch((error) => { | |
| console.error('Service Worker registration failed:', error); | |
| }); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment