Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Responsive Analog Clock</title> | |
| <style> | |
| :root { | |
| --bg: #ffffff; | |
| --ink: #0f172a; /* primary text/face color */ | |
| --muted: #64748b; /* minute tick color */ | |
| --accent: #ef4444; /* second hand */ | |
| --accent-weak: #fca5a5; | |
| --ring: #0f172a; | |
| --shadow: 0 20px 40px rgba(2, 6, 23, 0.12); | |
| --ring-width: clamp(6px, 0.8vmin, 10px); | |
| --center-cap: clamp(8px, 1vmin, 14px); | |
| --major-tick: clamp(4px, 0.6vmin, 6px); | |
| --minor-tick: clamp(1px, 0.25vmin, 2px); | |
| --num-size: clamp(14px, 3.4vmin, 28px); | |
| --brand-size: clamp(11px, 2.6vmin, 16px); | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { | |
| height: 100%; | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--ink); | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; | |
| } | |
| .wrap { | |
| min-height: 100%; | |
| display: grid; | |
| grid-template-rows: auto 1fr auto; | |
| align-items: center; | |
| justify-items: center; | |
| gap: 1rem; | |
| padding: 2rem 1rem 1.5rem; | |
| } | |
| header { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 1rem; | |
| justify-content: center; | |
| width: 100%; | |
| max-width: 1100px; | |
| } | |
| .title { | |
| font-weight: 700; | |
| letter-spacing: 0.02em; | |
| font-size: clamp(18px, 2.6vmin, 28px); | |
| } | |
| .subtitle { | |
| color: var(--muted); | |
| font-size: clamp(12px, 2.2vmin, 16px); | |
| } | |
| a.brand { | |
| margin-left: auto; | |
| text-decoration: none; | |
| color: var(--ink); | |
| font-size: var(--brand-size); | |
| opacity: 0.8; | |
| border-bottom: 1px solid color-mix(in oklab, var(--ink), transparent 80%); | |
| transition: opacity .2s ease, transform .2s ease; | |
| } | |
| a.brand:hover { opacity: 1; transform: translateY(-1px); } | |
| .digital { | |
| font-variant-numeric: tabular-nums; | |
| letter-spacing: 0.06em; | |
| font-weight: 600; | |
| font-size: clamp(16px, 3.2vmin, 24px); | |
| padding: 0.35rem 0.75rem; | |
| border-radius: 999px; | |
| border: 1px solid color-mix(in oklab, var(--ink), transparent 88%); | |
| background: #fff; | |
| box-shadow: var(--shadow); | |
| user-select: none; | |
| } | |
| .clock { | |
| position: relative; | |
| width: min(86vmin, 540px); | |
| aspect-ratio: 1; | |
| border-radius: 50%; | |
| border: var(--ring-width) solid var(--ring); | |
| background: | |
| radial-gradient(circle at 50% 50%, #fff 0 68%, #f8fafc 69% 100%); /* subtle inner ring */ | |
| box-shadow: var(--shadow); | |
| overflow: hidden; | |
| isolation: isolate; | |
| } | |
| /* Minute ring (60 ticks) */ | |
| .clock::before { | |
| content: ""; | |
| position: absolute; | |
| inset: clamp(10px, 2.2vmin, 18px); | |
| border-radius: 50%; | |
| background: | |
| /* minor ticks (59) */ | |
| repeating-conic-gradient( | |
| from -90deg, | |
| color-mix(in oklab, var(--muted), transparent 40%) 0deg, | |
| color-mix(in oklab, var(--muted), transparent 40%) 1deg, | |
| transparent 1deg 6deg | |
| ), | |
| /* major ticks (12) */ | |
| repeating-conic-gradient( | |
| from -90deg, | |
| var(--muted) 0deg, | |
| var(--muted) 2.2deg, | |
| transparent 2.2deg 30deg | |
| ); | |
| -webkit-mask: radial-gradient(circle, #0000 calc(100% - clamp(12px, 1.6vmin, 16px)), #000 calc(100% - clamp(12px, 1.6vmin, 16px))); | |
| mask: radial-gradient(circle, #0000 calc(100% - clamp(12px, 1.6vmin, 16px)), #000 calc(100% - clamp(12px, 1.6vmin, 16px))); | |
| pointer-events: none; | |
| } | |
| /* Numbers */ | |
| .numbers { | |
| position: absolute; | |
| inset: 0; | |
| border-radius: 50%; | |
| } | |
| .num { | |
| position: absolute; | |
| left: 50%; | |
| top: 50%; | |
| transform-origin: center center; | |
| font-weight: 700; | |
| font-size: var(--num-size); | |
| user-select: none; | |
| pointer-events: none; | |
| color: var(--ink); | |
| filter: drop-shadow(0 1px 0 rgba(2,6,23,0.06)); | |
| transition: transform 0.2s ease; | |
| } | |
| .num small { | |
| font-size: 0.72em; | |
| font-weight: 600; | |
| opacity: 0.85; | |
| } | |
| /* Hands */ | |
| .hands { position: absolute; inset: 0; } | |
| .hand { | |
| position: absolute; | |
| left: 50%; | |
| bottom: 50%; | |
| transform: translateX(-50%) rotate(0deg); | |
| transform-origin: 50% 100%; | |
| border-radius: 999px; | |
| will-change: transform; | |
| } | |
| .hand.hour { | |
| width: clamp(7px, 1vmin, 10px); | |
| height: 24%; | |
| background: linear-gradient(#111, #1f2937); | |
| box-shadow: 0 6px 10px rgba(2,6,23,0.15); | |
| } | |
| .hand.minute { | |
| width: clamp(5px, 0.8vmin, 8px); | |
| height: 34%; | |
| background: linear-gradient(#111, #1f2937); | |
| box-shadow: 0 6px 10px rgba(2,6,23,0.15); | |
| } | |
| /* Second hand (animated with CSS) */ | |
| .hand.second { | |
| width: clamp(2.5px, 0.55vmin, 4px); | |
| height: 40%; | |
| background: var(--accent); | |
| box-shadow: 0 6px 10px rgba(239, 68, 68, 0.3); | |
| --spin-duration: 60s; | |
| animation: spin linear infinite; | |
| animation-duration: var(--spin-duration); | |
| } | |
| /* Smooth continuous animation, not tied to wall-clock seconds */ | |
| @keyframes spin { | |
| from { transform: translateX(-50%) rotate(0deg); } | |
| to { transform: translateX(-50%) rotate(360deg); } | |
| } | |
| /* Optional: pause if user prefers reduced motion */ | |
| @media (prefers-reduced-motion: reduce) { | |
| .hand.second { animation: none; } | |
| } | |
| /* Center cap */ | |
| .pivot { | |
| position: absolute; | |
| left: 50%; | |
| top: 50%; | |
| width: clamp(16px, 2.4vmin, 22px); | |
| height: clamp(16px, 2.4vmin, 22px); | |
| transform: translate(-50%, -50%); | |
| background: #fff; | |
| border: clamp(3px, 0.5vmin, 4px) solid var(--ink); | |
| border-radius: 50%; | |
| box-shadow: inset 0 1px 1px rgba(2,6,23,0.15), 0 2px 4px rgba(2,6,23,0.12); | |
| z-index: 2; | |
| } | |
| .pivot::after { | |
| content: ""; | |
| position: absolute; | |
| left: 50%; | |
| top: 50%; | |
| width: var(--center-cap); | |
| height: var(--center-cap); | |
| transform: translate(-50%, -50%); | |
| background: var(--accent); | |
| border-radius: 50%; | |
| box-shadow: 0 0 0 2px color-mix(in oklab, #fff, transparent 25%); | |
| } | |
| /* Helper: tiny brand at bottom inside the clock */ | |
| .brand-inside { | |
| position: absolute; | |
| bottom: clamp(10px, 2.4vmin, 18px); | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: clamp(10px, 2.2vmin, 12px); | |
| color: color-mix(in oklab, var(--ink), transparent 50%); | |
| user-select: none; | |
| pointer-events: none; | |
| } | |
| /* Accessibility: focus outline on digital readout for keyboard users */ | |
| .digital:focus-visible { | |
| outline: 2px solid color-mix(in oklab, var(--ink), transparent 60%); | |
| outline-offset: 4px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <header> | |
| <div class="title">Responsive Analog Clock</div> | |
| <div class="subtitle">White background • CSS-animated second hand</div> | |
| <a class="brand" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noreferrer noopener">Built with anycoder</a> | |
| </header> | |
| <div class="digital" id="digital" aria-live="polite" title="Current time"> | |
| <!-- JS fills HH:MM:SS --> | |
| </div> | |
| <div class="clock" id="clock" aria-label="Analog clock"> | |
| <div class="numbers" id="numbers" aria-hidden="true"></div> | |
| <div class="hands" aria-hidden="true"> | |
| <div class="hand hour" id="hourHand"></div> | |
| <div class="hand minute" id="minuteHand"></div> | |
| <div class="hand second" id="secondHand"></div> | |
| </div> | |
| <div class="pivot" aria-hidden="true"></div> | |
| <div class="brand-inside">anycoder</div> | |
| </div> | |
| </div> | |
| <script> | |
| (function () { | |
| const els = { | |
| digital: document.getElementById('digital'), | |
| clock: document.getElementById('clock'), | |
| numbers: document.getElementById('numbers'), | |
| hour: document.getElementById('hourHand'), | |
| minute: document.getElementById('minuteHand'), | |
| second: document.getElementById('secondHand') | |
| }; | |
| // Create numbers (12 → Roman numerals), 24 → Arabic numerals | |
| function makeNumbers(use24 = false) { | |
| els.numbers.innerHTML = ''; | |
| const fragment = document.createDocumentFragment(); | |
| for (let i = 1; i <= 12; i++) { | |
| const n = document.createElement('div'); | |
| n.className = 'num'; | |
| const label = use24 ? (i + 12) : toRoman(i); | |
| n.textContent = label; | |
| n.dataset.h = i; // store hour | |
| fragment.appendChild(n); | |
| } | |
| els.numbers.appendChild(fragment); | |
| layoutNumbers(); | |
| } | |
| function toRoman(num) { | |
| const map = [ | |
| [1000,'M'], [900,'CM'], [500,'D'], [400,'CD'], | |
| [100,'C'], [90,'XC'], [50,'L'], [40,'XL'], | |
| [10,'X'], [9,'IX'], [5,'V'], [4,'IV'], [1,'I'] | |
| ]; | |
| let out = ''; | |
| for (const [v, s] of map) { | |
| while (num >= v) { out += s; num -= v; } | |
| } | |
| return out; | |
| } | |
| function layoutNumbers() { | |
| const radiusPct = 42; // percentage of radius for text baseline | |
| const nums = els.numbers.querySelectorAll('.num'); | |
| nums.forEach(n => { | |
| const h = parseInt(n.dataset.h, 10); | |
| const deg = h * 30; // 360/12 | |
| n.style.transform = `translate(-50%, -50%) rotate(${deg}deg) translateY(-${radiusPct}%) rotate(${-deg}deg)`; | |
| }); | |
| } | |
| // Update hands every frame (hour/minute) to catch time changes smoothly | |
| let lastSec = -1; | |
| function update() { | |
| const now = new Date(); | |
| const ms = now.getMilliseconds(); | |
| const s = now.getSeconds() + ms / 1000; // smooth fraction | |
| const m = now.getMinutes() + s / 60; | |
| const h = (now.getHours() % 12) + m / 60; | |
| // Hour hand | |
| els.hour.style.transform = `translateX(-50%) rotate(${h * 30}deg)`; | |
| // Minute hand | |
| els.minute.style.transform = `translateX(-50%) rotate(${m * 6}deg)`; | |
| // Digital readout (update only when seconds change to reduce reflow) | |
| const secInt = now.getSeconds(); | |
| if (secInt !== lastSec) { | |
| els.digital.textContent = now.toLocaleTimeString([], { hour12: false }); | |
| lastSec = secInt; | |
| } | |
| requestAnimationFrame(update); | |
| } | |
| // Responsive: re-layout numbers when size changes | |
| const ro = new ResizeObserver(() => layoutNumbers()); | |
| ro.observe(els.clock); | |
| // Initialize | |
| makeNumbers(false); | |
| update(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |