Canvasを使用した2Dグラフィックス描画において、パスの定義や線のスタイル、塗りつぶし方法は非常に重要です。本記事では、パスの起点設定、線の描画・塗りつぶし、矩形の描画、非零正交ルールおよび奇偶ルールといった基本的な概念を解説し、実際の折れ線グラフや棒グラフの作成例も紹介します。
パスの基本操作と状態管理
Canvasの描画には、まず描画の開始点を指定する必要があります。
ctx.moveTo(x, y):描画の出発点を設定します(座標は左上原点からの相対値)。ctx.lineTo(x, y):現在の位置から指定された点まで線を引きます。ctx.beginPath():新しいパスの開始を宣言。既存のパスとの区別が可能になります。ctx.closePath():最後の点と最初の点を自動的に結び、閉じた領域を作成します。
描画スタイルの設定
ctx.stroke():定義されたパスに沿って輪郭線(ストローク)を描画します。色はstrokeStyleで設定。ctx.fill():閉じられたパスの内部を塗りつぶします。fillStyleで色やグラデーションを指定。ctx.setLineDash([5, 10]):線のダッシュパターンを設定(例:5ピクセル実線 → 10ピクセル空白)。ctx.lineDashOffset = 10:ダッシュパターンのオフセット値を調整。
矩形の描画手法
ctx.rect(x, y, width, height):矩形のパスを定義しますが、描画は行いません。ctx.fillRect(x, y, width, height):矩形を即座に塗りつぶします。ctx.strokeRect(x, y, width, height):矩形の輪郭線を即座に描画します。ctx.clearRect(x, y, width, height):指定領域の内容をクリア(消去)します。
実践例:複数の図形の描画
以下のコードは、異なる形状とスタイルを組み合わせて描画するサンプルです。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 1. 直線の描画
ctx.moveTo(20, 20);
ctx.lineTo(180, 20);
ctx.strokeStyle = '#3a7';
ctx.lineWidth = 2;
ctx.stroke();
// 2. 虚線の描画(ダッシュパターン)
ctx.beginPath();
ctx.moveTo(20, 50);
ctx.lineTo(180, 50);
ctx.setLineDash([6, 4]);
ctx.lineDashOffset = 2;
ctx.stroke();
// 3. 三角形の描画(閉じる)
ctx.beginPath();
ctx.moveTo(250, 30);
ctx.lineTo(300, 100);
ctx.lineTo(200, 100);
ctx.closePath();
ctx.fillStyle = 'rgba(255, 100, 50, 0.7)';
ctx.fill();
ctx.strokeStyle = '#f00';
ctx.stroke();
// 4. 円環の描画(非零正交ルール)
ctx.beginPath();
ctx.arc(400, 60, 50, 0, Math.PI * 2, false); // 外側
ctx.arc(400, 60, 30, 0, Math.PI * 2, true); // 内側(逆時計回り)
ctx.fillStyle = 'gold';
ctx.fill(); // 非零正交ルールにより、内側は塗りつぶされない
塗りつぶしルール:非零正交と奇偶ルール
パスの内部を塗りつぶす際に使用されるルールは2種類あります。
非零正交ルール(default)
任意の点から外へ放射線を引き、その線がパスの辺と交差する回数をカウント。順時計回りは+1、逆時計回りは-1。最終的な合計が0でなければ、その領域は塗りつぶされます。
// 円環の描画例
ctx.beginPath();
ctx.arc(500, 60, 50, 0, Math.PI * 2, false); // 外円(時計回り)
ctx.arc(500, 60, 30, 0, Math.PI * 2, true); // 内円(逆時計回り)
ctx.fillStyle = 'red';
ctx.fill(); // 内部は空洞になる
奇偶ルール(evenodd)
放射線がパスと交差する回数が奇数なら塗りつぶし、偶数なら塗りつぶしません。
ctx.beginPath();
ctx.arc(600, 60, 50, -Math.PI/2, Math.PI*3/2, false);
ctx.moveTo(600, 10);
ctx.lineTo(550, 50);
ctx.lineTo(600, 90);
ctx.lineTo(650, 50);
ctx.closePath();
ctx.fillStyle = 'blue';
ctx.fill('evenodd'); // 塗りつぶされる領域が異なる
棒グラフの実装例
以下は、データ配列に基づいて棒グラフを描画するクラスの実装です。
class BarChart {
constructor(ctx) {
this.ctx = ctx;
this.m = 10; // グリッド間隔
this.width = ctx.canvas.width;
this.height = ctx.canvas.height;
this.cols = Math.floor(this.width / this.m);
this.rows = Math.floor(this.height / this.m);
this.barWidth = 30;
this.originX = 50;
this.originY = 350;
}
draw(data) {
this.drawGrid();
this.drawAxes();
this.drawBars(data);
}
drawGrid() {
this.ctx.strokeStyle = '#eee';
for (let i = 1; i < this.rows; i++) {
this.ctx.beginPath();
this.ctx.moveTo(0, i * this.m + 0.5);
this.ctx.lineTo(this.width, i * this.m + 0.5);
this.ctx.stroke();
}
for (let j = 1; j < this.cols; j++) {
this.ctx.beginPath();
this.ctx.moveTo(j * this.m + 0.5, 0);
this.ctx.lineTo(j * this.m + 0.5, this.height);
this.ctx.stroke();
}
}
drawAxes() {
const x = this.originX;
const y = this.originY;
const lenX = 600, lenY = 300;
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.ctx.lineTo(x + lenX, y);
this.ctx.lineTo(x + lenX - 10, y - 5);
this.ctx.lineTo(x + lenX - 10, y + 5);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.ctx.lineTo(x, y - lenY);
this.ctx.lineTo(x - 5, y - lenY + 10);
this.ctx.lineTo(x + 5, y - lenY + 10);
this.ctx.fill();
this.ctx.strokeStyle = '#000';
this.ctx.stroke();
}
drawBars(data) {
data.forEach(item => {
const barHeight = item.value * 2; // スケーリング
this.ctx.fillStyle = item.color;
this.ctx.fillRect(
this.originX + item.x,
this.originY - barHeight,
this.barWidth,
barHeight
);
});
}
}
// データ
const bars = [
{ x: 0, value: 25, color: 'red' },
{ x: 60, value: 120, color: 'blue' },
{ x: 120, value: 75, color: 'green' },
{ x: 180, value: 200, color: 'purple' }
];
// 実行
new BarChart(document.getElementById('canvas').getContext('2d')).draw(bars);