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
)
;
// змінюється на 2 пікселі кожні 20 мс, що приблизно 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'
;
}
Клацніть, щоб побачити демо-версію:
<!DOCTYPE HTML>
<html>
<head>
<style>
#train {
position: relative;
cursor: pointer;
}
</style>
</head>
<body>
<img id="train" src="https://js.cx/clipart/train.gif">
<script>
train.onclick = function() {
let start = Date.now();
let timer = setInterval(function() {
let timePassed = Date.now() - start;
train.style.left = timePassed / 5 + 'px';
if (timePassed > 2000) clearInterval(timer);
}, 20);
}
</script>
</body>
</html>
Використання requestAnimationFrame
Уявімо, що у нас є кілька анімацій, запущених одночасно.
Якщо ми запустимо їх окремо, то навіть якщо кожен з них має setInterval(..., 20)
, то браузеру доведеться перемальовувати набагато частіше, ніж кожні 20ms
.
Це тому, що вони мають різний час запуску, тому “кожні 20 мс” відрізняється для різних анімацій. Інтервали не вирівняні. Отже, ми матимемо кілька незалежних прогонів протягом 20ms
.
Іншими словами, це:
setInterval
(
function
(
)
{
animate1
(
)
;
animate2
(
)
;
animate3
(
)
;
}
,
20
)
…легше, ніж три незалежні виклики:
setInterval
(
animate1,
20
)
;
// незалежні анімації
setInterval
(
animate2,
20
)
;
// в різних місцях коду
setInterval
(
animate3,
20
)
;
Ці кілька незалежних перемальовувань слід згрупувати разом, щоб зробити перемальовування простішим для браузера, а отже, зменшити навантаження на процесор і виглядати більш плавно.
Є ще одна річ, про яку слід пам’ятати. Іноді процесор перевантажений, або є інші причини перемальовувати рідше (наприклад, коли вкладку браузера приховано), тому нам дійсно не слід запускати його кожні 20ms
.
Але як дізнатися про це в JavaScript? Існує специфікація Animation timing, яка надає функцію requestAnimationFrame
. Вона вирішує всі ці питання і навіть більше.
Синтаксис:
let
requestId =
requestAnimationFrame
(
callback)
Це запланує запуск функції callback
на найближчий час, коли браузер захоче виконати анімацію.
Якщо ми зробимо зміни в елементах у callback
, то вони будуть згруповані разом з іншими викликами requestAnimationFrame
і з CSS-анімаціями. Таким чином, буде виконано один перерахунок геометрії та перемальовування замість багатьох.
Повернуте значення requestId
може бути використане для скасування виклику:
// скасовуємо заплановане виконання колбеку
cancelAnimationFrame
(
requestId)
;
Функція callback
отримує один аргумент – час, що пройшов з початку завантаження сторінки в мілісекундах. Цей час також можна отримати за допомогою виклику performance.now().
Зазвичай callback
виконується найближчим часом, якщо тільки процесор не перевантажений або батарея ноутбука майже розрядилася, або якщо є інший привід.
Код нижче показує час між першими 10 викликами для requestAnimationFrame
. Зазвичай це 10-20 мс:
<
script
>
let
prev =
performance.
now
(
)
;
let
times =
0
;
requestAnimationFrame
(
function
measure
(
time
)
{
document.
body.
insertAdjacentHTML
(
"beforeEnd"
,
Math.
floor
(
time -
prev)
+
" "
)
;
prev =
time;
if
(
times++
<
10
)
requestAnimationFrame
(
measure)
;
}
)
</
script
>
Структурована анімація
Тепер ми можемо зробити більш універсальну функцію анімації на основі requestAnimationFrame
:
function
animate
(
{
timing,
draw,
duration}
)
{
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
приймає 3 параметри, які описують анімацію:
duration
-
Загальний час анімації. Наприклад,
1000
. timing(timeFraction)
-
Функція часу, подібна до CSS-властивості
transition-timing-function
, яка отримує частку часу, що минув (0
на початку,1
в кінці) і повертає завершення анімації (якy
на кривій Безьє).Наприклад, лінійна функція означає, що анімація триває рівномірно з однаковою швидкістю:
function
linear
(
timeFraction
)
{
return
timeFraction;
}
Її графік:
Це те саме, що й
transition-timing-function: linear
. Також існують цікавіші варіанти, показані нижче. draw(progress)
-
Функція, яка приймає стан завершення анімації і малює її. Значення
progress=0
означає початковий стан анімації, аprogress=1
– кінцевий.Це та функція, яка фактично малює анімацію.
Вона може переміщати елемент:
function
draw
(
progress
)
{
train.
style.
left=
progress+
'px'
;
}
…Або робити щось інше, ми можемо анімувати будь-що, у будь-який спосіб.
Давайте анімуємо width
елемента від 0
до 100%
за допомогою нашої функції.
Натисніть на елемент для демонстрації:
function animate({duration, draw, timing}) {
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);
}
});
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
progress {
width: 5%;
}
</style>
<script src="animate.js"></script>
</head>
<body>
<progress id="elem"></progress>
<script>
elem.onclick = function() {
animate({
duration: 1000,
timing: function(timeFraction) {
return timeFraction;
},
draw: function(progress) {
elem.style.width = progress * 100 + '%';
}
});
};
</script>
</body>
</html>
Код до неї:
animate
(
{
duration
:
1000
,
timing
(
timeFraction
)
{
return
timeFraction;
}
,
draw
(
progress
)
{
elem.
style.
width =
progress *
100
+
'%'
;
}
}
)
;
На відміну від CSS-анімацій, тут ми можемо створити будь-яку функцію часу і будь-яку функцію малювання. Функція часу не обмежується кривими Безьє. А draw
може не тільки змінювати властивості елементів, а й створювати нові елементи, наприклад, для анімації феєрверків або чогось подібного.
Функції часу (timing functions)
Вище ми бачили найпростішу, лінійну функцію часу.
Давайте розглянемо інші варіанти. Тут ми відтворимо анімацію руху з різними функціями часу, щоб побачити, як вони працюють.
Степінь n
Якщо ми хочемо прискорити анімацію, ми можемо використати progress
у степені n
.
Наприклад, параболічна крива:
function
quad
(
timeFraction
)
{
return
Math.
pow
(
timeFraction,
2
)
}
Графік:
Побачити в дії (натисніть, щоб активувати):
…Або кубічна крива, або навіть ще більше значення n
. Збільшення степеня призводить до швидшого прискорення.
Ось графік progress
в степені 5
:
У дії:
Дуга
Функція:
function
circ
(
timeFraction
)
{
return
1
-
Math.
sin
(
Math.
acos
(
timeFraction)
)
;
}
Графік:
Back: стрільба з лука
Ця функція виконує “стрільбу з лука”. Спочатку ми “натягуємо тятиву”, а потім “стріляємо”.
На відміну від попередніх функцій, вона залежить від додаткового параметра x
– “коефіцієнта еластичності”. Саме ним визначається відстань “натягу тятиви”.
Код:
function
back
(
x,
timeFraction
)
{
return
Math.
pow
(
timeFraction,
2
)
*
(
(
x +
1
)
*
timeFraction -
x)
}
Графік для x = 1.5
:.
Для анімації ми використовуємо його з певним значенням x
. Приклад для x = 1.5
:
Відскок
Уявіть, що ми кидаємо м’яч. Він падає вниз, потім кілька разів відскакує назад і зупиняється.
Функція bounce
робить те саме, але у зворотному порядку: “відскакування” починається негайно. Для цього використовується декілька спеціальних коефіцієнтів:
function
bounce
(
timeFraction
)
{
for
(
let
a =
0
,
b =
1
;
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
)
}
}
}
У дії:
Еластична анімація
Ще одна “еластична” функція, яка приймає додатковий параметр x
для “початкового діапазону”.
function
elastic
(
x,
timeFraction
)
{
return
Math.
pow
(
2
,
10
*
(
timeFraction -
1
)
)
*
Math.
cos
(
20
*
Math.
PI
*
x /
3
*
timeFraction)
}
Графік для x=1.5
:.
У дії для x=1.5
:
Реверс: ease*
Отже, у нас є набір функцій часу. Їх пряме використання називається “easeIn”.
Іноді нам потрібно показати анімацію у зворотному напрямку. Це робиться за допомогою трансформації “easeOut”.
easeOut
У режимі “easeOut” функція timing
поміщається в обгортку timingEaseOut
:
timingEaseOut
(
timeFraction)
=
1
-
timing
(
1
-
timeFraction)
Іншими словами, у нас є функція “перетворення” makeEaseOut
, яка бере “звичайну” функцію часу і повертає обгортку навколо неї:
// отримує функцію часу, повертає перетворений варіант
function
makeEaseOut
(
timing
)
{
return
function
(
timeFraction
)
{
return
1
-
timing
(
1
-
timeFraction)
;
}
}
Наприклад, ми можемо взяти функцію bounce
, описану вище, і застосувати її:
let
bounceEaseOut =
makeEaseOut
(
bounce)
;
Тоді відскік буде не на початку, а в кінці анімації. Виглядає навіть краще:
#brick {
width: 40px;
height: 20px;
background: #EE6B47;
position: relative;
cursor: pointer;
}
#path {
outline: 1px solid #E8C48E;
width: 540px;
height: 20px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<div id="path">
<div id="brick"></div>
</div>
<script>
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 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)
}
}
}
let bounceEaseOut = makeEaseOut(bounce);
brick.onclick = function() {
animate({
duration: 3000,
timing: bounceEaseOut,
draw: function(progress) {
brick.style.left = progress * 500 + 'px';
}
});
};
</script>
</body>
</html>
Тут ми бачимо, як перетворення змінює поведінку функції:
Якщо на початку є анімаційний ефект, наприклад, підстрибування – він буде показаний в кінці.
На графіку вище звичайний bounce має червоний колір, а easeOut bounce – синій.
- Звичайний bounce – об’єкт відскакує внизу, а потім в кінці різко підстрибує догори.
- Після
easeOut
– він спочатку стрибає вгору, а потім підстрибує там.
easeInOut
Ми також можемо показати ефект як на початку, так і в кінці анімації. Така трансформація називається “easeInOut”.
Знаючи функцію часу, ми обчислюємо стан анімації таким чином:
if
(
timeFraction <=
0.5
)
{
// перша половина анімації
return
timing
(
2
*
timeFraction)
/
2
;
}
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 {
outline: 1px solid #E8C48E;
width: 540px;
height: 20px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<div id="path">
<div id="brick"></div>
</div>
<script>
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 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)
}
}
}
let bounceEaseInOut = makeEaseInOut(bounce);
brick.onclick = function() {
animate({
duration: 3000,
timing: bounceEaseInOut,
draw: function(progress) {
brick.style.left = progress * 500 + 'px';
}
});
};
</script>
</body>
</html>
Трансформація “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 {
margin-top: 10px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<textarea id="textExample" rows="5" cols="60">He took his vorpal sword in hand:
Long time the manxome foe he sought—
So rested he by the Tumtum tree,
And stood awhile in thought.
</textarea>
<button onclick="animateText(textExample)">Run the animated typing!</button>
<script>
function animateText(textArea) {
let text = textArea.value;
let to = text.length,
from = 0;
animate({
duration: 5000,
timing: bounce,
draw: function(progress) {
let result = (to - from) * progress + from;
textArea.value = text.slice(0, Math.ceil(result))
}
});
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 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)
}
}
}
</script>
</body>
</html>
Підсумки
Анімації, з якими CSS не може впоратися, або ті, що потребують чіткого контролю, можуть бути реалізовані за допомогою JavaScript. JavaScript-анімація повинна бути реалізована через requestAnimationFrame
. Цей вбудований метод дозволяє передавати колбек, що буде виконуватися, коли браузер буде готуватись до перемальовування. Зазвичай це відбувається дуже швидко, але точний час залежить від браузера.
Коли сторінка знаходиться у фоновому режимі, перемальовування взагалі не відбувається, тому колбек не запуститься: анімація буде призупинена і не буде споживати ресурси. І це чудово.
Ось допоміжна функція animate
для налаштування більшості анімацій:
function
animate
(
{
timing,
draw,
duration}
)
{
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.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)