263 lines
7.0 KiB
HTML
263 lines
7.0 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<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 {
|
||
|
|
--size: min(78vmin, 520px);
|
||
|
|
--border: 8px;
|
||
|
|
--rim: 10px;
|
||
|
|
--tick: #c7cbd1;
|
||
|
|
--face: #ffffff;
|
||
|
|
--numbers: #111;
|
||
|
|
--hands-hour: #111;
|
||
|
|
--hands-minute: #111;
|
||
|
|
--hands-second: #e63946;
|
||
|
|
--center: #111;
|
||
|
|
}
|
||
|
|
|
||
|
|
* { box-sizing: border-box; }
|
||
|
|
|
||
|
|
html, body {
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
margin: 0;
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
background: #ffffff; /* white background */
|
||
|
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||
|
|
color: #111;
|
||
|
|
}
|
||
|
|
|
||
|
|
.clock {
|
||
|
|
position: relative;
|
||
|
|
width: var(--size);
|
||
|
|
aspect-ratio: 1;
|
||
|
|
background: var(--face);
|
||
|
|
border: var(--border) solid #111;
|
||
|
|
border-radius: 50%;
|
||
|
|
box-shadow:
|
||
|
|
0 12px 28px rgba(0,0,0,.08),
|
||
|
|
0 2px 6px rgba(0,0,0,.05) inset;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Outer decorative rim */
|
||
|
|
.clock::before {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
inset: calc(var(--border) + var(--rim));
|
||
|
|
border-radius: 50%;
|
||
|
|
border: 2px solid #e6e8eb;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Minute/hour ticks */
|
||
|
|
.ticks {
|
||
|
|
position: absolute;
|
||
|
|
inset: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tick {
|
||
|
|
position: absolute;
|
||
|
|
left: 50%;
|
||
|
|
top: 50%;
|
||
|
|
width: 2px;
|
||
|
|
height: 8%;
|
||
|
|
background: var(--tick);
|
||
|
|
transform-origin: 50% 50%;
|
||
|
|
opacity: .9;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Thicker hour ticks (12) */
|
||
|
|
.tick.hour {
|
||
|
|
width: 4px;
|
||
|
|
height: 12%;
|
||
|
|
background: #8f95a1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Numerals */
|
||
|
|
.numeral {
|
||
|
|
position: absolute;
|
||
|
|
left: 50%;
|
||
|
|
top: 50%;
|
||
|
|
color: var(--numbers);
|
||
|
|
font-weight: 600;
|
||
|
|
transform-origin: 50% 50%;
|
||
|
|
user-select: none;
|
||
|
|
-webkit-user-select: none;
|
||
|
|
text-shadow: 0 1px 0 rgba(0,0,0,0.05);
|
||
|
|
}
|
||
|
|
.numeral span {
|
||
|
|
display: inline-block;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
font-size: clamp(14px, 5.2vmin, 28px);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Hands */
|
||
|
|
.hand {
|
||
|
|
position: absolute;
|
||
|
|
left: 50%;
|
||
|
|
top: 50%;
|
||
|
|
transform-origin: 50% 100%;
|
||
|
|
transform: translate(-50%, -100%) rotate(0deg);
|
||
|
|
border-radius: 999px;
|
||
|
|
box-shadow: 0 1px 2px rgba(0,0,0,.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.hand.hour {
|
||
|
|
width: 8px;
|
||
|
|
height: 26%;
|
||
|
|
background: var(--hands-hour);
|
||
|
|
z-index: 3;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hand.minute {
|
||
|
|
width: 6px;
|
||
|
|
height: 36%;
|
||
|
|
background: var(--hands-minute);
|
||
|
|
z-index: 4;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hand.second {
|
||
|
|
width: 3px;
|
||
|
|
height: 42%;
|
||
|
|
background: var(--hands-second);
|
||
|
|
z-index: 5;
|
||
|
|
/* Tail counterweight */
|
||
|
|
}
|
||
|
|
.hand.second::after {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
left: 50%;
|
||
|
|
bottom: 0;
|
||
|
|
transform: translateX(-50%);
|
||
|
|
width: 3px;
|
||
|
|
height: 16%;
|
||
|
|
background: var(--hands-second);
|
||
|
|
border-radius: 3px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Center cap */
|
||
|
|
.center-cap {
|
||
|
|
position: absolute;
|
||
|
|
left: 50%;
|
||
|
|
top: 50%;
|
||
|
|
width: 14px;
|
||
|
|
height: 14px;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
background: var(--center);
|
||
|
|
border-radius: 50%;
|
||
|
|
z-index: 6;
|
||
|
|
box-shadow: 0 0 0 3px #fff, 0 1px 3px rgba(0,0,0,.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* CSS-animated second hand (smooth sweep) */
|
||
|
|
.spin {
|
||
|
|
animation-name: sweep;
|
||
|
|
animation-duration: 60s;
|
||
|
|
animation-timing-function: linear;
|
||
|
|
animation-iteration-count: infinite;
|
||
|
|
animation-play-state: running;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes sweep {
|
||
|
|
from { transform: translate(-50%, -100%) rotate(0deg); }
|
||
|
|
to { transform: translate(-50%, -100%) rotate(360deg); }
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Accessibility: reduce motion users still get a ticking second hand */
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
.spin {
|
||
|
|
animation-duration: 1s; /* rapid ticks */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="clock" id="clock">
|
||
|
|
<div class="ticks" aria-hidden="true"></div>
|
||
|
|
<div class="numeral" aria-hidden="true"><span>12</span></div>
|
||
|
|
<div class="numeral" aria-hidden="true"><span>3</span></div>
|
||
|
|
<div class="numeral" aria-hidden="true"><span>6</span></div>
|
||
|
|
<div class="numeral" aria-hidden="true"><span>9</span></div>
|
||
|
|
|
||
|
|
<div class="hand hour" id="hourHand"></div>
|
||
|
|
<div class="hand minute" id="minuteHand"></div>
|
||
|
|
<div class="hand second spin" id="secondHand"></div>
|
||
|
|
|
||
|
|
<div class="center-cap" aria-hidden="true"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
(function () {
|
||
|
|
const hourHand = document.getElementById('hourHand');
|
||
|
|
const minuteHand = document.getElementById('minuteHand');
|
||
|
|
const secondHand = document.getElementById('secondHand');
|
||
|
|
|
||
|
|
// Create minute and hour ticks (60 total, 12 hour markers)
|
||
|
|
const ticksContainer = document.querySelector('.ticks');
|
||
|
|
for (let i = 0; i < 60; i++) {
|
||
|
|
const tick = document.createElement('div');
|
||
|
|
tick.className = 'tick' + (i % 5 === 0 ? ' hour' : '');
|
||
|
|
tick.style.transform = `translate(-50%, -50%) rotate(${i * 6}deg)`;
|
||
|
|
ticksContainer.appendChild(tick);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Position numerals
|
||
|
|
const numerals = document.querySelectorAll('.numeral');
|
||
|
|
const labels = ['12', '3', '6', '9'];
|
||
|
|
numerals.forEach((el, i) => {
|
||
|
|
const angle = i * 90; // 0,90,180,270
|
||
|
|
el.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
|
||
|
|
el.firstElementChild.textContent = labels[i];
|
||
|
|
el.firstElementChild.style.left = '50%';
|
||
|
|
el.firstElementChild.style.top = '50%';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Helpers
|
||
|
|
const setAngle = (el, deg) => {
|
||
|
|
el.style.transform = `translate(-50%, -100%) rotate(${deg}deg)`;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initial set from system time
|
||
|
|
function updateHands() {
|
||
|
|
const now = new Date();
|
||
|
|
const ms = now.getMilliseconds();
|
||
|
|
const s = now.getSeconds() + ms / 1000;
|
||
|
|
const m = now.getMinutes() + s / 60;
|
||
|
|
const h = (now.getHours() % 12) + m / 60;
|
||
|
|
|
||
|
|
setAngle(hourHand, h * 30);
|
||
|
|
setAngle(minuteHand, m * 6);
|
||
|
|
// Second hand is CSS-animated; align its animation phase to current time.
|
||
|
|
const delay = -(now.getSeconds() + ms / 1000); // start at exact current second
|
||
|
|
secondHand.style.animationDelay = `${delay}s`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keep minute/hour hands in sync every minute (minimal JS)
|
||
|
|
updateHands();
|
||
|
|
let minuteTimer = null;
|
||
|
|
const scheduleNextMinuteUpdate = () => {
|
||
|
|
const now = new Date();
|
||
|
|
const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds() + 25; // +25ms buffer
|
||
|
|
clearTimeout(minuteTimer);
|
||
|
|
minuteTimer = setTimeout(() => {
|
||
|
|
updateHands();
|
||
|
|
scheduleNextMinuteUpdate();
|
||
|
|
}, Math.max(250, msToNextMinute));
|
||
|
|
};
|
||
|
|
scheduleNextMinuteUpdate();
|
||
|
|
|
||
|
|
// Also re-sync on visibility change (when tab becomes active again)
|
||
|
|
document.addEventListener('visibilitychange', () => {
|
||
|
|
if (!document.hidden) updateHands();
|
||
|
|
});
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|