Visit Mozilla.org

Canvas tutorial:Drawing shapes

出典: MDC


目次

[編集] グリッド

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

[編集] 図形を描く

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

[編集] 矩形

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

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

三つの関数は同じパラメタをとります。xy は 矩形の左上の角の canvas 上での位置(原点から相対的)を指定します。 widthheight は非常に明解です。 動作中のこれらの関数を見てみましょう。

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

[編集] 矩形の例

この例を見る

function draw(){
  var canvas = document.getElementById('tutorial');
  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);
  }
}

結果は右の画像のように見えるはずです。fillRect 関数は100x100 ピクセルの大きな黒色正方形を描きます。clearRect 関数は中心から 60x60 ピクセルの正方形を取り除き、最後に strokeRect が消去された正方形の中に 50x50 ピクセルの矩形の輪郭を描きます。 後のページで clearRect の代わりのメソッドを見て、描く図形の色と輪郭のスタイルを変更する方法を見ます。

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

[編集] パスを描く

パスを使って図形を描くには、二つの余分な作業が必要です。

beginPath()
closePath()
stroke()
fill()

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

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

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

最後の作業は stroke と/または fill メソッドを呼ぶことでしょう。これらのうちの一つを呼ぶことは実際に canvas に図形を描きます。 stroke は輪郭の描かれた図形を描き、一方で fill は単色の図形を描きます。

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

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

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

[編集] moveTo

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

moveTo(x, y)

moveTo 関数は二つの引数 xy をとります。それらは新しい始点の座標です。

canvas が初期化されるか beginPath メソッドが呼ばれると始点は座標(0,0)に設定されます。ほとんどの場合 moveTo メソッドを始点を他の場所に置くために使います。moveTo メソッドを繋がっていないパスを描くために使うこともできます。右のスマイリーを見てください。私は moveTo メソッドを使った場所をマークしました(赤い線)

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

[編集] moveTo

この例を見る

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();

注意: 繋がっている線を見るには moveTo メソッドを取り除いてください。
注意: arc 関数とそのパラメタの解説は下を見てください。

[編集]

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

lineTo(x, y)

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

[編集] lineTo

下の例では二つの三角形が描かれています。一つは塗られてもう一つは輪郭線が描かれています。(その結果は右の画像で見ることができます。) 最初に新しい図形のパスを始めるために beginPath メソッドが呼ばれています。次に 始点を任意の位置に移動するために moveTo メソッドが呼ばれています。三角形の二つの側面を作る二つの線が描かれています。

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

この例を見る

// 塗られた三角形
ctx.beginPath();
ctx.moveTo(25,25);
ctx.lineTo(105,25);
ctx.lineTo(25,105);
ctx.fill();

// 輪郭の描かれた三角形
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke();

[編集] 円弧

円弧や円を描くために arc メソッドを使います。仕様書は arcTo メソッドも述べています。これは Safari はサポートしていますが、現在の Gecko ブラウザは実装していません。

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

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

警告: Firefox ベータビルドでは最後のパラメタは clockwise です。最終リリースは上記のようにサポートするでしょう。現在の形のこのメソッドを使っている全てのスクリプトは最終バージョンが出ると更新する非通用があるでしょう。

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

[編集] arc の例

以下の例は上で見てきた例よりすこし複雑です。私は全て異なる角度と塗りを持った 12 の異なる円弧を描きます。もし私が上のスマイリーのようにこの例を書いたなら、非常に長い文のリストになったでしょう、第二に円弧を描くとき私は全ての出発点を知る必要があるでしょう。ここで使ったような 90、180、270 度の円弧にはこれは問題にならないでしょう、しかしより複雑なものにはこの方法は難しいすぎるでしょう。

二つの for ループは円の行と列のループです。全ての円弧毎に私は beginPath を使って新しいパスを始めました。次に何が行われているか読みやすくするために全てのパラメタを変数として書きました。普通これは一文になったでしょう。 xy 座標は十分明確です。 radiusstartAngle は固定です。 endAngle は 180 度(最初の列)から始まって 90 度ずつ完全な円(最後の行)を作るように増加します。 clockwise パラメタの文は最初と 3 番目の列では時計回りの円弧として 2 番目と 4 番目の列では反時計回りの円弧という結果になります。最後に、if 文は上半分は輪郭を描画された円弧に下半分は塗られた円弧を作ります。

この例を見る

for (i=0;i<4;i++){
  for(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, clockwise);

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

[編集] ベジェと二次曲線

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


quadraticCurveTo(cp1x, cp1y, x, y) // Firefox 1.5 では壊れています (下の回避方法を見てください)
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

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

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

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

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

[編集] quadraticCurveTo の例

この例を見る

// 二次曲線の例
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();

ただ一つの二次ベジェ制御点から両方の三次ベジェ制御点を正しく計算することによってどんな二次ベジェ曲線でも三次ベジェ曲線に変換するのは可能です、逆は真ではありませんが。二次のベジェ曲線への三次ベジェ曲線の正確な変換は三次の用語が 0 のときのみ可能です。より一般には複数の二次ベジェ曲線を使って三次ベジェ曲線を近似するために下位区分方法が使われます。

[編集] bezierCurveTo の例

この例を見る

// ベジェ曲線の例
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();

[編集] Firefox 1.5 の quadraticCurveTo() バグの回避法

quadraticCurveTo() の Firefox 1.5 の実装にはバグがあります。二次曲線ではなく bezierCurveTo() が呼ばれたのと同じく一つの二次制御点 (x,y)座標 を二度繰り返します。これにより quadraticCurveTo() は正しくない結果を引き起こします。もしあなたが quadraticCurveTo() を使う必要があるなら、自分で二次ベジェ曲線を三次ベジェ曲線に変換しなくてはなりません。つまりあなたは動作している bezierCurveTo() メソッドを使うことができます。

var currentX, currentY;  // lineTo/moveTo/bezierCurveTo もしくは quadraticCurveToFixed() に送られた最後の x,y を設定する

function quadraticCurveToFixed( cpx, cpy, x, y ) {
  /*
   下の方程式のために以下の変数名の接頭辞が使われます:
     qp0 は二次曲線の始点 (you must keep this from your last point sent to moveTo(), lineTo(), or bezierCurveTo() ).
     qp1 は二次曲線の制御点 (これは quadraticCurveTo() に送ったであろう cpx,cpy ).
     qp2 は二次曲線の終点 (これは quadraticCurveTo() に送ったであろう x,y 引数 ).
   これらの点を三次制御点に必要な二つを計算するために変換します。(始/終点 は二次と三次曲線の両方で同じです。)

   二つの三次制御点の方程式は:
     cp0=qp0 and cp3=qp2
     cp1 = qp0 + 2/3 *(qp1-qp0)
     cp2 = cp1 + 1/3 *(qp2-qp0)

   以下のそのコードで、別々にxと y の値を両方計算しなければなりません。

    cp1x = qp0x + 2.0/3.0*(qp1x - qp0x);
    cp1y = qp0y + 2.0/3.0*(qp1y - qp0y);
    cp2x = cp1x + (qp2x - qp0x)/3.0;
    cp2y = cp1y + (qp2y - qp0y)/3.0;

   We will now
     a) qp0x と qp0y 変数を (''あなた''が moveTo/lineTo/bezierCurveTo 毎に保存しなくてはならない) currentX と currentY に置換する。
     b) qp1x と qp1y 変数を (あなたが quadraticCurveTo に渡したであろう) cpx と cpy に置換する。
     c) qp2x と qp2y 変数を x と y に置換する。
   which leaves us with:
  */
  var cp1x = currentX + 2.0/3.0*(cpx - currentX);
  var cp1y = currentY + 2.0/3.0*(cpy - currentY);
  var cp2x = cp1x + (x - currentX)/3.0;
  var cp2y = cp1y + (y - currentY)/3.0;

  // そして、実行するために三次ベジェ曲線を呼びます
  bezierCurveTo( cp1x, cp1y, cp2x, cp2y, x, y );

  currentX = x;
  currentY = y;
}

[編集] 矩形

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

rect(x, y, width, height)

このメソッドは 4 つの引数をとります。xy パラメタは新しい矩形パスの左上の角の座標を定義します。widthheight は矩形の幅と高さを定義します。

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

[編集] 組み合わせ

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

[編集]

私はこの完全な、しかし注意すべき最も大切なことは roundedRect 関数と fillStyle プロパティの利用。より複雑な図形を描くためにあなた自身の関数を定義することはとても便利で時間を節約します。このスクリプトの中でそれは私に私が現在より 2 倍のコードの行を取ったでしょう。
fillStyle プロパティをとても深くこのチュートリアルの後半でみるでしょう。私がここで使ったそれは塗りの色を初期値の黒から白にそしてもう一度黒に変更しました。

この例を見る

function draw() {
  var ctx = document.getElementById('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,true);
  ctx.lineTo(31,37);
  ctx.fill();
  for(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.quadraticCurveTo(x,y+height,x+radius,y+height);
  ctx.lineTo(x+width-radius,y+height);
  ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
  ctx.lineTo(x+width,y+radius);
  ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
  ctx.lineTo(x+radius,y);
  ctx.quadraticCurveTo(x,y,x,y+radius);
  ctx.stroke();
}

« »