JavaScript-анимации
С помощью JavaScript-анимаций можно делать вещи, которые нельзя реализовать на CSS.
Например, движение по сложному пути с временной функцией, отличной от кривой Безье, или canvas-анимации.
Использование setInterval
Анимация реализуется через последовательность кадров, каждый из которых немного меняет HTML/CSS-свойства.
Например, изменение style.left от 0px до 100px – двигает элемент. И если мы будем делать это с помощью setInterval , изменяя на 2px с небольшими интервалами времени, например 50 раз в секунду, тогда изменения будут выглядеть плавными. Принцип такой же, как в кино: 24 кадров в секунду достаточно, чтобы создать эффект плавности.
Псевдокод мог бы выглядеть так:
let timer = setInterval(function() < if (animation complete) clearInterval(timer); else increase style.left by 2px >, 20); // изменять на 2px каждые 20ms, это около 50 кадров в секунду
Более детальная реализация этой анимации:
let start = Date.now(); // запомнить время начала let timer = setInterval(function() < // сколько времени прошло с начала анимации? let timePassed = Date.now() - start; if (timePassed >= 2000) < clearInterval(timer); // закончить анимацию через 2 секунды return; >// отрисовать анимацию на момент timePassed, прошедший с начала анимации draw(timePassed); >, 20); // в то время как timePassed идёт от 0 до 2000 // left изменяет значение от 0px до 400px function draw(timePassed) < train.style.left = timePassed / 5 + 'px'; >
Для просмотра примера, кликните на него:
#train
Использование requestAnimationFrame
Теперь давайте представим, что у нас есть несколько анимаций, работающих одновременно.
Если мы запустим их независимо с помощью setInterval(. 20) , тогда браузеру будет необходимо выполнять отрисовку гораздо чаще, чем раз в 20ms .
Это происходит из-за того, что каждая анимация имеет своё собственное время старта и «каждые 20 миллисекунд» для разных анимаций – разные. Интервалы не выравнены и у нас будет несколько независимых срабатываний в течение 20ms .
setInterval(function() < animate1(); animate2(); animate3(); >, 20)
…Меньше нагружают систему, чем три независимых функции:
setInterval(animate1, 20); // независимые анимации setInterval(animate2, 20); // в разных местах кода setInterval(animate3, 20);
Эти независимые перерисовки лучше сгруппировать вместе, тогда они будут легче для браузера, а значит – не грузить процессор и более плавно выглядеть.
Существует ещё одна вещь, про которую надо помнить: когда CPU перегружен или есть другие причины делать перерисовку реже (например, когда вкладка браузера скрыта), нам не следует делать её каждые 20ms .
Но как нам узнать об этом в JavaScript? Спецификация Animation timing описывает функцию requestAnimationFrame , которая решает все описанные проблемы и делает даже больше.
let requestId = requestAnimationFrame(callback)
Такой вызов планирует запуск функции callback на ближайшее время, когда браузер сочтёт возможным осуществить анимацию.
Если в callback происходит изменение элемента, тогда оно будет сгруппировано с другими requestAnimationFrame и CSS-анимациями. Таким образом браузер выполнит один геометрический пересчёт и отрисовку, вместо нескольких.
Значение requestId может быть использовано для отмены анимации:
// отмена запланированного запуска callback cancelAnimationFrame(requestId);
Функция callback имеет один аргумент – время прошедшее с момента начала загрузки страницы в миллисекундах. Это значение может быть получено с помощью вызова performance.now().
Как правило, callback запускается очень скоро, если только не перегружен CPU или не разряжена батарея ноутбука, или у браузера нет какой-то ещё причины замедлиться.
Код ниже показывает время между первыми 10 запусками requestAnimationFrame . Обычно оно 10-20 мс:
Структура анимации
Теперь мы можем создать более сложную функцию анимации с помощью requestAnimationFrame :
function animate() < let start = performance.now(); requestAnimationFrame(function animate(time) < // timeFraction изменяется от 0 до 1 let timeFraction = (time - start) / duration; if (timeFraction >1) timeFraction = 1; // вычисление текущего состояния анимации let progress = timing(timeFraction); draw(progress); // отрисовать её if (timeFraction < 1) < requestAnimationFrame(animate); >>); >
Функция animate имеет три аргумента, которые описывают анимацию:
Продолжительность анимации. Например, 1000 .
Функция расчёта времени, как CSS-свойство transition-timing-function , которая будет вычислять прогресс анимации (как ось y у кривой Безье) в зависимости от прошедшего времени ( 0 в начале, 1 в конце).
Например, линейная функция значит, что анимация идёт с одной и той же скоростью:
function linear(timeFraction)
Это как если бы в transition-timing-function передать значение linear . Ниже будут представлены более интересные примеры.
Функция отрисовки, которая получает аргументом значение прогресса анимации и отрисовывает его. Значение progress=0 означает, что анимация находится в начале, и значение progress=1 – в конце.
Эта та функция, которая на самом деле и рисует анимацию.
Вот как она могла бы двигать элемент:
function draw(progress)
…Или делать что-нибудь ещё. Мы можем анимировать что угодно, как захотим.
Теперь давайте используем нашу функцию, чтобы анимировать свойство width от 0 до 100% .
Нажмите на элемент для того, чтобы посмотреть пример:
function animate() < let start = performance.now(); requestAnimationFrame(function animate(time) < let timeFraction = (time - start) / duration; if (timeFraction >1) timeFraction = 1; let progress = timing(timeFraction) draw(progress); if (timeFraction < 1) < requestAnimationFrame(animate); >>); >
progress elem.onclick = function() < animate(< duration: 1000, timing: function(timeFraction) < return timeFraction; >, draw: function(progress) < elem.style.width = progress * 100 + '%'; >>); >;
animate(< duration: 1000, timing(timeFraction) < return timeFraction; >, draw(progress) < elem.style.width = progress * 100 + '%'; >>);
В отличие от CSS-анимаций, можно создать любую функцию расчёта времени и любую функцию отрисовки. Функция расчёта времени не будет ограничена только кривой Безье, а функция draw может менять не только свойства, но и создавать новые элементы (например, для создания анимации фейерверка).
Функции расчёта времени
Мы уже рассмотрели самый простой пример линейной функции расчёта времени выше.
Давайте посмотрим другие. Мы попробуем выполнить анимации с разными функциями расчёта времени, чтобы посмотреть как они работают.
Степень n
Если мы хотим ускорить анимацию, мы можем возвести progress в степень n .
Например, параболическая кривая:
function quad(timeFraction)
Если раньше анимационный эффект, такой как отскоки, был в начале, то после трансформации он будет показан в конце.
На графике выше красным цветом обозначена обычная функция и синим – после easeOut .
- Обычный скачок – объект сначала медленно скачет внизу, а затем резко подпрыгивает вверх.
- Обратный easeOut – объект вначале прыгает вверх, и затем скачет там.
easeInOut
Мы можем применить эффект дважды – в начале и конце анимации. Такая трансформация называется «easeInOut».
Для функции расчёта времени, анимация будет вычисляться следующим образом:
if (timeFraction else < // вторая половина анимации return (2 - timing(2 * (1 - timeFraction))) / 2; >
function makeEaseInOut(timing) < return function(timeFraction) < if (timeFraction < .5) return timing(2 * timeFraction) / 2; else return (2 - timing(2 * (1 - timeFraction))) / 2; >> bounceEaseInOut = makeEaseInOut(bounce);
В действии, bounceEaseInOut :
#brick < width: 40px; height: 20px; background: #EE6B47; position: relative; cursor: pointer; >#path
Функция «easeInOut» объединяет два графика в один: easeIn (обычный) для первой половины анимации и easeOut (обратный) – для второй половины.
Разница хорошо заметна, если сравнивать графики easeIn , easeOut и easeInOut для функции circ :
- Красный обычный вариант circ ( easeIn ).
- Зелёный – easeOut .
- Синий – easeInOut .
Как видно, график первой половины анимации представляет собой уменьшенный easeIn , а второй – уменьшенный easeOut . В результате, анимация начинается и заканчивается одинаковым эффектом.
Более интересная функция «draw»
Вместо передвижения элемента мы можем делать что-нибудь ещё. Всё, что нам нужно – это правильно написать функцию draw .
Вот пример «скачущей» анимации набирающегося текста:
textarea < display: block; border: 1px solid #BBB; color: #444; font-size: 110%; >button
Итого
JavaScript может помочь в тех случаях, когда CSS не справляется или нужен жёсткий контроль над анимацией. JavaScript-анимации должны быть сделаны с помощью requestAnimationFrame . Это встроенный метод браузера, который вызывает переданную в него функцию в тот момент, когда браузер готовится совершить перерисовку (обычно это происходит быстро, но конкретные задержки зависят от браузера).
Когда вкладка скрыта, на ней совсем не происходит перерисовок, и функция не будет вызвана: анимация будет приостановлена и не потратит ресурсы. Это хорошо.
Вспомогательная функция animate для создания анимации:
function animate() < let start = performance.now(); requestAnimationFrame(function animate(time) < // timeFraction изменяется от 0 до 1 let timeFraction = (time - start) / duration; if (timeFraction >1) timeFraction = 1; // вычисление текущего состояния анимации let progress = timing(timeFraction); draw(progress); // отрисовать её if (timeFraction < 1) < requestAnimationFrame(animate); >>); >
- duration – общая продолжительность анимации в миллисекундах.
- timing – функция вычисления прогресса анимации. Получается момент времени от 0 до 1, возвращает прогресс анимации, обычно тоже от 0 до 1.
- draw – функция отрисовки анимации.
Конечно, мы могли бы улучшить вспомогательную функцию и добавить в неё больше наворотов. Но JavaScript-анимации не каждый день используются, а только когда хотят сделать что-то интересное и необычное. Не стоит усложнять функцию до тех пор пока это вам не понадобились.
JavaScript-анимации могут использовать любые функции расчёта времени. Мы рассмотрели множество примеров и их вариаций, чтобы сделать их ещё более универсальными. В отличие от CSS, мы здесь не ограничены только кривой Безье.
То же самое и с draw : мы можем анимировать всё что угодно, не только CSS-свойства.
Анимация на JavaScript с помощью Canvas и ES6 классов
В этой статье, на примере создания анимации шаров на , мы разберём как написать ES6 класс для этой задачи и его использовать. Сам класс будет включать в себя достаточное большое количество различных свойств и методов, включая статические.
Что мы будем создавать?
В этой статье мы на реальном примере попрактикуемся в создании ES6 классов на JavaScript. В качестве задачи мы выберем создание анимации на . А если быть более точным, то создадим движущиеся шары, отталкивающиеся друг друга и от границ холста.

Выполненное задание находится на GitHub: 01-html5-canvas-moving-balls.
Подготовка к разработке
Перед тем как начать писать код на JavaScript, создадим HTML-документ и подключим к нему js-файл посредством тега :
Практика по ES6 классам. Создание анимации в canvas
– это элемент HTML5, представляющий собой холст, на котором мы можем рисовать с помощью JavaScript.
Создание объекта, который будет отвечать за всё наше приложение
Начнём написание JavaScript кода с создания объекта посредством литерального синтаксиса:
JavaScript
const app = { canvas: document.querySelector('canvas'), width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }
Созданный объект в этом коде мы присвоили переменной app . Состоит объект в текущем виде из 3 свойств:
- canvas – сюда мы положили элемент , который получили с помощью метода document.querySelector ;
- width и height – в эти ключи мы поместили доступную ширину и высоту HTML- документа, которые соответственно узнали через свойства clientWidth и clientHeight .
Теперь добавим в объект app метод draw . Этот метод мы будем вызывать постоянно, как только браузер будет готов обновить анимацию на экране. Чтобы это сделать нам потребуется метод requestAnimationFrame , который имеется в объекте window . Этот метод сообщает браузеру то, что вы хотите выполнить анимацию. И как только браузер будет готов это сделать, она сразу выполнится. То есть по факту нам нужно в draw с помощью метода window.requestAnimationFrame снова вызвать метод draw() :
JavaScript
draw() { window.requestAnimationFrame(this.draw.bind(this)); }
Теперь метод draw будет вызываться постоянно. Но перед тем как снова вызвать draw() нам необходимо сначала стереть весь холст. То есть очистить на нём все шары в их текущих положениях. После этого сделать новый кадр, то есть нарисовать шары, но уже в их новых положениях.
В итоге, метод draw будет иметь следующий код :
JavaScript
draw() { // стираем весь холст this.ctx.clearRect(0, 0, this.width, this.height) // рисуем новое положение каждого шара Circle.contains.forEach((item) => { item.draw() }); // опять запускаем draw() window.requestAnimationFrame(this.draw.bind(this)) }
Пока мы ещё не создали класс Circle и конечно эту часть кода мы написали бы в реальности потом, но, чтобы не возвращаться к этому моменту разберём сейчас, что здесь происходит:
JavaScript
Circle.contains.forEach((item) => { item.draw(); });
У класса Circle имеется статическое свойство contains . Это свойство будет содержать массив объектов, которые мы создадим посредством этого класса. Другими словами, в этом свойстве будут находиться все созданные нами шары, которые нужно рисовать на . Далее мы их перебираем посредством метода forEach и для каждого из них вызываем метод draw() . draw() в этом контексте – это метод шаров. Он выполняет рисование конкретного шара на холсте. Вот так всё просто .
Создадим в app ещё один метод. Назовём его init . С помощью него мы будем инициализировать всё наше приложение или, другими словами, запускать его:
JavaScript
init() { // получаем контекст 2d this.ctx = this.canvas.getContext('2d'); // чтобы canvas занимал всю ширину и высоту viewport [this.canvas.width, this.canvas.height] = [this.width, this.height] // запускаем анимацию window.requestAnimationFrame(this.draw.bind(this)) }
В init мы сначала посредством специального метода getContext(‘2d’) получим инструменты с помощью которых будет рисовать на холсте . Один из его инструментов – это метод clearRect , который мы использовали ранее в draw() для стирания всего холста. Объект, который нам возвращает getContext(‘2d’) сохраним в свойство ctx объекта app .
После этого мы элементу установим в качестве ширины и высоты всю доступную ширину и высоту HTML-документа. Ну и на последней строчке запустим анимацию, то есть вызовем метод draw объекта app , но не посредственно, а используя window.requestAnimationFrame . В этом случае он запустится, как только браузер будет готов это сделать.
Создание класса Circle
Объявим ES6 класс Circle . Этот класс будет отвечать за всё, что касается шаров. Начнём создание класса с добавлением в него статического свойства contains и конструктора:
JavaScript
class Circle { static contains = []; constructor(x, y, dx, dy, r, color) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.r = r; this.color = color; this.constructor.contains.push(this); }
Статическое свойство contains будем использовать для хранения всех созданных объектов типа Circle . Помещать эти объекты в данное свойство будем автоматически при вызове constructor() . Для этого в конце конструктора мы написали эту строчку:
JavaScript
this.constructor.contains.push(this);
Статические свойства и методы класса находятся не в самих объектах или их прототипах , а в конструкторе класса Circle . Поэтому мы сначала получаем его с помощью this.constructor . Затем мы обращаемся к contains и добавляем в это свойство посредством метода push созданный экземпляр класса.
Код конструктора очень простой. Здесь на вход мы принимаем аргументы, которые будем получать посредством следующих параметров: x , y , dx , dy , r и color . Их будем использовать для установки значений соответствующим свойствам :
- x и y – это координаты центра окружности;
- dx и dy – определяют направление движения шара и его скорость, другими словами, на сколько сместится шар по x и y при следующей отрисовке кадра;
- r – это радиус окружности;
- color – задаёт цвет заливки окружности.
Для удобства создания шара со случайными значениями свойств напишем статический метод create :
JavaScript
static create() { const r = 15 const x = Math.random() * (app.width - r * 2) + r const y = Math.random() * (app.height - r * 2) + r const dx = (Math.random() - 0.5) * 5 const dy = (Math.random() - 0.5) * 5 const color = `#${Math.floor(Math.random() * 16777215).toString(16)}` new this(x, y, dx, dy, r, color) }
Этот метод очень простой. Он будет устанавливать в качестве радиуса постоянное значение, в данном случае 15px , а всем остальным параметрам случайные числа в определённом диапазоне с использованием Math.random() .
После этого с помощью new this() мы можем создать новый экземпляр класса Circle . this в статическом методе указывает на сам класс, то есть в данном случае на Circle .
Чтобы сделать анимацию шаров более интересной добавим в Circle два метода: processBorderCollision и processCollision .
Первый метод будет ограничивать выход шара за пределы , то есть проверять коллизию шара и границ. В случае её обнаружения изменять направление шара на противоположное:
JavaScript
processBorderCollision() this.x - this.r = app.height } this.processBorderCollision(); this.x += this.dx; this.y += this.dy; }
Этот метод будет выполнять одну очень простую вещь: обновлять значение свойств x и y шаров, а также вызывать два предыдущих метода для решения коллизий.
И наконец метод draw , который будет непосредственно рисовать окружность в соответствии со значениями свойств:
JavaScript
draw() { app.ctx.beginPath(); app.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false); app.ctx.fillStyle = this.color; app.ctx.fill(); this.update(); }
В конце этого метода мы вызываем this.update() . С помощью него как мы уже знаем, обновляются координаты окружности, а также свойства dx и dy . Таким образом при следующем вызове draw окружность будет нарисована уже в другом месте в соответствии с новыми значениями свойств x и y .
Полный код скрипта доступен на GitHub: canvas.js.
Создание нужного количества шаров и запуск приложения
Теперь, когда у нас всё готово, необходимо в конце файла создать нужное количество шаров с помощью цикла for . Допустим, 25 :
JavaScript
for (let i = 0; i < 25; i++) { Circle.create(); }
Для создания объекта мы использовали здесь статический метод create . Сохранять все созданные шары нам никуда не нужно, так как вы уже знаете, они автоматически помещаются в статическое свойство contains .
И последнее действие, которое нам необходимо сделать – это инициализировать наше приложение или, иными словами, запустить всё, что мы с вами написали:
JavaScript
app.init();
На этом всё, наслаждаемся созданной анимацией:
Как работает JS: анимация средствами CSS и JavaScript
Анимация — неотъемлемая часть современных веб-интерфейсов. От того, насколько она уместна, привлекательна и производительна, зависит немалая доля впечатлений пользователя от работы с сайтом или веб-приложением. Сегодня, в переводе тринадцатой части серии материалов, посвящённых особенностям JavaScript и связанных с ним технологий, мы поговорим об анимации, выполняемой средствами CSS и JS, а также обсудим подходы к её оптимизации.

Обзор
Наверняка вы знаете о том, что анимация, помимо чисто утилитарной роли, оказывает огромное влияние на привлекательность веб-приложений. Пользователи всё больше и больше обращают внимание на UX-дизайн проектов, что приводит к тому, что владельцы веб-ресурсов приходят к осознанию важности создания на своих сайтах таких условий, которые позволили бы пользователям комфортно там себя чувствовать. Всё это, помимо визуальной привлекательности страниц и удобства работы с ними, ведёт к тому, что веб-проекты становятся «тяжелее», к тому, что в их интерфейсах растёт число динамических элементов. Всё это требует более сложных анимаций, которые, например, позволяют организовать плавное изменение состояния веб-страниц в ходе работы пользователя с ними. Сегодня анимация не считается чем-то особенным. Пользователи становятся требовательнее, они уже привыкли ожидать наличия у веб-проектов интерактивных отзывчивых интерфейсов.
Однако, анимация интерфейсов — это не так уж и просто. Что анимировать? Когда анимировать? Какие ощущения должна вызывать анимация? Поиск ответов на эти вопросы может потребовать немалых усилий.
JavaScript-анимация и CSS-анимация
Создавать анимации можно двумя основными способами: с помощью JavaScript, используя API веб-анимации, и средствами CSS. Выбор способа зависит от конкретной задачи, поэтому сразу хочется отметить, что нельзя однозначно говорить о преимуществе одной технологии над другой.
▍CSS-анимация
CSS-анимация — это самый простой способ заставить что-либо двигаться по экрану. Начнём с простого примера, демонстрирующего перемещение элемента по осям X и Y. Делается это с помощью CSS-трансформации translate , которая настроена на длительность в 1000 мс.
.box < -webkit-transform: translate(0, 0); -webkit-transition: -webkit-transform 1000ms; transform: translate(0, 0); transition: transform 1000ms; >.box.move
При добавлении класса move значение transform меняется и начинается переход. Помимо длительности, мы можем настраивать динамику анимации ( easing ). Сущность этой настройки сводится к тому, что она влияет на то, как пользователь воспринимает анимацию. О динамике анимации мы поговорим позже.
На иллюстрации ниже показана поддержка CSS-переходов современными браузерами.

Поддержка CSS-переходов современными браузерами
Как видно, эта возможность отличается очень высоким уровнем поддержки.
Если, как и в предыдущем фрагменте кода, вы создадите отдельные CSS-классы для управления анимацией, затем можно включать и отключать анимацию средствами JavaScript.
Предположим, имеется следующий элемент.
Sample content.
С помощью JavaScript можно запускать и останавливать его анимацию.
var boxElements = document.getElementsByClassName('box'), boxElementsLength = boxElements.length, i; for (i = 0; i
В этом фрагменте кода мы берём все элементы, которым назначен класс box и добавляем к ним класс move для того, чтобы запустить их анимацию.
Подобные возможности совместного использования CSS — для описания анимаций, и JS — для её запуска и отключения, делают приложение хорошо сбалансированным. Разработчик может сосредоточиться на управлении состоянием элементов из JavaScript, просто назначая подходящие классы целевым элементам, позволяя браузеру самостоятельно выполнять анимации, описанные средствами CSS. Если углубиться в подобный сценарий работы с анимацией, то можно прослушивать событие transitionend элемента, но делать так стоит лишь при условии поддержки старых версий Internet Explorer.
Событие transitionend вызывается в конце перехода. Вот как с ним работать.
var boxElement = document.querySelector('.box'); // Получить первый элемент с классом box. boxElement.addEventListener('transitionend', onTransitionEnd, false); function onTransitionEnd() < // Обработать завершение перехода. >
Для того чтобы сделать элементы веб-интерфейсов динамичнее, в дополнение к использованию CSS-переходов можно воспользоваться CSS-анимациями. Они дают разработчику гораздо больший уровень контроля за отдельными ключевыми кадрами анимации, длительностями этапов анимации и итерациями анимации.
Ключевые кадры используются для того, чтобы сообщить браузеру о том, какие значения CSS-свойства должны иметь в заданные моменты. Браузер самостоятельно находит промежуточные значения для свойств при переходе от одного ключевого кадра к другому.
/** * Это - упрощённая версия без префиксов * разработчиков браузеров. Если включить их сюда * (а в реальном коде это нужно), объём кода * значительно возрастёт! */ .box < /* Выберем анимацию */ animation-name: movingBox; /* Укажем длительность анимации */ animation-duration: 2300ms; /* Укажем - сколько раз мы хотим повторить анимацию */ animation-iteration-count: infinite; /* Это приводит к выполнению анимации в обратном порядке на каждой нечётной итерации */ animation-direction: alternate; >@keyframes movingBox < 0% < transform: translate(0, 0); opacity: 0.4; >25% < opacity: 0.9; >50% < transform: translate(150px, 200px); opacity: 0.2; >100% < transform: translate(40px, 30px); opacity: 0.8; >>
Вот страница, на которой показана работа этого кода.
Применяя CSS-анимации, саму анимацию описывают независимо от целевого элемента, а затем используют свойство animation-name для выбора необходимой анимации.
CSS-анимации, до сих пор, иногда требуют использования префиксов разработчиков браузеров. Так, префикс -webkit- используется в браузерах Safari, Safari Mobile, и в браузере Android. В браузерах Chrome, Opera, Internet Explorer, и Firefox анимации работают без префиксов. Для того чтобы создать CSS-код с префиксами, можно воспользоваться множеством вспомогательных инструментов, что позволяет разработчику, в исходном коде анимаций, обходиться без префиксов.
▍JavaScript-анимация
Создавать анимации средствами JavaScript, с применением API веб-анимации, сложнее, чем использовать CSS-переходы и CSS-анимации, но этот подход обычно даёт разработчику гораздо большие возможности.
JS-анимации описывают в коде приложения. Как и любой другой код их можно, например, помещать в объекты. Вот пример JS-кода, который нужно написать для того, чтобы воссоздать описанный выше CSS-переход.
var boxElement = document.querySelector('.box'); var animation = boxElement.animate([ , ], 500); animation.addEventListener('finish', function() < boxElement.style.transform = 'translate(150px, 200px)'; >);
По умолчанию применение API веб-анимации модифицирует лишь внешний вид элемента. Если требуется, чтобы объект оставался в той позиции, в которую он был перемещён в ходе анимации, нужно, по завершении анимации, модифицировать его стиль. Именно поэтому в вышеприведённом примере мы прослушиваем событие finish и устанавливаем свойство элемента box.style.transform в значение translate(150px, 200px) , которое выражает то же самое, что было сделано с объектом с помощью второй трансформации, выполненной средствами JS.
При использовании JavaScript-анимации разработчик обладает полным контролем над стилями элемента на каждом этапе анимации. Это означает, что анимацию можно замедлять, приостанавливать, останавливать, обращать, как угодно манипулировать элементами. Это особенно полезно в ходе создания сложных приложений, при разработке которых используется объектно-ориентированный подход, так как поведение элементов можно должным образом инкапсулировать.
Динамика анимации
Естественные перемещения объектов дают пользователям ощущение комфорта при работе с веб-приложениями, что ведёт к более качественному пользовательскому опыту.
Если говорить о том, как объекты движутся в реальном мире, то можно отметить, что они, например, не перемещаются линейно. В физическом мире движущиеся предметы ускоряются и замедляются, так как на них воздействуют самые разные факторы окружающей среды. Человеческий мозг приучен ожидать от объектов подобных перемещений, поэтому, анимируя веб-приложения, следует это учитывать.
Вот пара терминов, которые пригодятся нам при разговоре о динамике анимации. А именно, поговорим о так называемых функциях плавности. Их применение позволяет влиять на динамику анимации.
- ease-in — это функция, при применении которой сначала анимация производится медленно, а затем — постепенно ускоряется.
- ease-out — это функция, при использовании которой анимация начинается быстро, а потом — постепенно замедляется.
Управление динамикой анимации позволяет сделать так, чтобы движение объектов воспринималось как более естественное.
▍Ключевые слова для управления динамикой анимации
CSS-переходы и анимации позволяют разработчику выбирать функции плавности. Существуют различные ключевые слова, воздействующие на динамику анимации. Кроме того, можно создавать и собственные функции плавности. Вот несколько ключевых слов, которые можно использовать в CSS для управления динамикой анимации:
- linear
- ease-in
- ease-out
- ease-in-out
▍Анимация linear
Ключевое слово linear позволяет использовать линейную анимацию. Фактически, эта анимация описывается линейной функцией, при применении которой объект анимируется с постоянной скоростью, без ускорений и замедлений.
Вот как выглядит график линейного CSS-перехода.

Анимация linear
Видно, что с течением времени значение равномерно возрастает. Линейные перемещения, однако, воспринимаются как неестественные. В целом, надо отметить, что подобных анимаций стоит избегать.
Вот как выглядит описание такой анимации:
transition: transform 500ms linear;
▍Анимация ease-out
Как уже было сказано, применение функции ease-out приводит к высокой скорости анимации в начале процесса (её скорость — выше, чем при применении линейной функции), которая замедляется в конце анимации. Вот как выглядит графическое представление такой анимации.

Анимация ease-out
В целом, подобная анимация лучше подходит для пользовательского интерфейса, так как её быстрое начало даёт ощущение отзывчивости элемента, в то время как замедление в конце анимации также делает её естественнее благодаря наличию неравномерности движения.
Существует множество способов достичь подобного эффекта, но самый простой — воспользоваться ключевым словом ease-out в CSS:
transition: transform 500ms ease-out;
▍Анимация ease-in
Эта анимация противоположна той, которую мы только что рассмотрели. Для неё характерна низкая скорость в начале и увеличение скорости в конце. Вот её графическое представление.

Анимация ease-in
В сравнении с анимацией ease-out , анимация ease-in выглядит необычно, так как она даёт ощущение низкого уровня отзывчивости элемента из-за медленного начала. Ускорение в конце так же создаёт странные ощущения, так как скорость анимации с течением времени растёт, в то время как объекты в реальном мире, перед остановкой, обычно снижают скорость.
Для того чтобы воспользоваться этой анимацией, аналогично предыдущим, можно использовать ключевое слово ease-in :
transition: transform 500ms ease-in;
▍Анимация ease-in-out
Эта анимация является комбинацией анимаций ease-in и ease-out . Вот как выглядит её график.

Анимация ease-in-out
Тут стоит отметить, что не рекомендуется использовать анимации, длительность которых слишком велика, так как они создают у пользователя ощущение того, что интерфейс перестал реагировать на его воздействия.
Воспользоваться этой анимацией можно с помощью ключевого слова ease-in-out :
transition: transform 500ms ease-in-out;
▍Создание собственных функций плавности
Разработчик может определять собственные функции плавности, которые дают гораздо лучший контроль над тем, какие ощущения у пользователя создают используемые в проекте анимации.
На самом деле, за ключевыми словами, о которых мы говорили выше ( ease-in , ease-out , linear ), стоят кривые Безье, подробности о применении которых для управления анимацией можно почитать здесь и здесь. Уделим им некоторое время, так как именно на них основано создание собственных функций плавности.
▍Кривые Безье
Для построения кривой Безье нужно четыре значения, или, говоря точнее — две пары чисел. Каждая пара описывает координаты X и Y опорной точки кубической кривой Безье. Сама кривая начинается в координате (0, 0), а заканчивается — в координате (1, 1). Настраивать можно свойства опорных точек. Значения X координат опорных точек должны находиться в диапазоне [0 ,1], значения Y также должны попадать в диапазон [0, 1], хотя надо отметить то, что спецификации не вполне проясняют этот момент.
Даже небольшое изменение значений X и Y координат опорных точек приводит к значительному изменению кривой. Взглянем на пару графиков кривых Безье, опорные точки которых имеют очень близкие, но различающиеся координаты.

Первая кривая Безье

Вторая кривая Безье
Как видите, два этих графика очень сильно отличатся друг от друга. Вот как выглядит описание второй кривой в CSS:
transition: transform 500ms cubic-bezier(0.465, 0.183, 0.153, 0.946);
Первые два числа — это координаты X и Y первой опорной точки, вторая пара — координаты второй.
Оптимизация производительности анимаций
Анимируя интерфейсы, надо следить за тем, чтобы частота кадров не падала ниже 60 FPS, в противном случае это плохо повлияет на то, как пользователи воспринимают анимированные страницы.
Как и за всё остальное в этом мире, за анимацию надо платить. При этом анимирование некоторых свойств обходится «дешевле», чем анимирование других. Например, анимирование свойств width и height элемента приводит к изменению его геометрии и может привести к тому, что другие элементы на странице переместятся или изменят размер. Этот процесс называется формированием макета страницы. Об этом мы говорили в одном из предыдущих материалов.
В целом, следует избегать анимации свойств элементов, которые вызывают изменение макета страницы или её перерисовку. Для большинства современных браузеров это означает ограничение анимациями opacity и transform .
▍CSS-свойство will-change
CSS-свойство will-change можно использовать для того, чтобы сообщать браузеру о том, что мы намереваемся изменить свойство элемента. Это позволяет браузеру заранее, до выполнения анимации, применить подходящие оптимизации. Однако не стоит злоупотреблять свойством will-change , так как это приведёт к нерациональной трате ресурсов браузера, что, в свою очередь, приведёт к проблемам с производительностью.
Вот, например, как добавить это свойство для анимаций transform и opacity :
Данное свойство понимают пока не все браузеры, но, в браузерах Chrome, Firefox и Opera его поддержка имеется.

Поддержка CSS-свойства will-change
▍JavaScript или CSS?
Что выбрать для анимации — API веб-анимации, вызываемое из JS, или CSS? Вероятно, вы помните, что выше мы говорили, что на подобный вопрос нельзя дать однозначного ответа. Однако, для того, чтобы всё-таки определиться с технологией, учтите следующие соображения:
- CSS-анимации и веб-анимации, при наличии их нативной поддержки, обычно обрабатываются потоком композиции (compositor thread). Он отличается от главного потока браузера (main thread), где выполняются задачи по стилизации элементов, по формированию макета, по выводу данных на экран и по выполнению JS-кода. Это означает, что если браузер выполняет какие-то сложные задачи в главном потоке, анимации будут выполняться нормально, без перерывов.
- Анимации transforms и opacity могут быть, во многих случаях, обработаны потоком композиции.
- Если какая-то анимация вызывает перерисовку страницы или изменение макета, поработать придётся главному потоку. Это справедливо и для CSS-анимаций, и для JS-анимаций. Дополнительная нагрузка на систему, вызванная изменением макета или перерисовкой страницы, вероятно, замедлит выполнение задач, решаемых средствами CSS или JavaScript, ставя систему в непростое положение.
▍Выбор объектов для анимации
Продуманная анимация делает проект интереснее и привлекательнее для пользователей. Им приятно работать с ним. Анимировать можно практически всё, что угодно: ширину и высоту элементов, их позицию на экране, цвета и фоны. Однако, планируя анимировать что-либо, стоит задуматься о производительности. Неудачно выбранная анимация может плохо повлиять на то, как пользователи воспримут проект, поэтому анимации должны быть и быстрыми, и уместными. На самом деле, стоит стремиться к тому, чтобы придать интерфейсу естественности и привлекательности, ограничившись минимумом анимации.
▍Использование анимаций для поддержки взаимодействия с пользователем
Не стоит анимировать что-либо только из-за того, что у вас есть техническая возможность это сделать. Вместо этого используйте тщательно подобранные анимации для того, чтобы улучшить взаимодействие элементов страниц с пользователем. Избегайте анимаций, которые прерывают деятельность пользователя или мешают ему.
▍Анимации, вызывающие большую нагрузку на систему
Хуже чем неуместная анимация может быть только анимация, которая «подвешивает» страницу. Пользователям любого веб-проекта такое точно не понравится.
Итоги
В этом материале мы рассказали об анимации элементов веб-страниц средствами CSS и JavaScript. Анимация — мощный инструмент, поэтому обращаться с ней стоит аккуратно. При правильном подходе анимация способна значительно улучшить впечатления пользователей от работы с веб-ресурсом.
Предыдущие части цикла статей:
Уважаемые читатели! Сталкивались ли вы когда-нибудь со случаями, когда анимация по-настоящему мешает работать с каким-нибудь веб-ресурсом?
Простые анимации
Вероятно, самым большим ограничением является то, что когда фигура нарисована, её уже нельзя двигать. Чтобы изобразить движение нам нужно перерисовать фигуру и всё, что было нарисовано до неё. Перерисовка сложных кадров занимает много времени, и производительность сильно зависит от скорости компьютера, на котором она выполняется.
Основные шаги анимации
Ниже перечислены необходимые шаги для того, чтобы нарисовать кадр:
- Очистить canvas Если фигура, которую вы собираетесь нарисовать, не занимает всю площадь canvas (как фон, например), то всё что было нарисовано ранее необходимо стереть. Проще всего это сделать при помощи метода clearRect() .
- Сохранить изначальное состояние canvas Если вы изменяете любые настройки (такие как стили, трансформации и т.п.), которые затрагивают состояние canvas и вы хотите убедиться, что оригинальное состояние используется каждый раз, когда был отрисован кадр, то вам следует сохранить это оригинальное состояние.
- Нарисовать анимированные фигуры Шаг на котором вы собственно отрисовываете кадр.
- Восстановить состояние canvas Если вы сохраняли состояние, восстановите его, прежде чем отрисовывать новый кадр.
Управление анимацией
Фигуры отрисовываются на canvas либо напрямую — при помощи методов canvas, либо с помощью сторонних функций. В нормальной ситуации результат станет виден на canvas после окончания выполнения скрипта. К примеру, цикл for использовать для анимации нельзя.
Это значит, нужен способ выполнения функций отрисовки через интервалы времени. Есть два способа для управления такой анимацией.
Запланированные обновления
Первый — это функции window.setInterval() (en-US), window.setTimeout() (en-US), и window.requestAnimationFrame() , которые могут быть использованы для вызова некоторой функции, через заданный промежуток времени.
Начинает периодически исполнять функцию function каждые delay миллисекунд.
Запускает выполнение указанной функции function через delay миллисекунд.
Сообщает браузеру, что вы хотите выполнить анимацию, и запрашивает, чтобы браузер вызвал указанную функцию callback для обновления анимации перед следующей перерисовкой.
Если вы не планируете никакого взаимодействия с пользователем, вы можете использовать функцию setInterval() , которая многократно выполняет, предоставленный ей код. Если же вы планируете создать игру, в которой контроль анимации осуществляется мышью или клавиатурой, то необходимо использовать setTimeout() . Установив EventListener , вы можете перехватываете любые действия пользователя и запустить соответствующие функции анимации.
Примечание: В примерах ниже мы будем использовать функцию window.requestAnimationFrame() для контроля анимации. Функция requestAnimationFrame является более эффективной для создания анимации, так как новая итерация вызывается, когда система готова к отрисовке нового кадра. Количество вызовов в секунду примерно равно 60 и уменьшается, когда вкладка неактивна. Для более подробного изучения цикла анимации, особенно для игр, прочитайте статью Анатомия видеоигр В Зоне разработке игр.
Анимированная солнечная система
В этом примере анимируется небольшая модель солнечной системы.
var sun = new Image(); var moon = new Image(); var earth = new Image(); function init() sun.src = "canvas_sun.png"; moon.src = "canvas_moon.png"; earth.src = "canvas_earth.png"; window.requestAnimationFrame(draw); > function draw() var ctx = document.getElementById("canvas").getContext("2d"); ctx.globalCompositeOperation = "destination-over"; ctx.clearRect(0, 0, 300, 300); // clear canvas ctx.fillStyle = "rgba(0,0,0,0.4)"; ctx.strokeStyle = "rgba(0,153,255,0.4)"; ctx.save(); ctx.translate(150, 150); // Earth var time = new Date(); ctx.rotate( ((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds(), ); ctx.translate(105, 0); ctx.fillRect(0, -12, 50, 24); // Shadow ctx.drawImage(earth, -12, -12); // Moon ctx.save(); ctx.rotate( ((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds(), ); ctx.translate(0, 28.5); ctx.drawImage(moon, -3.5, -3.5); ctx.restore(); ctx.restore(); ctx.beginPath(); ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit ctx.stroke(); ctx.drawImage(sun, 0, 0, 300, 300); window.requestAnimationFrame(draw); > init();
canvas id="canvas" width="300" height="300">canvas>
Анимированные часы
В этом примере создаются анимированные часы, показывающие правильное время.
function clock() var now = new Date(); var ctx = document.getElementById("canvas").getContext("2d"); ctx.save(); ctx.clearRect(0, 0, 150, 150); ctx.translate(75, 75); ctx.scale(0.4, 0.4); ctx.rotate(-Math.PI / 2); ctx.strokeStyle = "black"; ctx.fillStyle = "white"; ctx.lineWidth = 8; ctx.lineCap = "round"; // Hour marks ctx.save(); for (var i = 0; i 12; i++) ctx.beginPath(); ctx.rotate(Math.PI / 6); ctx.moveTo(100, 0); ctx.lineTo(120, 0); ctx.stroke(); > ctx.restore(); // Minute marks ctx.save(); ctx.lineWidth = 5; for (i = 0; i 60; i++) if (i % 5 != 0) ctx.beginPath(); ctx.moveTo(117, 0); ctx.lineTo(120, 0); ctx.stroke(); > ctx.rotate(Math.PI / 30); > ctx.restore(); var sec = now.getSeconds(); var min = now.getMinutes(); var hr = now.getHours(); hr = hr >= 12 ? hr - 12 : hr; ctx.fillStyle = "black"; // write Hours ctx.save(); ctx.rotate( hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec, ); ctx.lineWidth = 14; ctx.beginPath(); ctx.moveTo(-20, 0); ctx.lineTo(80, 0); ctx.stroke(); ctx.restore(); // write Minutes ctx.save(); ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec); ctx.lineWidth = 10; ctx.beginPath(); ctx.moveTo(-28, 0); ctx.lineTo(112, 0); ctx.stroke(); ctx.restore(); // Write seconds ctx.save(); ctx.rotate((sec * Math.PI) / 30); ctx.strokeStyle = "#D40000"; ctx.fillStyle = "#D40000"; ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(-30, 0); ctx.lineTo(83, 0); ctx.stroke(); ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2, true); ctx.fill(); ctx.beginPath(); ctx.arc(95, 0, 10, 0, Math.PI * 2, true); ctx.stroke(); ctx.fillStyle = "rgba(0,0,0,0)"; ctx.arc(0, 0, 3, 0, Math.PI * 2, true); ctx.fill(); ctx.restore(); ctx.beginPath(); ctx.lineWidth = 14; ctx.strokeStyle = "#325FA2"; ctx.arc(0, 0, 142, 0, Math.PI * 2, true); ctx.stroke(); ctx.restore(); window.requestAnimationFrame(clock); > window.requestAnimationFrame(clock);
canvas id="canvas" width="150" height="150">canvas>
Зацикленная панорама
В этом примере панорама прокручивается слева направо. Мы используем фото национального парка Йосемити взятое из Википедии, но вы можете использовать любое изображение, большее элемента canvas.
var img = new Image(); // User Variables - customize these to change the image being scrolled, its // direction, and the speed. img.src = "capitan_meadows,_yosemite_national_park.jpg"; var CanvasXSize = 800; var CanvasYSize = 200; var speed = 30; //lower is faster var scale = 1.05; var y = -4.5; //vertical offset // Main program var dx = 0.75; var imgW; var imgH; var x = 0; var clearX; var clearY; var ctx; img.onload = function () imgW = img.width * scale; imgH = img.height * scale; if (imgW > CanvasXSize) x = CanvasXSize - imgW; > // image larger than canvas if (imgW > CanvasXSize) clearX = imgW; > // image larger than canvas else clearX = CanvasXSize; > if (imgH > CanvasYSize) clearY = imgH; > // image larger than canvas else clearY = CanvasYSize; > //Get Canvas Element ctx = document.getElementById("canvas").getContext("2d"); //Set Refresh Rate return setInterval(draw, speed); >; function draw() //Clear Canvas ctx.clearRect(0, 0, clearX, clearY); //If image is if (imgW CanvasXSize) //reset, start from beginning if (x > CanvasXSize) x = 0; > //draw aditional image if (x > CanvasXSize - imgW) ctx.drawImage(img, x - CanvasXSize + 1, y, imgW, imgH); > > //If image is > Canvas Size else //reset, start from beginning if (x > CanvasXSize) x = CanvasXSize - imgW; > //draw aditional image if (x > CanvasXSize - imgW) ctx.drawImage(img, x - imgW + 1, y, imgW, imgH); > > //draw image ctx.drawImage(img, x, y, imgW, imgH); //amount to move x += dx; >
Заметьте, что ширина и высота должны совпадать со значениями CanvasXZSize и CanvasYSize .
canvas id="canvas" width="800" height="200">canvas>