【Dart】DartでWebコンテンツを作ってみる その2 ~CanvasElementによる2D描画~

この記事は約14分で読めます。

CanvasElementを使って描画してみる

ベースとなるコード

今回はCanvasElementを使って図形を描画してみます。まず、CanvasElementを配置して空の描画関数drawを作成しただけのベースコードは下記のようになります。

import 'dart:html';

void draw(CanvasRenderingContext2D context) {}

void main(List<String> arguments) {
  final body = document.body;
  if (body == null) {
    return;
  }

  final canvas = CanvasElement();
  draw(canvas.context2D);

  body.append(canvas);
}

draw関数は引数としてCanvasRenderingContext2Dを取るようにしています。このCanvasRenderingContext2Dを使用して様々な図形を描画することができます。

矩形の塗りつぶし

それでは、まず背景を塗りつぶしてみましょう。draw関数を以下のように変更します。

void draw(CanvasRenderingContext2D context) {
  // canvasのサイズ取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    // 取得できない場合は描画しない
    return;
  }

  // 背景の塗りつぶし
  context.setFillColorRgb(192, 192, 192);
  context.fillRect(0, 0, width, height);
}

塗りつぶし色を指定するsetFillColorRgb関数と背景を塗りつぶすために矩形の塗りつぶしを行うfillRect関数を使用しています。これを実行すると下のように背景がグレーになります。

円の描画

次に円を描いてみましょう。draw関数を以下のように変更します。

void draw(CanvasRenderingContext2D context) {
  // canvasのサイズ取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    // 取得できない場合は描画しない
    return;
  }

  // 円の中心と半径
  final cx = width * 0.5, cy = height * 0.5;
  final r = ((width < height) ? width : height) * 0.4;

  // 背景の塗りつぶし
  context.setFillColorRgb(192, 192, 192);
  context.fillRect(0, 0, width, height);

  // 円の描画
  context.beginPath();
  context.ellipse(cx, cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.setStrokeColorRgb(0, 0, 255);
  context.stroke();
}

上のコードに記載していませんが、円周率piを使用するためにdart:mathのインポートもファイルの冒頭で行います。実行すると下記のようになります。

このコードではbeginPath関数、ellipse関数、closePath関数、setStrokeColorRgb関数、stroke関数が新しく登場しました。

関数内容
beginPathパス情報の開始をCanvasRenderingContext2Dに知らせる
ellipse楕円のパス情報をCanvasRenderingContext2Dに追加する
closePathパス情報の終了をCanvasRenderingContext2Dに知らせる
setStrokeColorRgbstroke時の色をRGBで指定する
strokeパス情報をもとに描画を実行する

beginPath関数とclosePath関数は対となる関数であり、セットで使用します。beginPath関数とclosePath関数の間でellipse関数などのパス情報を追加する関数を呼ぶことで、ひとまとまりの独立したパス情報として追加することができます。stroke関数はCanvasRenderingContext2Dの保持しているパス情報をもとに線の描画を実行します。このとき、setStrokeColorRgb関数で指定した色が使用されます。

もしbeginPath関数とclosePath関数を忘れてしまうと、前後で追加したパス情報と繋がったパス情報として扱われてしまい、意図しない描画結果となってしまうことがあります。例えば、下記のようにellipse関数を2回呼んで円を2つ描こうとすると、直線も描画されてしまいます。

void draw(CanvasRenderingContext2D context) {
  // canvasのサイズ取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    // 取得できない場合は描画しない
    return;
  }

  // 円の中心と半径
  final cx = width * 0.5, cy = height * 0.5;
  final r = ((width < height) ? width : height) * 0.4;

  // 背景の塗りつぶし
  context.setFillColorRgb(192, 192, 192);
  context.fillRect(0, 0, width, height);

  // 円の描画を2回続けて呼ぶ
  context.beginPath();
  context.ellipse(0.8 * cx, 0.8 * cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.ellipse(1.2 * cx, 1.2 * cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.setStrokeColorRgb(0, 0, 255);
  context.stroke();
}

1つ目の円の終点から2つ目の円の始点への移動もパス情報として追加されてしまうためです。それぞれのellipse関数をbeginPath関数とclosePath関数で囲み、その都度stroke関数を呼ぶようにすれば円のみが描画されるようになります。コードとしては以下のようになります。

void draw(CanvasRenderingContext2D context) {
  // canvasのサイズ取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    // 取得できない場合は描画しない
    return;
  }

  // 円の中心と半径
  final cx = width * 0.5, cy = height * 0.5;
  final r = ((width < height) ? width : height) * 0.4;

  // 背景の塗りつぶし
  context.setFillColorRgb(192, 192, 192);
  context.fillRect(0, 0, width, height);

  // 円の描画色を設定
  context.setStrokeColorRgb(0, 0, 255);

  // 1つ目の円の描画
  context.beginPath();
  context.ellipse(0.8 * cx, 0.8 * cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.stroke();

  // 2つ目の円の描画
  context.beginPath();
  context.ellipse(1.2 * cx, 1.2 * cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.stroke();
}

実行した結果、意図通りに2つの円のみが描かれています。

直線の描画

それでは次に直線を描いてみましょう。draw関数を下記のように変更します。

void draw(CanvasRenderingContext2D context) {
  // canvasのサイズ取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    // 取得できない場合は描画しない
    return;
  }

  // 円の中心と半径
  final cx = width * 0.5, cy = height * 0.5;
  final r = ((width < height) ? width : height) * 0.4;

  // 背景の塗りつぶし
  context.setFillColorRgb(192, 192, 192);
  context.fillRect(0, 0, width, height);

  // 円の描画
  context.beginPath();
  context.ellipse(cx, cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.setStrokeColorRgb(0, 0, 255);
  context.stroke();

  // 星の描画
  context.beginPath();
  context.moveTo(cx, cy - r);
  for (var i = 0; i < 5; i++) {
    final rad = 0.5 * pi + 2.0 * pi / 5 * 2 * i;
    context.lineTo(cx + r * cos(rad), cy - r * sin(rad));
  }
  context.closePath();
  context.setStrokeColorRgb(255, 0, 0);
  context.stroke();
}

描画結果は下記のようになります。

moveTo関数とlineTo関数が新たに登場しています。

関数内容
moveTo指定した座標に移動するパス情報をCanvasRenderingContext2Dに追加する
lineTo指定した座標に線を描画しながら移動するパス情報をCanvasRenderingContext2Dに追加する

moveTo関数で直線を開始したい座標に移動し、描画する直線の終点に向かってlineTo関数で移動するというイメージです。さらに描画した直線の終点を新たな始点として直線を描きたい場合は続けてlineTo関数を呼ぶことができます。別の座標から直線を描きたい場合はmoveTo関数で移動すればOKです。

ここまでで矩形の塗りつぶし、円の描画、直線の描画を行うことが出来ました。

再描画する

前節では1度だけ描画を行いましたが、ユーザの操作などに応じて再描画を行いたいこともあると思います。その方法について見ていきましょう。全体のコードを下記のように修正してみました。

import 'dart:html';
import 'dart:math';

void draw(CanvasRenderingContext2D context, int num) {
  // canvasのサイズ取得
  final width = context.canvas.width, height = context.canvas.height;
  if (width == null || height == null) {
    // 取得できない場合は描画しない
    return;
  }

  // 描画内容の消去
  context.clearRect(0, 0, width, height);

  // 円の中心と半径
  final cx = width * 0.5, cy = height * 0.5;
  final r = ((width < height) ? width : height) * 0.4;

  // 円の描画
  context.beginPath();
  context.ellipse(cx, cy, r, r, 0.0, 0.0, 2.0 * pi, null);
  context.closePath();
  context.setStrokeColorRgb(0, 0, 255);
  context.stroke();

  // 星の描画
  context.beginPath();
  context.moveTo(cx, cy - r);
  for (var i = 0; i < num; i++) {
    final rad = 0.5 * pi + 2.0 * pi / num * (num ~/ 2) * i;
    context.lineTo(cx + r * cos(rad), cy - r * sin(rad));
  }
  context.closePath();
  context.setStrokeColorRgb(255, 0, 0);
  context.stroke();
}

void updateText(Text text, int num) {
  text.data = '$num';
}

void main(List<String> arguments) {
  final body = document.body;
  if (body == null) {
    return;
  }

  var num = 5;
  final text = Text('');
  updateText(text, num);

  final canvas = CanvasElement();
  draw(canvas.context2D, num);

  body.append(canvas);
  body.append(Element.br());
  body.append(ButtonElement()
    ..text = '-'
    ..onClick.listen((event) {
      num -= 2;
      if (num < 3) {
        num = 3;
      }
      draw(canvas.context2D, num);
      updateText(text, num);
    }));
  body.append(ButtonElement()
    ..text = '+'
    ..onClick.listen((event) {
      draw(canvas.context2D, num += 2);
      updateText(text, num);
    }));
  body.append(text);
}

動作は下のようになります。

まず、draw関数の第二引数に整数値をとるように変更しました。また、main関数の方ではボタンを押下する度に整数値numを更新してdraw関数を呼ぶように変更しています。updateText関数は前回と代わらないので説明を省略します。

draw関数の中では、新たにclearRect関数を呼ぶようになりました。代わりに説明の都合でfillRect関数を削除しています。clearRect関数は指定した領域の描画内容を消去する関数です。前述のコードからclearRect関数をコメントアウトしてみると描画内容を消去しないまま次の描画を行うので、ボタンを押下する度に重ねて描画してしまう下記のような動作となります。

fillRect関数を呼んでいる場合は、それによって前回の描画内容が塗りつぶされるので、clearRect関数は呼ばなくても問題ありません。状況に応じてfillRect関数やclearRect関数を使い分けていただければよいと思います。

まとめ

この記事では、CanvasElementによる矩形、円、直線の描画や再描画の方法を記載しました。紹介した関数は下記になります。

関数内容
clearRect指定した矩形領域の描画内容を消去する
fillRect指定した矩形領域を塗りつぶす
ellipse楕円のパス情報をCanvasRenderingContext2Dに追加する
beginPathパス情報の開始をCanvasRenderingContext2Dに知らせる
closePathパス情報の終了をCanvasRenderingContext2Dに知らせる
moveTo指定した座標に移動するパス情報を追加する
lineTo指定した座標に線を描画しながら移動するパス情報を追加する
strokeパス情報をもとに描画を実行する
setFillColorRgb塗りつぶし時の色をRGBで指定する
setStrokeColorRgbstroke時の色をRGBで指定する

次回はタイマーと組み合わせたアニメーションと3D描画について見ていきたいと思います。

コメント

タイトルとURLをコピーしました