Время переменных
В этой статье я расскажу, как собрал демку с использованием кастомных свойств CSS (они же CSS-переменные) и по ходу проапгрейдил своё мышление в контексте CSS.
Вёрстка аналоговых часов не то чтобы оригинальная идея, но она показалась мне удобным способом показать особенности использования кастомных свойств.
Итак, вот что вышло:
Вот как это устроено:
- JS генерирует данные о текущем времени и отправляет их в HTML и CSS;
- HTML использует эти данные для отображения времени в текстовом виде;
- CSS переводит числа в визуально понятный человеку графический вид.
Итак, начнём с разметки.
<time class="clock">
<span class="clock__hand clock__hand--hour"></span>:
<span class="clock__hand clock__hand--minute"></span>:
<span class="clock__hand clock__hand--second"></span> <svg class="clock__face" aria-hidden="true" role="presentation">
<circle class="clock__face-stroke"></circle>
</svg>
</time>
Общий контейнер часов <time>, три стрелки и декоративная SVG для отрисовки циферблата. Сразу продумаем доступность:
- в спанах стрелок с помощью JavaScript будет записываться текущее время числами;
- элементу <time> будет задано соответсвующее значение атрибута datetime;
- декоративный элемент <svg> скроем от скринридеров атрибутом aria-hidden и снимем с него смысловую нагрузку с помощью атрибута role.
Теперь CSS. Я не буду описывать весь код демки (лучше с ней поиграть вживую), а остановлюсь только на ключевых моментах.
Определим настройки внешнего вида часов — диаметр и толщину рамки часов:
.clock {
--clock-diameter: 250px;
--clock-border-width: 5px;
}
Тут же используем эти переменные:
.clock {
width: var(--clock-diameter);
height: var(--clock-diameter);
border: var(--clock-border-width) solid #000000;
}
Пока всё просто.
Теперь стрелки часов. Сначала определим ширину и высоту стрелок и спозиционируем их по центру циферблата. По умолчанию стрелки будут шириной 1px и высотой 50%. В зависимости от ширины и высоты абсолютно спозиционированных стрелок вычисляются их координаты top и left:
.clock__hand {
--hand-width: 1px;
--hand-height: 50%;
position: absolute; top: calc(50% - var(--hand-height));
left: calc(50% - calc(var(--hand-width) / 2)); width: var(--hand-width);
height: var(--hand-height);
}/* С кастомными свойствами можно производить арифметические операции с помощью функции calc() */
Осталось определить цвет стрелок и задать им поворот вокруг оси часов. Стрелки часов изначально будут чёрного цвета и в не повёрнутом состоянии:
.clock__hand {
background-color: var(--hand-color, #000000);
transform: rotate(var(--turn, 0turn));
}/* При использовании кастомного свойства можно задать
фолбэк-значение */
И теперь переопределим в модификаторах блоков характеристики стрелок:
.clock__hand--hour {
--hand-width: 6px;
--hand-height: 30%;
}.clock__hand--minute {
--hand-width: 4px;
--hand-height: 40%;
}.clock__hand--second {
--hand-width: 2px;
--hand-height: 45%;
--hand-color: red;
}
Вот что получилось:
Развернём стрелки для наглядности:
.clock__hand--hour {
--turn: 0.25turn;
}.clock__hand--minute {
--turn: 0.6turn;
}.clock__hand--second {
--turn: 0.8turn;
}/* Полный поворот круга — 1turn, да, есть такая единица измерения */
Текущее время будем тоже хранить в трёх кастомных свойствах:
.clock {
--hours: 10;
--minutes: 30;
--seconds: 15;
}
Теперь встаёт вопрос, как перевести часы, минуты и секунды в единицы поворота круга. И тут на помощь приходит школьная математика.
Начнём с секундной стрелки. Секунд в полном обороте круга 60 — это соответствует значению поворота 1turn. А нам нужно узнать, сколько turn в n-секунде. Получается такая пропорция:
xturn - nсек
1turn - 60сек
То есть x = 1turn * nсек / 60сек
. Переведём эти вычисления в CSS:
.clock__hand--second {
--seconds-in-minute: 60;
--turn: calc(1turn * var(--seconds) / var(--seconds-in-minute));
}
Проверим, вот что получилось:
Всё ок, 15 секунд, как и задано в --seconds
.
Далее минутная стрелка. Тут всё точно так же, как и у секундной — в часе 60 минут:
.clock__hand--second {
--minutes-in-hour: 60;
--turn: calc(1turn * var(--minutes) / var(--minutes-in-hour));
}
Картинка подтверждает, что всё ок:
И осталась часовая стрелка. В обороте круга 12 часов:
.clock__hand--hour {
--hours-in-day-half: 12;
--turn: calc(1turn * var(--hours) / var(--hours-in-day-half));
}
Всё работает, но хочется, чтобы часовая стрелка двигалась ещё дополнительно в течение часа в зависимости от минут, а не просто раз в час перескакивала на следующее деление:
Поэтому для часовой стрелки давайте рассчитаем дополнительный поворот в пределах одного часа по тому же принципу, что и раньше.
Снова составим пропорцию: 1/12 turn — это 1 час или 60 минут. Нам нужно узнать, какое значение turn будет на n-минуте часа.
xturn - nмин
1/12turn - 60мин
То есть x = 1/12turn * nмин / 60мин
. Переведём эти вычисления в CSS и сложим основной поворот часовой стрелки с дополнительным:
.clock__hand--hour {
--hours-in-day-half: 12;
--hours-turn: calc(1turn * var(--hours) / var(--hours-in-day-half));
--min-in-hour: 60;
--minutes-turn: calc((1 / var(--hours-in-day-half)) * 1turn * var(--minutes) / var(--min-in-hour));
--turn: calc(var(--hours-turn) + var(--minutes-turn));
}
Получилось именно то, что нужно:
А вот так это работает в браузере:
Теперь оживим получившуюся конструкцию реальными данными.
В JS будем каждую секунду задавать значения нашим кастомным свойствам --hours
, --minutes
и --seconds
:
const clock = document.querySelector('.clock');const setCustomProperty = (name, value) => {
clock.style.setProperty(`--${name}`, value);
};const setTimeVariables = (time) => {
setCustomProperty('hours', time.getHours());
setCustomProperty('minutes', time.getMinutes());
setCustomProperty('seconds', time.getSeconds());
};const setTime = () => setTimeVariables(new Date());setTime();
setInterval(setTime, 1000);
Не забудем про скринридеры и доступность. Для этого в HTML будем обновлять текстовый контент элементов в теге <time> и его атрибут datetime:
const clock = document.querySelector('.clock');
const hoursBox = document.querySelector('.clock__hand--hour');
const minutesBox = document.querySelector('.clock__hand--minute');
const secondsBox = document.querySelector('.clock__hand--second');const setTimeLayout = (time) => {
hoursBox.textContent = time.getHours();
minutesBox.textContent = time.getMinutes();
secondsBox.textContent = time.getSeconds();
clock.setAttribute('datetime', time.toISOString());
}const setTime = () => setTimeLayout(new Date());setTime();
setInterval(setTime, 1000);
Вот как это выглядит живьём в дев-тулз:
В общем, попробовав раз, хочется дальше повсеместно использовать кастомные свойства, настолько органичны они оказались в использовании. Останавливает сейчас только поддержка IE (в Edge кастомные свойства уже работают в последних двух версиях). Но для свежих проектов и демок — это маст-хэв!