Реакция: Создание простых аналоговых часов

Недавно мне нужно было создать простой компонент аналоговых часов для моего проекта React. Теперь я хочу поделиться тем, как я к этому подошел.

Я создал компонент React, рисующий циферблат часов и три стрелки для часов, минут и секунд. Для стилизации часов я использовал HTML с помощью Styled Components. Конечно, это можно было бы сделать с помощью SVG или рисования на холсте. Возможно, я рассмотрю эти варианты позже. На данный момент я хотел сделать все просто, и поскольку у нас всего 3 движущиеся части, это может не слишком сильно повлиять на производительность.

Давайте начнем.

1. Создание циферблата часов и его стилизация

Сначала нам нужен базовый компонент. Он не будет делать ничего, кроме рисования циферблата часов.

import React  from 'react'
import styled from 'styled-components'

const DemoClock: React.FC = () => {
  return <Clock />
}

const Clock = styled.div`
  background-color: white;
  border-radius: 50%;
  border: black 1px solid;
  height: 100px;
  margin-bottom: 0.5rem;
  position: relative;
  width: 100px;
`

export default DemoClock
Войти в полноэкранный режим Выйдите из полноэкранного режима

Это создаст круглый белый фон с черной рамкой диаметром 100 пикселей.

2. Добавьте стрелки

Теперь мы добавим стрелки. Для примера возьмем часовую стрелку.

const Hours = styled.div`
  background-color: black;
  border-radius: 2.5px;
  height: 30px;
  left: calc(50% - 2.5px);
  position: absolute;
  top: 25px;
  width: 5px;
`
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Это создаст черную стрелку длиной 30 пикселей с закругленными краями и небольшим перекрытием 5 px в центре.

Давайте посмотрим на некоторые детали:

Затем мы добавим стрелку к часам.

const DemoClock: React.FC = () => {
  return (
    <Clock>
      <Hours />
    </Clock>
  )
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

Повторите это для минутной и секундной стрелок. Теперь у моих часов есть все три стрелки.

const DemoClock: React.FC = () => {
  return (
    <Clock>
      <Hours />
      <Minutes />
      <Seconds />
    </Clock>
  )
}

const Clock = styled.div`
  background-color: white;
  border-radius: 50%;
  border: black 1px solid;
  height: 100px;
  margin-bottom: 0.5rem;
  position: relative;
  width: 100px;
`

const Hours = styled.div`
  background-color: black;
  border-radius: 2.5px;
  height: 30px;
  left: calc(50% - 2.5px);
  position: absolute;
  top: 25px;
  width: 5px;
`

const Minutes = styled(Hours)`
  height: 45px;
  top: 10px;
`

const Seconds = styled(Hours)`
  background-color: red;
  height: 50px;
  top: 5px;
  width: 1px;
`
Войти в полноэкранный режим Выйдите из полноэкранного режима

3. Чтобы стрелка часов отображала текущее время

Давайте начнем с отображения текущего часа. Для этого добавим к стилизованному компоненту реквизит time, чтобы иметь возможность передать ему любой объект Date.

Мы знаем, что на часах 12 часов, поэтому мы можем рассчитать угол наклона стрелки для каждого часа, разделив 360 градусов на 12. Это даст нам 30 градусов в час. Есть небольшая оговорка: getHours() возвращает до 24 часов в сутки. Поэтому мы должны убедиться, что получим только 12 часов, используя модуль 12.

interface DateProps {
  time: Date
}

const Hours = styled.div<DateProps>`
  ...
  transform-origin: center calc(100% - 5px);
  transform: rotateZ(${({ time }) => ((time.getHours() % 12) * 30}deg);
`
Вход в полноэкранный режим Выход из полноэкранного режима

Мы также должны установить точку вращения в центр часов. Это можно сделать, задав начало координат преобразования. Используя calc(100% - 5px), мы позаботимся о перекрытии стрелки на 5 пикселей.

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

Мы умножаем часы на 60 и прибавляем к ним текущие минуты. Таким образом, значение будет отражать текущее время в минутах. Но теперь угол каждой единицы измерения отличается. У нас есть 12 * 60 = 720 минут в 12 часах, поэтому мы можем рассчитать угол каждой минуты, разделив 360 градусов на 720. Это даст нам 0,5 градуса в минуту.

const Hours = styled.div<DateProps>`
  ...
  transform: rotateZ(${({ time }) => ((time.getHours() % 12) * 60 + time.getMinutes()) * 0.5}deg);
`
Вход в полноэкранный режим Выход из полноэкранного режима

4. Повторите для минут и секунд

Аналогичным образом мы добавим вращение минутной и секундной стрелок.

const Minutes = styled(Hours)`
  ...
  transform: rotateZ(${({ time }) => (time.getMinutes() * 60 + time.getSeconds()) * 0.1}deg);
`

const Seconds = styled(Hours)`
  ...
  transform: rotateZ(${({ time }) => time.getSeconds()  * 6}deg);
`
Войти в полноэкранный режим Выйти из полноэкранного режима

5. Обновление времени

Теперь нам осталось добавить время в компонент часов. Мы можем сделать это с помощью состояния, содержащего текущее время, и таймера, обновляющего время каждую секунду. Убедитесь, что интервал очищается, когда компонент размонтирован.

const DemoClock: React.FC = () => {
  const [time, setTime] = useState(() => new Date())

  useEffect(() => {
    const interval = setInterval(() => {
      const now = new Date()
      setTime(now)
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return (
    <Clock>
      <Hours time={time} />
      <Minutes time={time} />
      <Seconds time={time} />
    </Clock>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Поэтому мы можем увеличить разрешение часов до 250 миллисекунд или даже ниже (в зависимости от ваших потребностей), но обновлять состояние только при изменении секунды.

  useEffect(() => {
    const interval = setInterval(() => {
      const now = new Date()
      if (now.getSeconds() !== time.getSeconds())) {
        setTime(now)
      }
    }, 250)

    return () => clearInterval(interval)
  }, [time])
Вход в полноэкранный режим Выход из полноэкранного режима

6. Еще одна вещь

Пока это работает, мы создали потенциальную проблему. Проблема, которая довольно специфична для стилизованных компонентов. Стилизованные компоненты создают новый класс для каждой уникальной комбинации реквизитов. Это означает, что если вы измените реквизиты компонента, класс будет создан заново. Это является проблемой для производительности. Решением является использование метода attr().

const Hours = styled.div.attrs<DateProps>(({ time }) => ({
  style:{
    transform: `rotateZ(${((time.getHours() % 12) * 60 + time.getMinutes()) * 0.5}deg)`,
  },
})).<DateProps>`
   ...
`
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Мы обнаружили, что работа со временем сопряжена с определенными трудностями (хотя мы только поцарапали поверхность — все становится довольно сложным, как только вам нужно синхронизироваться с сервером, требуется точность и/или приходится иметь дело с часовыми поясами). Но вот они: работающие часы.

Посмотрите на готовую реализацию в этом gist.

Вы можете продолжить и усовершенствовать часы: Попробуйте добавить поле дня месяца, добавить индикаторы для часов и попробовать различные варианты дизайна стрелок, используя чистый css или svg. Сцена в вашем распоряжении.

Вот и все. Надеюсь, вы получили удовольствие от проведенного времени.

Оцените статью
devanswers.ru
Добавить комментарий