Имитация стрелочных часов с помощью новых тригонометрических функций CSS sin() и cos ()


Тригонометрические функции CSS есть в последних версиях Firefox и Safari. Наличие такого рода математической мощи в CSS открывает целую кучу возможностей. В этом материале мы применим пару новых функций: sin()
и cos()
.
В конвейере есть и другие тригонометрические функции, tan()
в том числе — так зачем фокусироваться только на sin()
и cos()
? Они идеально подходят для идеи, которая заключается в размещении текста вдоль края круга.
Вот что имеется в виду. Опять же, на данный момент это поддерживается только в Firefox и Safari:
Итак, это не совсем похоже на слова, образующие круглую форму, но мы размещаем текстовые символы вдоль круга, чтобы сформировать циферблат. Вот некоторая разметка, которую мы можем использовать для запуска:
<div class="clock">
<div class="clock-face">
<time datetime="12:00">12</time>
<time datetime="1:00">1</time>
<time datetime="2:00">2</time>
<time datetime="3:00">3</time>
<time datetime="4:00">4</time>
<time datetime="5:00">5</time>
<time datetime="6:00">6</time>
<time datetime="7:00">7</time>
<time datetime="8:00">8</time>
<time datetime="9:00">9</time>
<time datetime="10:00">10</time>
<time datetime="11:00">11</time>
</div>
</div>
Далее, вот несколько супер базовых стилей для .clock-face
контейнера. Используем <time>
тег с datetime
атрибутом.
.clock {
--_ow: clamp(5rem, 60vw, 40rem);
--_w: 88cqi;
aspect-ratio: 1;
background-color: tomato;
border-radius: 50%;
container-type: inline;
display: grid;
height: var(--_ow);
place-content: center;
position: relative;
width var(--_ow);
}
Немного украсим там всё, но только для того, чтобы получить основную форму и цвет фона, которые помогут нам увидеть, что мы делаем. Обратите внимание, как мы сохраняем width
значение в переменной CSS. Мы воспользуемся этим позже. Пока смотреть особо не на что:

Похоже на какой-то эксперимент в области современного искусства, верно? Давайте введем новую переменную, --_r
, для хранения радиуса окружности, который равен половине ширины окружности. Таким образом, при изменении width (--_w
) значение radius (--_r
) также будет обновляться — благодаря другой математической функции CSS,calc()
:
.clock {
--_w: 300px;
--_r: calc(var(--_w) / 2);
/* rest of styles */
}
Теперь немного математики. Окружность равна 360 градусам. У нас на часах 12 меток, поэтому мы хотим размещать цифры через каждые 30 градусов (360 / 12
). В math-land круг начинается с 3 часов, поэтому полдень на самом деле минус 90 градусов от этого, что составляет 270 градусов (360 - 90
) .
Давайте добавим еще одну переменную, --_d
, которую мы можем использовать для установки значения степени для каждого числа на циферблате. Мы собираемся увеличить значения на 30 градусов, чтобы завершить наш круг:
.clock time:nth-child(1) { --_d: 270deg; }
.clock time:nth-child(2) { --_d: 300deg; }
.clock time:nth-child(3) { --_d: 330deg; }
.clock time:nth-child(4) { --_d: 0deg; }
.clock time:nth-child(5) { --_d: 30deg; }
.clock time:nth-child(6) { --_d: 60deg; }
.clock time:nth-child(7) { --_d: 90deg; }
.clock time:nth-child(8) { --_d: 120deg; }
.clock time:nth-child(9) { --_d: 150deg; }
.clock time:nth-child(10) { --_d: 180deg; }
.clock time:nth-child(11) { --_d: 210deg; }
.clock time:nth-child(12) { --_d: 240deg; }
Хорошо, сейчас самое время запачкать руки функциями sin()
иcos()
! Что мы хотим сделать, так это использовать их для получения координат X и Y для каждого числа, чтобы мы могли правильно разместить их вокруг циферблата.
Формула для координаты X такова radius + (radius * cos(degree))
. Давайте добавим это в нашу новую --_x
переменную:
--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
Формула для координаты Y такова radius + (radius * sin(degree))
. У нас есть все, что нам нужно для вычисления этого:
--_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));
Для настройки чисел нам нужно выполнить несколько хозяйственных действий, поэтому давайте добавим к ним некоторый базовый стиль, чтобы убедиться, что они абсолютно расположены и размещены с нашими координатами:
.clock-face time {
--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
--_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));
--_sz: 12cqi;
display: grid;
height: var(--_sz);
left: var(--_x);
place-content: center;
position: absolute;
top: var(--_y);
width: var(--_sz);
}
Обратите --_sz
внимание, что мы будем использовать для width
и height
чисел через мгновение. Давайте посмотрим, что у нас есть на данный момент.

Это определенно больше похоже на часы! Видите, как верхний левый угол каждого числа расположен в правильном месте по кругу? Нам нужно “уменьшить” радиус при вычислении позиций для каждого числа. Мы можем вычесть размер числа (--_sz
) из размера окружности (--_w
), прежде чем вычислять радиус:
--_r: calc((var(--_w) - var(--_sz)) / 2);

Намного лучше! Давайте изменим цвета, чтобы они выглядели более элегантно:

Мы могли бы остановиться прямо здесь! Мы достигли цели размещения текста по кругу, верно? Но что такое часы без стрелок, чтобы показывать часы, минуты и секунды?
Давайте используем для этого одну анимацию CSS. Сначала давайте добавим еще три элемента в нашу разметку,
<div class="clock">
<!-- after <time>-tags -->
<span class="arm seconds"></span>
<span class="arm minutes"></span>
<span class="arm hours"></span>
<span class="arm center"></span>
</div>
Затем некоторая общая разметка для всех трех плеч. Опять же, большая часть этого - просто убедиться, что рычаги абсолютно расположены и размещены соответствующим образом:
.arm {
background-color: var(--_abg);
border-radius: calc(var(--_aw) * 2);
display: block;
height: var(--_ah);
left: calc((var(--_w) - var(--_aw)) / 2);
position: absolute;
top: calc((var(--_w) / 2) - var(--_ah));
transform: rotate(0deg);
transform-origin: bottom;
width: var(--_aw);
}
Мы будем использовать одну и ту же анимацию для всех трех рычагов:
@keyframes turn {
to {
transform: rotate(1turn);
}
}
Единственное различие заключается во времени, которое требуется отдельным стрелкам, чтобы совершить полный оборот. Для стрелки часов требуется 12 часов, чтобы сделать полный оборот. animation-duration
Свойство принимает значения только в миллисекундах и секундах. Давайте придерживаться секунд, что составляет 43 200 секунд (60 seconds * 60 minutes * 12 hours
).
animation: turn 43200s infinite;
Минутной стрелке требуется 1 час, чтобы совершить полный оборот. Но мы хотим, чтобы это была многоступенчатая анимация, чтобы движение между рычагами было ступенчатым, а не линейным. Нам понадобится 60 шагов, по одному на каждую минуту:
animation: turn 3600s steps(60, end) infinite;
Секундная стрелка почти такая же, как и минутная, но ее продолжительность составляет 60 секунд вместо 60 минут:
animation: turn 60s steps(60, end) infinite;
Давайте обновим свойства, которые мы создали в общих стилях:
.seconds {
--_abg: hsl(0, 5%, 40%);
--_ah: 145px;
--_aw: 2px;
animation: turn 60s steps(60, end) infinite;
}
.minutes {
--_abg: #333;
--_ah: 145px;
--_aw: 6px;
animation: turn 3600s steps(60, end) infinite;
}
.hours {
--_abg: #333;
--_ah: 110px;
--_aw: 6px;
animation: turn 43200s linear infinite;
}
Что, если мы хотим начать с текущего времени? Нам нужно немного JavaScript:
const time = new Date();
const hour = -3600 * (time.getHours() % 12);
const mins = -60 * time.getMinutes();
app.style.setProperty('--_dm', `${mins}s`);
app.style.setProperty('--_dh', `${(hour+mins)}s`);
Добавим id="app"
к циферблату и установим для него два новых пользовательских свойства, которые устанавливают отрицательное animation-delay
значение. getHours()
Метод объекта JavaScipt Date
использует 24-часовой формат, поэтому мы используем remainder
оператор для преобразования его в 12-часовой формат.
В CSS нам animation-delay
также нужно добавить:
.minutes {
animation-delay: var(--_dm, 0s);
/* other styles */
}
.hours {
animation-delay: var(--_dh, 0s);
/* other styles */
}
Еще одна вещь. Используя CSS @supports
и свойства, которые мы уже создали, мы можем предоставить запасной вариант для браузеров, которые не поддерживают sin()
и cos()
:
@supports not (left: calc(1px * cos(45deg))) {
time {
left: 50% !important;
top: 50% !important;
transform: translate(-50%,-50%) rotate(var(--_d)) translate(var(--_r)) rotate(calc(-1*var(--_d)))
}
}
И вуаля! Наши часы готовы! Вот еще одна финальная демонстрация. Опять же, на данный момент это поддерживается только в Firefox и Safari.
Что еще мы можем сделать?
Здесь мы просто балуемся, но мы можем быстро превратить наши часы в круговую галерею изображений, заменив <time>
теги на <img>
затем обновив значения width (--_w
) и radius (--_r
):
Давайте попробуем еще один вариант. Мы можем создать вот такой шаблон, который называется “Луна” и состоит из множества точек, образующих круг.

На этот раз мы будем использовать неупорядоченный список, поскольку круги не следуют определенному порядку. Мы даже не собираемся помещать все элементы списка в разметку. Вместо этого давайте внедрим в них JavaScript и добавим несколько элементов управления, которые мы можем использовать для управления конечным результатом.
Элементы управления - это входные данные диапазона (<input type="range">)
которые мы обернем в a <form>
и прослушаем input
событие.
<form id="controls">
<fieldset>
<label>Number of rings
<input type="range" min="2" max="12" value="10" id="rings" />
</label>
<label>Dots per ring
<input type="range" min="5" max="12" value="7" id="dots" />
</label>
<label>Spread
<input type="range" min="10" max="40" value="40" id="spread" />
</label>
</fieldset>
</form>
Мы запустим этот метод для “ввода”, который создаст кучу <li>
элементов с переменной degree (--_d
), которую мы использовали ранее, примененной к каждому из них. Мы также можем перепрофилировать нашу переменную radius (--_r
) .
Также сделаем так, чтобы точки были разных цветов. Итак, давайте рандомизируем (ну, не полностью рандомизируем) значение цвета HSL для каждого элемента списка и сохраним его как новую переменную CSS,--_bgc
:
const update = () => {
let s = "";
for (let i = 1; i <= rings.valueAsNumber; i++) {
const r = spread.valueAsNumber * i;
const theta = coords(dots.valueAsNumber * i);
for (let j = 0; j < theta.length; j++) {
s += `<li style="--_d:${theta[j]};--_r:${r}px;--_bgc:hsl(${random(
50,
25
)},${random(90, 50)}%,${random(90, 60)}%)"></li>`;
}
}
app.innerHTML = s;
}
random()
Метод выбирает значение в пределах определенного диапазона чисел:
const random = (max, min = 0, f = true) => f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;
И это все. Мы используем JavaScript для рендеринга разметки, но как только она визуализируется, она нам действительно не нужна. Функции sin()
и cos()
помогают нам расположить все точки в нужных местах.
Заключительные мысли
Размещение объектов по кругу - довольно простой пример для демонстрации возможностей тригонометрических функций, таких как sin()
и cos()
. Но это действительно здорово, что мы получаем современные функции CSS, которые предоставляют новые решения для старых обходных путей. Уверены, что мы увидим гораздо более интересные, сложные и креативные варианты использования, особенно с поддержкой браузеров в Chrome и Edge.