2024年1月20日

JavaScriptアニメーション

JavaScriptアニメーションは、CSSではできないことを処理できます。

たとえば、ベジェ曲線とは異なるタイミング関数を使用して複雑なパスに沿って移動したり、キャンバス上でアニメーションを実行したりできます。

setIntervalの使用

アニメーションは、一連のフレーム(通常はHTML / CSSプロパティの小さな変更)として実装できます。

たとえば、style.left0pxから100pxに変更すると、要素が移動します。 そして、setIntervalでそれを増やし、2pxずつ、50分の1秒のような小さな遅延で変更すると、滑らかに見えます。 これは映画と同じ原理です。毎秒24フレームで滑らかに見えます。

擬似コードは次のようになります

let timer = setInterval(function() {
  if (animation complete) clearInterval(timer);
  else increase style.left by 2px
}, 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';
}

デモを見るにはクリックしてください

結果
index.html
<!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';
}

…または何か他のことをしてください、私たちはどんな方法ででも何でもアニメーション化できます。

関数を使用して、要素のwidth0から100%にアニメーション化してみましょう。

デモを見るには、要素をクリックしてください

結果
animate.js
index.html
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の累乗

アニメーションを高速化したい場合は、累乗nprogressを使用できます。

たとえば、放物線

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);

その後、バウンスは最初ではなく、アニメーションの最後に表示されます。さらに良く見えます

結果
style.css
index.html
#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

結果
style.css
index.html
#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タイミング関数のeaseIneaseOut、およびeaseInOutのグラフを比較すると、効果がはっきりとわかります。

  • circの通常のバリアント(easeIn)です。
  • -easeOut
  • -easeInOut

ご覧のとおり、アニメーションの前半のグラフは縮小されたeaseInであり、後半は縮小されたeaseOutです。 その結果、アニメーションは同じ効果で開始および終了します。

より興味深い「描画」

要素を移動する代わりに、何か他のことができます。必要なのは、適切なdrawを書くことだけです。

アニメーション化された「バウンス」テキストタイピングは次のとおりです

結果
style.css
index.html
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プロパティだけでなく、何でもアニメーション化できます。

タスク

重要度:5

跳ねるボールを作ります。 クリックして、どのように見えるかを確認します

タスクのサンドボックスを開きます。

バウンスするには、CSSプロパティtopposition:absoluteを、position:relativeのフィールド内のボールに使用できます。

フィールドの底辺の座標はfield.clientHeightです。CSSのtopプロパティはボールの上端を参照します。そのため、0からfield.clientHeight - ball.clientHeightまで移動する必要があります。これがボールの上端の最終的な最下位です。

「バウンド」効果を得るには、タイミング関数bounceeaseOutモードで使用できます。

アニメーションの最終的なコードは次のとおりです。

let to = field.clientHeight - ball.clientHeight;

animate({
  duration: 2000,
  timing: makeEaseOut(bounce),
  draw(progress) {
    ball.style.top = to * progress + 'px'
  }
});

サンドボックスでソリューションを開きます。

重要度:5

ボールを右に跳ねさせます。このように。

アニメーションコードを記述します。左への距離は100pxです。

前のタスク跳ねるボールをアニメーション化するのソリューションをソースとして使用します。

タスク跳ねるボールをアニメーション化するでは、アニメーション化するプロパティは1つだけでした。今度はもう1つ必要です:elem.style.left

水平座標は別の法則によって変化します。「バウンド」するのではなく、徐々に増加してボールを右に移動させます。

そのためにもう1つanimateを書くことができます。

時間関数としてlinearを使用することもできますが、makeEaseOut(quad)のようなものの方がはるかに見栄えがします。

コード

let height = field.clientHeight - ball.clientHeight;
let width = 100;

// animate top (bouncing)
animate({
  duration: 2000,
  timing: makeEaseOut(bounce),
  draw: function(progress) {
    ball.style.top = height * progress + 'px'
  }
});

// animate left (moving to the right)
animate({
  duration: 2000,
  timing: makeEaseOut(quad),
  draw: function(progress) {
    ball.style.left = width * progress + "px"
  }
});

サンドボックスでソリューションを開きます。

チュートリアルマップ