React + Styled Components — идеальная анимация. Параметризованная анимация

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Введение

Рассмотрим способ реализации “параметризованной” css анимации React компонента с помощью styled components. Параметризованная потому что css анимация описывается через параметры, которые динамически рассчитываются на основе пропсов и состояний компонента при его рендеринге.

Идея возникла при необходимости создания анимации таймера:

Требуемая анимация
Требуемая анимация

Анимация цифры не представляет трудности, а вот бегущая закрашенная часть границы достаточно интересна. Длина закрашенной части границы уменьшается пропорционально тому, как таймер стремится к 0.

Если принять изначально, что таймер считает всегда 10 секунд, то написать css анимацию вполне просто. Но хочется сделать красивый универсальный компонент, в который просто можно передать количество секунд и получить красивый результат. То есть анимация должна рассчитываться в зависимости от стартового значения таймера.

Для решения поставленной задачи пришла идея воспользоваться одной из основных возможностей styled components — передачей пропсов для параметризации стилей  (тут написано на сколько это круто).


К делу

Создаем компонент. На вход компонент принимает только один параметр time — время сколько необходимо считать. Дальше создаем состояние компонента currentNum — текущее значение счетчика.

  • В useEffect прописываем обновление счетчика, в случае изменения time.

  • <GreyFonPopup> — просто внешний div, который закрывает всё приложение серой полупрозрачной пленкой (потому что компонент разрабатывался как pop-up).

  • <TimerContainer> — блок таймера на котором и происходит анимация рамки.

  • <NumberContainer> и вложенные в него блоки <span> служат для анимации переворачивания цифры счетчика, подробно разбираться в них не будем. Единственное, важно отметить, что на span висит анимация длительностью в секунду, поэтому событие onAnimationEnd() используется как таймер для отсчета и в нём уменьшается счетчик компонента.

Для создания на основе пропсов анимации прокидываем их в TimerContainer.

import { useEffect, useState } from 'react';
import { GreyFonPopup, NumberContainer, TimerContainer } from './style';

interface IPropsTimer {
  time: number;
}

export const Timer: React.FC<IPropsTimer> = ({ time }) => {

  const [currentNum, setCurrentNum] = useState(time);

  useEffect(() => {
    setCurrentNum(time);
  }, [time])

  return (
    <GreyFonPopup>
      <TimerContainer
        time={time}
        currentNum={currentNum}
      >
        <NumberContainer>
          <span
            key={currentNum}
            onAnimationEnd={
              () => {
                currentNum > 1 && setCurrentNum(currentNum - 1);
              }
            }
          >{currentNum}</span>
          <span
            key={-currentNum}
          >{currentNum - 1}</span>
        </NumberContainer>
      </TimerContainer>
    </GreyFonPopup>
  )
}

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

Принцип создания анимированной рамки. Источник тут
Принцип создания анимированной рамки. Источник тут

Следовательно, в нашем случае на псевдоэлементе before, работают две анимации.
Первая — простое бесконечно вращение, вторая — анимация отвечающая за изменение длины закрашенной части границы (угла заливки градиента), которую необходимо рассчитывать исходя из начального и текущего значения таймера.

Для расчета и инициализации этой анимации вызываем функцию animateBorder() и передаем в неё рассчитанные на основе пропсов параметры.

360 * props.currentNum / props.time — текущая величина светящейся части контура в градусах;
360 / props.time  — шаг изменения длины светящейся части контура в градусах.

export const TimerContainer = styled.div<{ time: number; currentNum: number }>`
  position: relative;
  z-index: 0;
  width: 200px;
  height: 150px;
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;  

  &::before{
    content: '';
    position: absolute;
    z-index: -2;
    left: -50%;
    top: -50%;
    width: 200%;
    height: 200%;
    animation: ${props => animateBorder(360 * props.currentNum / props.time, 360 / props.time)}, 
              ${animateBorderRotate} 4s linear infinite;
  }

  &::after{
    content: '';
    position: absolute;
    z-index: -1;
    left: 2px;
    top: 2px;
    width: calc(100% - 4px);
    height: calc(100% - 4px);
    background: #14181f;
    border-radius: 12px;
  }
`;

Функция для создания анимации animateBorder(), как уже было сказано, принимает два параметра:

deg — значение в градусах, соответствующее текущей длине окрашенной линии границы, step — шаг в градусах, на который надо изменить длину этой линии.

Сама функция создает ключевые кадры анимации и возвращает css анимацию на их основе.

const animateBorder = (deg: number, step: number) => {
  const anim = keyframes`
    0%{
      background: conic-gradient(
        #37f 0deg ${`${deg}deg`},
        #14181f ${`${deg}deg`} 0deg
        );
    }
    100%{
      background: conic-gradient(
        #37f 0deg ${`${deg - step}deg`},
        #14181f ${`${deg - step}deg`} 0deg
        );
    }
  `
  return css`${anim} 1s linear forwards 1`
};

Но так как градиент фона не любит анимироваться, визуально анимация будет совсем не та. Линия уменьшается не плавно, а как-будто от неё периодически откусывают кусок:

Выход есть, пусть и не очень красивый. Вручную прописать промежуточные состояния. Однако если прописать порядка 10 промежуточных точек, то при счете от большого значения (то есть при малом шаге изменения длины) всё будет работать, а при счете от малых значений (большой шаг изменения длины), визуально будет выглядеть отрывисто. Если же описать много промежуточных точек, то будет выглядеть всегда плавно, но будет не рационально для анимации счета от большого числа, и код будет сильно раздут.

Поэтому воспользуемся ещё одним преимуществом JS — напишем цикл. В цикле будем составлять ключевые кадры анимации, и сделаем количество точек зависящим от шага анимации.

const animateBorder = (deg: number, step: number) => {
  let myKeyframes = ``;
  const delta = Math.round(400 / (360 / step));
  for (let i = 0; i < delta + 1; i++) {
    myKeyframes += `
              ${100 * i / delta}%{
                background: conic-gradient(
                  #37f 0deg ${`${deg - step * i / delta}deg`}, 
                  #14181f ${`${deg - step * i / delta}deg`} 0deg
                  );
              }`
  }

  const anim2 = keyframes`${myKeyframes}`;

  return css`${anim2} 1s linear forwards 1`
};

Ну вот и красота. Мы написали css анимацию компонента, которая зависит от переданных в него параметров, и сделали это без описания кучи разных классов и анимации для них.

Код проекта с итоговой версией таймер на Github  

А тут можно посмотреть на работу таймера   

P.S. надеюсь данный материал поможет кому-то в создании прекрасной анимации. Если же я допустил ошибки или вы имеете своё мнение по данному подходу, с удовольствием жду вас в комментариях.

Источник: https://habr.com/ru/articles/751120/


Интересные статьи

Интересные статьи

Привет, друзья! В этой статье я хочу поговорить с вами об управлении состоянием и повторном рендеринге в React.js. Что такое состояние и зачем им управлять? Состояние/state можно определить к...
Пожалуй, каждый разработчик сталкивается с ситуациями, когда два и более компонентов оказываются тесно связаны логически, но в DOM отображаются раздельно, вперемешку с другими элементами UI. Лобовое р...
От переводчика. Привет! Про Kotlin есть стереотип, будто бы это язык для разработки только под Android. На самом деле, это совсем не так: язык официально поддерживает несколько платформ ...
Сбор данных в React — это одно. Хранение и кэширование этих данных — это другая история. Возможности кажутся бесконечными, а различия часто тонкие, что делает выбор прави...
В данной статье приведены примеры вопросов и задач по React Hooks для собеседования Читать далее