|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>Non-Linear Timer</title> |
| 7 | + <!-- Load Tailwind CSS --> |
| 8 | + <script src="https://cdn.tailwindcss.com"></script> |
| 9 | + <style> |
| 10 | + /* Custom styles for the slider to be more visible and touch-friendly */ |
| 11 | + input[type=range] { |
| 12 | + -webkit-appearance: none; |
| 13 | + width: 100%; |
| 14 | + height: 12px; |
| 15 | + background: #cbd5e1; /* slate-300 */ |
| 16 | + border-radius: 6px; |
| 17 | + cursor: pointer; |
| 18 | + outline: none; |
| 19 | + } |
| 20 | + |
| 21 | + /* Thumb styling for Webkit/Blink */ |
| 22 | + input[type=range]::-webkit-slider-thumb { |
| 23 | + -webkit-appearance: none; |
| 24 | + appearance: none; |
| 25 | + width: 24px; |
| 26 | + height: 24px; |
| 27 | + border-radius: 50%; |
| 28 | + background: #1e3a8a; /* blue-900 */ |
| 29 | + cursor: pointer; |
| 30 | + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); |
| 31 | + margin-top: -6px; /* Center the thumb vertically */ |
| 32 | + } |
| 33 | + </style> |
| 34 | +</head> |
| 35 | +<body class="bg-gray-50 flex items-center justify-center min-h-screen p-4 font-sans"> |
| 36 | + |
| 37 | + <div id="timer-card" class="bg-white p-8 md:p-12 rounded-xl shadow-2xl w-full max-w-xl transition-all duration-300"> |
| 38 | + <h1 class="text-3xl font-extrabold text-gray-900 mb-6 text-center">Set Your Timer</h1> |
| 39 | + |
| 40 | + <!-- Time Display --> |
| 41 | + <div id="time-display" class="text-6xl md:text-8xl font-mono font-bold text-center text-blue-800 mb-10 select-none"> |
| 42 | + 00:00:05 |
| 43 | + </div> |
| 44 | + |
| 45 | + <!-- Timer Status Message --> |
| 46 | + <p id="status-message" class="text-center text-sm mb-6 h-5 text-gray-500"> |
| 47 | + Slide and release to start |
| 48 | + </p> |
| 49 | + |
| 50 | + <!-- Slider Input --> |
| 51 | + <input type="range" id="timer-slider" min="0" max="100" value="0" class="w-full"> |
| 52 | + |
| 53 | + <!-- Custom Modal for Alarm/Message --> |
| 54 | + <div id="alarm-modal" class="hidden fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4"> |
| 55 | + <div class="bg-white p-8 rounded-xl shadow-2xl max-w-md w-full text-center"> |
| 56 | + <h2 id="modal-title" class="text-3xl font-bold text-red-600 mb-4">Timer Complete!</h2> |
| 57 | + <p class="text-lg text-gray-700 mb-6">Your countdown has finished.</p> |
| 58 | + <button onclick="closeAlarmModal()" class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition duration-150"> |
| 59 | + Close |
| 60 | + </button> |
| 61 | + </div> |
| 62 | + </div> |
| 63 | + </div> |
| 64 | + |
| 65 | + <script> |
| 66 | + // --- CONSTANTS AND STATE --- |
| 67 | + const MIN_TIME_SECONDS = 5; // 5 seconds |
| 68 | + const MAX_TIME_SECONDS = 36000; // 10 hours (10 * 60 * 60) |
| 69 | + const EXP_FACTOR = MAX_TIME_SECONDS / MIN_TIME_SECONDS; // 7200 |
| 70 | + |
| 71 | + let totalTimeSeconds = MIN_TIME_SECONDS; |
| 72 | + let remainingSeconds = MIN_TIME_SECONDS; |
| 73 | + let timerInterval = null; |
| 74 | + let isRunning = false; |
| 75 | + |
| 76 | + // --- DOM ELEMENTS --- |
| 77 | + const timeDisplay = document.getElementById('time-display'); |
| 78 | + const slider = document.getElementById('timer-slider'); |
| 79 | + const statusMessage = document.getElementById('status-message'); |
| 80 | + const timerCard = document.getElementById('timer-card'); |
| 81 | + const alarmModal = document.getElementById('alarm-modal'); |
| 82 | + |
| 83 | + // --- MATH/MAPPING LOGIC --- |
| 84 | + |
| 85 | + /** |
| 86 | + * Maps a linear slider value (0-100) to a non-linear time in seconds. |
| 87 | + * The exponential formula makes it more sensitive at the low (5s) end. |
| 88 | + * T = T_min * (T_max / T_min) ^ (x / 100) |
| 89 | + * @param {number} sliderValue - The value from the range input (0 to 100). |
| 90 | + * @returns {number} The calculated time in seconds. |
| 91 | + */ |
| 92 | + function mapSliderToTime(sliderValue) { |
| 93 | + // Formula: 5 * (7200)^(sliderValue / 100) |
| 94 | + const time = MIN_TIME_SECONDS * Math.pow(EXP_FACTOR, sliderValue / 100); |
| 95 | + return Math.round(time); // Use Math.round to get clean integer seconds |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * Formats total seconds into HH:mm:ss string. |
| 100 | + * @param {number} seconds - The total number of seconds. |
| 101 | + * @returns {string} The formatted time string. |
| 102 | + */ |
| 103 | + function formatTime(seconds) { |
| 104 | + const h = String(Math.floor(seconds / 3600)).padStart(2, '0'); |
| 105 | + const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0'); |
| 106 | + const s = String(seconds % 60).padStart(2, '0'); |
| 107 | + return `${h}:${m}:${s}`; |
| 108 | + } |
| 109 | + |
| 110 | + // --- TIMER CONTROL --- |
| 111 | + |
| 112 | + /** |
| 113 | + * Updates the display every second and manages the countdown. |
| 114 | + */ |
| 115 | + function updateTimer() { |
| 116 | + if (remainingSeconds <= 0) { |
| 117 | + stopTimer(); |
| 118 | + triggerAlarm(); |
| 119 | + return; |
| 120 | + } |
| 121 | + |
| 122 | + remainingSeconds--; |
| 123 | + timeDisplay.textContent = formatTime(remainingSeconds); |
| 124 | + let minI = 0; |
| 125 | + let minVal = MAX_TIME_SECONDS; |
| 126 | + for (let i = 0; i <= 100; i++) { |
| 127 | + const delta = Math.abs(mapSliderToTime(i) - remainingSeconds); |
| 128 | + if (delta < minVal) { |
| 129 | + minVal = delta; |
| 130 | + minI = i; |
| 131 | + } |
| 132 | + } |
| 133 | + slider.value = minI; |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * Stops the current timer interval. |
| 138 | + */ |
| 139 | + function stopTimer() { |
| 140 | + if (timerInterval) { |
| 141 | + clearInterval(timerInterval); |
| 142 | + timerInterval = null; |
| 143 | + } |
| 144 | + isRunning = false; |
| 145 | + } |
| 146 | + |
| 147 | + /** |
| 148 | + * Starts the countdown based on the currently set remainingSeconds. |
| 149 | + */ |
| 150 | + function startTimer() { |
| 151 | + if (isRunning) { |
| 152 | + stopTimer(); |
| 153 | + } |
| 154 | + |
| 155 | + // Set the interval to run every 1000 milliseconds (1 second) |
| 156 | + timerInterval = setInterval(updateTimer, 1000); |
| 157 | + isRunning = true; |
| 158 | + statusMessage.textContent = 'Timer running...'; |
| 159 | + timerCard.classList.add('ring-4', 'ring-blue-400/50'); |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * Handles the alarm UI state when the timer reaches zero. |
| 164 | + */ |
| 165 | + function triggerAlarm() { |
| 166 | + statusMessage.textContent = 'TIME IS UP!'; |
| 167 | + timerCard.classList.remove('ring-4', 'ring-blue-400/50'); |
| 168 | + timerCard.classList.add('ring-4', 'ring-red-500/80'); |
| 169 | + alarmModal.classList.remove('hidden'); |
| 170 | + } |
| 171 | + |
| 172 | + /** |
| 173 | + * Closes the alarm modal and resets the UI state. |
| 174 | + */ |
| 175 | + function closeAlarmModal() { |
| 176 | + alarmModal.classList.add('hidden'); |
| 177 | + timerCard.classList.remove('ring-4', 'ring-red-500/80'); |
| 178 | + slider.disabled = false; |
| 179 | + resetDisplay(totalTimeSeconds); // Reset display to the last set value |
| 180 | + statusMessage.textContent = 'Slide and release to start'; |
| 181 | + } |
| 182 | + window.closeAlarmModal = closeAlarmModal; // Expose to global scope for HTML onclick |
| 183 | + |
| 184 | + /** |
| 185 | + * Updates the time display to a specific value and updates the internal state. |
| 186 | + */ |
| 187 | + function resetDisplay(seconds) { |
| 188 | + remainingSeconds = seconds; |
| 189 | + timeDisplay.textContent = formatTime(seconds); |
| 190 | + } |
| 191 | + |
| 192 | + // --- EVENT LISTENERS --- |
| 193 | + |
| 194 | + // 1. Update time display continuously while the user drags the slider (input event) |
| 195 | + slider.addEventListener('input', (event) => { |
| 196 | + if (isRunning) { |
| 197 | + stopTimer(); |
| 198 | + statusMessage.textContent = 'Timer paused. Release slider to restart.'; |
| 199 | + timerCard.classList.remove('ring-4', 'ring-blue-400/50'); |
| 200 | + } |
| 201 | + |
| 202 | + const value = parseInt(event.target.value); |
| 203 | + totalTimeSeconds = mapSliderToTime(value); |
| 204 | + resetDisplay(totalTimeSeconds); |
| 205 | + |
| 206 | + // Hide alarm if it was showing while interacting with slider |
| 207 | + if (!alarmModal.classList.contains('hidden')) { |
| 208 | + alarmModal.classList.add('hidden'); |
| 209 | + } |
| 210 | + }); |
| 211 | + |
| 212 | + // 2. Start the timer when the user releases the slider (change event) |
| 213 | + slider.addEventListener('change', () => { |
| 214 | + if (totalTimeSeconds > 0) { |
| 215 | + startTimer(); |
| 216 | + } |
| 217 | + }); |
| 218 | + |
| 219 | + // --- INITIALIZATION --- |
| 220 | + |
| 221 | + // Set initial state based on slider value 0 |
| 222 | + document.addEventListener('DOMContentLoaded', () => { |
| 223 | + // Update time once on load for initial value (0 -> 5s) |
| 224 | + const initialValue = parseInt(slider.value); |
| 225 | + totalTimeSeconds = mapSliderToTime(initialValue); |
| 226 | + resetDisplay(totalTimeSeconds); |
| 227 | + }); |
| 228 | + </script> |
| 229 | +</body> |
| 230 | +</html> |
| 231 | + |
0 commit comments