en.javascript.info/5-animation/3-js-animation/article.md
2015-04-18 01:40:37 +03:00

508 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# JS-Анимация
JavaScript-анимация применяется там, где не подходит CSS.
Например, по сложной траектории, с временной функцией, выходящей за рамки кривых Безье, на canvas. Иногда её используют для анимации в старых IE.
[cut]
## setInterval
С точки зрения HTML/CSS, анимация -- это постепенное изменение стиля DOM-элемента. Например, увеличение координаты `style.left` от `0px` до `100px` сдвигает элемент.
Если увеличивать `left` от `0` до `100` при помощи `setInterval`, делая по 50 изменений в секунду, то это будет выглядеть как плавное перемещение. Тот же принцип, что и в кино: для непрерывной анимации достаточно 24 или больше вызовов `setInterval` в секунду.
Псевдо-код для анимации выглядит так:
```js
var fps = 50; // 50 кадров в секунду
var timer = setInterval(function() {
if (время вышло) clearInterval(timer);
else немного увеличить left
}, 1000 / fps)
```
Более полный пример кода анимации:
```js
var start = Date.now(); // сохранить время начала
var timer = setInterval(function() {
// вычислить сколько времени прошло с начала анимации
var timePassed = Date.now() - start;
if (timePassed >= 2000) {
clearInterval(timer); // конец через 2 секунды
return;
}
// рисует состояние анимации, соответствующее времени timePassed
draw(timePassed);
}, 20);
// в то время как timePassed идёт от 0 до 2000
// left принимает значения от 0 до 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
```
Кликните для демонстрации:
[codetabs height=200 src="move"]
## requestAnimationFrame
Если у нас не один такой `setInterval`, а несколько в разных местах кода, то браузеру нужно в те же 20мс работать со страницей уже несколько раз. А ведь кроме `setInterval` есть ещё другие действия, к примеру, прокрутка страницы, которую тоже надо нарисовать.
Если все действия по перерисовке производить независимо, то будет выполняться много двойной работы.
Гораздо выгоднее с точки зрения производительности -- сгруппировать все перерисовки в одну и запускать их централизованно, все вместе.
Для этого в JavaScript-фреймворках, которые поддерживают анимацию, есть единый таймер:
```js
setInterval(function() {
/* отрисовать все анимации */
}, 20);
```
...Все анимации, которые запускает такой фреймворк, добавляются в общий список, и раз в 20мс единый таймер проверяет его, запускает текущие, удаляет завершившиеся.
Современные браузеры, кроме IE9-, поддерживают стандарт [Animation timing](http://www.w3.org/TR/animation-timing/), который представляет собой дальнейший шаг в этом направлении. Он позволяет синхронизировать наши анимации со встроенными механизмами обновления страницы. То есть, сгруппированы будут не только наши, но и CSS-анимации и другие браузерные перерисовки.
При этом графический ускоритель будет использован максимально эффективно, и исключена повторная обработка одних и тех же участков страницы. А значит -- меньше будет загрузка CPU, да и сама анимация станет более плавной.
Для этого используется функция [requestAnimationFrame](http://www.w3.org/TR/animation-timing/#dom-windowanimationtiming-requestanimationframe).
Синтаксис:
```js
var requestId = requestAnimationFrame(callback)
```
Такой вызов планирует запуск `callback` в ближайшее время, когда браузер сочтёт возможным осуществить анимацию.
Если запланировать в `callback` какое-то рисование, то оно будет сгруппировано с другими `requestAnimationFrame` и с внутренними перерисовками браузера.
Возвращаемое значение `requestId` служит для отмены запуска:
```js
// отменить запланированное выше выполнение callback
cancelAnimationFrame(requestId);
```
Функция `callback` получает один аргумент -- время, прошедшее с начала загрузки страницы, результат вызова [performance.now()](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now).
Как правило, запуск `callback` происходит очень скоро. Если у процессора большая загрузка или батарея у ноутбука почти разряжена -- то пореже.
Если вы запустите этот код, то увидите промежутки между первыми 20 запусками `requestAnimationFrame`. Как правило, это 10-20мс, но бывает и больше и меньше. Это оптимальная частота анимации с точки зрения браузера.
```html
<!--+ run height=40 refresh -->
<script>
var prev = performance.now();
var times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
if (times++ < 10) requestAnimationFrame(measure);
})
</script>
```
Функция анимации на основе `requestAnimationFrame`:
```js
// Рисует функция draw
// Продолжительность анимации duration
function animate(draw, duration) {
var start = performance.now();
requestAnimationFrame(function animate(time) {
// определить, сколько прошло времени с начала анимации
var timePassed = time - start;
// возможно небольшое превышение времени, в этом случае зафиксировать конец
if (timePassed > duration) timePassed = duration;
// нарисовать состояние анимации в момент timePassed
draw(timePassed);
// если время анимации не закончилось - запланировать ещё кадр
if (timePassed < duration) {
requestAnimationFrame(animate);
}
});
}
```
Использование для поезда:
```js
animate(function(timePassed) {
train.style.left = timePassed / 5 + 'px';
}, 2000);
```
В действии:
[codetabs src="move-raf"]
## Структура анимации
На основе `requestAnimationFrame` можно соорудить и гораздо более мощную, но в то же время простую функцию анимации.
У анимации есть три основных параметра:
<dl>
<dt>`duration`</dt>
<dd>Общее время, которое должна длиться анимация, в мс. Например, `1000`.</dd>
<dt>`timing(timeFraction)`</dt>
<dd>Временная функция, которая, по аналогии с CSS-свойством `transition-timing-function`, будет по текущему времени вычислять состояние анимации.
Она получает на вход непрерывно возрастающее число `timeFraction` -- от `0` до `1`, где `0` означает самое начало анимации, а `1` -- её конец.
Её результатом должно быть значение завершённости анимации, которому в CSS transitions на кривых Безье соответствует координата `y`.
Также по аналогии с `transition-timing-function` должны соблюдаться условия:
<ul>
<li>timing(0) = 0</li>
<li>timing(1) = 1</li>
</ul>
...То есть, анимация начинается в точке `(0,0)` -- нулевое время и нулевой прогресс и заканчивается в `(1, 1)` -- прошло полное время, и процесс завершён.
Например, функция-прямая означает равномерное развитие процесса:
```js
function linear(timeFraction) {
return timeFraction;
}
```
Её график:
<img src="linear.png">
Как видно, её график полностью совпадает с `transition-timing-function: linear`, и эффект абсолютно такой же.
Есть и другие, более интересные варианты, мы рассмотрим их чуть позже.
</dd>
<dt>`draw(progress)`</dt>
<dd>Функция, которая получает состояние завершённости анимации и рисует его. Значению `progress=0` соответствует начальная точка анимации, `progress=1` -- конечная.
Именно эта функция и осуществляет, собственно, анимацию.
Например, может двигать элемент:
```js
function draw(progress) {
train.style.left = progress + 'px';
}
```
Возможны любые варианты, анимировать можно что угодно и как угодно.
</dd>
</dl>
Анимируем ширину элемента `width` от `0` до `100%`, используя нашу функцию.
Кликните для демонстрации:
[codetabs height=60 src="width"]
Код для запуска анимации:
```js
animate({
duration: 1000,
timing: function(timeFraction) {
return timeFraction;
},
draw: function(progress) {
elem.style.width = progress * 100 + '%';
}
});
```
## Временные функции
Выше мы видели самую простую, линейную временную функцию.
Рассмотрим примеры анимации движения с использованием различных `timing`.
### В степени n
Вот еще один простой случай -- `progress` в степени `n`. Частные случаи - квадратичная, кубическая функции и т.д.
Для квадратичной функции:
```js
function quad(progress) {
return Math.pow(progress, 2)
}
```
**График квадратичной функции:**
<img src="quad.png">
Пример для квадратичной функции (клик для просмотра):
[iframe height=40 src="quad" link]
Увеличение степени влияет на ускорение. Например, график для 5-й степени:
<img src="quint.png">
В действии:
[iframe height=40 src="quint" link]
### Дуга
Функция:
```js
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction))
}
```
**График:**
<img src="circ.png">
[iframe height=40 src="circ" link]
### Back: стреляем из лука
Эта функция работает по принципу лука: сначала мы "натягиваем тетиву", а затем "стреляем".
В отличие от предыдущих функций, эта зависит от дополнительного параметра `x`, который является "коэффициентом упругости". Он определяет расстояние, на которое "оттягивается тетива".
Её код:
```js
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}
```
**График для `x = 1.5`:**
<img src="back.png">
Пример для `x = 1.5`:
[iframe height=40 src="back" link]
### Отскок bounce
Представьте, что мы отпускаем мяч, он падает на пол, несколько раз отскакивает и останавливается.
Функция `bounce` делает то же самое, только наоборот: "подпрыгивание" начинается сразу.
Эта функция немного сложнее предыдущих и использует специальные коэффициенты:
```js
function bounce(timeFraction) {
for (var a = 0, b = 1, result; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
```
Код взят из MooTools.FX.Transitions. Конечно же, есть и другие реализации `bounce`.
Пример:
[iframe height=40 src="bounce" link]
### Упругая анимация
Эта функция зависит от дополнительного параметра `x`, который определяет начальный диапазон.
```js
function elastic(x, timeFraction) {
return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}
```
**График для `x=1.5`:**
<img src="elastic.png">
Пример для `x=1.5`:
[iframe height=40 src="elastic" link]
## Реверсивные функции ease*
Итак, у нас есть коллекция временных функций.
Их прямое использование называется "easeIn".
**Иногда нужно показать анимацию в обратном режиме. Преобразование функции, которое даёт такой эффект, называется "easeOut"**.
### easeOut
В режиме "easeOut", значение timing вычисляется по формуле: `timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)`
Например, функция `bounce` в режиме "easeOut":
```js
// обычный вариант
function bounce(timeFraction) {
for (var a = 0, b = 1, result; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2);
}
}
}
// преобразователь в easeOut
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
*!*
var bounceEaseOut = makeEaseOut(bounce);
*/!*
```
Полный пример -- откок в `bounceEaseOut` теперь не в начале, а в конце (и это куда красивее):
[codetabs src="bounce-easeout"]
На этом графике видно, как преобразование `easeOut` изменяет поведение функции:
<img src="bounce-inout.png">
Если есть анимационный эффект, такой как подпрыгивание -- он будет показан в конце, а не в начале (или наоборот, в начале, а не в конце).
Красным цветом обозначен <span style="color:#EE6B47">обычный вариант</span>, а <span style="color:#62C0DC">синим</span> -- `easeOut`.
<ul>
<li>Обычно анимируемый объект сначала медленно скачет внизу, а затем, в конце, резко достигает верха..</li>
<li>А после `easeOut` -- он сначала прыгает наверх, а затем медленно скачет внизу.</li>
</ul>
### easeInOut
А еще можно сделать так, чтобы показать эффект *и в начале и в конце* анимации. Соответствующее преобразование называется "easeInOut".
Его код выглядит так:
```js
if (timeFraction <= 0.5) { // первая половина анимации)
return timing(2 * timeFraction) / 2;
} else { // вторая половина
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
```
Код, который трансформирует `timing`:
```js
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`:
[codetabs src="bounce-easeinout"]
Трансформация "easeInOut" объединяет в себе два графика в один: `easeIn` для первой половины анимации и `easeOut` -- для второй.
Это отлично видно, если посмотреть графики `easeIn`, `easeOut` и `easeInOut` на примере функции `circ`:
<img src="circ-ease.png">
<ul>
<li><span style="color:#EE6B47">Красным</span> цветом обозначен обычный вариант функции `circ`.</li>
<li><span style="color:#8DB173">Зелёным</span> -- `easeOut`.</li>
<li><span style="color:#62C0DC">Синим</span> -- `easeInOut`.</li>
</ul>
Как видно, график первой половины анимации представляет собой уменьшенный "easeIn", а второй -- уменьшенный "easeOut". В результате, анимация начинается и заканчивается одинаковым эффектом.
[summary]
Процесс анимации полностью в ваших руках благодаря `timing`. Её можно сделать настолько реалистичной, насколько захочется.
Впрочем, исходя из практики, можно сказать, что варианты `timing`, описанные выше, покрывают 95% потребностей в анимации.
[/summary]
## Сложные варианты step
Анимировать можно все, что угодно. Вместо движения, как во всех предыдущих примерах, можно изменять любые CSS свойства... И не только!
Достаточно лишь написать соответствующий `draw`.
### Набор текста
Можно, к примеру, анимировать набор текста в "скачущем" режиме:
[codetabs src="text"]
## Итого
Анимация выполняется путём вызовов `requestAnimationFrame`. Для поддержки IE9- желательно подключить полифилл, который будет внутри использовать `setTimeout`. Это будет всё равно лучше, чем независимые `setInterval`.
Реализация анимации -- очень простая и вместе с тем гибкая:
```js
function animate(options) {
var start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction от 0 до 1
var timeFraction = (time - start) / options.duration;
if (timeFraction > 1) timeFraction = 1;
// текущее состояние анимации
var progress = options.timing(timeFraction)
options.draw(progress);
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
```
Основные параметры:
<ul>
<li>`duration` -- длительность анимации в мс.</li>
<li>`timing` -- функция, которая определяет состояние анимации каждый кадр. Получает часть времени от 0 до 1, возвращает завершенность анимации от 0 до 1.
</li>
<li>`draw` -- функция, которая отрисовывает состояние анимации от 0 до 1.</li>
</ul>
Эту функцию можно улучшить, например добавить коллбэк `complete` для вызова в конце анимации.
Мы рассмотрели ряд примеров для `timing` и трансформации `easeOut`, `easeInOut`, которые позволяют их разнообразить. В отличие от CSS мы не ограничены кривыми Безье, можно реализовать всё, что угодно.
Это же относится и к функции `draw`.
Такая реализация анимации имеет три основных области применения:
<ul>
<li>Нестандартные задачи и требования, не укладывающиеся в рамки CSS.</li>
<li>Поддержка IE9-.</li>
<li>Графика, рисование на canvas.</li>
</ul>