JavaScriptアニメーションは、CSSではできないことを処理できます。
たとえば、ベジェ曲線とは異なるタイミング関数を使用して複雑なパスに沿って移動したり、キャンバス上でアニメーションを実行したりできます。
setIntervalの使用
アニメーションは、一連のフレーム(通常はHTML / CSSプロパティの小さな変更)として実装できます。
たとえば、style.left
を0px
から100px
に変更すると、要素が移動します。 そして、setInterval
でそれを増やし、2pxずつ、50分の1秒のような小さな遅延で変更すると、滑らかに見えます。 これは映画と同じ原理です。毎秒24フレームで滑らかに見えます。
擬似コードは次のようになります
let
timer =
setInterval
(
function
(
)
{
if
(
animation complete)
clearInterval
(
timer)
;
else
increase style.
left by 2
px
}
,
20
)
;
// change by 2px every 20ms, about 50 frames per second
アニメーションのより完全な例
let
start =
Date.
now
(
)
;
// remember start time
let
timer =
setInterval
(
function
(
)
{
// how much time passed from the start?
let
timePassed =
Date.
now
(
)
-
start;
if
(
timePassed >=
2000
)
{
clearInterval
(
timer)
;
// finish the animation after 2 seconds
return
;
}
// draw the animation at the moment timePassed
draw
(
timePassed)
;
}
,
20
)
;
// as timePassed goes from 0 to 2000
// left gets values from 0px to 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)
を持っている場合でも、個別に実行すると、ブラウザは20ミリ秒ごとに再描画する必要があります。
これは、開始時間が異なるため、異なるアニメーション間で「20ミリ秒ごと」が異なるためです。 間隔は揃っていません。 したがって、20ミリ秒以内に複数回の独立した実行が行われます。
言い換えれば、これは
setInterval
(
function
(
)
{
animate1
(
)
;
animate2
(
)
;
animate3
(
)
;
}
,
20
)
…3つの独立した呼び出しよりも軽いです
setInterval
(
animate1,
20
)
;
// independent animations
setInterval
(
animate2,
20
)
;
// in different places of the script
setInterval
(
animate3,
20
)
;
これらのいくつかの独立した再描画は、ブラウザにとって再描画を容易にし、CPU負荷を軽減してスムーズに見せるために、まとめてグループ化する必要があります。
もう1つ覚えておくべきことがあります。 CPUが過負荷になっている場合や、再描画の頻度を下げる必要がある場合(ブラウザタブが非表示になっている場合など)があるため、実際には20ミリ秒ごとに実行するべきではありません。
しかし、JavaScriptでそれをどのように知ることができますか? 関数requestAnimationFrame
を提供する仕様アニメーションタイミングがあります。 これらの問題すべて、さらにそれ以上の問題に対処します。
構文
let
requestId =
requestAnimationFrame
(
callback)
これは、ブラウザがアニメーションを実行したい最も近い時間にcallback
関数を実行するようにスケジュールします。
callback
で要素に変更を加えると、それらは他のrequestAnimationFrame
コールバックおよびCSSアニメーションとグループ化されます。 したがって、多くのジオメトリ再計算と再描画ではなく、1つのジオメトリ再計算と再描画が行われます。
返された値requestId
を使用して、呼び出しをキャンセルできます
// cancel the scheduled execution of callback
cancelAnimationFrame
(
requestId)
;
callback
は、ページの読み込み開始からの経過時間をミリ秒単位で1つの引数として受け取ります。 この時間は、performance.now()を呼び出すことによっても取得できます。
通常、CPUが過負荷になっている場合、ラップトップのバッテリ残量がほとんどない場合、またはその他の理由がない限り、callback
はすぐに実行されます。
以下のコードは、requestAnimationFrame
の最初の10回の実行間の時間を示しています。 通常は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 goes from 0 to 1
let
timeFraction =
(
time -
start)
/
duration;
if
(
timeFraction >
1
)
timeFraction =
1
;
// calculate the current animation state
let
progress =
timing
(
timeFraction)
draw
(
progress)
;
// draw it
if
(
timeFraction <
1
)
{
requestAnimationFrame
(
animate)
;
}
}
)
;
}
関数animate
は、基本的にアニメーションを記述する3つのパラメーターを受け入れます
期間
-
アニメーションの合計時間。 1000のように。
タイミング(timeFraction)
-
経過時間の割合(開始時は
0
、終了時は1
)を取得し、アニメーションの完了(ベジェ曲線のy
など)を返す、CSSプロパティtransition-timing-function
のようなタイミング関数。たとえば、線形関数は、アニメーションが同じ速度で均一に進むことを意味します
function
linear
(
timeFraction
)
{
return
timeFraction;
}
そのグラフ:
それはちょうど
transition-timing-function:linear
のようです。 以下に示すより興味深いバリアントがあります。 描画(進行状況)
-
アニメーションの完了状態を取得して描画する関数。 値
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
はプロパティを超えて、花火アニメーションなどの新しい要素を作成できます。
タイミング関数
上記で最も単純な線形タイミング関数を確認しました。
もっと見てみましょう。 さまざまなタイミング関数を使用して移動アニメーションを試して、それらがどのように機能するかを確認します。
nの累乗
アニメーションを高速化したい場合は、累乗n
でprogress
を使用できます。
たとえば、放物線
function
quad
(
timeFraction
)
{
return
Math.
pow
(
timeFraction,
2
)
}
グラフ
動作を確認する(クリックしてアクティブにする)
…または3次曲線、あるいはさらに大きいn
。 パワーを上げると、スピードアップが速くなります。
5
の累乗のprogress
のグラフは次のとおりです。
動作中
アーク
関数
function
circ
(
timeFraction
)
{
return
1
-
Math.
sin
(
Math.
acos
(
timeFraction)
)
;
}
グラフ
戻る:弓の射撃
この関数は「弓の射撃」を行います。 まず「弓の弦を引く」してから「撃つ」。
以前の関数とは異なり、追加パラメータ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
を受け入れるもう1つの「弾性」関数。
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
があります。
// accepts a timing function, returns the transformed variant
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>
ここで、変換が関数の動作をどのように変更するかを確認できます
バウンスなどのアニメーション効果が最初に
上のグラフでは、通常のバウンスは赤色で、easeOutバウンスは青色です。
- 通常のバウンス-オブジェクトは下部でバウンスし、最後に急激に上部にジャンプします。
easeOut
後-最初に上部にジャンプし、そこでバウンスします。
easeInOut
アニメーションの最初と最後にエフェクトを表示することもできます。 変換は「easeInOut」と呼ばれます。
タイミング関数を考えると、アニメーションの状態を次のように計算します
if
(
timeFraction <=
0.5
)
{
// first half of the animation
return
timing
(
2
*
timeFraction)
/
2
;
}
else
{
// second half of the animation
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」変換は、2つのグラフを1つに結合します。アニメーションの前半はeaseIn
(通常)、後半はeaseOut
(反転)です。
circ
タイミング関数のeaseIn
、easeOut
、およびeaseInOut
のグラフを比較すると、効果がはっきりとわかります。
- 赤は
circ
の通常のバリアント(easeIn
)です。 - 緑-
easeOut
。 - 青-
easeInOut
。
ご覧のとおり、アニメーションの前半のグラフは縮小されたeaseIn
であり、後半は縮小されたeaseOut
です。 その結果、アニメーションは同じ効果で開始および終了します。
より興味深い「描画」
要素を移動する代わりに、何か他のことができます。必要なのは、適切な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
を介して実装する必要があります。 この組み込みメソッドを使用すると、ブラウザが再描画を準備するときに実行されるコールバック関数を設定できます。 通常は非常に sớmですが、正確な時間はブラウザによって異なります。
ページがバックグラウンドにある場合、再描画はまったく行われないため、コールバックは実行されません。アニメーションは中断され、リソースは消費されません。 それは素晴らしいことです。
ほとんどのアニメーションを設定するためのヘルパーanimate
関数は次のとおりです
function
animate
(
{
timing,
draw,
duration}
)
{
let
start =
performance.
now
(
)
;
requestAnimationFrame
(
function
animate
(
time
)
{
// timeFraction goes from 0 to 1
let
timeFraction =
(
time -
start)
/
duration;
if
(
timeFraction >
1
)
timeFraction =
1
;
// calculate the current animation state
let
progress =
timing
(
timeFraction)
;
draw
(
progress)
;
// draw it
if
(
timeFraction <
1
)
{
requestAnimationFrame
(
animate)
;
}
}
)
;
}
オプション
duration
-ミリ秒単位の合計アニメーション時間。timing
-アニメーションの進行状況を計算する関数。 0から1までの時間の割合を取得し、通常は0から1までのアニメーションの進行状況を返します。draw
-アニメーションを描画する関数。
確かに、私たちはそれを改善し、より多くのベルとホイッスルを追加することができましたが、JavaScriptアニメーションは毎日適用されるわけではありません。 それらは、何か面白くて非標準的なことをするため
JavaScriptアニメーションでは、任意のタイミング関数を使用できます。 私たちは、それらをさらに用途が広くするために、多くの例と変換を取り上げました。 CSSとは異なり、ここではベジェ曲線に限定されません。
draw
についても同様です。CSSプロパティだけでなく、何でもアニメーション化できます。