Ну вот, наконец, у нас есть тригонометрические CSS функции! И если вы используете браузеры последних версий, то эти функции у вас будут точно работать. Наличие такого рода математических способностей CSS открывает целый ряд новых возможностей. И вот, в этом уроке мы пощупаем эти возможности и познакомимся с парой новых функций: sin()
и cos()
.
В наборе есть и другие тригонометрические функции, в том числе tan()
, так почему же выбраны именно sin()
и cos()
? Дело в том, что они идеально подходят для нашей задумки, которая заключается в размещении текста по краю круга. А конкретно, вот что именно нам хотелось бы получить. Напоминаем, что на момент написания статьи это работает только в новых браузерах последних версий.
See the Pen CSS sin() and cos() Demo 4 by Mads Stoumann.
Как видите, это не совсем слова, выведенные по кругу, но мы размещаем текстовые символы по краю круга, образуя циферблат часов.
И перед началом работы создадим вот такую разметку:
<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 для хранения радиуса окружности, который равен половине ширины окружности. Таким образом, при изменении ширины --_w значение радиуса --_r также обновится. Это будет сделано благодаря другой математической функции CSS - calc()
:
.clock {
--_w: 300px;
--_r: calc(var(--_w) / 2);
/* остальные стили */
}
Теперь немного математики. Окружность равна 360 градусам. У нас на часах 12 меток, поэтому нам нужно разместить цифры через каждые 30 градусов (360 / 12). Окружность здесь начинается с 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 будет такой радиус + (радиус * cos(градус_цифры)). Зададим ее в отдельной переменной --_x:
--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
Формула для вычисления координаты Y будет такой радиус + (радиус * sin(градус_цифры)). Зададим ее в отдельной переменной --_y:
--_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, которую в данный момент мы будем использовать для ширины и высоты цифр. Давайте посмотрим, что мы имеем на данном этапе.
Это уже больше похоже на часы! Обратите внимание, что верхний левый угол каждой цифры расположен в правильном месте на окружности. Теперь, нам нужно "уменьшить" радиус при вычислении позиций для каждой цифры. Мы можем, прежде чем вычислять радиус, вычесть размер цифры (--_sz) из размера окружности (--_w):
--_r: calc((var(--_w) - var(--_sz)) / 2);
Так гораздо лучше! Давайте изменим цвета, чтобы все выглядело более элегантно:
В принципе здесь можно было бы и закончить. Ведь мы достигли поставленной цели разместить текста по кругу, верно? Но что такое часы без стрелок, показывающих часы, минуты и секунды?
Давайте для этого используем CSS анимацию. Для начала в нашу разметку добавим несколько дополнительных элементов:
<div class="clock">
<!-- после тегов <time> -->
<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
принимает значение длительности только в миллисекундах и секундах. Давайте остановимся на секундах. Тогда значение для часовой стрелки будет равно 43200 секундам (60 секунд * 60 минут * 12 часов):
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()
объекта Date использует 24-часовой формат, поэтому мы при помощи оператора вычисления остатка преобразуем его результат в 12-часовой формат.
В CSS стилях нам также нужно добавить animation-delay
:
.minutes {
animation-delay: var(--_dm, 0s);
/* другие стили */
}
.hours {
animation-delay: var(--_dh, 0s);
/* другие стили */
}
Еще кое-что. Используя 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)))
}
}
И вуаля! Наши часы готовы!
See the Pen CSS sin() and cos() Demo 4 by Mads Stoumann.
Что еще можно сделать?
Ну, например, мы можем быстро превратить наши часы в галерею круглых изображений, заменив теги <time>
на <img>
, а затем обновив значения ширины (--_w) и радиуса (--_r):
See the Pen CSS sin() and cos() Demo 2 by Alexei Goloviznin.
Давайте попробуем кое-что. Ранее мы уже видели, что в процессе создания наши часы выглядели, как эксперимент в области современного искусства. Мы можем опереться на это и воссоздать шаблон, который когда-то был выставлен в одной художественной галерее. Помнится, назывался он "Луна" и состояли из множества точек, образующих круг.
На этот раз мы будем использовать неупорядоченный список, поскольку круги не следуют определенному порядку. Мы даже не будем встраивать все элементы списка в разметку. Вместо этого давайте внедрим код JavaScript и добавим небольшую систему управления, которую мы сможем использовать для манипулирования конечным результатом.
Система управления представляет собой элементы ввода диапазона (<input type="range">
), которые мы обернем в тег <form>
и будем отслеживать событие input.
<form id="controls">
<fieldset>
<label>Число колец
<input type="range" min="2" max="12" value="10" id="rings" />
</label>
<label>Точек на кольцо
<input type="range" min="5" max="12" value="7" id="dots" />
</label>
<label>Разброс
<input type="range" min="10" max="40" value="40" id="spread" />
</label>
</fieldset>
</form>
При событии "input" мы запустим метод, который создаст набор элементов <li>
с переменной градусов (--_d), которую мы использовали ранее. Мы также можем изменить назначение нашей переменной радиуса (--_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()
помогают нам расположить все точки в нужных местах.
See the Pen CSS sin() and cos() Demo 3 by Alexei Goloviznin.
Заключение
Размещение объектов по кругу - довольно простой пример, демонстрирующий возможности тригонометрических функций, таких как sin()
и cos()
. Но действительно здорово, что мы получаем современные функции CSS, которые предоставляют новые решения для старых обходных путей, мы уверены, что скоро появятся гораздо более интересные, сложные и креативные варианты их использования.