<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>多语言轮盘时钟</title>
<style>
*,
::before,
::after {
box-sizing: border-box;
}
:root {
--clr-bg: rgb(3 3 3);
--clock-size: 800px;
--clock-clr: rgb(12, 74, 110);
}
body {
margin: 0;
min-height: 100svh;
display: grid;
place-content: center;
font-family: system-ui;
background-color: var(--clr-bg);
background-image: radial-gradient(rgb(8, 47, 73), rgb(8, 47, 60));
background-blend-mode: difference;
}
.clock {
position: fixed;
inset: 0;
margin: auto;
width: var(--clock-size);
height: var(--clock-size);
aspect-ratio: 1;
place-content: center;
background: var(--clock-clr);
border-radius: 50%;
}
@media (width < 800px) {
.clock {
left: 0;
right: auto;
translate: calc((50% - 2rem) * -1) 0;
}
}
.clock::before {
content: '';
position: absolute;
inset: 1px;
margin: auto;
background-color: rgba(0 0 0 / 0.85);
clip-path: polygon(
0 0,
100% 0,
100% 48%,
50% 48%,
50% 52%,
100% 52%,
100% 100%,
0 100%
);
border-radius: 50%;
z-index: 20;
}
.clock > div {
position: absolute;
inset: 0;
margin: auto;
width: var(--clock-d);
height: var(--clock-d);
font-size: var(--f-size, 0.9rem);
aspect-ratio: 1;
isolation: isolate;
aspect-ratio: 1;
border-radius: 50%;
}
.clock > div:nth-of-type(1) {
--clock-d: calc(var(--clock-size) - 20px);
}
.clock > div:nth-of-type(2) {
--clock-d: calc(var(--clock-size) - 130px);
}
.clock > div:nth-child(3) {
--clock-d: calc(var(--clock-size) - 195px);
}
.clock > div:nth-child(4) {
--clock-d: calc(var(--clock-size) - 260px);
}
.clock > div:nth-child(5) {
--clock-d: calc(var(--clock-size) - 350px);
}
.clock > div:nth-child(6) {
--clock-d: calc(var(--clock-size) - 470px);
}
.clock > div:nth-child(7) {
--clock-d: calc(var(--clock-size) - 600px);
}
.clock-face {
position: relative;
width: 100%;
height: 100%;
aspect-ratio: 1;
border-radius: 50%;
transition: 300ms linear;
}
.clock-face > * {
position: absolute;
transform-origin: center;
white-space: nowrap;
color: white;
opacity: 0.75;
&.active {
opacity: 1;
}
}
.clock > .current-lang-display {
position: absolute;
inset: 0;
margin: auto;
z-index: 100;
display: grid;
place-content: center;
background-color: var(--clock-clr);
border: 1px solid rgba(255 255 255 / 0.25);
color: white;
border-radius: 50%;
width: 40px;
height: 40px;
aspect-ratio: 1/1;
cursor: pointer;
transition: 300ms ease-in-out;
font-size: 1.5rem;
outline: none;
&:focus-visible,
&:hover {
background-color: white;
}
}
.current-lang-display::before,
.current-lang-display::after {
content: ': ';
color: white;
position: absolute;
z-index: 199;
top: 50%;
right: 0;
font-size: 0.9rem;
translate: 283px -10px;
}
.current-lang-display::after {
font-size: 0.9rem;
translate: 250px -10px;
}
dialog {
width: min(calc(100% - 2rem), 380px);
padding: 1rem;
border: none;
border-radius: 999px;
background: rgba(0 0 0 / 0.25);
text-align: center;
aspect-ratio: 1;
overflow: visible;
@starting-style {
opacity: 0;
scale: 0;
}
transition: opacity 500ms ease-in,
scale 500ms cubic-bezier(0.28, -0.55, 0.27, 1.55);
}
dialog[open]::backdrop {
background-color: rgba(from black r g b / 0.5);
backdrop-filter: blur(3px);
opacity: 1;
@starting-style {
opacity: 0;
}
transition: opacity 1000ms ease-in;
}
dialog .btn-dialog-close {
position: absolute;
top: 0rem;
right: 25%;
aspect-ratio: 1;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: black;
font-size: 1.2rem;
color: white;
border: none;
outline: none;
cursor: pointer;
transition: rotate 300ms ease-in-out;
z-index: 11;
&:focus-visible,
&:hover {
rotate: 90deg;
}
}
.language-options {
position: absolute;
inset: 0;
margin: auto;
border-radius: 50%;
aspect-ratio: 1/1;
overflow: hidden;
}
.language-options > label {
position: absolute;
transform: translate(-50%, -50%);
cursor: pointer;
font-size: 0.9rem;
aspect-ratio: 1/1;
border-radius: 50%;
width: 36px;
height: 36px;
transition: 300ms ease-in-out;
display: grid;
place-content: center;
transform-origin: center;
&.active {
color: white;
background: var(--clock-clr);
}
&:focus-visible,
&:hover {
scale: 1.1;
z-index: 2;
}
}
.language-title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: white;
font-size: 1.2rem;
@starting-style {
opacity: 0;
}
transition: opacity 300ms ease-in-out;
}
.flag-icon {
font-size: 1.5rem;
display: grid;
place-content: center;
}
.language-options input[type='radio'] {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
clip: rect(0, 0, 0, 0);
overflow: hidden;
}
</style>
</head>
<body>
<div class="clock" data-date="2024-12-25">
<div>
<div data-clock="years" data-numbers="101" class="clock-face"></div>
</div>
<div>
<div data-clock="seconds" data-numbers="60" class="clock-face"></div>
</div>
<div>
<div data-clock="minutes" data-numbers="60" class="clock-face"></div>
</div>
<div>
<div data-clock="hours" data-numbers="24" class="clock-face"></div>
</div>
<div>
<div data-clock="days" data-numbers="31" class="clock-face"></div>
</div>
<div>
<div data-clock="months" data-numbers="12" class="clock-face"></div>
</div>
<div>
<div data-clock="day-names" data-numbers="7" class="clock-face"></div>
</div>
<button type="button" id="current-lang" class="current-lang-display">
en
</button>
</div>
<dialog id="language-dialog">
<button
type="button"
id="btn-dialog-close"
class="btn-dialog-close"
autofocus
>
✕
</button>
<div id="language-options" class="language-options"></div>
</dialog>
<script>
console.clear();
const languageFlags = [
{ code: 'ar-SA', name: 'Arabic (Saudi Arabia)', flag: '🇸🇦' },
{ code: 'cs-CZ', name: 'Czech (Czech Republic)', flag: '🇨🇿' },
{ code: 'da-DK', name: 'Danish (Denmark)', flag: '🇩🇰' },
{ code: 'de-DE', name: 'German (Germany)', flag: '🇩🇪' },
{ code: 'el-GR', name: 'Greek (Greece)', flag: '🇬🇷' },
{ code: 'en-US', name: 'English (US)', flag: '🇺🇸' },
{ code: 'en-GB', name: 'English (UK)', flag: '🇬🇧' },
{ code: 'es-ES', name: 'Spanish (Spain)', flag: '🇪🇸' },
{ code: 'es-MX', name: 'Spanish (Mexico)', flag: '🇲🇽' },
{ code: 'fi-FI', name: 'Finnish (Finland)', flag: '🇫🇮' },
{ code: 'fr-CA', name: 'French (Canada)', flag: '🇨🇦' },
{ code: 'fr-FR', name: 'French (France)', flag: '🇫🇷' },
{ code: 'he-IL', name: 'Hebrew (Israel)', flag: '🇮🇱' },
{ code: 'hi-IN', name: 'Hindi (India)', flag: '🇮🇳' },
{ code: 'hu-HU', name: 'Hungarian (Hungary)', flag: '🇭🇺' },
{ code: 'it-IT', name: 'Italian (Italy)', flag: '🇮🇹' },
{ code: 'ja-JP', name: 'Japanese (Japan)', flag: '🇯🇵' },
{ code: 'ko-KR', name: 'Korean (South Korea)', flag: '🇰🇷' },
{ code: 'nl-NL', name: 'Dutch (Netherlands)', flag: '🇳🇱' },
{ code: 'no-NO', name: 'Norwegian (Norway)', flag: '🇳🇴' },
{ code: 'pl-PL', name: 'Polish (Poland)', flag: '🇵🇱' },
{ code: 'pt-BR', name: 'Portuguese (Brazil)', flag: '🇧🇷' },
{ code: 'pt-PT', name: 'Portuguese (Portugal)', flag: '🇵🇹' },
{ code: 'ro-RO', name: 'Romanian (Romania)', flag: '🇷🇴' },
{ code: 'ru-RU', name: 'Russian (Russia)', flag: '🇷🇺' },
{ code: 'sv-SE', name: 'Swedish (Sweden)', flag: '🇸🇪' },
{ code: 'th-TH', name: 'Thai (Thailand)', flag: '🇹🇭' },
{ code: 'tr-TR', name: 'Turkish (Turkey)', flag: '🇹🇷' },
{ code: 'vi-VN', name: 'Vietnamese (Vietnam)', flag: '🇻🇳' },
{ code: 'zh-CN', name: 'Chinese (Simplified, China)', flag: '🇨🇳' },
];
const RADIUS = 140;
const defaultRegions = languageFlags.reduce((map, lang) => {
const baseLang = lang.code.split('-')[0];
if (!map[baseLang]) {
map[baseLang] = lang.code;
}
return map;
}, {});
function getLocale() {
let language =
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
'en-US';
if (language.length === 2) {
language =
defaultRegions[language] || `${language}-${language.toUpperCase()}`;
}
return language;
}
let locale = getLocale();
const currentLangDisplay = document.getElementById('current-lang');
const languageDialog = document.getElementById('language-dialog');
const languageOptionsContainer =
document.getElementById('language-options');
const closeButton = document.getElementById('btn-dialog-close');
function drawClockFaces() {
const clockFaces = document.querySelectorAll('.clock-face');
const currentDate = new Date();
const currentDay = currentDate.getDate();
const currentMonth = currentDate.getMonth();
const currentYear = currentDate.getFullYear();
const currentWeekday = currentDate.getDay();
const currentHours = currentDate.getHours();
const currentMinutes = currentDate.getMinutes();
const currentSeconds = currentDate.getSeconds();
const totalDaysInMonth = new Date(
currentYear,
currentMonth + 1,
0
).getDate();
const weekdayNames = Array.from({ length: 7 }, (_, i) =>
new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(
new Date(2021, 0, i + 3)
)
);
const monthNames = Array.from({ length: 12 }, (_, i) =>
new Intl.DateTimeFormat(locale, { month: 'long' }).format(
new Date(2021, i)
)
);
clockFaces.forEach((clockFace) => {
clockFace.innerHTML = '';
const clockType = clockFace.getAttribute('data-clock');
const numbers = parseInt(clockFace.getAttribute('data-numbers'), 10);
const RADIUS = clockFace.offsetWidth / 2 - 20;
const center = clockFace.offsetWidth / 2;
let valueSet;
let currentValue;
switch (clockType) {
case 'seconds':
valueSet = Array.from({ length: 60 }, (_, i) =>
String(i).padStart(2, '0')
);
currentValue = String(currentSeconds).padStart(2, '0');
break;
case 'minutes':
valueSet = Array.from({ length: 60 }, (_, i) =>
String(i).padStart(2, '0')
);
currentValue = String(currentMinutes).padStart(2, '0');
break;
case 'hours':
valueSet = Array.from({ length: 24 }, (_, i) =>
String(i).padStart(2, '0')
);
currentValue = String(currentHours).padStart(2, '0');
break;
case 'days':
valueSet = Array.from(
{ length: totalDaysInMonth },
(_, i) => i + 1
);
currentValue = currentDay;
break;
case 'months':
valueSet = monthNames;
currentValue = currentMonth;
break;
case 'years':
valueSet = Array.from({ length: 101 }, (_, i) => 2000 + i);
currentValue = currentYear;
break;
case 'day-names':
valueSet = weekdayNames;
currentValue = currentWeekday;
break;
default:
return;
}
valueSet.forEach((value, i) => {
const angle = i * (360 / numbers);
const x = center + RADIUS * Math.cos((angle * Math.PI) / 180);
const y = center + RADIUS * Math.sin((angle * Math.PI) / 180);
const element = document.createElement('span');
element.classList.add('number');
element.textContent = value;
element.style.left = `${x}px`;
element.style.top = `${y}px`;
element.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
clockFace.appendChild(element);
});
const currentIndex = valueSet.indexOf(
typeof valueSet[0] === 'string'
? String(currentValue)
: currentValue
);
const rotationAngle = -((currentIndex / numbers) * 360);
clockFace.style.transform = `rotate(${rotationAngle}deg)`;
});
}
function rotateClockFaces() {
const clockFaces = document.querySelectorAll('.clock-face');
const lastAngles = {};
function updateRotations() {
const now = new Date();
const currentSecond = now.getSeconds();
const currentMinute = now.getMinutes();
const currentHour = now.getHours();
const currentDay = now.getDate();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
const currentWeekday = now.getDay();
clockFaces.forEach((clockFace) => {
const clockType = clockFace.getAttribute('data-clock');
const totalNumbers = parseInt(
clockFace.getAttribute('data-numbers'),
10
);
let currentValue;
switch (clockType) {
case 'seconds':
currentValue = currentSecond;
break;
case 'minutes':
currentValue = currentMinute;
break;
case 'hours':
currentValue = currentHour;
break;
case 'days':
currentValue = currentDay - 1;
break;
case 'months':
currentValue = currentMonth;
break;
case 'years':
currentValue = currentYear - 2000;
break;
case 'day-names':
currentValue = currentWeekday;
break;
default:
return;
}
const targetAngle = (360 / totalNumbers) * currentValue;
const clockId = clockFace.id || clockType;
const lastAngle = lastAngles[clockId] || 0;
const delta = targetAngle - lastAngle;
const shortestDelta = ((delta + 540) % 360) - 180;
const newAngle = lastAngle + shortestDelta;
clockFace.style.transform = `rotate(${newAngle * -1}deg)`;
lastAngles[clockId] = newAngle;
const numbers = clockFace.querySelectorAll('.number');
numbers.forEach((number, index) => {
if (index === currentValue) {
number.classList.add('active');
} else {
number.classList.remove('active');
}
});
});
requestAnimationFrame(updateRotations);
}
updateRotations();
}
function createLanguageOptions() {
const centerX = languageOptionsContainer.offsetWidth / 2;
const centerY = languageOptionsContainer.offsetHeight / 2;
languageFlags.forEach((lang, index, arr) => {
const angle = (index / arr.length) * 2 * Math.PI;
const x = centerX + RADIUS * Math.cos(angle);
const y = centerY + RADIUS * Math.sin(angle);
const radioWrapper = document.createElement('label');
radioWrapper.title = lang.name;
radioWrapper.style.left = `${x}px`;
radioWrapper.style.top = `${y}px`;
const radioInput = document.createElement('input');
radioInput.type = 'radio';
radioInput.name = 'language';
radioInput.value = lang.code;
if (lang.code === locale) {
radioInput.checked = true;
radioWrapper.classList.add('active');
}
const flag = document.createElement('span');
flag.classList.add('flag-icon');
flag.innerText = lang.flag;
radioWrapper.appendChild(radioInput);
radioWrapper.appendChild(flag);
languageOptionsContainer.appendChild(radioWrapper);
radioWrapper.addEventListener('mouseover', () =>
showTitle(lang.name, radioWrapper)
);
radioWrapper.addEventListener('mouseleave', () => hideTitle());
radioInput.addEventListener('change', () => {
locale = radioInput.value;
setCurrentLangDisplay(lang);
drawClockFaces();
document.querySelector('label.active')?.classList.remove('active');
radioWrapper.classList.add('active');
closeDialog();
});
});
}
let titleDisplay = null;
function showTitle(languageName) {
if (titleDisplay) {
titleDisplay.remove();
}
titleDisplay = document.createElement('div');
titleDisplay.classList.add('language-title');
titleDisplay.textContent = languageName;
languageOptionsContainer.appendChild(titleDisplay);
}
function hideTitle() {
if (titleDisplay) {
titleDisplay.textContent = '';
}
}
function setCurrentLangDisplay(lang) {
currentLangDisplay.textContent = lang.flag;
currentLangDisplay.title = lang.name;
showTitle(lang.name);
}
function openDialog() {
languageDialog.showModal();
createLanguageOptions();
addDialogCloseListener();
}
function closeDialog() {
languageDialog.close();
removeLanguageOptions();
removeDialogCloseListener();
}
function removeLanguageOptions() {
languageOptionsContainer.innerHTML = '';
}
function addDialogCloseListener() {
languageDialog.addEventListener('click', closeDialogOnClickOutside);
}
function removeDialogCloseListener() {
languageDialog.removeEventListener('click', closeDialogOnClickOutside);
}
function closeDialogOnClickOutside(e) {
if (e.target === languageDialog) {
closeDialog();
}
}
closeButton.addEventListener('click', closeDialog);
currentLangDisplay.addEventListener('click', openDialog);
drawClockFaces();
rotateClockFaces();
setCurrentLangDisplay(languageFlags.find((lang) => lang.code === locale));
</script>
</body>
</html>