7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

株式会社サイバー・バズAdvent Calendar 2021

Day 24

Canvas APIでの描画をする時の落とし穴と便利メソッド

Last updated at Posted at 2021-12-24

2021年、Canvas APIで複雑なものを比較的たくさん描いてきました。
Canvas API で何か描画しようと思った時に、予め知っておくと良いのではないかと思うことについてまとめてます。

CanvasのDOMに上書きされていく

Canvas API は、呼び出された順番にCanvasのDOMが上書きされていきます。
ゆえに、「どの順番で、何を描画させていくか」が結構重要になってきます。
※「リセットをする、という上書き」をしない限り、最初に描いた内容の上に次の描画が塗り重ねられていきます。

Canvas APIを用いて、ポケモンのコダックを描いてみたことがあるのですが、各パーツの描画順番を間違えると、完全なコダックの描画に失敗します。
(正しいケースと間違えたケース)

正しいケース

export const kDuck = (ctx: CanvasRenderingContext2D) => {
  comb(ctx, "#383838"); // トサカ
  rightArm(ctx, "#F2C86B"); // 右腕
  head(ctx, "#F2C86B"); // 頭
  eyes(ctx, "#eee"); // 両目       ※「頭」パーツの後に描画することが正しい
  tail(ctx, "#F2C86B"); // 尻尾
  rightFoot(ctx, "#C4C2BB"); // 右足
  bodyAndLeftArm(ctx, "#F2C86B"); // 胴体と左腕
  beak(ctx, "#C4C2BB"); // くちばし
  leftFoot(ctx, "#C4C2BB"); // 左足
};

aaa.jpg

間違えたケース

export const kDuck = (ctx: CanvasRenderingContext2D) => {
  comb(ctx, "#383838"); // トサカ
  rightArm(ctx, "#F2C86B"); // 右腕
  eyes(ctx, "#eee"); // 両目       ※「頭」パーツの前に描画しているので正しくない
  head(ctx, "#F2C86B"); // 頭
  tail(ctx, "#F2C86B"); // 尻尾
  rightFoot(ctx, "#C4C2BB"); // 右足
  bodyAndLeftArm(ctx, "#F2C86B"); // 胴体と左腕
  beak(ctx, "#C4C2BB"); // くちばし
  leftFoot(ctx, "#C4C2BB"); // 左足
};

bbb.jpg

実際の描画物を見れば一目瞭然ですが、後者のコダックは、目がありません。
これは実際には描画されているけれど、「目」パーツの描画の順番が、「頭」パーツよりも先に実行されてしまっているため、あとから描画された「頭」パーツに隠れて見えなくなってしまっています。
描画する順番を誤るとこのような悲惨なコダックを描いてしまうことになるので気をつけましょう。

その時の状態に応じて、描き分けたいケースの対処

例えば Reactで Canvas APIを使うケースなどでは、state(状態)によって描画したいものを変えたいといったことが発生すると思います。
そんな時は、描画メソッド等のroot(根本的)な部分にリセット用の描画を施しましょう
Canvas DOMに width(x方向), height(y方向) を指定し、そのサイズに応じて座標(x,y)方向を特定の色で埋めることで、上書きされることなく、都度描きたいものだけ表示させられるようになります。
以下の例は、widthが880px、heightが680pxです。

  ctx.beginPath()
  ctx.fillStyle = '#EEEEEE' // デフォルトの <Canvas/> 空間の背景色
  ctx.moveTo(0, 0) // <Canvas/> 空間の座標始点(デフォルト左上)
  ctx.lineTo(0, 680) // 左下末端
  ctx.lineTo(880, 680) // 右下末端
  ctx.lineTo(880, 0) // 右上末端
  ctx.fill()
  ctx.closePath()

stroke()とfill() メソッドの順番に注意

描画の際、輪郭線を表現したい場合は、必ずfill() メソッドよりもstroke()メソッドを後に実行する必要があります。
以下に、コダックの頭パーツを描画している関数があります。


// コダックの頭
const head = (ctx: CanvasRenderingContext2D, fillColor: string) => {
  ctx.beginPath();
  ctx.strokeStyle = "#383838";
  ctx.fillStyle = fillColor;
  ctx.moveTo(399, 147); //
  ctx.quadraticCurveTo(298, 142, 262, 212);
  ctx.quadraticCurveTo(235, 275, 323, 316);
  ctx.quadraticCurveTo(399, 345, 472, 306);
  ctx.quadraticCurveTo(533, 269, 486, 189);
  ctx.quadraticCurveTo(452, 153, 399, 147); // 
  ctx.fill();
  ctx.stroke();
  ctx.closePath();
};

頭の輪郭線を描きたいと思った場合、当然stroke() メソッドを実行することで描画可能です。

しかし、下記のコードのように、 stroke() メソッドの後にfill()メソッドが呼び出されると、輪郭線がなくなってしまいます。

// const head = (ctx: CanvasRenderingContext2D, fillColor: string) => {
// ...
  ctx.quadraticCurveTo(533, 269, 486, 189);
  ctx.quadraticCurveTo(452, 153, 399, 147); // 
  ctx.stroke(); 
  ctx.fill(); // stroke() の後に実行している
  ctx.closePath();

stroke()の実行で輪郭線が描画されはしたものの、fill()メソッドの実行によって、fillStyleカラーに塗りつぶされることになります。
「輪郭線を描きたいのに、描画されない...」と思った時は、描画したい対象のコードのstroke() メソッドとfill()メソッドの呼び出し順に誤りがないか確認してみましょう。

save()メソッド と restore()メソッドを使いこなす

例えば特定のパーツだけサイズを変えたり、角度を変えたくなるケースが発生するとします。
サイズを変えるにはscale()メソッドを、
https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/scale
角度を変えるには rotate()メソッドをそれぞれ使うことで変更できます。
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate

しかし、先にも述べた「その時の状態によって描画したいもの」が変わるケースなど(角度やサイズの変更も含みます)では、scale()メソッドやrotate()メソッドを使うと、状態が変化するたびに、複利的に角度やサイズも変わっていってしまいます。

ddd.jpg

もちろん、意図的に、複利的なサイズ変更を望まれているのであればこれでも問題ありません。
一方で、描きたい対象は、あくまでも1つで良いケース(コダックが1体だけ描かれることを意図している時)の場合は、 save()メソッドと restore()メソッドを使いこなすことが重要になってきます。
https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/save
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/restore

少し例を変更し、「コダックのくちばしパーツだけ、サイズ変更する」ケースで考えています。

① くちばしパーツの前で、 save()メソッドを実行
scale()メソッドを実行
③ くちばしパーツを構成する関数で描画を実行
④ restore()メソッドを実行

export const kDuck = (ctx: CanvasRenderingContext2D) => {
  comb(ctx, "#383838"); // トサカ
  rightArm(ctx, "#F2C86B"); // 右腕
  head(ctx, "#F2C86B"); // 頭
  eyes(ctx, "#eee"); // 両目
  tail(ctx, "#F2C86B"); // 尻尾
  rightFoot(ctx, "#C4C2BB"); // 右足
  bodyAndLeftArm(ctx, "#F2C86B"); // 胴体と左腕

  ctx.save(); // scale() メソッドの実行前に、bodyAndLeftArmまでの描画を一時保存
  ctx.scale(1.5, 1.5); // 以降の描画対象のサイズが1.5倍になるscale()を実行
  beak(ctx, "#C4C2BB"); // くちばしパーツを描画
  ctx.restore(); //bodyAndLeftArmまでの描画を元の情報のまま復活

  leftFoot(ctx, "#C4C2BB"); // 左足 パーツは、1.5倍にならず、元のサイズのまま描画される
};

スクリーンショット 2021-12-24 20.02.58.jpg

「コダックとしての描画」は崩れてしまっていますが、くちばしパーツだけサイズが大きくなり、他のパーツは元のサイズのままであることがお分かりいただけるかと思います。

特定のパーツや、特定の描画のみ、サイズや角度を変更したいケースでは、save()メソッドとrestore()メソッドを上手く使うことで、再利用性の高いパーツに切り出すことも可能です。

変更したいサイズや角度の具体的な数値を引数に取るような関数に切り出すことで、 Reactなどのstateの変更に応じて、同じ関数で描画を変えていくことが可能になります。

なかなか scale()メソッドや rotate()メソッドまで使うケースは多くないかと思いますが、使う機会があれば、ぜひ一緒にsave()メソッドとrestore()メソッドも使ってみてください。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?