iA


多摩美 - メディアリテラシー 2010
HTML 5 canvas要素 + Javascriptで作る、動的コンテンツ


HTML 5 + Javascpritで作る動的なコンテンツ

前回の授業では、HTML 5について、その成り立ちや特徴を解説した上で、新しい構造化のための要素について解説しました。また、あわせてCSS3によって格段に進歩した表現力について実際にサンプルページを作りながら解説しました。

今までは、Webページで動的に図や画像やアニメーションを描画するにはFlashが代表的な選択肢でした。しかし、ここ数年でその状況が大きく動きつつあります。Flashに代表されるような動的なコンテンツが、専用のプラグイン無しに、HTMLの要素とそれと連携するスクリプトだけで実現できるようになりつつあるのです。こうした技術は、今後のWebの動向を大きく変える可能性を秘めた技術として脚光を浴びています。

HTML 5で動的に画像やアニメーションを生成するためには、canvas要素という新規に導入された要素と、canvas要素と連動するJavascriptという言語がキーとなります。まず最初にこのcanvas要素とJavascriptの基本についてみていきましょう。

canvas要素とは

canvas要素はスクリプト(一般的に JavaScript)を使って図形を描くために使われる新しい HTML 要素です。canvas要素は元々はMac OS X v10.4の内部でWebKitコンポーネントとして、DashboardウィジェットやSafariでのアプリケーションを強化するために、2004年[1]にアップルが最初に導入した技術です。その後、Mozilla FirefoxやOperaでも採用され、WHATWGで、新しい標準規格として標準化されました。

canvas要素は現在Mozilla Firefox、Google Chrome、Safari、Operaなどの主要なブラウザで実装されいます。しかしながら、Internet Exproloer 8以前のバージョンには対応していません(ただし、IE 9のプレビューリリースには実装済み)。また現状ではブラウザによって同じコードを記述してもその解釈にばらつきがあります。canvas要素を使用する際には、こうした点に留意しながら使用する必要があるでしょう。

canvas要素の書式

canvas要素の書式自身は、img要素とよく似ています。例えば以下のように記述すると、幅400pixel、高さ300pixelの大きさでcanvas要素が初期化されます。

<canvas id="tutorial" width="400" height="300"></canvas>

canvas要素のid属性は、Javascriptなどのスクリプトでどのcanvas要素に対して処理を行うのかを特定する際に必要となります。canvas要素を使用する際には常にid属性を付けてユニークな(一意な)名称を指定すると良いでしょう。この例の場合はtutorialというid名をつけています。

canvasへの描画 – 基本テンプレート

canvas要素とJavascriptを連携させてHTML内で使用するための基本となるテンプレートを下記に示します。

<html>
  <head>
    <title>Canvas tutorial</title>
    <script type="text/javascript">
      function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
          var ctx = canvas.getContext('2d');
        }
      }
    </script>
    <style type="text/css">
      canvas { border: 1px solid #999; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

17行目にあるcanvas要素が図形を描画する幅400pixel高さ300pixelの画面を生成しています。canvas要素にはtutorialというid属性が設定されています。

4〜11行目のscript要素に挟まれた記述が、Javascriptで書かれたcanvas要素に対する処理を行っている部分です。

このJavascriptを注意深く観察すると、全体がdraw()関数で囲まれていることがわかるでしょう (5行目の function draw() 以下)。このdraw()関数は、ページが読みこまれた際に実行されるように設定されています。その設定をしている部分が、16行目のbody要素の属性 onload=”draw();” の部分です。

body関数は、まず document.getElementById(‘tutorial’); という命令を実行した結果を canvas という変数に格納しています。これは、HTMLの要素の中からtutorialというid属性を指定した要素を抽出しています。その結果抽出されたcanvas要素を、canvasという変数に代入しているのです。

7行目から9行目は、実際にcanvas要素の中に図形を描き始めるために、描画機能にアクセスしている部分です。具体的には、getContext という命令を使用して、2D(二次元)の描画コンテクストにアクセスしています。現状では、canvas要素の描画コンテキストは2Dコンテクストしか用意されていません。しかし、近い将来には、3Dコンテクストが使えるようになるかもしれません。

以上の処理によって、所定のcanvas要素内に図形を描く準備が完了しました。

図形を描く 1 – 矩形

まず始めに、矩形(長方形)を描画してみましょう。canvasでは矩形を描くための3つの関数が用意されています。

  • fillRect(x,y,width,height) : 塗られた矩形を描く
  • strokeRect(x,y,width,height) : 矩形の輪郭を描く
  • clearRect(x,y,width,height) : 指定された領域を消去し、完全な透明にする

では、この3つの関数をそれぞれ利用してcanvas要素内に矩形を描いてみましょう。

<html>
  <head>
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            var ctx = canvas.getContext('2d');
            ctx.fillRect(50,50,300,200);
            ctx.clearRect(120,80,200,140);
            ctx.strokeRect(200,20,180,260);
        }
    }
    </script>
    <style type="text/css">
      canvas { border: 1px solid #999; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

図形を描く 2 – パスを描く

canvasでは一つのまとまった図形を描くのは矩形しかありません。他の図形を描くには、一つ以上のパスを組み合わせて作らなくてはなりません。

パスを使用して図形を描くための命令には、次のようなものがあります。

  • beginPath() – パスの開始
  • closePath() – パスを閉じる(始点に向けて直線を描くことで図形を閉じる)
  • stroke() – 線でパスを描く
  • fill() – 塗り潰しでパスを描く
  • moveTo(x, y) – パスの始点を移動する

パスを組み合せて図形を描く際には、まずbeginPath()を呼びだしてパスを開始します。次に必要であれば、moveTo(x, y)でパスの始点を移動します。その後で以下のセクションで紹介する直線を描く命令や、円弧を描く命令、ベジェ曲線を描く命令などを組み合せてパスを組み合せて図形を描いていきます。

パスの最後に始点まで戻って図形を閉じたい場合には、closePath()を用います。

パスの描画の方法は2つの方法があります。stroke()は外枠の線だけを描画します。fill()はパスに囲まれた領域を塗り潰します。

図形を描く 3 – 直線

では、直線を組み合せて図形を描いてみましょう。直線を描くには次の命令を使用します。

  • lineTo(x, y) – 現在の始点の座標から、座標(x, y)に向けて直線を描く

直線を組み合せたパスによる描画のサンプルです。

<html>
  <head>
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            var ctx = canvas.getContext('2d');
            //輪郭線による描画
            ctx.beginPath();
            ctx.moveTo(50,50);
            ctx.lineTo(360,200);
            ctx.lineTo(140,250);
            ctx.closePath();
            ctx.stroke();

            //塗り潰しによる描画
            ctx.beginPath();
            ctx.moveTo(50,250);
            ctx.lineTo(160,20);
            ctx.lineTo(340,50);
            ctx.closePath();
            ctx.fill();
        }
    }
    </script>
    <style type="text/css">
      canvas { border: 1px solid #999; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

図形を描く 4 – 円弧

円弧や円を描くために arc メソッドを使います。arcメソッドの書式は下記の通りです。

  • arc(x, y, radius, startAngle, endAngle, anticlockwise)

arc()は5つの引数をとります。x と y は円の中心です。Radius は半径です。startAngle と endAngle パラメタは円弧の始まりと終点をラジアン角(0°〜360°を、0 〜2 * PIで表現する角度の単位)で定義します。始まりと終わりの角度は x 軸から計算します。anticlockwise パラメタは true の時には円弧を反時計回りに、それ以外は時計回りの方向に描くBool値(trueかfalseか)です。

では円弧を組み合せて図形を描いてみましょう。

<html>
  <head>
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            var ctx = canvas.getContext('2d');
            //円弧1
            ctx.beginPath();
            ctx.arc(200,150,100,0,Math.PI*2,false);
            ctx.stroke();
            //円弧2
            ctx.beginPath();
            ctx.arc(200,150,80,0,Math.PI*1.5,false);
            ctx.stroke();
            //円弧3
            ctx.beginPath();
            ctx.arc(200,150,60,Math.PI*0.25,Math.PI*1.0,true);
            ctx.stroke();
            //円弧4
            ctx.beginPath();
            ctx.arc(200,150,20,0,Math.PI*2.0,true);
            ctx.fill();
        }
    }
    </script>
    <style type="text/css">
      canvas { border: 1px solid #999; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

色の設定

色を図形に適用するためのプロパティは以下の3つです。

  • fillStyle = color – 塗りの色
  • strokeStyle = color – 線の色
  • globalAlpha = transparency value – 透明度

では、半透明の色を設定して塗り潰してみましょう。

<html>
  <head>
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            var ctx = canvas.getContext('2d');
            //全体の透明度
            ctx.globalAlpha = 0.5;
            //円弧1
            ctx.beginPath();
            ctx.fillStyle = '#3399FF';
            ctx.arc(150,150,80,0,Math.PI*2.0,true);
            ctx.fill();
            //円弧2
            ctx.beginPath();
            ctx.fillStyle = '#FF9933';
            ctx.arc(250,150,80,0,Math.PI*2.0,true);
            ctx.fill();
        }
    }
    </script>
    <style type="text/css">
      canvas { border: 1px solid #999; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

くり返し

for文を使用して、くりかえしの処理を行うことも可能です。for文の書式は以下の通りです。

for(《初期化》; 《ループの継続条件》; 《カウンタ変数の更新》){
    《文》
}

例えば、処理を100回くりかえしたい場合は以下のように記述します。

for(i = 0; i < 100; i = i + 1){
    《繰り返す処理の内容》
}

ではfor文による繰り返しを利用して、ランダムに直線を描画していくプログラムを作成してみましょう。

画面内のランダムな場所を始点にして、画面内のランダムな場所まで直線を描く処理を1000回繰り返してみます。直線の線の色もランダムに設定しています。

<html>
  <head>
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    function draw(){
        var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            var ctx = canvas.getContext('2d');
            //全体の透明度
            ctx.globalAlpha = 0.3;
            //1000回処理をくりかえす
            for(i = 0; i < 1000; i++){
                ctx.beginPath();
                //ランダムな色を生成
                var r = Math.floor(Math.random() * 256);
                var g = Math.floor(Math.random() * 256);
                var b = Math.floor(Math.random() * 256);
                ctx.strokeStyle = 'rgb(' + r + ',' + g + ',' + b + ')';
                //ランダムな場所に始点を移動
                ctx.moveTo(Math.random()*400, Math.random()*300);
                //ランダムな場所まで線を描く
                ctx.lineTo(Math.random()*400, Math.random()*300);
                ctx.stroke();
            }
        }
    }
    </script>
    <style type="text/css">
      canvas { border: 1px solid #999; }
    </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

アニメーション

アニメーションをするには一定時間ごとに関数を実行して、状態を更新することでアニメーションを実現します。

指定した時間ごとに関数を実行するには、setInterval()メソッドを使用します。例えば、100ミリ秒ごとに関数update()を実行するためには、下記のように指定します。

setInterval(draw, 100);

では、簡単なアニメーションのサンプルとして、画面の4隅でバウンドしながらうごきまわるパーティクルを作成してみましょう。

<html>
  <head>
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    var speedX = 3.0;
    var speedY = 4.0;
    var locX = 200;
    var locY = 150;
    var ctx;

    function init(){
	var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            ctx = canvas.getContext('2d');
	    setInterval(draw, 33);
	}
    }

    function draw(){
	ctx.globalCompositeOperation = "source-over";
	ctx.fillStyle = "rgba(8,8,12,.2)";
	ctx.fillRect(0, 0, 400, 300);
	ctx.globalCompositeOperation = "lighter";

	//位置を更新
	locX += speedX;
	locY += speedY;
	
	if(locX < 0 || locX > 400){
	    speedX *= -1;
	}

	if(locY < 0 || locY > 300){
	    speedY *= -1;
	}
	
	//更新した座標で円を描く
	ctx.beginPath();
        ctx.fillStyle = '#3399FF';
        ctx.arc(locX, locY, 4, 0, Math.PI*2.0, true);
        ctx.fill();
    }
    </script>
    <style type="text/css">
      canvas { background-color:#000; border: 1px solid #999; }
    </style>
  </head>
  <body onload="init();">
    <canvas id="tutorial" width="400" height="300"></canvas>
  </body>
</html>

実際の動きを確認する

大量の物体をアニメーションする

バウンドするパーティクルのサンプルを発展させて、さらに複雑なアニメーションを作成してみましょう。

パーティクルのパラメータを配列で管理することで、沢山のパーティクルを同時に動かしてみましょう。1つのパーティクルに対して、以下のパラメータを設定してみます。

  • speedX:X軸方向のスピード
  • speedY:Y軸方向のスピード
  • locX:現在のX座標の位置
  • locY:現在のY座標の位置
  • radius:半径
  • r:赤色の成分
  • g:緑色の成分
  • b:青色の成分
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Canvas tutorial template</title>
    <script type="text/javascript">
    const NUM = 500;
    const WIDTH = 640;
    const HEIGHT = 480;
    var speedX = new Array(NUM);
    var speedY = new Array(NUM);
    var locX = new Array(NUM);
    var locY = new Array(NUM);
    var radius = new Array(NUM);
    var r =  new Array(NUM);
    var g =  new Array(NUM);
    var b =  new Array(NUM);
    var ctx;

    function init(){
	var canvas = document.getElementById('tutorial');
        if (canvas.getContext){
            ctx = canvas.getContext('2d');
	    for(var i = 0; i < NUM; i++){
		speedX[i] = Math.random() * 8.0 - 4.0;
		speedY[i] = Math.random() * 8.0 - 4.0;
		locX[i] = WIDTH / 2;
		locY[i] = HEIGHT / 2;
		radius[i] = Math.random() * 8.0 + 1.0;
		r[i] = Math.floor(Math.random() * 64);
		g[i] = Math.floor(Math.random() * 64);
		b[i] = Math.floor(Math.random() * 64);
	    }
	    setInterval(draw, 33);
	}
    }

    function draw(){
	ctx.globalCompositeOperation = "source-over";
	ctx.fillStyle = "rgba(8,8,12,.1)";
	ctx.fillRect(0, 0, WIDTH, HEIGHT);
	ctx.globalCompositeOperation = "lighter";

	for(var i = 0; i < NUM; i++){
	    //位置を更新
	    locX[i] += speedX[i];
	    locY[i] += speedY[i];
	    
	    if(locX[i] < 0 || locX[i] > WIDTH){
		speedX[i] *= -1.0;
	    }

	    if(locY[i] < 0 || locY[i] > HEIGHT){
		speedY[i] *= -1.0;
	    }
	    
	    //更新した座標で円を描く
	    ctx.beginPath();
	    ctx.fillStyle = 'rgb(' + r[i] + ',' + g[i] + ',' + b[i] + ')';
            ctx.arc(locX[i], locY[i], radius[i], 0, Math.PI*2.0, true);
            ctx.fill();
	}
    }
    </script>
    <style type="text/css">
      canvas { background-color:#000; border: 1px solid #999; }
    </style>
  </head>
  <body onload="init();">
    <canvas id="tutorial" width="640" height="480"></canvas>
  </body>
</html>

実際の動きを確認する

さらに高度なアニメーション表現へ

さらにマウスによるインタラクションを加えたサンプルを紹介します。このサンプルは、Daniel Puhe氏によるLiquid Particlesというプログラムをもとにしています。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Canvas tutorial template</title>
    <script type="text/javascript">

/*
 * This example based on Liquid Particles 
 * by Daniel Puhe (http://spielzeugz.de)
 */

var PI_2        = Math.PI * 2;
var canvasW     = 640;
var canvasH     = 480;
var numMovers   = 600;
var friction    = .96;
var movers      = [];

var canvas;
var ctx;
var canvasDiv;
var outerDiv;

var mouseX;
var mouseY;
var mouseVX;
var mouseVY;
var prevMouseX;
var prevMouseY;
var isMouseDown;
    
function init(){
    canvas = document.getElementById("mainCanvas");
    if ( canvas.getContext ){
	setup();
	setInterval( draw , 33 );
    }
}

function setup(){
    outerDiv  = document.getElementById("outer");
    canvasDiv = document.getElementById("canvasContainer");
    ctx       = canvas.getContext("2d");
    
    var i = numMovers;
    while ( i-- ){
	var m = new Mover();
	m.x   = canvasW * 0.5;
	m.y   = canvasH * 0.5;
	m.vX  = Math.cos(i) * Math.random() * 34;
	m.vY  = Math.sin(i) * Math.random() * 34;
	movers[i] = m;
    }
    
    mouseX = prevMouseX = canvasW * 0.5;
    mouseY = prevMouseY = canvasH * 0.5;
    
    document.onmousedown = onDocMouseDown;
    document.onmouseup   = onDocMouseUp;
    document.onmousemove = onDocMouseMove;
}

function draw(){
    ctx.globalCompositeOperation = "source-over";
    ctx.fillStyle = "rgba(8,8,12,.65)";
    ctx.fillRect( 0 , 0 , canvasW , canvasH );
    ctx.globalCompositeOperation = "lighter";
    
    mouseVX    = mouseX - prevMouseX;
    mouseVY    = mouseY - prevMouseY;
    prevMouseX = mouseX;
    prevMouseY = mouseY;
    
    var toDist   = canvasW * 0.86;
    var stirDist = canvasW * 0.125;
    var blowDist = canvasW * 0.5;
    
    var Mrnd = Math.random;
    var Mabs = Math.abs;
    
    var i = numMovers;
    while ( i-- ){
	var m  = movers[i];
	var x  = m.x;
	var y  = m.y;
	var vX = m.vX;
	var vY = m.vY;
	
	var dX = x - mouseX;
	var dY = y - mouseY; 
	var d  = Math.sqrt( dX * dX + dY * dY );
	if( d == 0 ) d = 0.001;
	dX /= d;
	dY /= d;
	
	if ( isMouseDown ){
	    if ( d < blowDist ){
		var blowAcc = ( 1 - ( d / blowDist ) ) * 14;
		vX += dX * blowAcc + 0.5 - Mrnd();
		vY += dY * blowAcc + 0.5 - Mrnd();
	    }
	}
	
	if ( d < toDist ){
	    var toAcc = ( 1 - ( d / toDist ) ) * canvasW * 0.0014;
	    vX -= dX * toAcc;
	    vY -= dY * toAcc;			
	}
	
	if ( d < stirDist ){
	    var mAcc = ( 1 - ( d / stirDist ) ) * canvasW * 0.00026;
	    vX += mouseVX * mAcc;
	    vY += mouseVY * mAcc;			
	}
	
	vX *= friction;
	vY *= friction;
	
	var avgVX = Mabs( vX );
	var avgVY = Mabs( vY );
	var avgV  = ( avgVX + avgVY ) * 0.5;
	
	if( avgVX < .1 ) vX *= Mrnd() * 3;
	if( avgVY < .1 ) vY *= Mrnd() * 3;
	
	var sc = avgV * 0.45;
	sc = Math.max( Math.min( sc , 3.5 ) , 0.4 );
	
	var nextX = x + vX;
	var nextY = y + vY;
	
	if ( nextX > canvasW ){
	    nextX = canvasW;
	    vX *= -1;
	}
	else if ( nextX < 0 ){
	    nextX = 0;
	    vX *= -1;
	}
	
	if ( nextY > canvasH ){
	    nextY = canvasH;
	    vY *= -1;
	}
	else if ( nextY < 0 ){
	    nextY = 0;
	    vY *= -1;
	}
	
	m.vX = vX;
	m.vY = vY;
	m.x  = nextX;
	m.y  = nextY;
	
	ctx.fillStyle = m.color;
	ctx.beginPath();
	ctx.arc( nextX , nextY , sc , 0 , PI_2 , true );
	ctx.closePath();
	ctx.fill();		
    }
}


function onDocMouseMove( e ){
    var ev = e ? e : window.event;
    mouseX = ev.clientX - outerDiv.offsetLeft - canvasDiv.offsetLeft;
    mouseY = ev.clientY - outerDiv.offsetTop  - canvasDiv.offsetTop;
}

function onDocMouseDown( e ){
    isMouseDown = true;
    return false;
}

function onDocMouseUp( e ){
    isMouseDown = false;
    return false;
}


function Mover(){
    this.color = "rgb(" + Math.floor( Math.random()*255 ) + "," + Math.floor( Math.random()*255 ) + "," + Math.floor( Math.random()*255 ) + ")";
    this.y     = 0;
    this.x     = 0;
    this.vX    = 0;
    this.vY    = 0;
    this.size  = 1; 
}


function rect( context , x , y , w , h ){
    context.beginPath();
    context.rect( x , y , w , h );
    context.closePath();
    context.fill();
}


function trace( str ){
    document.getElementById("output").innerHTML = str;
}

    </script>
    <style type="text/css">
      canvas { background-color:#000; border: 1px solid #999; }
    </style>
  </head>
  <body onload="init();">
    <div id="outer">
      <div id="canvasContainer">
	<canvas id="mainCanvas" width="640" height="480"></canvas>
	<div id="output"></div>
      </div>
    </div>
  </body>
</html>

実際の動きを確認する