Сегодня будем заниматься красотой и анимациями: сделаем универсальные часы с движущимся циферблатом, который покажет нам всё текущее время от года до секунд. А ещё прикрутим сюда выбор языка, чтобы название месяца и дня недели отображалось правильно в разных странах. Внутри много хардкора и кода, а на выходе будет такое:
Логика проекта
Обычно в наших проектах всё происходит только в скрипте, но не в этот раз: от стилей тут тоже много зависит — анимация, расположение элементов и их внешний вид. Поэтому будем делать так:
- Создадим сразу все нужные файлы — index.html, style.css и script.js. На старте они будут пустые, а в процессе мы их наполним.
- Разместим на странице в блоках два контейнера: один для циферблата, а второй — для выбора языка. Внутри циферблата создадим тоже много контейнеров, каждый из которых будет отвечать за что-то своё: минуты, секунды, дни недели и так далее.
- Добавим в эти контейнеры элементы с помощью скрипта.
- Настроим стили, которые превратят то, что выдал скрипт, в круглый циферблат.
- Добавим анимацию вращения.
- Наконец, добавим выбор языков (тоже с анимацией).
Работы много, поэтому приступим.
Наполняем index.html
Так как у нас будет много данных для вывода, создадим много контейнеров для их структурирования. Чтобы всем этим было проще управлять, данные о времени объединим в один большой контейнер — так мы сможем применять некоторые стили сразу ко всем контейнерам внутри.
В центр часов положим кнопку выбора языка и сделаем её в виде флага (но это чуть позже, в скрипте). Отдельно сделаем контейнер с диалоговым окном выбора языка — там будет кнопка закрытия и блок со всеми языками-флагами.
Из классики: подключение стилей в самом начале и скрипта — в конце. Вроде всё, пишем код. Внешне на странице почти ничего не изменится, но нам главное сейчас — собрать каркас:
<!DOCTYPE html>
<html lang="ru-RU">
<head>
<!-- Базовые настройки документа -->
<meta charset="UTF-8"> <!-- Кодировка UTF-8 -->
<title>Часы на 100 лет вперёд</title> <!-- Заголовок страницы -->
<link rel="stylesheet" href="style.css"> <!-- Подключение стилей -->
</head>
<body>
<!-- Основная структура часов -->
<div class="clock" data-date="2025-04-55">
<!-- Контейнеры для каждого типа времени (годы, секунды и так далее) -->
<div>
<div data-clock="years" data-numbers="101" class="clock-face"></div> <!-- 100-летний круг -->
</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 src="script.js"></script>
</body>
</html>
Подготавливаем скрипт
Раньше мы приступали к стилям после создания HTML-файла, но не в этот раз. Всё дело в том, что сейчас нам просто нечего оформлять: все данные будут генерироваться в скрипте. А вот когда сгенерируем — тогда и оформим.
Первое, что мы делаем, — создаём константы, которые будут потом часто использоваться в проекте. Самое большое — это локализация и выбор языка, поэтому опишем это в самом начале:
// Массив поддерживаемых языков с флагами
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: '🇨🇳' },
];
Дальше зададим радиус круга для выбора языка (это пригодится позже) и получим доступ к разным элементам на странице по их Id (и начнём уже ими пользоваться):
const RADIUS = 140; // Радиус круга для кнопок выбора языка
// Получаем DOM-элементы по их Id
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');
Выбираем стартовый язык
Так как мы решили поддерживать сразу много языков и давать пользователю возможность выбора, нам нужно научиться с ними работать.
Логика тут такая:
- Нам нужно установить язык («локаль» по-программистски), который будет выводиться при запуске проекта.
- Для этого мы пишем функцию
getLocale()
— она определит стартовый язык из настроек страницы. Иногда функция определения может не срабатывать в некоторых браузерах, поэтому установим язык по умолчанию (английский). - Если язык в настройках страницы указан коротко, то с помощью
defaultRegions
мы найдём полное название языка и вернём его в функциюgetLocale()
. - JavaScript умеет автоматически работать с выбранными нами языками — это значит, что при смене локали скрипт сам подставит нужное название месяца и дня недели на выбранном языке. Вот такая удобная автоматизация.
Запишем это на JavaScript:
// Создание карты регионов по умолчанию
const defaultRegions = languageFlags.reduce((map, lang) => {
const baseLang = lang.code.split('-')[0]; // Извлекаем базовый язык (например, 'en' из 'en-US')
if (!map[baseLang]) {
map[baseLang] = lang.code; // Сохраняем язык
}
return map;
}, {});
// Функция определения текущей локали
function getLocale() {
// Получаем основной язык из navigator.languages или navigator.language
let language = (navigator.languages && navigator.languages[0]) || navigator.language || 'en-US';
// Если язык указан коротко (например, 'en'), добавляем регион из defaultRegions
if (language.length === 2) {
language = defaultRegions[language] || `${language}-${language.toUpperCase()}`;
}
// Возвращаем язык
return language;
}
let locale = getLocale(); // Текущая локаль
На экране всё ещё ничего нет, зато мы определились с языком. Можно двигаться дальше.
Выводим данные для циферблата
Продолжаем работать со скриптом: теперь получим и выведем на экран все данные, которые нам нужны для работы часов. Для этого напишем функцию drawClockFaces(), которая даст нам всё что нужно для дальнейших действий.
Что делает эта функция:
- Получает текущую дату и время во всех подробностях от секунды до года.
- Получает название дня недели и месяца.
- Дальше идёт работа с каждым контейнером-циферблатом по отдельности.
- Получает количество элементов в каждом контейнере (их мы задали в HTML в блоке
data-numbers
). - Задаёт радиус для каждого контейнера и его центр вращения.
- В каждый контейнер добавляет свои значения в зависимости от циферблата. Если это секунды и минуты — то значения от 0 до 59, если это годы — то от 2000 до 2100 и так далее.
- Создаёт на странице все элементы из каждого списка.
- Вычисляет их позицию и угол поворота в зависимости от места элемента и добавляет эти свойства к каждому элементу.
- Устанавливает все часы в стартовое значение по умолчанию.
👉 В скрипте есть сразу неочевидный момент: мы обращаемся к стилям и определяем их свойства, хотя у нас нет ещё заполненного файла style.css
. Так можно делать, потому что скрипт создаёт стили на уже загруженной странице — и с ними тоже можно работать: создавать, менять значения и удалять.
// Функция отрисовки циферблатов
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); // X координата
const y = center + RADIUS * Math.sin((angle * Math.PI) / 180); // Y координата
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);
});
// Вращаем циферблат, чтобы текущее значение было в позиции "3 часа"
const currentIndex = valueSet.indexOf(
typeof valueSet[0] === 'string' ? String(currentValue) : currentValue
);
const rotationAngle = -((currentIndex / numbers) * 360);
clockFace.style.transform = `rotate(${rotationAngle}deg)`;
});
}
Если мы сейчас вызовем эту функцию, то получим что-то очень странное на странице. Время подключать нормальные стили:
Настраиваем стили
Открываем файл style.css и первое, что делаем, — сбрасываем стандартные стили, задаём переменные и устанавливаем основные стили страницы: шрифт, цвета, отступы и фон:
/* Сброс стандартных стилей */
*,
::before,
::after {
box-sizing: border-box; /* Упрощает расчёт размеров элементов */
}
/* CSS-переменные для цветов и размеров */
:root {
--clr-bg: rgb(3 3 3); /* Цвет фона */
--clock-size: 800px; /* Размер часов */
--clock-clr: rgb(12, 74, 110); /* Основной цвет часов */
}
/* Основные стили страницы */
body {
margin: 0;
min-height: 100svh; /* Минимальная высота = высоте viewport */
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; /* Эффект наложения фона */
}
Теперь займёмся основным контейнером со всеми циферблатами. Здесь нам нужно:
- Сделать круг (потому что это классические часы).
- Поместить его по центру.
- Сразу предусмотреть то, что проект могут открыть на мобилке.
- Добавить общие стили для каждого круга (с минутами, секундами и прочим) — радиус, центр и всё такое.
- Задать индивидуальные размеры для каждого кольца.
Запишем это в виде CSS-команд:
/* Основной контейнер часов */
.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 > 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; /* Изолируем контекст наложения */
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;
}
/* Активная цифра (текущее значение) */
.clock-face > *.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;
}
Наконец, добавим красоты, чтобы подсветить текущее время: сделаем простую маску, чтобы она выделяла актуальные данные:
/* Полупрозрачная маска для подсветки текущего времени */
.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; /* Поверх других элементов */
}
Работаем с языками
Перед тем как добавлять анимации, сделаем крутую фичу — выбор языка. JavaScript умеет это делать автоматически, поэтому наша задача — объяснить, что и где заменить.
Первое, что для этого сделаем, — скрытие названия текущего языка, чтобы при выборе других он нам не мешал. Причём сделаем относительно абсурдно: сначала выведем название языка, потом предусмотрим, чтобы при наведении на другой флаг язык всё-таки показывался, и сделаем функцию скрытия названия для языка, чтобы не показывался текущий.
Запишем это на JS:
// Показ названия языка в центре
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);
Наконец, напишем большую функцию, которая загрузит все флаги в диалоговое окно и даст пользователю возможность выбора языка. Логика такая:
- Загружаем все флаги из стартового массива.
- Считаем относительно круга позицию каждого флага (сейчас мы этого не увидим, потому что нет стилей, но в будущем пригодится).
- Каждый флаг добавляем на страницу в элемент label.
- Добавляем радиокнопку к каждому флагу, чтобы можно было выбрать только один язык из предложенных.
- Добавляем обработчики событий, если пользователь передумал выбирать язык.
- Пишем обработчик, который при выборе языка даст команду обновить надписи в нужных элементах.
Вот как это выглядит в коде:
// Создание кнопок выбора языка
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); // X координата
const y = centerY + RADIUS * Math.sin(angle); // Y координата
// Создаём элемент label
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();
});
});
}
После всех обновлений при клике на флаг получаем такую картинку — выглядит некрасиво, поэтому снова возвращаемся к стилям.
Добавляем стили к окну с языками
Возвращаемся к файлу style.css, чтобы настроить красивый внешний вид диалогового окна выбора языков. Вот что нам нужно тут сделать:
- Прописать общий стиль окна — сделать его круглым, как и всё остальное на странице.
- Добавить анимацию открытия и эффектов при наведении на другой флаг.
- Настроить внешний вид кнопки закрытия окна (если пользователь передумал выбирать язык) и убрать её из центра, чтобы она не закрывала текущий флаг.
- Оформить красиво флаги и добавить анимацию наведения на них.
- Написать название языка при наведении на флаг.
- Скрыть радиокнопки, чтобы они не мешали интерфейсу.
Как обычно, прокомментировали каждый блок, чтобы было проще понять, что происходит в каждом:
/* Стили диалогового окна */
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;
}
/* Кнопка закрытия диалога */
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;
}
/* Эффекты кнопки закрытия */
.btn-dialog-close:focus-visible,
.btn-dialog-close: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;
}
/* Активный язык */
.language-options > label.active {
color: white;
background: var(--clock-clr);
}
/* Эффекты при наведении */
.language-options > label:focus-visible,
.language-options > label: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;
}
Смотрите, что получилось в итоге:
Добавляем разделители в циферблат
Сейчас визуально всё хорошо, но не хватает привычного двоеточия между минутами и секундами. Добавим это тоже в стилях:
/* Разделители времени (двоеточия) */
.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;
}
Финал — добавляем анимацию
Последнее, что нам осталось сделать в проекте, — добавить анимацию вращения, чтобы часы обновлялись и всегда показывали верное время.
Что будем здесь делать:
- Получаем доступ ко всем циферблатам.
- Получаем текущие значения времени.
- Находим количество элементов в каждом циферблате.
- Вычисляем угол поворота при смене времени для каждого циферблата.
- Считаем углы поворотов для того, чтобы анимация получалась плавной, а не дёрганой и резкой.
- Сохраняем новый угол поворота.
- Обновляем активные элементы в каждом циферблате, чтобы знать, на сколько сейчас повёрнуто каждое поле.
- Обновляем кадры анимации.
Эта механика — сложная, и чтобы в ней разобраться, надо держать в уме две ключевые вещи: активный элемент и угол поворота. Но если вы помните немного из школьного курса геометрии, то собрать всё вместе будет не очень сложно.
Запишем код на JavaScript:
// Функция анимации вращения циферблатов
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; // 0 = Sunday
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();
}
Уф! Справились! Результат — на гифке ниже. Или можно посмотреть, как всё это работает, на странице проекта.
<!DOCTYPE html>
<html lang="ru-RU">
<head>
<!-- Базовые настройки документа -->
<meta charset="UTF-8"> <!-- Кодировка UTF-8 -->
<title>Часы на 100 лет вперёд</title> <!-- Заголовок страницы -->
<link rel="stylesheet" href="style.css"> <!-- Подключение стилей -->
</head>
<body>
<!-- Основная структура часов -->
<div class="clock" data-date="2025-04-55">
<!-- Контейнеры для каждого типа времени (годы, секунды и так далее) -->
<div>
<div data-clock="years" data-numbers="101" class="clock-face"></div> <!-- 100-летний круг -->
</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 src="script.js"></script>
</body>
</html>
/* Сброс стандартных стилей */
*,
::before,
::after {
box-sizing: border-box; /* Упрощает расчёт размеров элементов */
}
/* CSS-переменные для цветов и размеров */
:root {
--clr-bg: rgb(3 3 3); /* Цвет фона */
--clock-size: 800px; /* Размер часов */
--clock-clr: rgb(12, 74, 110); /* Основной цвет часов */
}
/* Основные стили страницы */
body {
margin: 0;
min-height: 100svh; /* Минимальная высота = высоте viewport */
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; /* Изолируем контекст наложения */
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;
}
/* Активная цифра (текущее значение) */
.clock-face > *.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;
}
/* Стили диалогового окна */
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;
}
/* Кнопка закрытия диалога */
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;
}
/* Эффекты кнопки закрытия */
.btn-dialog-close:focus-visible,
.btn-dialog-close: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;
}
/* Активный язык */
.language-options > label.active {
color: white;
background: var(--clock-clr);
}
/* Эффекты при наведении */
.language-options > label:focus-visible,
.language-options > label: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;
}
/* Разделители времени (двоеточия) */
.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;
}
// Массив поддерживаемых языков с флагами
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; // Радиус круга для кнопок выбора языка
// Получаем DOM-элементы по их Id
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');
// Создание карты регионов по умолчанию
const defaultRegions = languageFlags.reduce((map, lang) => {
const baseLang = lang.code.split('-')[0]; // Извлекаем базовый язык (например, 'en' из 'en-US')
if (!map[baseLang]) {
map[baseLang] = lang.code; // Сохраняем язык
}
return map;
}, {});
// Функция определения текущей локали
function getLocale() {
// Получаем основной язык из navigator.languages или navigator.language
let language = (navigator.languages && navigator.languages[0]) || navigator.language || 'en-US';
// Если язык указан коротко (например, 'en'), добавляем регион из defaultRegions
if (language.length === 2) {
language = defaultRegions[language] || `${language}-${language.toUpperCase()}`;
}
// Возвращаем язык
return language;
}
let locale = getLocale(); // Текущая локаль
// Функция отрисовки циферблатов
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); // X координата
const y = center + RADIUS * Math.sin((angle * Math.PI) / 180); // Y координата
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);
});
// Вращаем циферблат, чтобы текущее значение было в позиции "3 часа"
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; // 0 = Sunday
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); // X координата
const y = centerY + RADIUS * Math.sin(angle); // Y координата
// Создаём элемент label
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));
Вам слово
Приходите к нам в соцсети поделиться своим мнением о проекте и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте: шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!