1 ноября 2022 г.

CSS-анимации

CSS позволяет создавать простые анимации без использования JavaScript.

JavaScript может быть использован для управления такими CSS-анимациями. Это позволяет делать более сложные анимации, используя небольшие кусочки кода.

CSS-переходы

Идея CSS-переходов проста: мы указываем, что некоторое свойство должно быть анимировано, и как оно должно быть анимировано. А когда свойство меняется, браузер сам обработает это изменение и отрисует анимацию.

Всё что нам нужно, чтобы начать анимацию – это изменить свойство, а дальше браузер сделает плавный переход сам.

Например, CSS-код ниже анимирует трёх-секундное изменениеbackground-color:

.animated {
  transition-property: background-color;
  transition-duration: 3s;
}

Теперь, если элементу присвоен класс .animated, любое изменение свойства background-color будет анимироваться в течение трёх секунд.

Нажмите кнопку ниже, чтобы анимировать фон:

<button id="color">Нажми меня</button>

<style>
  #color {
    transition-property: background-color;
    transition-duration: 3s;
  }
</style>

<script>
  color.onclick = function() {
    this.style.backgroundColor = 'red';
  };
</script>

Существует 4 свойства для описания CSS-переходов:

  • transition-property – свойство перехода
  • transition-duration – продолжительность перехода
  • transition-timing-function – временная функция перехода
  • transition-delay – задержка начала перехода

Далее мы рассмотрим их все, а сейчас ещё заметим, что есть также общее свойство transition, которое позволяет задать их одновременно в последовательности: property duration timing-function delay, а также анимировать несколько свойств одновременно.

Например, у этой кнопки анимируются два свойства color и font-size одновременно:

<button id="growing">Нажми меня</button>

<style>
#growing {
  transition: font-size 3s, color 2s;
}
</style>

<script>
growing.onclick = function() {
  this.style.fontSize = '36px';
  this.style.color = 'red';
};
</script>

Теперь рассмотрим каждое свойство анимации по отдельности.

transition-property

В transition-property записывается список свойств, изменения которых необходимо анимировать, например: left, margin-left, height, color.

Анимировать можно не все свойства, но многие из них. Значение свойства all означает «анимируй все свойства».

transition-duration

В transition-duration можно определить, сколько времени займёт анимация. Время должно быть задано в формате времени CSS: в секундах s или миллисекундах ms.

transition-delay

В transition-delay можно определить задержку перед началом анимации. Например, если transition-delay: 1s, тогда анимация начнётся через 1 секунду после изменения свойства.

Отрицательные значения также допустимы. В таком случае анимация начнётся с середины. Например, если transition-duration равно 2s, а transition-delay-1s, тогда анимация займёт одну секунду и начнётся с середины.

Здесь приведён пример анимации, сдвигающей цифры от 0 до 9 с использованием CSS-свойства transform со значением translate:

Результат
script.js
style.css
index.html
stripe.onclick = function() {
  stripe.classList.add('animate');
};
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: linear;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Кликните на цифру для начала анимации:

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script src="script.js"></script>
</body>

</html>

Свойство transform анимируется следующим образом:

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
}

В примере выше JavaScript-код добавляет класс .animate к элементу, после чего начинается анимация:

stripe.classList.add('animate');

Можно начать анимацию «с середины», с определённого числа, например, используя отрицательное значение transition-delay, соответствующие необходимому числу.

Если вы нажмёте на цифру ниже, то анимация начнётся с последней секунды:

Результат
script.js
style.css
index.html
stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
};
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: linear;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Click below to animate:
  <div id="digit"><div id="stripe">0123456789</div></div>

  <script src="script.js"></script>
</body>
</html>

JavaScript делает это с помощью нескольких строк кода:

stripe.onclick = function() {
  let sec = new Date().getSeconds() % 10;
  // например, значение -3s здесь начнут анимацию с третьей секунды
  stripe.style.transitionDelay = '-' + sec + 's';
  stripe.classList.add('animate');
};

transition-timing-function

Временная функция описывает, как процесс анимации будет распределён во времени. Будет ли она начата медленно и затем ускорится или наоборот.

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

Это свойство может принимать два вида значений: кривую Безье или количество шагов. Давайте начнём с кривой Безье, как с наиболее часто используемой.

Кривая Безье

Временная функция может быть задана, как кривая Безье с 4 контрольными точками, удовлетворяющими условиям:

  1. Первая контрольная точка: (0,0).
  2. Последняя контрольная точка: (1,1).
  3. Для промежуточных точек значение x должно быть 0..1, значение y может принимать любое значение.

Синтаксис для кривых Безье в CSS: cubic-bezier(x2, y2, x3, y3). Нам необходимо задать только вторую и третью контрольные точки, потому что первая зафиксирована со значением (0,0) и четвёртая – (1,1).

Временная функция описывает то, насколько быстро происходит анимации во времени.

  • Ось x – это время: 0 – начальный момент, 1 – последний момент transition-duration.
  • Ось y указывает на завершение процесса: 0 – начальное значение свойства, 1 – конечное значение.

Самым простым примером анимации является равномерная анимация с линейной скоростью. Она может быть задана с помощью кривой cubic-bezier(0, 0, 1, 1).

Вот как выглядит эта «кривая»:

…Как мы видим, это прямая линия. Значению времени (x) соответствует значение завершённости анимации (y), которое равномерно изменяется от 0 к 1.

В примере ниже поезд «едет» слева направо с одинаковой скоростью (нажмите на поезд):

Результат
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">

</body>

</html>

В свойстве transition указана следующая кривая Безье:

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, 0, 1, 1);
  /* JavaScript устанавливает свойство left равным 450px */
}

…А как показать замедляющийся поезд?

Мы можем использовать другую кривую Безье: cubic-bezier(0.0, 0.5, 0.5 ,1.0).

Её график:

Как видим, анимация начинается быстро: кривая быстро поднимается вверх, и затем все медленнее и медленнее.

Ниже временная функция в действии (нажмите на поезд):

Результат
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 0px;
  transition: left 5s cubic-bezier(0.0, 0.5, 0.5, 1.0);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'">

</body>

</html>

CSS:

.train {
  left: 0;
  transition: left 5s cubic-bezier(0, .5, .5, 1);
  /* JavaScript устанавливает свойство left равным 450px */
}

Есть несколько встроенных обозначений кривых Безье: linear, ease, ease-in, ease-out и ease-in-out.

linear это короткая запись для cubic-bezier(0, 0, 1, 1) – прямой линии, которую мы видели раньше.

Другие названия – это также сокращения для других cubic-bezier:

ease* ease-in ease-out ease-in-out
(0.25, 0.1, 0.25, 1.0) (0.42, 0, 1.0, 1.0) (0, 0, 0.58, 1.0) (0.42, 0, 0.58, 1.0)

* – используется по умолчанию, если не задана другая временная функция.

Для того, чтобы замедлить поезд, мы можем использовать ease-out:

.train {
  left: 0;
  transition: left 5s ease-out;
  /* transition: left 5s cubic-bezier(0, .5, .5, 1); */
}

Но получившийся результат немного отличается.

Кривая Безье может заставить анимацию «выпрыгивать» за пределы диапазона.

Контрольные точки могут иметь любые значения по оси y: отрицательные или сколь угодно большие. В таком случае кривая Безье будет скакать очень высоко или очень низко, заставляя анимацию выходить за её нормальные пределы.

В приведённом ниже примере код анимации:

.train {
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
  /* JavaScript sets left to 400px */
}

Свойство left будет анимироваться от 100px до 400px.

Но когда вы нажмёте на поезд, вы увидите следующее:

  • Сначала, поезд поедет назад: left станет меньше, чем 100px.
  • Затем он поедет вперёд, немного дальше, чем 400px.
  • И затем вернётся назад в значение 400px.
Результат
style.css
index.html
.train {
  position: relative;
  cursor: pointer;
  width: 177px;
  height: 160px;
  left: 100px;
  transition: left 5s cubic-bezier(.5, -1, .5, 2);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='400px'">

</body>

</html>

Если мы взглянем на кривую Безье из примера, становится понятно поведение поезда.

Мы вынесли координату y для первой опорной точки ниже нуля и выше единицы для третьей опорной точки, поэтому кривая вышла за пределы «обычного» квадрата. Значения y вышли из «стандартного» диапазона 0..1.

Как мы знаем, ось y измеряет «завершённость процесса анимации». Значение y = 0 соответствует начальному значению анимируемого свойства и y = 1 – конечному значению. Таким образом, y<0 делает значение свойства left меньше начального значения и y>1 – больше конечного.

Это, конечно, «мягкий» вариант. Если значение y будут -99 и 99, то поезд будет гораздо сильнее «выпрыгивать» за пределы.

Как сделать кривую Безье необходимую для конкретной задачи? Существует множество инструментов.

  • К примеру, мы можем сделать это на сайте https://cubic-bezier.com.
  • Браузернные инструменты разработчика также имеют специальную поддержку для создания кривых Безье в CSS:
    1. Откройте инструменты разработчика при помощи F12 (Mac: Cmd+Opt+I).
    2. Выберете вкладку Elements, затем обратите внимание на под-панель Styles в правой стороне.
    3. Свойства CSS со словом cubic-bezier будут иметь иконку перед этим словом.
    4. Кликните по иконке, чтобы отредактировать кривую.

Шаги

Временная функция steps(количество шагов[, start/end]) позволяет разделить анимацию на шаги.

Давайте рассмотрим это на уже знакомом нам примере с цифрами.

Ниже представлен список цифр, без какой-либо анимации, который мы будем использовать в качестве основы:

Результат
style.css
index.html
#digit {
  border: 1px solid red;
  width: 1.2em;
}

#stripe {
  display: inline-block;
  font: 32px monospace;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="digit"><div id="stripe">0123456789</div></div>

</body>
</html>

В HTML, вереница цифр заключена в <div id="digits"> фиксированной длины:

<div id="digit">
  <div id="stripe">0123456789</div>
</div>

Div-элемент #digit имеет фиксированную ширину и границу, поэтому он выглядит как красное окно.

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

Чтобы добиться этого, мы скроем #stripe за пределами #digit, используя overflow: hidden, а затем, шаг за шагом будем сдвигать #stripe влево.

Всего будет 9 шагов, один шаг для каждой цифры:

#stripe.animate  {
  transform: translate(-90%);
  transition: transform 9s steps(9, start);
}

Первый аргумент временной функции steps(9, start) – количество шагов. Трансформация будет разделена на 9 частей (10% каждая). Временной интервал также будет разделён на 9 частей, таким образом свойство transition: 9s обеспечивает нам 9 секунд анимации, что даёт по одной секунде на цифру.

Вторым аргументом является одно из ключевых слов: start или end.

start – означает, что в начале анимации нам необходимо перейти на первый шаг немедленно.

В действии:

Результат
style.css
index.html
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: steps(9, start);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Кликните на цифру для начала анимации:

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script>
    digit.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>


</body>

</html>

Щелчок по цифре немедленно изменяет её на 1 (первый шаг), а затем изменяется в начале следующей секунды.

Анимация будет происходить так:

  • 0s-10% (первое изменение в начале первой секунды, сразу после нажатия)
  • 1s-20%
  • 8s-90%
  • (на протяжении последней секунды отображается последнее значение).

Здесь первое изменение было немедленным из-за start в steps.

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

Анимация будет происходить так:

  • 0s0
  • 1s-10% (первое изменение произойдёт в конце первой секунды)
  • 2s-20%
  • 9s-90%

Пример step(9, end) в действии (обратите внимание на паузу перед первым изменением цифры):

Результат
style.css
index.html
#digit {
  width: .5em;
  overflow: hidden;
  font: 32px monospace;
  cursor: pointer;
}

#stripe {
  display: inline-block
}

#stripe.animate {
  transform: translate(-90%);
  transition-property: transform;
  transition-duration: 9s;
  transition-timing-function: steps(9, end);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  Кликните на цифру для начала анимации:

  <div id="digit"><div id="stripe">0123456789</div></div>

  <script>
    digit.onclick = function() {
      stripe.classList.add('animate');
    }
  </script>


</body>

</html>

Существуют также некоторые заранее определённые сокращения для steps(...):

  • step-start – то же самое, что steps(1, start). Оно означает, что анимация начнётся сразу и произойдёт в один шаг. Таким образом она начнётся и завершится сразу, как будто и нет никакой анимации.
  • step-end – то же самое, что steps(1, end): выполнит анимацию за один шаг в конце transition-duration.

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

Событие: «transitionend»

Когда завершается анимация, срабатывает событие transitionend.

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

Например, корабль в приведённом ниже примере начинает плавать туда и обратно по клику, каждый раз все дальше и дальше вправо:

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

boat.onclick = function() {
  //...
  let times = 1;

  function go() {
    if (times % 2) {
      // плыть вправо
      boat.classList.remove('back');
      boat.style.marginLeft = 100 * times + 200 + 'px';
    } else {
      // плыть влево
      boat.classList.add('back');
      boat.style.marginLeft = 100 * times - 200 + 'px';
    }

  }

  go();

  boat.addEventListener('transitionend', function() {
    times++;
    go();
  });
};

Объект события transitionend содержит ряд полезных свойств:

event.propertyName
Имя свойства, анимация которого завершилась. Может быть полезным, если мы анимируем несколько свойств.
event.elapsedTime
Время (в секундах), которое заняла анимация, без учёта transition-delay.

Ключевые кадры

Мы можем объединить несколько простых анимаций вместе, используя CSS-правило @keyframes.

Оно определяет «имя» анимации и правила: что, когда и где анимировать. После этого можно использовать свойство animation, чтобы назначить анимацию на элемент и определить её дополнительные параметры.

Ниже приведён пример с пояснениями:

<div class="progress"></div>

<style>
  @keyframes go-left-right {        /* объявляем имя анимации: "go-left-right" */
    from { left: 0px; }             /* от: left: 0px */
    to { left: calc(100% - 50px); } /* до: left: 100%-50px */
  }

  .progress {
    animation: go-left-right 3s infinite alternate;
    /* применить анимацию "go-left-right" на элементе
       продолжительностью 3 секунды
       количество раз: бесконечно (infinite)
       менять направление анимации каждый раз (alternate)
    */

    position: relative;
    border: 2px solid green;
    width: 50px;
    height: 20px;
    background: lime;
  }
</style>

Существует множество статей про @keyframes, а также детальная спецификация.

Скорее всего, вам нечасто понадобится @keyframes, разве что на вашем сайте все постоянно в движении.

Итого

CSS-анимации позволяют плавно, или не очень, менять одно или несколько свойств.

Они хорошо решают большинство задач по анимации. Также мы можем реализовать анимации через JavaScript, более подробно об этом – в следующей главе.

Ограничения CSS-анимаций в сравнении с JavaScript-анимациями:

Достоинства
  • Простые анимации делаются просто.
  • Быстрые и не создают нагрузку на CPU.
Недостатки
  • JavaScript-анимации более гибкие. В них может присутствовать любая анимационная логика, как например «взорвать» элемент.
  • Можно изменять не только свойства. Мы можем создавать новые элементы с помощью JavaScript для анимации.

Большинство анимаций может быть реализовано с использованием CSS, как описано в этой главе. А событие transitionend позволяет запускать JavaScript после анимации, поэтому CSS-анимации прекрасно интегрируются с кодом.

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

Задачи

важность: 5

Реализуйте анимацию, как в примере ниже (клик на самолёт):

  • При нажатии картинка изменяет размеры с 40x24px до 400x240px (увеличивается в 10 раз).
  • Время анимации 3 секунды.
  • По окончании анимации вывести сообщение: «Анимация закончилась!».
  • Если во время анимации будут дополнительные клики по картинке – они не должны ничего «сломать».

Открыть песочницу для задачи.

CSS для анимации двух свойств width и height:

/* original class */

#flyjet {
  transition: all 3s;
}

/* JS adds .growing */
#flyjet.growing {
  width: 400px;
  height: 240px;
}

При разработке следует учитывать, что событие transitionend сработает два раза – для каждого свойства (высота и ширина). Таким образом, если не предусмотреть дополнительную проверку, тогда сообщение появится два раза.

Открыть решение в песочнице.

важность: 5

Модифицируйте решение предыдущей задачи Анимировать самолёт (CSS) , чтобы в процессе анимации изображение выросло больше своего стандартного размера 400x240px («выпрыгнуло»), а затем вернулось к нему.

Должно получиться, как в примере ниже (клик на самолёт):

В качестве исходного кода возьмите решение прошлой задачи.

Для такой анимации необходимо подобрать правильную кривую Безье. Для того чтобы самолёт «выпрыгнул», она должна иметь y>1 на одном из участков.

Например, мы можем указать y>1 для обеих контрольных точек: cubic-bezier(0.25, 1.5, 0.75, 1.5).

График кривой Безье:

Открыть решение в песочнице.

важность: 5

Напишите функцию showCircle(cx, cy, radius), которая будет рисовать постепенно растущий круг.

  • cx,cy – координаты центра круга относительно окна браузера,
  • radius – радиус круга.

Нажмите на кнопку ниже, чтобы увидеть как это должно выглядеть:

В исходном коде уже указаны правильные CSS-стили круга, таким образам задача заключается в том, чтобы сделать правильную анимацию.

Открыть песочницу для задачи.

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

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

В решении задачи функция showCircle(cx, cy, radius) рисует окружность, но не даёт возможности отследить, когда она будет готова.

В аргументы добавьте колбэк: showCircle(cx, cy, radius, callback) который будет вызываться по завершении анимации. Колбэк в качестве аргумента должен получить круг <div>.

Вот пример:

showCircle(150, 150, 100, div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

Демонстрация работы:

За основу возьмите решение задачи Анимированный круг.

Карта учебника

Комментарии

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.