Last active
January 29, 2026 23:01
-
-
Save rndmcnlly/61d8c753906c966329cbf67d14ed915e 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
| <!-- Vibe-coded with Gambit v1.4 (https://bayleaf.chat/?model=gambit) --> | |
| <!-- PITCH TIMER: Because every second of their attention is precious --> | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>⏱️ PITCH TIMER</title> | |
| <style> | |
| /* === TWEAK THESE === */ | |
| :root { | |
| --color-safe: #00ff88; | |
| --color-warn: #ffaa00; | |
| --color-danger: #ff3366; | |
| --color-overtime: #ff0000; | |
| --bg-dark: #0a0a0f; | |
| --ring-size: min(70vmin, 400px); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| background: var(--bg-dark); | |
| color: white; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| user-select: none; | |
| } | |
| .timer-container { | |
| position: relative; | |
| width: var(--ring-size); | |
| height: var(--ring-size); | |
| } | |
| .timer-ring { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 50%; | |
| background: conic-gradient( | |
| var(--current-color, var(--color-safe)) calc(var(--progress, 1) * 360deg), | |
| #222 0deg | |
| ); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| box-shadow: | |
| 0 0 60px color-mix(in srgb, var(--current-color, var(--color-safe)) 40%, transparent), | |
| inset 0 0 80px rgba(0,0,0,0.8); | |
| transition: box-shadow 0.3s; | |
| } | |
| .timer-ring::before { | |
| content: ''; | |
| position: absolute; | |
| width: 85%; | |
| height: 85%; | |
| background: var(--bg-dark); | |
| border-radius: 50%; | |
| } | |
| .time-display { | |
| position: relative; | |
| z-index: 1; | |
| font-size: clamp(3rem, 15vmin, 8rem); | |
| font-weight: 200; | |
| font-variant-numeric: tabular-nums; | |
| letter-spacing: -0.02em; | |
| text-shadow: 0 0 30px currentColor; | |
| } | |
| .controls { | |
| margin-top: 2rem; | |
| display: flex; | |
| gap: 1rem; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| button { | |
| padding: 0.8rem 2rem; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| border: 2px solid #444; | |
| border-radius: 2rem; | |
| background: #1a1a24; | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| button:hover { background: #2a2a3a; border-color: #666; transform: scale(1.05); } | |
| button:active { transform: scale(0.98); } | |
| button.primary { border-color: var(--color-safe); color: var(--color-safe); } | |
| /* === WARNING STATES === */ | |
| .warn-10 .timer-ring { animation: pulse-warn 1s ease-in-out infinite; } | |
| .warn-5 .timer-ring { animation: pulse-danger 0.5s ease-in-out infinite; } | |
| .warn-5 .time-display { animation: shake 0.1s linear infinite; } | |
| @keyframes pulse-warn { | |
| 0%, 100% { box-shadow: 0 0 60px var(--color-warn), inset 0 0 80px rgba(0,0,0,0.8); } | |
| 50% { box-shadow: 0 0 100px var(--color-warn), 0 0 150px var(--color-warn), inset 0 0 80px rgba(0,0,0,0.8); } | |
| } | |
| @keyframes pulse-danger { | |
| 0%, 100% { box-shadow: 0 0 80px var(--color-danger), inset 0 0 80px rgba(0,0,0,0.8); } | |
| 50% { box-shadow: 0 0 120px var(--color-danger), 0 0 200px var(--color-danger), inset 0 0 80px rgba(0,0,0,0.5); } | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-3px) rotate(-1deg); } | |
| 75% { transform: translateX(3px) rotate(1deg); } | |
| } | |
| /* === OVERTIME CHAOS === */ | |
| .overtime { animation: overtime-bg 0.3s linear infinite; } | |
| .overtime .timer-ring { | |
| animation: overtime-ring 0.2s linear infinite; | |
| filter: saturate(1.5); | |
| } | |
| .overtime .time-display { color: var(--color-overtime); } | |
| @keyframes overtime-bg { | |
| 0%, 100% { background: var(--bg-dark); } | |
| 50% { background: #1a0000; } | |
| } | |
| @keyframes overtime-ring { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.02); } | |
| } | |
| /* === SKULL EXPLOSION === */ | |
| .skull-container { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 100; | |
| } | |
| .skull { | |
| position: absolute; | |
| font-size: 4rem; | |
| animation: skull-explode 2s ease-out forwards; | |
| filter: drop-shadow(0 0 20px var(--color-overtime)); | |
| } | |
| @keyframes skull-explode { | |
| 0% { | |
| transform: translate(-50%, -50%) scale(0) rotate(0deg); | |
| opacity: 1; | |
| } | |
| 20% { transform: translate(-50%, -50%) scale(1.5) rotate(20deg); } | |
| 100% { | |
| transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.5) rotate(var(--rot)); | |
| opacity: 0; | |
| } | |
| } | |
| .status { | |
| position: fixed; | |
| top: 1rem; | |
| font-size: 0.9rem; | |
| color: #666; | |
| } | |
| .duration-hint { | |
| position: fixed; | |
| bottom: 1rem; | |
| font-size: 0.8rem; | |
| color: #444; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Design question: Does dramatic time pressure improve or hurt pitch delivery? --> | |
| <div class="status" id="status"></div> | |
| <div class="timer-container"> | |
| <div class="timer-ring" id="ring"> | |
| <div class="time-display" id="display">1:30</div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="primary" id="startBtn">▶ START</button> | |
| <button id="resetBtn">↺ RESET</button> | |
| <button id="fullscreenBtn">⛶ FULLSCREEN</button> | |
| </div> | |
| <div class="skull-container" id="skulls"></div> | |
| <div class="duration-hint">Tip: Add #duration=60 to URL for different times</div> | |
| <script> | |
| // ============================================ | |
| // === CONFIGURATION (edit these constants) === | |
| // ============================================ | |
| const DEFAULT_DURATION = 90; // seconds | |
| const WARN_THRESHOLD_1 = 10; // yellow warning | |
| const WARN_THRESHOLD_2 = 5; // red danger | |
| const SKULL_COUNT = 12; // skulls in explosion | |
| // Parse URL hash for custom duration | |
| function getDuration() { | |
| const hash = location.hash.slice(1); | |
| const match = hash.match(/duration=(\d+)/); | |
| return match ? parseInt(match[1], 10) : DEFAULT_DURATION; | |
| } | |
| // ============================================ | |
| // === STATE === | |
| // ============================================ | |
| let duration = getDuration(); | |
| let remaining = duration; | |
| let running = false; | |
| let interval = null; | |
| let wakeLock = null; | |
| let exploded = false; | |
| const display = document.getElementById('display'); | |
| const ring = document.getElementById('ring'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const fullscreenBtn = document.getElementById('fullscreenBtn'); | |
| const status = document.getElementById('status'); | |
| const skulls = document.getElementById('skulls'); | |
| // ============================================ | |
| // === RENDERING === | |
| // ============================================ | |
| function formatTime(sec) { | |
| const negative = sec < 0; | |
| const abs = Math.abs(sec); | |
| const m = Math.floor(abs / 60); | |
| const s = abs % 60; | |
| return (negative ? '-' : '') + m + ':' + String(s).padStart(2, '0'); | |
| } | |
| function render() { | |
| display.textContent = formatTime(remaining); | |
| const progress = Math.max(0, remaining / duration); | |
| ring.style.setProperty('--progress', progress); | |
| // Color states | |
| let color; | |
| if (remaining <= 0) color = 'var(--color-overtime)'; | |
| else if (remaining <= WARN_THRESHOLD_2) color = 'var(--color-danger)'; | |
| else if (remaining <= WARN_THRESHOLD_1) color = 'var(--color-warn)'; | |
| else color = 'var(--color-safe)'; | |
| ring.style.setProperty('--current-color', color); | |
| // Body classes for animations | |
| document.body.classList.remove('warn-10', 'warn-5', 'overtime'); | |
| if (remaining <= 0) document.body.classList.add('overtime'); | |
| else if (remaining <= WARN_THRESHOLD_2) document.body.classList.add('warn-5'); | |
| else if (remaining <= WARN_THRESHOLD_1) document.body.classList.add('warn-10'); | |
| } | |
| // ============================================ | |
| // === SKULL EXPLOSION === | |
| // ============================================ | |
| function explodeSkulls() { | |
| if (exploded) return; | |
| exploded = true; | |
| skulls.innerHTML = ''; | |
| for (let i = 0; i < SKULL_COUNT; i++) { | |
| const skull = document.createElement('div'); | |
| skull.className = 'skull'; | |
| skull.textContent = '💀'; | |
| skull.style.left = '50%'; | |
| skull.style.top = '50%'; | |
| skull.style.setProperty('--dx', (Math.random() - 0.5) * 600 + 'px'); | |
| skull.style.setProperty('--dy', (Math.random() - 0.5) * 600 + 'px'); | |
| skull.style.setProperty('--rot', (Math.random() - 0.5) * 720 + 'deg'); | |
| skull.style.animationDelay = (i * 0.05) + 's'; | |
| skulls.appendChild(skull); | |
| } | |
| } | |
| // ============================================ | |
| // === TIMER CONTROL === | |
| // ============================================ | |
| function tick() { | |
| remaining--; | |
| render(); | |
| if (remaining === 0) explodeSkulls(); | |
| } | |
| function start() { | |
| if (running) { | |
| // Pause | |
| running = false; | |
| clearInterval(interval); | |
| startBtn.textContent = '▶ RESUME'; | |
| releaseWakeLock(); | |
| } else { | |
| // Start/Resume | |
| running = true; | |
| interval = setInterval(tick, 1000); | |
| startBtn.textContent = '⏸ PAUSE'; | |
| requestWakeLock(); | |
| } | |
| } | |
| function reset() { | |
| running = false; | |
| clearInterval(interval); | |
| duration = getDuration(); | |
| remaining = duration; | |
| exploded = false; | |
| skulls.innerHTML = ''; | |
| startBtn.textContent = '▶ START'; | |
| document.body.classList.remove('warn-10', 'warn-5', 'overtime'); | |
| releaseWakeLock(); | |
| render(); | |
| } | |
| // ============================================ | |
| // === FULLSCREEN & WAKE LOCK === | |
| // ============================================ | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen().catch(() => {}); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| async function requestWakeLock() { | |
| try { | |
| if ('wakeLock' in navigator) { | |
| wakeLock = await navigator.wakeLock.request('screen'); | |
| status.textContent = '🔒 Screen wake lock active'; | |
| } | |
| } catch (e) { | |
| status.textContent = '⚠️ Wake lock unavailable'; | |
| } | |
| } | |
| function releaseWakeLock() { | |
| if (wakeLock) { | |
| wakeLock.release(); | |
| wakeLock = null; | |
| status.textContent = ''; | |
| } | |
| } | |
| // Re-acquire wake lock on visibility change | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.visibilityState === 'visible' && running) { | |
| requestWakeLock(); | |
| } | |
| }); | |
| // Listen for hash changes | |
| window.addEventListener('hashchange', reset); | |
| // ============================================ | |
| // === EVENT BINDINGS === | |
| // ============================================ | |
| startBtn.addEventListener('click', start); | |
| resetBtn.addEventListener('click', reset); | |
| fullscreenBtn.addEventListener('click', toggleFullscreen); | |
| // Spacebar to start/pause | |
| document.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space') { e.preventDefault(); start(); } | |
| if (e.code === 'KeyR') reset(); | |
| if (e.code === 'KeyF') toggleFullscreen(); | |
| }); | |
| // Initialize | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment