自主的20%るぅる

各々が自主的に好き勝手書くゆるふわ会社ブログ

Web で物理アニメーション・その2 「単純なアニメーションを作ろう」

前回の回答

さて、まずは前回の宿題の答え合わせから。

問題は

  • 三角形を一つ、canvas 上に描画してください

でしたね。

では回答。

const main = () => {
  // キャンバス要素の取得
  const canvas = document.getElementById(’canvas’);
  // キャンバスを操作する API を取得
  const ctx = canvas.getContext(’2d’);

  // 線を引く作業を開始
  ctx.beginPath();

  // パスを作成
  ctx.moveTo(120, 120);

  ctx.lineTo(80, 190);
  ctx.lineTo(160, 190);
  ctx.closePath();

  // 塗りつぶしの色を指定
  ctx.fillStyle = ’rgb(150, 33, 253)’;

  // 塗りつぶしを実行
  ctx.fill();
};

document.addEventListener(’DOMContentLoaded’, main);

回答としては正三角形に近い形にしてみましたが、
ヒントにもあったように「角が 3 つあれば三角形」なので、
長方形をパスをつないで描画したサンプルから一点削っただけでも OK です。

これも OK 。

アニメーションとは?

さて、今回の本題に入っていきましょう。

まず、アニメーションをどうやって作るか?というお話から。

そもそも、アニメーションの仕組みはどのようになっているか、という話ですが、
簡単におはなしすると、パラパラ漫画と同じ仕組みなんですね。
(これはよく聞くお話ですよね!)

例えば、こんな感じの 1 ~ 4 の画像を用意してみましょう。
これを順番に一つずつ表示してみると…

丸が動いているように見えますね!
アニメーションは、こんな感じで画像をうまく差し替えることで
あたかも動いているように見せかける技なんですね。

canvas で丸を動かす

一定のタイミングで丸を描画し直してみよう

さて、アニメーションの考え方が分かったところで、
実際に canvas で丸が動くアニメーションを作ってみましょう。

要は、一定のタイミングで丸を何度も書き直せばいいわけですよね。

というわけで作ったのがこのコード。

const main = () => {
  const canvas = document.getElementById(’canvas’);
  const ctx = canvas.getContext(’2d’);
  ctx.fillStyle = ’rgb(63, 81, 181)’;

  let x = 50;

  const draw = () => {
    ctx.beginPath();
    ctx.arc(x, 240, 40, 0, Math.PI*2, false)
    ctx.fill();

    // x を 20 増加
    x += 20;
    // 100 ミリ秒(0.1 秒)後に draw を再度呼び出し
    setTimeout(draw, 100, x + 20);
  };

  // 一回目描画実行
  draw();
};

document.addEventListener(’DOMContentLoaded’, main);

main の中に draw という関数を作りました。

これを実行するたびに、丸が描画されるということですね。

ctx.arc(x, 240, 40, 0, Math.PI*2, false)

x は関数の外でまず定義しておいて、
描画を実行したタイミングで x += 20; で x を 20 ずつ増やします。

    // 100 ミリ秒(0.1 秒)後に draw を再度呼び出し
    setTimeout(draw, 100);

draw 関数内で setTimeout を使用し、draw 自身を呼び直すことで、
draw が終了したら 0.1 秒後に再度 draw が呼び出される…という形にして
繰り返し実行されるようになっています。

最後に、

  // 一回目描画実行
  draw();

2 回目以降は先ほどの setTimeout で実行されるので、
1 回目だけは普通に呼び出してあげる必要がありますね。

つまり、先ほどの setTimeout と組み合わせて、

draw() // x は 50
↓ 0.1秒後
draw() // x は 70
↓ 0.1秒後
draw() // x は 90
↓ 0.1秒後
...

という感じで描画されていくということになります。

さて、コードの動きが分かってきたところで
実行してみましょうか。

あれ?なんか思ってたのと違う…

丸を描画する前に、 canvas をまっさらにしないと!

そういえば、一度書いた円を消していないですね!
円を消さずに何度も書いているので、同じ紙に何度も丸を書いているみたいになってしまったようです。

ということで、draw 関数の最初に、canvas を一度きれいにするように命令を追加しましょう。

const main = () => {
  const canvas = document.getElementById(’canvas’);
  const ctx = canvas.getContext(’2d’);
  ctx.fillStyle = ’rgb(63, 81, 181)’;

  let x = 50;

  const draw = () => {
    // canvas 内をまっさらにする
    ctx.clearRect(0, 0, 640, 480);

    ctx.beginPath();
    ctx.arc(x, 240, 40, 0, Math.PI*2, false)
    ctx.fill();

    // 100 ミリ秒(0.1 秒)後に draw を再度呼び出し
    setTimeout(draw, 100, x + 20);
  };

  // x を 20 増加
  x += 20;
  // 一回目描画実行
  draw();
};

document.addEventListener(’DOMContentLoaded’, main);

増えたのはこの部分だけ

    // canvas 内をまっさらにする
    ctx.clearRect(0, 0, 640, 480);

canvas には、全てを消すという命令はありません。
clearRect は、消す範囲を指定してあげる必要があります。
ということで、消す範囲を canvas 全体として指定してあげました。

clearRect の引数は、前回長方形を簡単に描画する方法でご紹介した fillRect と同じです。

clearRect(x軸のスタート, y軸のスタート, 横幅, 高さ)

なので、 スタート地点は 0 (原点)として、横幅と高さは canvas のサイズと同じものを指定しました。

これで、丸を書く前に毎回 canvas がまっさらになるので、
今度こそうまくいくはず…

実行してみましょう!

今度は成功ですね!
きちんと丸が左から右へ動いているようにアニメーションできました!

より「効率的な」アニメーションを目指して

さて、これで目的は達成されたのですが、
一つだけ懸念点があります。

それはここ

    // 100 ミリ秒(0.1 秒)後に draw を再度呼び出し
    setTimeout(draw, 100, x + 20);

特に問題ない処理のように見えますが、ちょっと気をつけておかなければならないことがあります。

setTimeout の挙動は「命令を出されてから指定秒数後(0.1)に、引数に渡された関数(draw)を実行する」というものです。
つまり、動きとしてはこう。

今は円を一つ描画する位なのであまり問題にはなりませんが、
正確には 「0.1 秒 + 描画にかかる時間」ごとに処理がうごいていることになります。

今後、物理アニメーションを作っていく上では、時間の扱いはとても大切なので
できるだけこのようなズレが発生しないようにしたいところです。

またもう一つ、こちらの方が割と本題なのですが、
画面の更新が 0.1 秒ごとに固定されてしまう、という問題があります。 

つまり、ページを見ている人が使っている PC やスマホの性能によって、
もっと頻繁に更新してなめらかなアニメーションを表示できる場合や、
逆に性能が追いついていなくて更新頻度を落としたい場合でも
問答無用で 0.1 秒ごとの更新を実施します。

さらには、別のタブを開いている等でページが表示されていないときにも、
お構いなしに 0.1 秒ごとに画面を更新します。

なんだかちょっと非効率ですよね?

ということで、それを解決する方法が JavaScript には用意されています。

requestAnimationFrame(実行する関数)

これは、画面の表示を更新するタイミングで、
引数に渡された関数を実行する命令になります。

先ほどの setTimeout と異なり、指定したタイミングでの更新はできませんが、
ブラウザ側がアニメーションするのに最も良いタイミングで
画面更新用の関数を呼び出してくれるのですね。

つまり、なめらかに動かすだけの余力がある時は、より頻繁に関数を実行し、
画面が隠れて表示されないなどの画面更新が必要ないタイミングでは
ほとんど更新用の関数を実行しないようにする、と言った制御を
勝手にやってくれます。

requestAnimationFrame を使って修正したコードはこんな感じ

const main = () => {
  const canvas = document.getElementById(’canvas’);
  const ctx = canvas.getContext(’2d’);
  ctx.fillStyle = ’rgb(63, 81, 181)’;

  // 開始時点の時間を取得しておく
  let prev_time = new Date();

  // 開始時点の x 座標を設定しておく
  let x = 50;

  const draw = () => {
    ctx.clearRect(0, 0, 640, 480);

    // 現在時刻を取得
    const now = new Date();

    // 開始時点から現在まで、何ミリ秒経過しているかを計算
    const diffMillisecond = now.getTime() - prev_time.getTime();
    // 前回地点 + (経過ミリ秒 * 0.2) で、現在位置を算出
    x = x + (diffMillisecond * 0.2);

    ctx.beginPath();
    ctx.arc(x, 240, 40, 0, Math.PI*2, false)
    ctx.fill();

    // 次回の実行のために、今回の時間を記録しておく
    prev_time = now;

    // 次回画面更新時に draw を実行するように指定
    requestAnimationFrame(draw);
  };

  // 一回目描画実行
  draw();
};

document.addEventListener(’DOMContentLoaded’, main);

ポイントはここ

    // 開始時点から現在まで、何ミリ秒経過しているかを計算
    const diffMillisecond = now.getTime() - prev_time.getTime();
    // 前回地点 + (経過ミリ秒 * 0.2) で、現在位置を算出
    x = x + (diffMillisecond * 0.2);

setTimeout では約 0.1 秒に一度更新される前提で、
毎回更新ごとに x 方向へ 20 だけ丸を動かしていました。

ですが、今回 requestAnimationFrame を使う場合には、
どのタイミングで更新関数が呼ばれるか分かりません。
前回画面表示を更新してから 0.01 秒で次が呼ばれるかもしれませんし、
もしかしたら 5 秒くらい間隔があいているかもしれません。

ということで、画面更新が呼ばれるたびに、
前回更新した時間から、何ミリ秒経過しているかを計算し、
その時間時間分だけ移動させる、と言うことをやっています。
(つまり、長い時間経過していれば、それだけ長い距離を移動させる)

今回の場合、0.1 秒で 20 移動するということは 1 ミリ秒 = 0.001 秒では 0.2 だけ移動するので、
diffMillisecond * 0.2 で移動距離を算出し、前回の x の位置に足して
現在の位置を求めているのですね。

修正版を実行した結果がこちら。

gif アニメーションなのでわかりにくいかと思いますが、
実際に手元でコードを動かしてもらったら
先ほどの 0.1 秒更新より、すごくなめらかにアニメーションしている事が分かるかと思います。

先ほどは少しカクカクだったので、動いている感が少なかったですが、
より適切に更新することで、しっかり動いているように見えるようになりました。

宿題

今回は以上です!お疲れ様でした!
今回は、物理演算などが入っていない、アニメーションの基礎を取り扱いました。

では、今回の宿題はこれ。

  • 以下のように、近づいて右下に抜けていくように見える円のアニメーションを作成してください

大体似たような感じになれば、上のアニメーションと全く同じである必要はありません。
ポイントは

  • 右方向に動く
  • 下方向に動く
  • 円が大きくなる

の三つが組み合わさっているアニメーションであるということです。
この 3 点が実施できていれば OK。

右方向に動くのはサンプルで実施したので、
さらにそこに下方向の動きと、円の大きさの動きが加わればいいですね。

ヒントとしては、

ctx.arc(中心の x の位置, 中心の y の位置, 円の半径, 0, Math.PI*2, false)

arc で描画する関数の引数が、こんな感じで指定できることを思い出せたら、
多分うまく行くんじゃないでしょうか!

それではまた来週!

Let’s share this article!

{ 関連記事 }

{ この記事を書いた人 }

アバター画像
takato_ezaki
記事一覧