Example 2 – Canvasアニメーションの基礎

Example 2では、Canvasを使って簡単なアニメーションを行うまでを解説します。次の分子動力学アニメーションへの接続性を保つため、閉じた箱の中における粒子(小さなボール)の運動を、この章のターゲットとしましょう。

Canvasグラフィックス

example-2-1

まず、Canvasグラフィックのための準備をします。まず、四角い枠の中に小さいボールを置くだけのプログラムです。body部から見てみましょう。

やることは、canvas要素を用意するだけです。div要素の入れ子にしておくと、何かと便利です。id属性は、javascriptからcanvasオブジェクトを取得するために必要です。さらに、幅と高さを指定するために、width/height属性を定義します。もしCSSのwidth/heightプロパティでサイズを指定すると、物理サイズだけが指定されます。論理サイズはデフォルトの(300,150)になるため、物理サイズと論理サイズの乖離が起きてしまいます。

<div>
	<canvas id='screen' width='200' height='200'></canvas>
</div>

javascript部分を以下に示します。基本になるのは、2次元描画コンテキストです。canvasオブジェクトのgetContext()メソッドを使って取得します(10行目)。getContext()の引数には2dしか使えません。

function drawBall(canvasId) {
	//canvasオブジェクトを取得する
	var canvas = document.getElementById(canvasId);

	//canvasのサイズを取得する
	var wx = canvas.width;
	var wy = canvas.height;

	//描画コンテキスト(基本オブジェクト)の作成
	var context = canvas.getContext('2d');

	//ボール(粒子)の位置座標
	var x = wx/2;
	var y = wy/2;

	//画面を初期化
	context.fillStyle = 'rgb(0,24,72)';
	context.fillRect(0,0,wx,wy);

	//ボール(粒子)を描く
	context.beginPath();
	context.arc(x,y,5,0,2*Math.PI,true);
	context.fillStyle = 'yellow';
	context.fill();
};
window.onload = function() {
	drawBall('screen');
};

17行目から24行目までが、描画命令です。2次元描画コンテキストには、多くの描画命令が定義されていますが、基本概念はここで示した6行で表現されます。

17行目では塗りつぶしスタイル(この場合は塗りつぶし色)を設定し、18行目では、そのスタイルを使って矩形領域を塗りつぶします。

21から24行目には注意が必要です。22行目はarc()メソッドを使って円弧を「描いて」います。しかし正確には、本当に描画しているのではなく、円弧のパスを定義しているだけです。実際の描画は24行目のfill()メソッドで行われます。

このように、2次元描画コンテキストの描画メソッドには、

  1. パスを定義するだけで、描画しないもの(arc(),rectangle()など)
  2. 定義されたパスを使って、実際に描画を行うもの(fill(),stroke()など)
  3. パスには関係なく、直接に描画するもの(fillRect(),strokeRect()など)

の3種類があります。少しややこしい所です。

21行目のbeginPath()メソッドは、パスを初期化するためです。今の段階では、arc()メソッドの直前にbeginPath()が必要である、と記憶しておいてください。

アニメーション

example-2-2

次に、小さいボールを動かすプログラムです(但し、ボールはすぐに枠の外へ飛び去ってしまいます)。ここで行うアニメーションの原理は、非常に簡単です。

「消しては、描く」を繰り返す

アニメーションに必要な命令が、setInterval()です。定期的に与えられた関数を実行します。setInterval()には、2個または3個の引数を与えます。第1引数は呼ばれる関数(callback関数)、第2引数は呼び出し間隔(ミリ秒で与える)です。

引数3個のバージョンでは、第3引数は任意の変数です。この変数はcallback関数に引数として与えられます。この引数は後述のように、描画のために定義したユーザオブジェクトのインスタンスである場合が多いでしょう。

次にボールアニメーションのjavascript部分を示します。ユーザオブジェクト(SimpleMotion)のメンバstart()の中で、setIntarval()を呼んでいます。callback()関数は、35-47行で定義される関数リテラルです。setIntarval()の第3引数がSimpleMotionの自分を表すthisインスタンスであることに注意してください。

一般に、アニメーションを行うためには多数のパラメータが必要です。これらを整理するためには、プロトタイプオブジェクトを作成して、thisインスタンスをcallback関数に渡すのが、良いアイデアです。

function SimpleMotion(canvasId) {
	//canvasオブジェクトを取得する
	var canvas = document.getElementById(canvasId);

	//canvasのサイズを取得する
	this.wx = canvas.width;
	this.wy = canvas.height;

	//描画コンテキスト(基本オブジェクト)の作成
	this.context = canvas.getContext("2d");

	//定数の定義
	this.twopi = 2*Math.PI;

	//描画時間間隔
	this.dt = 1/60;

	//粒子のパラメータ(粒子の色や半径など)
	var v0 = 200;
	var angle = 2/3*Math.PI;
	this.r = 5;
	this.c  = 'yellow';
	this.cb = 'rgba(0,24,72,0.1)';

	//粒子の初期状態
	this.x = this.wx/2;  //位置のx成分
	this.y = this.wy/2;  //位置のy成分
	this.vx = v0*Math.cos(angle); //速度のx成分
	this.vy = v0*Math.sin(angle); //速度のy成分
};

//描画を開始する
SimpleMotion.prototype.start = function() {
	this.interval = setInterval(function(t){
		//速度×時間間隔だけ移動
		t.x += t.vx*t.dt; 
		t.y += t.vy*t.dt;

		//各ステップ毎に画面を初期化
		t.context.fillStyle = t.cb;
		t.context.fillRect(0,0,t.wx,t.wy);

		//粒子を描く
		t.context.beginPath();
		t.context.arc(t.x,t.y,t.r,0,t.twopi,true);
		t.context.fillStyle = t.c;
		t.context.fill();
	},this.dt*1000,this);
};

//ユーザオブジェクトを作成して、アニメーションを開始する
window.onload = function() {
	 new SimpleMotion('screen').start();
}

描画間隔が1/60秒=16.7ミリ秒で与えられていることに注意してください。人間の目の特性から、

アニメーションの描画間隔は、15〜20ミリ秒が下限

でしょう。これ以上速くしても、人間の目が追随できないため、アニメーション効果には影響ありません。

位置と速度

このプログラムには、「位置と速度」が導入されています。アニメーションでは、描画から次の描画の間に、物体の位置が

速度×描画間隔

の分だけ変わります。これを表しているのが、上記プログラムの36,37行です。「位置と速度」は、アニメーションの基本です。

ここで登場しているのは1個の小さいボールですが、大きな物体も、粒子が寄り集まったものと考えれば、必ず「位置と速度」という概念があります。その意味でアニメーションは、Example 3 以降で説明するニュートン力学の概念と切り離せない関係があります。

ボールを枠内に閉じ込める

example-2-3

上のプログラムでは、ボールはすぐに枠の外に飛び出します。これを避けるために、ボールを壁で跳ね返らせることにします。「壁の跳ね返り」は、良く使われるテクニックです。そのためには、上の35行に次のようなコードを挿入します。

最終的に得られるのがこのプログラムです。

SimpleMotion.prototype.start = function() {
	this.interval = setInterval(function(t){
	 	//上下左右の壁で跳ね返る
		if (t.x <= t.r && t.vx < 0) {
			t.vx = -t.vx;
			t.x  = t.r;
		} else if (t.x >= t.wx - t.r && t.vx > 0) {
			t.vx = -t.vx;
			t.x  = t.wx - t.r;
		}
		if (t.y <= t.r && t.vy < 0) {
			t.vy = -t.vy;
			t.y  = t.r;
		} else if (t.y >= t.wy - t.r && t.vy > 0) {
			t.vy = -t.vy;
			t.y  = t.wy - t.r;
		}

		//速度×時間間隔だけ移動
		t.x += t.vx*t.dt; 
		t.y += t.vy*t.dt;

		//各ステップ毎に画面を初期化
		t.context.fillStyle = t.cb;
		t.context.fillRect(0,0,t.wx,t.wy);

		//粒子を描く
		t.context.beginPath();
		t.context.arc(t.x,t.y,t.r,0,t.twopi,true);
		t.context.fillStyle = t.c;
		t.context.fill();
	},this.dt*1000,this);
};