canvasに関する知見がたまったのでまとめようと思います(今更)。
対象読者
beginPath()とclosePath()をとりあえず使えばいいと思っているCanvas初心者
前置き
以下本記事では「canvas」をelement.getContext('2d')で取得したコンテキストの意味で使います
Canvas 概要
大雑把には Canvas は「パスに沿って一筆書きする」感じです。
パスとは?
パスとは、 サブパスのリストです。サブパスは始点と終点を持ちます。サブパスには線分やベジェ曲線などがあります(それ以外もたくさんある)。パスは1つのcanvasにつき1つあります。
仕様書によると、サブパスは、 「1つ以上の、直線か曲線で結ばれた点」と「閉じているか」という情報を持ちます 。「閉じているか」というのは、そのサブパスの終点が、パスの1つ目のサブパスと直線でつながっているか、ということです。
canvasは一筆書きで描画するので、パス内の隣接したサブパスは基本的につながっています(一部例外あり)。
const ctx = document.getElementById('canvas').getContext('2d'); ctx.beginPath(); // パスを空にする ctx.moveTo(0, 0); // パスに(0, 0)というサブパスを追加する ctx.lineTo(20, 30); // (0, 0)から(20, 30)に直線を引き(ただし、画面にはまだ表示しない)、サブパス(20, 30)をパスに入れる ctx.stroke(); // 画面に表示する
サブパスを作るメソッド
サブパスをつくるメソッドはたくさんあります。
- arc()
- arcTo()
- bezierCurveTo()
- closePath()
- lineTo()
- moveTo()
- quadraticCurveTo()
- rect()
これらには、最後のサブパスと始点を直線で結ぶメソッドと結ばないメソッドがあります。
arc()
、arcTo()
、closePath()
は直線で結びます。
bezierCurveTo()
、quadraticCurveTo()
は最後のサブパスを始点としてベジェ曲線を描画します。
lineTo()
は最後のサブパスから直線を引きます。「始点」と最後のサブパスを接続している、わけではないので分けました。
moveTo()
は 直線を結ばずに サブパスを追加のみ行います。
rect()
も 始点と最後のサブパスを結びません 。rect()
は頂点4つのサブパスを追加します。サブパスの終点は(widthとheightが正ならば)左上です。
strokeText()とstrokeRect()
strokeText()
はパスにサブパスを追加せず、文字を描画するのみです。文字のサブパスといわれてもどう定義したらいいのかわかりませんから当然ですね。
strokeRect()
も同様にサブパスに追加せず描画します(rect()
メソッドが存在しているのになんであるんでしょうかね?)。
beginPath()とmoveTo()の違い
一見するとmoveTo()
は暗黙的にbeginPath()
を呼び出しているように思われます。では、beginPath()
の意味とは何なのでしょうか?
二つの違い、それは「パスを空にする」ことです。これはmoveTo()
では行えません。
ctx.stroke()
は 現在のパスにあるサブパスを描画します。そのため、「赤い三角形と青い四角形を描画したい」ときは、まず赤い三角形をつくり、赤色でstroke()
し、beginPath()
を呼んで三角形のサブパスを削除しておく必要があります。そうしないとパスに三角形も含まれているので、青い四角形をstroke()
するときに一緒に青い三角形も描画されてしまいます。
ctx.beginPath(); ctx.moveTo(30, 30); ctx.lineTo(30 + 30 * Math.cos(2 * Math.PI * (-60 / 360)), 30 + 30 * Math.sin(2 * Math.PI * (-60 / 360))); ctx.lineTo(30 + 30 * Math.cos(2 * Math.PI * (-120 / 360)), 30 + 30 * Math.sin(2 * Math.PI * (-120 / 360))); ctx.closePath(); ctx.strokeStyle = 'rgb(255, 0, 0)'; ctx.stroke(); // 赤い三角形を描画 ctx.beginPath(); ctx.moveTo(100, 30); ctx.lineTo(100, 60); ctx.lineTo(160, 60); ctx.lineTo(160, 30); ctx.closePath(); ctx.strokeStyle = 'rgb(0, 0, 255)'; ctx.stroke(); // 青い四角形を描画
失敗例がこちら
ctx.beginPath(); ctx.moveTo(30, 30); ctx.lineTo(30 + 30 * Math.cos(2 * Math.PI * (-60 / 360)), 30 + 30 * Math.sin(2 * Math.PI * (-60 / 360))); ctx.lineTo(30 + 30 * Math.cos(2 * Math.PI * (-120 / 360)), 30 + 30 * Math.sin(2 * Math.PI * (-120 / 360))); ctx.closePath(); ctx.strokeStyle = 'rgb(255, 0, 0)'; ctx.stroke(); // 赤い三角形を描画 // ctx.beginPath(); // ここをコメントアウトしたので、パスが空にならない ctx.moveTo(100, 30); ctx.lineTo(100, 60); ctx.lineTo(160, 60); ctx.lineTo(160, 30); ctx.closePath(); ctx.strokeStyle = 'rgb(0, 0, 255)'; ctx.stroke(); // 青い四角形と、さっきの赤い三角形の上に青い三角形を描画
(赤い三角形と青い三角形が重なっているため、紫色になっています)
fill
canvasにはfill()
というメソッドがあります。これは、現在のパスを 非ゼロ巻数規則(Nonzero winding number rule) にしたがって塗りつぶすメソッドです。
もし最後のパスが閉じていなかったら、閉じていないサブパスの中で最初のサブパスと、最後のサブパスが直線で結ばれて塗りつぶされます。
非ゼロ巻数規則とは
塗りつぶすべきか判定したい点Pをおきます。塗りつぶしたい図形を曲線Cとします(これがパスにあたります)。
- Pから好きな方向へPを始点とした半直線を引きます。
- CがPから見て左から右へ半直線を横切ったら巻数を1足します。
- 右から左へ横切ったら巻数を1引きます。
- 半直線とCの交点すべてを見終わったとき、巻数が0なら点PはCの外側にあるということなので、塗りつぶしません。
- 0でないならCの内側にあるので塗りつぶします。
これが非ゼロ巻数規則です。fill()
はこれに従い塗りつぶします。
fillText()とfillRect()
さきのstrokeText()
、strokeRect()
と同様に、fillに関してもfillText()
、fillRect()
が用意されています。やはり、サブパスを使って描画するのが適切でないからです。
終わりに
なんとなく歯切れが悪い終わりかたになってしまいました。そのうち追記します。