Создаем часы при помощи новых тригонометрических CSS функций sin() и cos()

alexei26/08/2023 - 10:03
Создаем часы при помощи новых тригонометрических CSS функций sin() и cos()

Ну вот, наконец, у нас есть тригонометрические 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-переменной. Мы будем использовать это позже. Пока здесь в общем то не на что особо обращать внимание:

Большой оранжевый круг с цифрами 1-12 расположенными вертикально

Очень похоже на картину современных художников, не так ли? Давайте введем новую переменную --_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, которую в данный момент мы будем использовать для ширины и высоты цифр. Давайте посмотрим, что мы имеем на данном этапе.

Большой оранжевый круг с цифрами 1-12 расположенными по краю

Это уже больше похоже на часы! Обратите внимание, что верхний левый угол каждой цифры расположен в правильном месте на окружности. Теперь, нам нужно "уменьшить" радиус при вычислении позиций для каждой цифры. Мы можем, прежде чем вычислять радиус, вычесть размер цифры (--_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, которые предоставляют новые решения для старых обходных путей, мы уверены, что скоро появятся гораздо более интересные, сложные и креативные варианты их использования.