no-image

JavaScriptとcanvasでアニメーションを作る

HTML5になってから、canvas要素というものが追加されました。canvas要素はJavaScriptから好きなグラフィックを描くことができ、使い方次第で様々な表現を実現できます。

やり方次第では、canvas要素にアニメーションを描画することもできます。そこで、この記事では、javaScriptとcanvas要素を使って、アニメーションを作る方法を紹介します。

canvas要素の扱い方

ここではcanvas要素の扱い方は、詳しくは紹介しません。「javascript canvas」や「html canvas」などでググってください。

canvas要素の大まかな使い方は以下の通りです:

  1. canvas要素を取得する
  2. canvas要素のコンテキストを取得する
  3. コンテキストを通して描画する

まずは静止画

いきなりアニメーションというのも難しいでしょう。まずは静止画を描画するところを目標にしてみます。

今回は、いくつかの円を描画します。以下のようなコードになります:

const WIDTH = 500;
const HEIGHT = 500;

//
// 前準備。
//

// canvas要素を作る。
const canvas = document.createElement('canvas');
canvas.width = WIDTH;
canvas.height = HEIGHT;

// コンテキストを取得しておく。
const context = canvas.getContext('2d');

// body要素に追加する。
document.body.appendChild(canvas);

//
// メイン処理。
//

// 表示する円を管理する配列を作成しておく。
// 少ない場合(1個とか2個とか)はいらないかも。
const circles = [];

// 円を追加する。
// ここでは円はx,y,radius(半径)を持ったオブジェクト。
// 単なるオブジェクトのかわりにCircleクラスを作っても良いかも。
circles.push({x:150, y:150, radius: 50});
circles.push({x:350, y:350, radius: 50});

// 各円を描画する。
for(const c of circles) {
  context.beginPath();
  context.fillStyle = 'rgb(0, 0, 0)'; // 黒色
  context.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
  context.fill();
}

止まっている円が表示されたでしょうか。canvas要素の基本的な使い方通り、コンテキストを取得して、描画しています。

このとき、ひとつのコツとして、描画データは描画前に全て揃っていることが好ましいです。つまり、描画データを全て準備してから、それらを全部一気に描画するということです。データの準備と描画がごちゃまぜになっているとプログラムの流れがわかりにくくなるので、こうしたほうが見通しが良くなります。

アニメーションに対応させる

次にこの静止画をアニメーションに対応させます。

アニメーションを実現するためには、以下のような手順が必要になります:

  1. 前に描画したものを消す
  2. 描画するものの位置やサイズなどを動かす
  3. 描画する
  4. 1に戻る

これを永遠に繰り返すことにより、アニメーションを実現できます。要は、パラパラ漫画ですね。描画を繰り返すたびに新しいグラフィックが描かれるので、動いているように見えるという原理です。

これをJavaScriptで実装してみましょう。

アニメーションのループ(繰り返し)にはwindow.requestAnimationFrameを使うと楽です。requestAnimationFrameは、一般的には秒間60回(60fps)のアニメーションを実現するための関数です。環境によってはもっと高いフレームレートになるかもしれません。

ループ処理をひとつの関数にまとめ、requestAnimationFrameに、ループさせたい関数を渡します。そしてループ関数の中で、またrequestAnimationFrameを呼び出します。

const WIDTH = 500;
const HEIGHT = 500;

//
// 前準備。
//

// canvas要素を作る。
const canvas = document.createElement('canvas');
canvas.width = WIDTH;
canvas.height = HEIGHT;

// コンテキストを取得しておく。
const context = canvas.getContext('2d');

// body要素に追加する。
document.body.appendChild(canvas);

//
// メイン処理。
//

// 表示する円を管理する配列を作成しておく。
// 少ない場合(1個とか2個とか)はいらないかも。
const circles = [];

// 円を追加する。
// ここでは円はx,y,radius(半径)を持ったオブジェクト。
// 単なるオブジェクトのかわりにCircleクラスを作っても良いかも。
circles.push({x:150, y:150, radius: 50});
circles.push({x:350, y:350, radius: 50});

// ループさせる関数。
// 前の描画を消す→オブジェクトの状態を更新する→オブジェクトを描画する→次のフレームに移る
// の繰り返し。
function loop(timestamp) {
  // 前の描画を消す。
  // 背景色が欲しい場合はかわりにfillRectを使い、
  //    context.fillStyle = 'rgb(255, 0, 0)';
  //    context.fillRect(0, 0, WIDTH, HEIGHT);
  // のようにする。
  context.clearRect(0, 0, WIDTH, HEIGHT);
  
  // 各円の状態を更新する。
  /* 今は特に何もしない */

  // 各円を描画する。
  for(const c of circles) {
    context.beginPath();
    context.fillStyle = 'rgb(0, 0, 0)'; // 黒色
    context.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
    context.fill();
  }
  
  // requestAnimationFrameを呼び出す。
  // requestAnimationFrameは1度の呼び出しで1回しか実行してくれないため
  // 毎回呼び出す必要がある。
  window.requestAnimationFrame((ts) => loop(ts));
}

// requestAnimationFrameを1回だけ呼び出す。
// あとはloop関数の中でrequestAnimationFrameが呼び出され
// その中でloop関数が実行され、そのloop関数の中でrequestAnimationFrameが…
// となるので永遠にアニメーションが続く。
window.requestAnimationFrame((ts) => loop(ts));


これでアニメーションに対応しました!前の描画を消す→オブジェクトを更新する→オブジェクトを描画する→次のフレームに移る、の流れが完成しました。今度も円が表示されたはずです。

しかしまだ動いてはいません。今度は円のパラメータをいじって、本当に動くかどうか確かめてみましょう。

円をアニメーションさせる

先ほどのコードの/* 今は特に何もしない */の部分をいじって、アニメーションさせてみましょう。

radius < 100のときにどんどんradiusを大きくしていって、100を超えたら元に戻るアニメーションを作ってみます。

const WIDTH = 500;
const HEIGHT = 500;

//
// 前準備。
//

// canvas要素を作る。
const canvas = document.createElement('canvas');
canvas.width = WIDTH;
canvas.height = HEIGHT;

// コンテキストを取得しておく。
const context = canvas.getContext('2d');

// body要素に追加する。
document.body.appendChild(canvas);

//
// メイン処理。
//

// 表示する円を管理する配列を作成しておく。
// 少ない場合(1個とか2個とか)はいらないかも。
const circles = [];

// 円を追加する。
// ここでは円はx,y,radius(半径)を持ったオブジェクト。
// 単なるオブジェクトのかわりにCircleクラスを作っても良いかも。
circles.push({x:150, y:150, radius: 50});
circles.push({x:350, y:350, radius: 50});

// ループさせる関数。
// 前の描画を消す→オブジェクトの状態を更新する→オブジェクトを描画する→次のフレームに移る
// の繰り返し。
function loop(timestamp) {
  // 前の描画を消す。
  // 背景色が欲しい場合はかわりにfillRectを使い、
  //    context.fillStyle = 'rgb(255, 0, 0)';
  //    context.fillRect(0, 0, WIDTH, HEIGHT);
  // のようにする。
  context.clearRect(0, 0, WIDTH, HEIGHT);
  
  // 各円の状態を更新する。
  for(const c of circles) {
    if(c.radius < 100) { c.radius ++; } // どんどん大きくなって…
    else { c.radius = 50; } // しぼむ
  }

  // 各円を描画する。
  for(const c of circles) {
    context.beginPath();
    context.fillStyle = 'rgb(0, 0, 0)'; // 黒色
    context.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
    context.fill();
  }
  
  // requestAnimationFrameを呼び出す。
  // requestAnimationFrameは1度の呼び出しで1回しか実行してくれないため
  // 毎回呼び出す必要がある。
  window.requestAnimationFrame((ts) => loop(ts));
}

// requestAnimationFrameを1回だけ呼び出す。
// あとはloop関数の中でrequestAnimationFrameが呼び出され
// その中でloop関数が実行され、そのloop関数の中でrequestAnimationFrameが…
// となるので永遠にアニメーションが続く。
window.requestAnimationFrame((ts) => loop(ts));

動きました!これでアニメーションの完成です。あとは円のパラメータを好きにいじることによって、好みのアニメーションをさせることができます。

図形の形状を変えてみてもいいでしょう。例えば四角形を描画するように変えてみるのも面白いかもしれません。

もっと複雑なアニメーション

ここまでの内容でアニメーションについては全て説明しました。しかし現実にはもっと複雑なアニメーションをさせたいはずです。例えば複数の形状や複数の動きが入り乱れるようなアニメーションです。

これを実現するには様々なアプローチが存在しますが、最も簡単なのはクラスを使うことでしょう。オブジェクトの種類だけクラスを作り、各クラスに更新メソッドと描画メソッドを実装するのです。例えば「四角形クラス」と「円クラス」を用意して、それぞれ別々の描画、動きをさせます。

const WIDTH = 500;
const HEIGHT = 500;

//
// 前準備。
//

// canvas要素を作る。
const canvas = document.createElement('canvas');
canvas.width = WIDTH;
canvas.height = HEIGHT;

// コンテキストを取得しておく。
const context = canvas.getContext('2d');

// body要素に追加する。
document.body.appendChild(canvas);

// 円(Circle)クラスを用意する。
// 円クラスは更新メソッドと描画メソッドを持っている。
class Circle {
  constructor(x, y, radius) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    
    this.velocity = 2; // この速度で膨張・縮小を繰り返す。
  }
  
  // 更新メソッド。
  // これが呼ばれるとオブジェクトの状態を更新する。
  update() {
    // 半径が100より大きいか50より小さかったら膨張速度を反転する。
    if(this.radius > 100 || this.radius < 50) {
      this.velocity = -this.velocity;
    }
    this.radius += this.velocity; // 半径をvelocityだけ増やす。
  }
  
  // 描画メソッド。
  // これが呼ばれるとオブジェクトを描画する。
  render(context) {
    context.beginPath();
    context.fillStyle = 'rgb(255, 0, 0)'; // 赤色
    context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
    context.fill();
  }
}

// 同様に四角形クラス。
class Rectangle {
  constructor(x, y, width, height) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    
    this.velocityX = 2; // この速度で横に移動する。
  }

  update() {
    if(this.x < 0 || this.x > WIDTH) {
      this.velocityX = -this.velocityX;
    }
    this.x += this.velocityX;
  }

  render(context) {
    context.beginPath();
    context.fillStyle = 'rgb(0, 0, 255)'; // 青色
    context.rect(this.x, this.y, this.width, this.height);
    context.fill();
  }
}

//
// メイン処理。
//

// オブジェクトを管理する配列。
const objects = [];

// 円と四角形を1個ずつ追加。
objects.push(new Circle(150, 150, 50));
objects.push(new Rectangle(350, 350, 50, 50));

// ループさせる関数。
function loop(timestamp) {
  // 前の描画を消す。
  context.clearRect(0, 0, WIDTH, HEIGHT);
  
  // 各オブジェクトの状態を更新する。
  objects.forEach((obj) => obj.update());

  // 各オブジェクトを描画する。
  objects.forEach((obj) => obj.render(context));
  
  // requestAnimationFrameを呼び出す。
  window.requestAnimationFrame((ts) => loop(ts));
}

// アニメーションを開始する。
window.requestAnimationFrame((ts) => loop(ts));

これで複雑なアニメーションも実現することができました!

あとは、形状を変えてみたり、クラスを追加してみたり、動きを変えてみたりするといいでしょう。

もっともっと複雑なことをする

実際には「円クラス」の動きが一種類とは限りませんし、「四角形クラス」に関しても同様です。同じ円でも違う動きをさせたいことも多いでしょう。そういったときには、Circleクラスにはupdateメソッドを実装せず、継承先のクラスで実装するとか、「動き」クラスを作って動きオブジェクトを注入するなどといったアプローチが考えられるでしょう。

また、動きに関してもより複雑な動作をさせたいこともあるでしょう。今回は直線的な動きのみを扱いましたが、例えば「JavaScriptの三角関数とcanvasで円運動アニメーションを作る」では三角関数を用いたアニメーションを取り扱っています。

他にも、単なるアニメーションだけではなく、インタラクティブなコンテンツを作ることも考えられます。例えば「JavaScriptで弾幕STGをフルスクラッチで作る その1 ゲームエンジン編」では弾幕シューティングゲームの作り方を題材にしています。

加えて、本格的な3Dグラフィックスを扱うこともできます。「WebGL2入門 基礎編」では3Dグラフィックスを扱うことができるWebGL2について説明しています。

canvas要素は大きな可能性を秘めている要素です。あなたの発想次第で、どんなものでも作れるでしょう。何か思いついたら、なんでも試してみてください。