canvas に図形を描く

canvas の環境をセットアップしましたので、canvas に描画する方法を詳しく見ていくことができます。この記事を読み終わると矩形、三角形、線、円弧、曲線を描く方法を学び、基本的な図形について理解できます。canvas にオブジェクトを描く際はパスを扱うことが不可欠ですので、その方法を見ていきます。

グリッド

描き始める前に、canvas のグリッドもしくは 座標空間 について話す必要があります。前のページの HTML テンプレートは幅 150 ピクセル、高さ 150 ピクセルの canvas 要素を持っていました。私はこの画像をデフォルトのグリッドに上書きして描きました。普通 グリッド上の 1 単位は canvas 上の 1 ピクセルに相当します。このグリッドの原点は左上の角 ( 座標 (0,0) ) に位置します。全ての要素がこの原点から相対的に配置されます。よって青い正方形の左上の場所は左から x ピクセル、上から y ピクセル (座標 (x,y) ) に来ます。このチュートリアルの後半で原点を他の位置へずらす方法、グリッドを回転したり、伸縮したりさえする方法を見ることになります。今はデフォルトで我慢しましょう。

矩形を描く

SVG とは異なり、<canvas> は 1 つの原始図形「矩形」のみをサポートしています。他の全ての図形は 1 つ以上のパスと呼ばれる、線で結ばれる点のリストを組み合わせて作らなくてはなりません。幸いなことに、とても複雑な図形を作ることが可能なパスを描く関数のコレクションがあります。

最初に矩形を見ていきましょう。canvas に矩形を描く 3 つの関数があります:

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

3 つの関数は同じパラメータをとります。xy は矩形の左上の角の canvas 上での位置 (原点から相対的) を指定します。widthheight は矩形のサイズを指定します。

下は、前のページの draw() 関数ですが、私は 3 つの関数を追加しました。

矩形の例

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    ctx.fillRect(25, 25, 100, 100);
    ctx.clearRect(45, 45, 60, 60);
    ctx.strokeRect(50, 50, 50, 50);
  }
}

結果は以下のように見えるはずです。

ScreenshotLive sample

fillRect() 関数は 100x100 ピクセルの大きな黒色正方形を描きます。clearRect() 関数は中心から 60x60 ピクセルの正方形を取り除き、最後に strokeRect() が消去された正方形の中に 50x50 ピクセルの矩形の輪郭を描きます。

後のページで clearRect() の代わりのメソッドを 2 つ見て、描く図形の色と輪郭のスタイルを変更する方法を見ます。

次の節でみるパス関数とことなり、全ての 3 つの矩形関数は直ちに canvas に描きます。

パスを描く

ほかの原始図形は、パスだけです。パスは点のリストであり、それらは曲線か否かといった形状、およびさまざまな幅や色を設定可能な線分で結ばれます。パスやサブパスは、閉じることができます。パスを使って図形を描くには、 いくつかの余分な作業が必要です。

  1. 始めに、パスを作成します。
  2. 次に、パスへ描画するために描画コマンドを使用します。
  3. そして、パスを閉じます。
  4. パスが作成されたら、描画するための stroke または fill を実行できます。

これらのステップで使用する関数を以下に示します:

beginPath()
新しいパスを作成します。パスを作成すると以降の描画コマンドは、パスを構築するために直接作用します。
パスのメソッド
オブジェクトのためにさまざまなパスを設定するメソッド群です。
closePath()
描画コマンドが再びコンテキストに送られるように、パスを閉じます。
stroke()
輪郭をなぞる方式で、図形を描きます。
fill()
パスの内部エリアを塗りつぶして、単色の図形を描きます。

パスを作る最初の作業は beginPath() メソッドを呼び出すことです。内部では、パスは図形を一緒に作るサブパス (線、円弧など) のリストとして保存されます。このメソッドが呼び出される毎に、リストは再初期化され新しい図形を始めることができます。

注記: beginPath() を呼び出した直後や canvas を新規作成した直後など、現在のパスが空であるときに最初にパスを構築するコマンドは、実際は何であるかにかかわらず常に moveTo() として扱われます。このためパスをリセットした後はほぼ必ず、開始位置を明示することが必要になるでしょう。

2 番目の作業は描かれる実際のパスを定義するメソッドを呼び出すことです。それらを簡単にみていくことにしましょう。

3 番目は任意の作業ですが、closePath() メソッドを呼び出すことです。このメソッドは現在の点から始点に向けて直線を描くことで図形を閉じようとします。もし図形がすでに閉じられているかリストに点がひとつしかない場合はこの関数は何もしません。

注記: fill() メソッドが呼ばれるときはどんな開いている図形は自動的に閉じられ、closePath() メソッドを使う必要はありません。これは、stroke() を呼び出すときはあてはまりません

三角形の描画

単純な図形 (三角形) を描くコードはこのようになります。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    ctx.beginPath();
    ctx.moveTo(75,50);
    ctx.lineTo(100,75);
    ctx.lineTo(100,25);
    ctx.fill();
  }
}

表示結果は以下の様になります。

ScreenshotLive sample

ペンの移動

とても役に立つ関数である moveTo() 関数自体は何も描画しませんが、上述のパスリストの一部です。あなたはこれを 1 枚の紙の上の 1 つの場所からペンか鉛筆を持ち上げてそれを次に置くと考えることができるでしょう。

moveTo(x, y)
xy で指定した座標に、ペンを移動します。

canvas が初期化されるか beginPath() メソッドが呼ばれると、ほとんどの場合 moveTo() メソッドを始点を他の場所に置くために使います。moveTo() メソッドを繋がっていないパスを描くために使うこともできます。下のスマイリーを見てください。

これをあなた自身で試すには、以下のコードを使うことができます。さきほど見た draw() 関数に貼り付けるだけです。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    ctx.beginPath();
    ctx.arc(75,75,50,0,Math.PI*2,true); // 外の円
    ctx.moveTo(110,75);
    ctx.arc(75,75,35,0,Math.PI,false);  // 口 (時計回り)
    ctx.moveTo(65,65);
    ctx.arc(60,65,5,0,Math.PI*2,true);  // 左目
    ctx.moveTo(95,65);
    ctx.arc(90,65,5,0,Math.PI*2,true);  // 右目
    ctx.stroke();
  }
}

表示結果は以下の様になります。

ScreenshotLive sample

繋がっている線を見るには moveTo() メソッドを取り除いてください。

注記: arc() 関数とそのパラメータの解説は 円弧 の章をご覧下さい。

直線を描くには lineTo() メソッドを使います。

lineTo(x, y)
現在の描画位置から xy で指定した位置に、線を描きます。

このメソッドは 2 つの引数 xy を取ります。それらは線の終点の座標です。始点は前回のパスに依存します。前回のパスの終点が始点になる、など。始点は moveTo() メソッドを使って変更することもできます。

次の例では 2 つの三角形が描かれています。 1 つは塗られてもう 1 つは輪郭線が描かれています。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    // Filled triangle
    ctx.beginPath();
    ctx.moveTo(25,25);
    ctx.lineTo(105,25);
    ctx.lineTo(25,105);
    ctx.fill();

    // Stroked triangle
    ctx.beginPath();
    ctx.moveTo(125,125);
    ctx.lineTo(125,45);
    ctx.lineTo(45,125);
    ctx.closePath();
    ctx.stroke();
  }
}

最初に新しい図形のパスを始めるために beginPath() メソッドが呼ばれています。次に 始点を任意の位置に移動するために moveTo() メソッドが呼ばれています。三角形の 2 つの側面を作る 2 つの線が描かれています。

ScreenshotLive sample

あなたは塗られた三角形と輪郭線の描かれたものとの違いに気がつくでしょう。上で述べたように、これはパスが塗られると図形は自動的に閉じられるからです。輪郭の描かれた三角形で closePath() を行わないと 2 つの線しか描かれず、三角形は完成しません。

円弧

円弧や円を描くために arc() または arcTo() メソッドを使います。

arc(x, y, radius, startAngle, endAngle, anticlockwise)
(x, y) を中心の位置、r を半径、startAngle を開始位置、endAngle を終端、anticlockwise を方向 (デフォルトは時計回り) とする円弧を描きます。
arcTo(x1, y1, x2, y2, radius)
指定した制御点と半径によって円弧を描き、その前の描画位置と直線で接続します。

arc メソッドを詳しく見ていきましょう。このメソッドは 6 つのパラメタをとります。xy は、円弧を描く円の中心座標です。radius は自明です。startAngleendAngle パラメタは円弧の始まりと終点をラジアンで定義します。始まりと終わりの角度は x 軸から計算します。anticlockwise パラメタは true の時には円弧を反時計回りに、それ以外は時計回りの方向に描くブーリアン値です。

注記: arc 関数の角度は度ではなく、ラジアンで計算されます。度からラジアンに変換するにはあなたは以下の JavaScript 式を使うことができます : radians = (Math.PI/180)*degrees

以下の例は上で見てきた例よりすこし複雑です。私は全て異なる角度と塗りを持った 12 の異なる円弧を描きます。

2 つの for ループは円の行と列のループです。全ての円弧毎に私は beginPath() を使って新しいパスを始めました。次に何が行われているか読みやすくするために全てのパラメタを変数として書きました。普通これは一文になったでしょう。

xy 座標は充分明確です。radiusstartAngle は固定です。endAngle は最初の列が 180 度 (半円) から始まって、最後の列で完全な円を作るように 90 度ずつ増加します。

clockwise パラメタの文は最初と 3 番目の列では時計回りの円弧として 2 番目と 4 番目の列では反時計回りの円弧という結果になります。最後に、if 文は上半分は輪郭を描画された円弧を、下半分は塗られた円弧を作ります。

注記: この例では、ほかの例より若干大きなサイズである 150 x 200 ピクセルの canvas が必要です。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    for(var i=0;i<4;i++){
      for(var j=0;j<3;j++){
        ctx.beginPath();
        var x = 25+j*50; // x 座標
        var y = 25+i*50; // y 座標
        var radius = 20; // 円弧の半径
        var startAngle = 0; // 円孤の始点
        var endAngle = Math.PI+(Math.PI*j)/2; // 円孤の終点
        var anticlockwise = i%2==0 ?false : true; // 時計回りまたは反時計回り

        ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

        if (i>1){
          ctx.fill();
        } else {
          ctx.stroke();
        }
      }
    }
  }
}
ScreenshotLive sample

ベジェと二次曲線

次に見ていく種類のパスはベジェ曲線です。三次および二次の種類で利用可能です。通常複雑な自然の図形を描くのに使われます。

quadraticCurveTo(cp1x, cp1y, x, y)
現在のペンの位置から x および y で指定した終端へ、cp1x および cp1y で指定した制御点を使用して二次ベジェ曲線を描きます。
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
現在のペンの位置から x および y で指定した終端へ、(cp1x, cp1y) および (cp2x, cp2y) で指定した制御点を使用して三次ベジェ曲線を描きます。

これらの違いは右の画像を使うことで説明することができます。二次ベジェ曲線は始点と終点 (青い点) と 1 つの制御点 (赤い点) を持つのに対して、三次ベジェ曲線は 2 つの制御点を持ちます。

それらのメソッドの両方の xy パラメータは終点の座標です。cp1xcp1y は最初の制御点、cp2xcp2y は 2 番目の制御点の座標です。

Adobe Illustrator のようなベクタードローイングソフトとは違い、何をやっているのかの直接の視覚的フィードバックが得られないので、二次および三次ベジェ曲線を使うことはとても挑戦的です。このことは複雑な図形を描くことをとても難しくします。以下の例で、いくつかの単純で基本的な図形を描きます、しかしもしあなたに時間と特に忍耐があればはるかに複雑な図形を作ることができます。

これらの例で非常に難しいものは何もありません。 どちらの場合も最終的に完全な図形をもたらす描かれたカーブの連続を見ます。

二次ベジェ曲線

この例では、吹き出しをレンダリングするために複数の二次ベジェ曲線を使用しています。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    // 二次曲線の例
    ctx.beginPath();
    ctx.moveTo(75,25);
    ctx.quadraticCurveTo(25,25,25,62.5);
    ctx.quadraticCurveTo(25,100,50,100);
    ctx.quadraticCurveTo(50,120,30,125);
    ctx.quadraticCurveTo(60,120,65,100);
    ctx.quadraticCurveTo(125,100,125,62.5);
    ctx.quadraticCurveTo(125,25,75,25);
    ctx.stroke();
  }
}
ScreenshotLive sample

三次ベジェ曲線

この例では、三次ベジェ曲線を使ってハートを描画します。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    // 三次ベジェ曲線の例
    ctx.beginPath();
    ctx.moveTo(75,40);
    ctx.bezierCurveTo(75,37,70,25,50,25);
    ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
    ctx.bezierCurveTo(20,80,40,102,75,120);
    ctx.bezierCurveTo(110,102,130,80,130,62.5);
    ctx.bezierCurveTo(130,62.5,130,25,100,25);
    ctx.bezierCurveTo(85,25,75,37,75,40);
    ctx.fill();
  }
}
ScreenshotLive sample

矩形

canvas に直接矩形を描く例 (矩形を描く) で見た 3 つのメソッドのほかに、パスリストに矩形を追加する rect() メソッドがあります。

rect(x, y, width, height)
(x, y) で指定した位置を左上の角にして、width および height で指定した幅および高さの矩形を描きます。

このメソッドが実行されると、パラメタに (0,0) を持った moveTo() メソッドが自動的に呼ばれます。すなわち、始点が標準の位置に置かれます。

組み合わせ

このページの全ての例で私は図形につき一種類のパス関数のみを使ってきました。しかし、図形を作るのに使用できるパスの種類の制限は一切ありません。そこで、この最後の例で私は非常に有名なゲームのキャラクタを作るために全てのパス関数を組み合わせることに挑戦しました。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    roundedRect(ctx,12,12,150,150,15);
    roundedRect(ctx,19,19,150,150,9);
    roundedRect(ctx,53,53,49,33,10);
    roundedRect(ctx,53,119,49,16,6);
    roundedRect(ctx,135,53,49,33,10);
    roundedRect(ctx,135,119,25,49,10);

    ctx.beginPath();
    ctx.arc(37,37,13,Math.PI/7,-Math.PI/7,false);
    ctx.lineTo(31,37);
    ctx.fill();

    for(var i=0;i<8;i++){
      ctx.fillRect(51+i*16,35,4,4);
    }

    for(i=0;i<6;i++){
      ctx.fillRect(115,51+i*16,4,4);
    }

    for(i=0;i<8;i++){
      ctx.fillRect(51+i*16,99,4,4);
    }

    ctx.beginPath();
    ctx.moveTo(83,116);
    ctx.lineTo(83,102);
    ctx.bezierCurveTo(83,94,89,88,97,88);
    ctx.bezierCurveTo(105,88,111,94,111,102);
    ctx.lineTo(111,116);
    ctx.lineTo(106.333,111.333);
    ctx.lineTo(101.666,116);
    ctx.lineTo(97,111.333);
    ctx.lineTo(92.333,116);
    ctx.lineTo(87.666,111.333);
    ctx.lineTo(83,116);
    ctx.fill();

    ctx.fillStyle = "white";
    ctx.beginPath();
    ctx.moveTo(91,96);
    ctx.bezierCurveTo(88,96,87,99,87,101);
    ctx.bezierCurveTo(87,103,88,106,91,106);
    ctx.bezierCurveTo(94,106,95,103,95,101);
    ctx.bezierCurveTo(95,99,94,96,91,96);
    ctx.moveTo(103,96);
    ctx.bezierCurveTo(100,96,99,99,99,101);
    ctx.bezierCurveTo(99,103,100,106,103,106);
    ctx.bezierCurveTo(106,106,107,103,107,101);
    ctx.bezierCurveTo(107,99,106,96,103,96);
    ctx.fill();

    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.arc(101,102,2,0,Math.PI*2,true);
    ctx.fill();

    ctx.beginPath();
    ctx.arc(89,102,2,0,Math.PI*2,true);
    ctx.fill();
  }
}

// 角丸の四角形を描画するためのユーティリティ関数

function roundedRect(ctx,x,y,width,height,radius){
  ctx.beginPath();
  ctx.moveTo(x,y+radius);
  ctx.lineTo(x,y+height-radius);
  ctx.arcTo(x,y+height,x+radius,y+height,radius);
  ctx.lineTo(x+width-radius,y+height);
  ctx.arcTo(x+width,y+height,x+width,y+height-radius,radius);
  ctx.lineTo(x+width,y+radius);
  ctx.arcTo(x+width,y,x+width-radius,y,radius);
  ctx.lineTo(x+radius,y);
  ctx.arcTo(x,y,x,y+radius,radius);
  ctx.stroke();
}

以下の様な表示結果となります。

ScreenshotLive sample

これらは非常に簡単な例ですので、詳細は割愛します。ポイントは fillStyle を使用している点と、独自関数 roundedRect() を定義している点です。この様に繰り返し利用する可能性のある処理を関数化しておくと、コード量を低減する事ができます。

fillStyle の詳細についてはこのチュートリアルの後半で説明します。プロパティで、塗りの色を初期値の黒から白に、そしてもう一度黒に変更しています。

Path2D オブジェクト

最後の例で見たように、オブジェクトを描くための一連のパスや描画コマンドを、canvas 状に置くことができます。コードをシンプルにしてパフォーマンスを向上させるために最近のバージョンのブラウザは、描画コマンドをキャッシュあるいは保存することを可能にする Path2D オブジェクトをサポートしています。これにより、パスをすばやく再実行できます。Path2D オブジェクトの構築方法を見てきましょう:

Path2D()
Path2D() コンストラクタは、新たにインスタンス化した Path2D オブジェクトを返します。任意で別のパス (コピーを作成)、あるいは SVG パスデータを構成する文字列を引数に指定できます。
new Path2D();     // 空のパスオブジェクトを作成する
new Path2D(path); // 別の Path2D オブジェクトを複製する
new Path2D(d);    // SVG パスデータからパスを作成する

これまで見てきた moveTorectarcquadraticCurveTo など、あらゆるパスメソッドPath2D オブジェクトで使用できます。

また Path2D API には、パスを結合するための addPath メソッドが追加されています。これは、複数の部品を組み合わせてオブジェクトを構築したい場合などに役立ちます。

Path2D.addPath(path [, transform])
変換行列 (任意指定) とともに、パスを追加します。

Path2D の例

この例では、矩形と円を作成します。どちらも Path2D オブジェクトとして保存しており、後で使用することができます。新たな Path2D API に合わせて、いくつかのメソッドが現在のパスに代わり任意で Path2D を受け入れられるように更新されました。ここでは、canvas に両方のオブジェクトを描くための path 引数として stroke および fill を使用しています。

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    var rectangle = new Path2D();
    rectangle.rect(10, 10, 50, 50);

    var circle = new Path2D();
    circle.moveTo(125, 35);
    circle.arc(100, 35, 25, 0, 2 * Math.PI);

    ctx.stroke(rectangle);
    ctx.fill(circle);
  }
}

ScreenshotLive sample

SVG パスを使用する

canvas の新たな Path2D API の、もうひとつの強力な機能が、canvas でパスを初期化するために SVG パスデータを使用できることです。これにより、SVG と canvas の両方でパスデータを再使用することができるでしょう。

パスは (M10 10) の位置に移動して、そこから右へ水平に 80 ポイント移動 (h 80)、下へ 80 ポイント移動 (v 80)、80 左へ移動 (h -80)、そして始点へ戻ります (z)。この例は Path2D コンストラクタのページで確認できます。

var p = new Path2D("M10 10 h 80 v 80 h -80 Z");

ドキュメントのタグと貢献者

 このページの貢献者: yyss, ethertank
 最終更新者: yyss,