【Day4】画面描画の方法、Canvas APIについて【QAC24】


# Canvas を使って画面描画をしてみよう！

今回は、Canvas API を使って画面描画をしてみます。

まずは`index.html`をいい感じにまっさらにして、Canvas 要素を追加しましょう。

```html:index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="/src/styles.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tauri App</title>
    <script type="module" src="/src/main.ts" defer></script>
  </head>

  <body>
    <canvas id="test-canvas" width="300" height="300"></canvas>
  </body>
</html>
```

次に、`main.ts`をもっとまっさらにして、Canvas の DOM を取得してみましょう。

```typescript:main.ts
window.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("test-canvas");
});
```

前回の記事で吹っ飛ばした気がしますが、`window.addEventListener("DOMContentLoaded", () => { ... });`は、DOM の読み込みが完了した時に実行される関数です。
んでたまに何度も出てきてる DOM っていうのが何なのかといいますと、HTML の要素 (`<div>...</div>`とか`<p>...</p>`とか) のことを、TypeScript では DOM と呼びます。
すなわち、「DOM の読み込み完了」とは、HTML の要素が全て読み込まれ（画面に描画され）た時を指します。

更に、`document.getElementById("test-canvas")`は、HTML における`id`が`test-canvas`である要素を取得する関数です。
さっきの`index.html`では、`<canvas id="test-canvas" width="300" height="300"></canvas>`という要素がありますので、これを取得しています。

という訳で、まずは Canvas の説明です。

# Canvas とは

Canvas というものは HTML5 で導入された、画面描画を行うためのやつです。
画面描画というと、画像や図形、文字なんかを描画することが出来ます。
つまりこれ、これまで CSS でなんかめっちゃ頑張って図形作成とかやってたような奴が、TypeScript から簡単に操作できるって話なんですよ。

まずはものは試しです。

Canvas に何かを描画するにはコンテキストというものを取得する必要があります。

```typescript:main.ts
window.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("test-canvas");
  const ctx = canvas.getContext("2d");
});
```

はい、まず Canvas 要素を取得して、それに`getContext`というのを呼び出して 2D の Context を取得して...

![スクリーンショット 2024-11-25 002847.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107984/5ca8975b-852d-ead7-17b8-db392b64cf6d.png)

あれ？
なんかエラー...？

あーあ、あなた TypeScript に怒られてますね。

## TypeScript で DOM をいじいじするときのお話

上記コードは JavaScript では、エラーもなく動作します。
が、TypeScript はそうはいきません。

まず、`const canvas = document.getElementById("test-canvas");`から見ていきましょう。

先ほどの画像を見ても分かるように、`: HTMLElement | null`と書いてあります。
これは、`canvas`が`HTMLElement`型か`null`型のどちらかであることを示しています。

なぜならば、`test-canvas`という id を持つ要素が存在しないかもしれないからです。
これではいけません。まさに Null Pointer Exception です。ｶﾞｯ...
`null`を消費しましょう。

```typescript:main.ts
window.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("test-canvas");
  if (!canvas) return;
  const ctx = canvas.getContext("2d");
});
```

if 文を使いましょう。
TypeScript での if 文は、`if (条件) { ... }`と書くことが出来ます。
(条件が真の時の処理が 1 行ですむ場合は、波かっこを省略することが出来ます)
`!canvas`というのですが、まず`!`は否定を表す演算子 (NOT) です。
`!true`は`false`、`!false`は`true`です。

そして、以下の記事にもあるように、TypeScript の条件式の中で、`null`は`false`として扱われます。
(というより、`null`じゃないような 1 とか存在する値は`true`として扱われます)

https://qiita.com/phi/items/723aa59851b0716a87e3#%E5%90%84%E5%9E%8B-%E5%80%A4%E3%81%AE%E8%A9%95%E4%BE%A1

つまり、`!canvas`は`canvas`が`null`である場合に`true`を返します。

これで、`canvas`が`null`である場合は、`return`で関数を終わらせ、`null`じゃない場合は次の処理に進むようになりました。

よし！ これでエラーは出なくな...あれ？

`getContext`がエラーを吐いていますね。
これは、`canvas`には`getContext`メソッドが存在しないからです。

いや待て、さっきお前「JavaScript なら正常に動く」言ってたやんかと。
ってことは`getContext`だかなんだかもあるんやろ？と。

原因は、変数`canvas`が`HTMLElement`だからです。

HTMLElement には`getContext`メソッドが存在しません。
なので、`canvas`が`<canvas></canvas>`であることを TypeScript に伝える必要があります。
`<canvas></canvas>`...すなわち`HTMLCanvasElement`には`getContext`メソッドが存在します。

そして、その TypeScript に伝える方法というのが、型アサーションです。

```typescript:main.ts
window.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("test-canvas") as HTMLCanvasElement | null;
  if (!canvas) return;
  const ctx = canvas.getContext("2d");
});
```

`document.getElementById("test-canvas")`の後ろに`as HTMLCanvasElement | null`と書いています。
これで、TypeScript は`canvas`が`HTMLCanvasElement`か`null`のどちらかであることを理解しました。

この型アサーションというのはとても強力なもので、型を強制的に変換することが出来ます。
今回、`HTMLElement`から`HTMLCanvasElement`への変換のため、互いの型が近いため正常に動きます。
しかし、`as HTMLCanvasElement | null`というのを`as number | null`とでも書いてみましょうか。
現在の私のエディタではエラーが発生しません。
しかし、人間の目で見る限り、`document.getElementById("test-canvas")`は`number`を返すことはないでしょう。

...というように、型アサーションは便利ながらとても強力なものなので、使いすぎには注意しましょう。

ちなみに、`as HTMLCanvasElement | null`を`as number`としてみると、私のエディタでもエラーを吐いてくれました。

> 型 'HTMLElement | null' から型 'number' への変換は、互いに十分に重複できないため間違っている可能性があります。意図的にそうする場合は、まず式を 'unknown' に変換してください。
> 型 'HTMLElement' は型 'number' と比較できません。ts(2352)

最後に、`ctx`にも同様に`null`消費を行いましょう。

```typescript:main.ts
window.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("test-canvas") as HTMLCanvasElement | null;
  if (!canvas) return;
  const ctx = canvas.getContext("2d");
  if (!ctx) return;
});
```

# 描画だ！

さて、ここまでで Canvas の Context を取得することが出来ました。
描画するにはその Context を使います。

試しに、 Canvas に四角形を描画してみましょう。

```typescript:main.ts
window.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("test-canvas") as HTMLCanvasElement | null;
  if (!canvas) return;
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  ctx.fillStyle = "red";
  ctx.fillRect(10, 10, 100, 100);
});
```

![スクリーンショット 2024-11-25 010441.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107984/2f02e9c9-9a52-ad76-1a02-614671f562b5.png)

おっ！？
赤い四角形が描画されました！

`ctx.fillStyle = "red";`で色 (特に `fill`するための色) を指定し、`ctx.fillRect(10, 10, 100, 100);`で四角形 (`Rectangular`) を左から 10px、上から 10px のところに 100x100 で描画しています。

はは...`rectangular`...四角形...AutoCAD...思い出しちゃったﾈ...

> 私は AutoCAD で「四角形」が`Rectang`であると学んだよ！

更に画像も描画しちゃいましょう！

ベーコンの画像でも描画してみますか。

https://qiita.com/satour/items/fc63fca0024fa8dc8eef#baconmockup

```typescript:main.ts
  const img = new Image();
  img.src = "https://baconmockup.com/200/200/";
  img.onload = () => {
    ctx.drawImage(img, 50, 50);
  };
```

（先ほどの`ctx.fillRect(...);`の下）

![スクリーンショット 2024-11-25 011152.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107984/bf4af24b-9a61-78ee-76fb-301e3afc4acb.png)

飯テロですか！？！？！？

と、このように画像も出すことが出来ちゃいます。

`const img = new Image();`で画像を作成しています。
これは、HTML の`<img>`要素と同じものです。
`img.src = "https://baconmockup.com/200/200/";`で画像の URL を指定しています。
そして、`img.onload = () => { ctx.drawImage(img, 50, 50); };`で画像の読み込みが完了した時にコンテキストに画像を、上から 50px、左から 50px の位置に描画しています。
画像の読み込みが終わる前に`ctx.drawImage(img, 50, 50);`を実行してしまうと、正常に描画されません。

似たような方法で`img.decode()`という方法もありますが、今回は省略します。
数日の間だけ頭に入れておいてください。

# まとめ
はい、もうこれであなたは何でも描画できるようになりましたね。

え？なに？ベーコンが邪魔だから動かしてほしい？
分かりましたよ...動かす...動かす...

...ということで、次回は Canvas でのアニメーションについてお話しようと思います。