Как стать автором
Обновить

Часы на синусах и косинусах в CSS

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров8.2K
Автор оригинала: Mads Stoumann

В этой статье осторожно прикоснёмся к sin() и cos(). Есть и другие функции, в том числе tan(), зачем же останавливаться только на синусе и косинусе? Они идеальны для размещения текста по краю круга. Об этом уже рассказывалось на CSS-Tricks, когда Крис поделился подходом к размещению текста по кругу с миксинами Sass. Это было шесть лет назад [оригинал опубликован 8 марта 2023 года], поэтому поработаем с размещением чисел по-новому.


Вот о чём я говорю:



Не совсем похоже на текст, образующий круглую форму, но символы размешаются вдоль круга в виде циферблата. Вот разметка, которой можно воспользоваться для начала:


<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 сохраняется в переменной. Это значение мы используем позже.


image


Похоже на какой-то эксперимент в области современного искусства, правда? Для хранения радиуса окружности, равного половине её ширины, введём переменную --_r. И благодаря другой математической функции CSS, calc(), при изменении ширины окружности--_w изменится её радиус --_r:


.clock {
  --_w: 300px;
  --_r: calc(var(--_w) / 2);
  /* остальные стили */
}

Теперь немного математики. В окружности 360 градусов. На часах 12 меток, поэтому разместить цифры нужно через каждые 30 градусов (360/12). В математике круг начинается с 3 часов на часах, так что «полдень» — это −90 градусов от этого значения: 360 − 90 = 270 градусов.


Добавим ещё одну переменную — --_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. Посмотрим, что у нас есть:


image


Опредёленно больше похоже на часы! Радиус при вычислении позиций каждого числа нужно сократить, чтобы левый верхний угол каждого числа располагался в подходящем месте. До вычисления радиуса размер числа (--_sz) можно вычесть из размера окружности (--_w):


--_r: calc((var(--_w) - var(--_sz)) / 2);

image


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


image


Остановиться можно прямо здесь. Мы расположили текст по кругу, правильно? Но что такое часы без стрелок, показывающих часы, минуты и секунды?


Добавим в разметку ещё три элемента:


<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 часов. Свойство CSS animation-duration принимает значения только в миллисекундах и секундах. Остановимся на секундах. Здесь значение равно 43 200 секундам — это 60 секунд * 60 минут * 12 часов:


animation: turn 43200s infinite;

Минутной стрелке на оборот требуется один час. Но хочется, чтобы анимация была многоступенчатой, то есть чтобы движение стрелок было ступенчатым, а не линейным. Для этого понадобится 60 шагов анимации, по одному на каждую минуту:


animation: turn 3600s steps(60, end) infinite;

Секундная стрелка движется плавно, продолжительность оборота составляет 60 секунд:


animation: turn 60s linear 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, как это сделал Мейт Маршалко с часами только на CSS. Метод getHours() JavaScipt-объекта Date использует 24-часовой формат, поэтому для преобразования в 12-часовой формат используем оператор остатка %.


И нужно добавить animation-delay в CSS:


.minutes {
  animation-delay: var(--_dm, 0s);
  /* остальные стили */
}

.hours {
  animation-delay: var(--_dh, 0s);
  /* остальные стили */
}

При помощи @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)))
  }
}

Вуаля! Наши часы готовы:



Что ещё можно сделать?


Заменой <time> на <img>, а также обновлением диаметра --_w и радиуса --_r можно быстро превратить часы в галерею круглых изображений:



Если код не работает, откройте этот спойлер со скриншотом

image


Я упоминал, что часы выглядели как эксперимент в области современного искусства. Можно опереться на это и воссоздать узор, который я видел на днях на плакате (к сожалению, я его не купил) в художественной галерее. Насколько я помню, он назывался «Луна» и состоял из множества точек, образующих круг:


image


На этот раз используем неупорядоченный список, ведь круги не следуют определённому порядку. Не нужно даже помещать все элементы списка в разметку. Вместо этого внедрим в них JavaScript и добавим несколько элементов управления.


Эти элементы представляют собой входные данные диапазона <input type="range">, которые мы обернём в <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> с примененной к каждому из них переменной градусов --_d. Эту переменную мы использовали ранее. А ещё можно изменить значение радиуса (--_r).


Хочется, чтобы точки были разноцветными. Давайте рандомизируем (ну, не полностью) HSL-значение цвета для каждого элемента списка и сохраним его как новую переменную — --_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().


Заключение


Размещение объектов по кругу — довольно простой пример, показывающий возможности тригонометрических функций. По-настоящему здорово, что мы получаем функции CSS, которые дают новые решения старых проблем. Уверен, мы увидим гораздо более интересные, сложные, творческие подходы к применению тригонометрических функций.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 14: ↑14 и ↓0+14
Комментарии8

Публикации

Истории

Работа

React разработчик
48 вакансий
Веб дизайнер
20 вакансий

Ближайшие события