Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Duodecimal Solar Time</title> | |
| <!-- Importing FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Importing a Google Font for technical numbers --> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Rajdhani:wght@300;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-color: #050505; | |
| --panel-bg: rgba(20, 20, 25, 0.7); | |
| --accent-primary: #00f2ff; /* Cyan */ | |
| --accent-secondary: #ff0055; /* Magenta */ | |
| --accent-tertiary: #ffe600; /* Yellow */ | |
| --text-main: #e0e0e0; | |
| --text-dim: #888; | |
| --clock-face: #111; | |
| --glass-border: 1px solid rgba(255, 255, 255, 0.1); | |
| --shadow-glow: 0 0 20px rgba(0, 242, 255, 0.2); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(circle at 50% 50%, #1a1a2e 0%, #000000 100%); | |
| color: var(--text-main); | |
| font-family: 'Rajdhani', sans-serif; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: var(--glass-border); | |
| background: rgba(0,0,0,0.5); | |
| backdrop-filter: blur(10px); | |
| z-index: 10; | |
| } | |
| .brand { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .brand i { color: var(--accent-primary); } | |
| .anycoder-link { | |
| font-size: 0.9rem; | |
| color: var(--text-dim); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| } | |
| .anycoder-link:hover { color: var(--accent-primary); } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 1fr 350px; | |
| gap: 2rem; | |
| padding: 2rem; | |
| height: calc(100vh - 70px); | |
| } | |
| /* Clock Section */ | |
| .clock-container { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| } | |
| .clock-face { | |
| width: 500px; | |
| height: 500px; | |
| border-radius: 50%; | |
| background: var(--clock-face); | |
| border: 2px solid var(--accent-primary); | |
| box-shadow: | |
| 0 0 30px rgba(0, 242, 255, 0.1), | |
| inset 0 0 50px rgba(0,0,0,0.8); | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| /* Decorative Rings */ | |
| .ring { | |
| position: absolute; | |
| border-radius: 50%; | |
| border: 1px dashed rgba(255,255,255,0.1); | |
| pointer-events: none; | |
| } | |
| .ring-1 { width: 90%; height: 90%; border-color: rgba(0, 242, 255, 0.2); } | |
| .ring-2 { width: 80%; height: 80%; border-style: dotted; } | |
| /* Hands */ | |
| .hand { | |
| position: absolute; | |
| bottom: 50%; | |
| left: 50%; | |
| transform-origin: bottom center; | |
| border-radius: 4px; | |
| z-index: 5; | |
| transition: transform 0.1s cubic-bezier(0.4, 2.08, 0.55, 0.44); /* Elastic tick */ | |
| } | |
| .hand-hour { | |
| width: 8px; | |
| height: 140px; | |
| background: var(--accent-secondary); | |
| margin-left: -4px; | |
| box-shadow: 0 0 10px var(--accent-secondary); | |
| } | |
| .hand-minute { | |
| width: 4px; | |
| height: 190px; | |
| background: var(--accent-primary); | |
| margin-left: -2px; | |
| box-shadow: 0 0 10px var(--accent-primary); | |
| z-index: 6; | |
| } | |
| .hand-second { | |
| width: 2px; | |
| height: 210px; | |
| background: var(--accent-tertiary); | |
| margin-left: -1px; | |
| box-shadow: 0 0 5px var(--accent-tertiary); | |
| z-index: 7; | |
| } | |
| .center-dot { | |
| width: 16px; | |
| height: 16px; | |
| background: #fff; | |
| border-radius: 50%; | |
| position: absolute; | |
| z-index: 10; | |
| box-shadow: 0 0 10px #fff; | |
| } | |
| /* Duodecimal Markers */ | |
| .marker { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| } | |
| .marker span { | |
| position: absolute; | |
| left: 50%; | |
| top: 10px; | |
| transform-origin: 0 240px; /* Radius - padding */ | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: var(--text-main); | |
| text-shadow: 0 0 5px rgba(255,255,255,0.5); | |
| } | |
| /* Custom rotated characters for 2 and 3 */ | |
| .rot-2 { display: inline-block; transform: rotate(180deg); } | |
| .rot-3 { display: inline-block; transform: rotate(180deg); } | |
| /* Digital Display */ | |
| .digital-readout { | |
| margin-top: 2rem; | |
| text-align: center; | |
| background: var(--panel-bg); | |
| padding: 1rem 2rem; | |
| border-radius: 8px; | |
| border: var(--glass-border); | |
| backdrop-filter: blur(5px); | |
| } | |
| .time-display { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 2.5rem; | |
| color: var(--accent-primary); | |
| text-shadow: var(--shadow-glow); | |
| } | |
| .label { | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| color: var(--text-dim); | |
| margin-bottom: 0.5rem; | |
| } | |
| .solar-diff { | |
| font-size: 1rem; | |
| color: var(--accent-tertiary); | |
| margin-top: 0.5rem; | |
| } | |
| /* Controls Panel */ | |
| .controls-panel { | |
| background: var(--panel-bg); | |
| border: var(--glass-border); | |
| border-radius: 12px; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| backdrop-filter: blur(15px); | |
| overflow-y: auto; | |
| } | |
| .panel-header { | |
| font-size: 1.2rem; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| padding-bottom: 1rem; | |
| margin-bottom: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .input-group label { | |
| font-size: 0.9rem; | |
| color: var(--text-dim); | |
| } | |
| .input-wrapper { | |
| position: relative; | |
| } | |
| .input-wrapper i { | |
| position: absolute; | |
| left: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--accent-primary); | |
| } | |
| input[type="number"] { | |
| width: 100%; | |
| background: rgba(0,0,0,0.3); | |
| border: 1px solid #333; | |
| color: white; | |
| padding: 10px 10px 10px 35px; | |
| border-radius: 6px; | |
| font-family: 'JetBrains Mono', monospace; | |
| transition: border-color 0.3s; | |
| } | |
| input[type="number"]:focus { | |
| outline: none; | |
| border-color: var(--accent-primary); | |
| } | |
| .btn { | |
| background: linear-gradient(45deg, var(--accent-primary), #00aaff); | |
| border: none; | |
| color: #000; | |
| padding: 12px; | |
| border-radius: 6px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(0, 242, 255, 0.4); | |
| } | |
| .btn-outline { | |
| background: transparent; | |
| border: 1px solid var(--accent-primary); | |
| color: var(--accent-primary); | |
| } | |
| .btn-outline:hover { | |
| background: rgba(0, 242, 255, 0.1); | |
| box-shadow: none; | |
| } | |
| .info-box { | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 1rem; | |
| border-radius: 6px; | |
| font-size: 0.85rem; | |
| line-height: 1.4; | |
| color: #ccc; | |
| } | |
| .info-box strong { color: #fff; } | |
| .status-indicator { | |
| display: inline-block; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #444; | |
| margin-right: 5px; | |
| } | |
| .status-active { background: #0f0; box-shadow: 0 0 5px #0f0; } | |
| /* Responsive */ | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| height: auto; | |
| overflow-y: auto; | |
| } | |
| .clock-face { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| .marker span { transform-origin: 0 140px; font-size: 1.2rem; } | |
| .hand-hour { height: 80px; } | |
| .hand-minute { height: 110px; } | |
| .hand-second { height: 120px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-clock-rotate-left"></i> | |
| <span>DuoSolar <small style="font-size:0.6em; opacity:0.7;">v1.0</small></span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Left: Clock Visualization --> | |
| <section class="clock-container"> | |
| <div class="clock-face"> | |
| <div class="ring ring-1"></div> | |
| <div class="ring ring-2"></div> | |
| <!-- Duodecimal Markers (1-10, Dek, El) --> | |
| <div class="marker" id="clockMarkers"> | |
| <!-- JS will populate positions --> | |
| </div> | |
| <div class="hand hand-hour" id="handHour"></div> | |
| <div class="hand hand-minute" id="handMinute"></div> | |
| <div class="hand hand-second" id="handSecond"></div> | |
| <div class="center-dot"></div> | |
| </div> | |
| <div class="digital-readout"> | |
| <div class="label">Local Solar Time (Base-12)</div> | |
| <div class="time-display" id="digitalTime">00:00:00</div> | |
| <div class="solar-diff" id="solarDiff">Offset: 00m</div> | |
| </div> | |
| </section> | |
| <!-- Right: Controls & Data --> | |
| <section class="controls-panel"> | |
| <div class="panel-header"> | |
| <i class="fa-solid fa-satellite-dish"></i> | |
| Coordinates | |
| </div> | |
| <div class="input-group"> | |
| <label>Latitude</label> | |
| <div class="input-wrapper"> | |
| <i class="fa-solid fa-arrows-up-down"></i> | |
| <input type="number" id="latInput" placeholder="e.g. 40.7128" step="0.0001"> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label>Longitude</label> | |
| <div class="input-wrapper"> | |
| <i class="fa-solid fa-arrows-left-right"></i> | |
| <input type="number" id="lonInput" placeholder="e.g. -74.0060" step="0.0001"> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label>Altitude (meters) - Optional</label> | |
| <div class="input-wrapper"> | |
| <i class="fa-solid fa-mountain"></i> | |
| <input type="number" id="altInput" placeholder="e.g. 10" step="1"> | |
| </div> | |
| </div> | |
| <button class="btn" id="btnLocate"> | |
| <i class="fa-solid fa-location-crosshairs"></i> Auto-Detect Location | |
| </button> | |
| <div class="panel-header" style="margin-top: 1rem;"> | |
| <i class="fa-solid fa-circle-info"></i> | |
| About Duodecimal | |
| </div> | |
| <div class="info-box"> | |
| <p><strong>Base-12 System:</strong> This clock uses the duodecimal (dozenal) system.</p> | |
| <ul style="margin-left: 20px; margin-top: 5px;"> | |
| <li><strong>2 & 3:</strong> Rotated 180° (↊, ↋).</li> | |
| <li><strong>10:</strong> Represented as <span style="color:var(--accent-tertiary)">X</span> (Dek).</li> | |
| <li><strong>11:</strong> Represented as <span style="color:var(--accent-tertiary)">E</span> (El).</li> | |
| </ul> | |
| <p style="margin-top: 5px;">Time is calculated based on the sun's actual position relative to your longitude (Local Apparent Solar Time), converted into 12-hour cycles.</p> | |
| </div> | |
| <div class="info-box" style="margin-top: auto;"> | |
| <div><span class="status-indicator" id="statusDot"></span> <span id="statusText">System Idle</span></div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| /** | |
| * DUODECIMAL CLOCK & SOLAR TIME CALCULATOR | |
| * | |
| * 1. Handles Base-12 conversion and display. | |
| * 2. Calculates Equation of Time and Solar Noon offset. | |
| * 3. Manages Geolocation. | |
| */ | |
| // DOM Elements | |
| const handHour = document.getElementById('handHour'); | |
| const handMinute = document.getElementById('handMinute'); | |
| const handSecond = document.getElementById('handSecond'); | |
| const digitalTime = document.getElementById('digitalTime'); | |
| const solarDiffDisplay = document.getElementById('solarDiff'); | |
| const clockMarkers = document.getElementById('clockMarkers'); | |
| const latInput = document.getElementById('latInput'); | |
| const lonInput = document.getElementById('lonInput'); | |
| const altInput = document.getElementById('altInput'); | |
| const btnLocate = document.getElementById('btnLocate'); | |
| const statusDot = document.getElementById('statusDot'); | |
| const statusText = document.getElementById('statusText'); | |
| // State | |
| let state = { | |
| lat: 0, | |
| lon: 0, | |
| alt: 0, | |
| isLocating: false | |
| }; | |
| // --- Initialization --- | |
| function init() { | |
| generateClockFace(); | |
| // Set default to UTC (approximate) until location is set | |
| state.lat = 0; | |
| state.lon = 0; | |
| // Try to get location automatically | |
| getLocation(); | |
| // Start Loop | |
| requestAnimationFrame(updateClock); | |
| } | |
| // --- Duodecimal Helpers --- | |
| function toDuodecimal(num) { | |
| // Returns the character for a base-12 digit (0-11) | |
| const map = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'X', 'E']; | |
| return map[num]; | |
| } | |
| function formatDozenalTime(h, m, s) { | |
| // Input: Decimal hours (0-23), minutes, seconds | |
| // Output: Formatted string HH:MM:SS in Base-12 | |
| // Convert total hours to base-12 | |
| // We treat hours as integer 0-11 (12h clock) or 0-23 (24h). | |
| // Let's do 12h format for analog consistency. | |
| let h12 = h % 12; | |
| if (h12 === 0) h12 = 12; // Display as 12 (10 in dozenal) or 0? | |
| // Wait, 12 in decimal is 10 in dozenal. | |
| // Convert decimal h12 to dozenal string | |
| let hDoz = h12.toString(12).toUpperCase(); | |
| // Convert minutes to dozenal | |
| let mDoz = m.toString(12).toUpperCase().padStart(2, '0'); | |
| // Convert seconds to dozenal | |
| let sDoz = s.toString(12).toUpperCase().padStart(2, '0'); | |
| return `${hDoz}:${mDoz}:${sDoz}`; | |
| } | |
| function getDozenalChar(val) { | |
| // Helper for the clock face markers | |
| if (val === 10) return 'X'; | |
| if (val === 11) return 'E'; | |
| return val.toString(); | |
| } | |
| function generateClockFace() { | |
| // Generate markers 1 to 10, X, E (representing 1-12 in base-12 logic) | |
| // Actually, an analog clock face usually has 12 positions. | |
| // Position 1 = 1, Position 10 = X (Decimal 10), Position 11 = E (Decimal 11), Position 12 = 10 (Decimal 12). | |
| // However, standard clocks go 1..12. | |
| // In Base 12, the sequence is 1, 2, 3, 4, 5, 6, 7, 8, 9, X, E, 10. | |
| const labels = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'X', 'E', '10']; | |
| labels.forEach((label, index) => { | |
| const span = document.createElement('span'); | |
| // Handle rotation for 2 and 3 as per prompt requirement | |
| if (label === '2') { | |
| span.innerHTML = '<span class="rot-2">2</span>'; | |
| } else if (label === '3') { | |
| span.innerHTML = '<span class="rot-3">3</span>'; | |
| } else { | |
| span.textContent = label; | |
| } | |
| // Calculate rotation: 360 / 12 = 30 deg per step | |
| // We start at 1 (30 deg), so index 0 is 30deg. | |
| const rotation = (index + 1) * 30; | |
| span.style.transform = `rotate(${rotation}deg)`; | |
| // Fix text rotation so it's upright? | |
| // Usually on analog clocks text rotates with the hand. | |
| // But for readability, let's keep it rotated with the position. | |
| clockMarkers.appendChild(span); | |
| }); | |
| } | |
| // --- Solar Time Calculation --- | |
| function calculateSolarTime(date, lat, lon, alt) { | |
| // Based on NOAA Solar Calculator simplified algorithm | |
| // 1. Get Julian Date | |
| // 2. Get Julian Century | |
| // 3. Geom Mean Long Sun (deg) | |
| // 4. Geom Mean Anom Sun (deg) | |
| // 5. Eccent Earth Orbit | |
| // 6. Sun Eq of Ctr | |
| // 7. Sun True Long (deg) | |
| // 8. Sun True Anom (deg) | |
| // 9. Sun Rad Vector (AU) | |
| // 10. Sun App Long (deg) | |
| // 11. Mean Obliq Ecliptic (deg) | |
| // 12. Obliq Corr (deg) | |
| // 13. Sun Rt Ascen (deg) | |
| // 14. Sun Declin (deg) | |
| // 15. Var y | |
| // 16. Eq of Time (minutes) | |
| // 17. True Solar Time | |
| // 18. Hour Angle | |
| // 19. Solar Zenith | |
| // 20. Solar Azimuth | |
| // For this app, we primarily need the Equation of Time (EoT) to convert Local Standard Time to Local Solar Time. | |
| // Formula: LST = LT + 4*(Lst - Llocal) + EoT | |
| // Where Lst is standard longitude for timezone (e.g., 75 for EST-5), Llocal is user longitude. | |
| const TZ = date.getTimezoneOffset(); // in minutes (negative for ahead of UTC) | |
| // Calculate Standard Meridian for current Timezone | |
| // Timezones are usually 15 deg wide. | |
| // Example: UTC-5 (EST) is -5 * 15 = -75 degrees. | |
| const standardMeridian = - (TZ / 60) * 15; | |
| // --- Step 1: Julian Date --- | |
| // Note: Javascript Date uses UTC internally for calculation methods | |
| const yr = date.getUTCFullYear(); | |
| const mo = date.getUTCMonth() + 1; | |
| const dy = date.getUTCDate(); | |
| let jd = 367*yr - Math.floor(7*(yr+Math.floor((mo+9)/12))/4) + Math.floor(275*mo/9) + dy + 1721013.5 + (date.getUTCHours() + date.getUTCMinutes()/60 + date.getUTCSeconds()/3600)/24 - 0.5*0; // Simplified JD calc | |
| // --- Step 2: Julian Century --- | |
| const jc = (jd - 2451545.0) / 36525.0; | |
| // --- Step 3: Geom Mean Long Sun (deg) --- | |
| let gmls = 280.46646 + jc * (36000.76983 + jc * 0.0003032); | |
| gmls = gmls % 360; | |
| if(gmls < 0) gmls += 360; | |
| // --- Step 4: Geom Mean Anom Sun (deg) --- | |
| let gmas = 357.52911 + jc * (35999.05029 - 0.0001537 * jc); | |
| // --- Step 5: Eccent Earth Orbit --- | |
| let eeo = 0.016708634 - jc * (0.000042037 + 0.0000001267 * jc); | |
| // --- Step 6: Sun Eq of Ctr --- | |
| let sec = Math.sin(gmas * Math.PI / 180) * (1.914602 - jc * (0.004817 + 0.000014 * jc)) + | |
| Math.sin(2 * gmas * Math.PI / 180) * (0.019993 - 0.000101 * jc) + | |
| Math.sin(3 * gmas * Math.PI / 180) * 0.000289; | |
| // --- Step 7: Sun True Long (deg) --- | |
| let stl = gmls + sec; | |
| // --- Step 8: Sun True Anom (deg) --- | |
| let sta = gmas + sec; | |
| // --- Step 9: Sun Rad Vector (AU) --- | |
| let srv = (1.000001018 * (1 - eeo * eeo)) / (1 + eeo * Math.cos(sta * Math.PI / 180)); | |
| // --- Step 10: Sun App Long (deg) --- | |
| let sal = stl - 0.00569 - 0.00478 * Math.sin((125.04 - 1934.136 * jc) * Math.PI / 180); | |
| // --- Step 11: Mean Obliq Ecliptic (deg) --- | |
| let moe = 23 + (26 + ((21.448 - jc * (46.815 + jc * (0.00059 - jc * 0.001813)))) / 60) / 60; | |
| // --- Step 12: Obliq Corr (deg) --- | |
| let oc = moe + 0.00256 * Math.cos((125.04 - 1934.136 * jc) * Math.PI / 180); | |
| // --- Step 13: Sun Rt Ascen (deg) --- | |
| let sra = Math.atan2(Math.cos(oc * Math.PI / 180) * Math.sin(sal * Math.PI / 180), Math.cos(sal * Math.PI / 180)) * 180 / Math.PI; | |
| // --- Step 14: Sun Declin (deg) --- | |
| let sd = Math.asin(Math.sin(oc * Math.PI / 180) * Math.sin(sal * Math.PI / 180)) * 180 / Math.PI; | |
| // --- Step 15: Var y --- | |
| let vy = Math.tan(oc * Math.PI / 180 / 2) * Math.tan(oc * Math.PI / 180 / 2); | |
| // --- Step 16: Eq of Time (minutes) --- | |
| let eot = 4 * (vy * Math.sin(2 * gmls * Math.PI / 180) - | |
| 2 * eeo * Math.sin(gmas * Math.PI / 180) + | |
| 4 * eeo * vy * Math.sin(gmas * Math.PI / 180) * Math.cos(2 * gmls * Math.PI / 180) - | |
| 0.5 * vy * vy * Math.sin(4 * gmls * Math.PI / 180) - | |
| 1.25 * eeo * eeo * Math.sin(2 * gmas * Math.PI / 180)); | |
| // --- Calculate True Solar Time --- | |
| // TST = StandardTime + 4(Lst - Llocal) + EoT | |
| // Note: Lst - Llocal. If user is West of Standard Meridian (e.g. 80 deg vs 75 deg), Lst-Llocal is -5. | |
| // 4 * -5 = -20 minutes. Sun is later. | |
| const timeOffset = 4 * (standardMeridian - lon); | |
| const totalOffsetMinutes = timeOffset + eot; | |
| // Current UTC time in minutes | |
| const utcHours = date.getUTCHours(); | |
| const utcMinutes = date.getUTCMinutes(); | |
| const utcSeconds = date.getUTCSeconds(); | |
| const utcMillis = date.getUTCMilliseconds(); | |
| // Convert current UTC to Decimal Hours | |
| const decimalTimeUTC = utcHours + utcMinutes/60 + utcSeconds/3600 + utcMillis/3600000; | |
| // Apply Timezone offset to get Local Standard Time (Decimal) | |
| // TZ is in minutes (negative if ahead of UTC) | |
| const decimalTimeLST = decimalTimeUTC - (TZ / 60); | |
| // Apply Solar Correction | |
| let decimalTimeSolar = decimalTimeLST + (totalOffsetMinutes / 60); | |
| // Normalize to 0-24 | |
| if (decimalTimeSolar < 0) decimalTimeSolar += 24; | |
| if (decimalTimeSolar >= 24) decimalTimeSolar -= 24; | |
| return { | |
| solarDecimal: decimalTimeSolar, | |
| eot: eot, | |
| timeCorrection: timeOffset | |
| }; | |
| } | |
| // --- UI Update Loop --- | |
| function updateClock() { | |
| const now = new Date(); | |
| // 1. Calculate Solar Time | |
| const solarData = calculateSolarTime(now, state.lat, state.lon, state.alt); | |
| const sDecimal = solarData.solarDecimal; | |
| // 2. Extract H, M, S from Decimal Solar Time | |
| let h = Math.floor(sDecimal); | |
| let m = Math.floor((sDecimal - h) * 60); | |
| let s = Math.floor(((sDecimal - h) * 60 - m) * 60); | |
| let ms = Math.floor(((((sDecimal - h) * 60 - m) * 60) - s) * 1000); | |
| // 3. Update Digital Display (Base 12) | |
| digitalTime.textContent = formatDozenalTime(h, m, s); | |
| // Update Offset Text | |
| const totalDiff = solarData.timeCorrection + solarData.eot; | |
| const sign = totalDiff >= 0 ? '+' : ''; | |
| solarDiffDisplay.textContent = `Solar Offset: ${sign}${totalDiff.toFixed(2)} min`; | |
| // 4. Update Analog Hands (Base 12 Geometry) | |
| // In Base 12, the circle is divided into 12 hours. | |
| // Hour hand: 360 deg / 12 = 30 deg per hour. | |
| // Minute hand: 360 deg / 12 = 30 deg per "dozenal minute" (which is 5 standard minutes). | |
| // Second hand: 360 deg / 12 = 30 deg per "dozenal second" (which is 5 standard seconds). | |
| // Convert standard time to "dozenal fractions" for smooth animation | |
| // Hour angle (Standard 0-24 -> 0-12 for face) | |
| // sDecimal is 0-24. We need 0-12. | |
| const h12 = sDecimal % 12; | |
| const hAngle = (h12 * 30) + (m * 0.5); // 30deg per hour + minutes contribution | |
| // Minute Angle | |
| // Standard minutes (0-60) map to 12 positions (0-11). | |
| // 60 / 12 = 5 mins per tick. | |
| // m / 5 = dozenal minute value. | |
| const mDozenalVal = m / 5 + (s / 300); // Add seconds for smoothness | |
| const mAngle = mDozenalVal * 30; | |
| // Second Angle | |
| // Standard seconds (0-60) map to 12 positions. | |
| // 60 / 12 = 5 seconds per tick. | |
| const sDozenalVal = s / 5 + (ms / 5000); | |
| const sAngle = sDozenalVal * 30; | |
| handHour.style.transform = `rotate(${hAngle}deg)`; | |
| handMinute.style.transform = `rotate(${mAngle}deg)`; | |
| handSecond.style.transform = `rotate(${sAngle}deg)`; | |
| requestAnimationFrame(updateClock); | |
| } | |
| // --- Geolocation Logic --- | |
| function getLocation() { | |
| if (!navigator.geolocation) { | |
| setStatus("Geolocation not supported", false); | |
| return; | |
| } | |
| setStatus("Locating...", true); | |
| state.isLocating = true; | |
| btnLocate.disabled = true; | |
| navigator.geolocation.getCurrentPosition( | |
| (position) => { | |
| state.lat = position.coords.latitude; | |
| state.lon = position.coords.longitude; | |
| state.alt = position.coords.altitude || 0; | |
| latInput.value = state.lat; | |
| lonInput.value = state.lon; | |
| altInput.value = state.alt; | |
| setStatus("Location Active", true); | |
| state.isLocating = false; | |
| btnLocate.disabled = false; | |
| }, | |
| (error) => { | |
| console.error(error); | |
| setStatus("Location Failed", false); | |
| state.isLocating = false; | |
| btnLocate.disabled = false; | |
| alert("Could not retrieve location. Please enter coordinates manually."); | |
| }, | |
| { enableHighAccuracy: true } | |
| ); | |
| } | |
| function setStatus(text, isActive) { | |
| statusText.textContent = text; | |
| if (isActive) { | |
| statusDot.classList.add('status-active'); | |
| } else { | |
| statusDot.classList.remove('status-active'); | |
| } | |
| } | |
| // --- Event Listeners --- | |
| btnLocate.addEventListener('click', getLocation); | |
| [latInput, lonInput, altInput].forEach(input => { | |
| input.addEventListener('change', () => { | |
| state.lat = parseFloat(latInput.value) || 0; | |
| state.lon = parseFloat(lonInput.value) || 0; | |
| state.alt = parseFloat(altInput.value) || 0; | |
| setStatus("Manual Coords Set", true); | |
| }); | |
| }); | |
| // Run | |
| init(); | |
| </script> | |
| </body> | |
| </html> |