【Day20】Canvasだけじゃ意味がない【QAC24】


# HTML は Canvas だけじゃないよ！

当たり前だけどね
だから報われないタグも整理して
書いていたいの
普通でしょう？

Canvas 以外の HTML Element を使用して、現在のマップ以外の UI 部分を作り始めたいと思います。

が、これも Map 同様に Class で似たような構造を提供できれば楽な気がしますね

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

  <body>
    <!-- メインのゲーム画面を表示するCanvas -->
    <div id="main">
      <canvas id="game" width="960" height="960">
        HTML5 Canvasがサポートされていません。<br>
        OSのアップグレードをご確認ください。
      </canvas>
    </div>

    <!-- UI -->
    <div id="uis"></div>
  </body>
</html>
```

まずはこんな風に、`<div id="uis"></div>`を追加しました。
ここに UI が表示されるようにしていきます。

## UI の基底クラスを作る

`lib/screen.ts`でも作ってみましょう。

```typescript:src/lib/screen.ts
import { GameMap } from "./map";

/**
 * UIなどを描画するのに使用できる、限定DOM空間を提供するクラス
 */
export abstract class Screen {
  public abstract screenId: string;

  public _map: GameMap | null = null;
  public get map() {
    if (!this._map) {
      throw new Error("Map is not set");
    }
    return this._map;
  }
  public set map(map: GameMap) {
    this._map = map;
  }

  public getUisDom() {
    const uis = document.querySelector("#uis");
    if (!uis) {
      throw new Error("UIs element not found");
    }
    return uis;
  }

  public get element() {
    const screen = this.getUisDom().querySelector(
      this.query
    ) as HTMLDivElement | null;
    if (!screen) {
      throw new Error("Screen element not found");
    }
    return screen;
  }

  public get showing() {
    return this.element.style.display === "block";
  }

  public getElementById<HTMLT extends HTMLElement>(
    elementId: string
  ): HTMLT | null {
    const element = this.element.querySelector(
      `#${elementId}`
    ) as HTMLT | null;
    return element;
  }

  public get query() {
    return `#${this.screenId}`;
  }

  public show() {
    this.element.style.display = "block";
  }

  public hide() {
    this.element.style.display = "none";
  }

  public build() {
    const uiElement = this.getUisDom();
    const screen = uiElement.querySelector(this.query);
    if (screen) {
      screen.remove();
    }
    const newScreen = document.createElement("div");
    newScreen.id = this.screenId;
    uiElement.appendChild(newScreen);
    return newScreen;
  }

  public destroy() {
    this.element.remove();
  }
}
```

今回は良心的なのでコードを全部書いてあげます。
すなわち自分でご理解くださいませ。

このクラスは、UI を表示するための基底クラスです。
仮想 DOM のように扱えるような`getElementById`なんかを提供しています。

...と、DOM について軽くは触れましたが詳しくは話していませんね。
実際のコードについては次回に触れると思います。

その`getElementById`には何やら特殊なジェネリクスが書いてありますね。

```typescript
export abstract class Screen {
  ...

  public getElementById<HTMLT extends HTMLElement>(
    elementId: string
  ): HTMLT | null {
    const element = this.element.querySelector(
      `#${elementId}`
    ) as HTMLT | null;
    return element;
  }

  ...
}
```

今までの`<T>`と同じように`HTMLT`という名前のジェネリクスタイプを定義しています。
かつ、その`HTMLT`は`HTMLElement`を継承しているという制約を持たせています。
これを使うことで、`getElementById`を呼び出す際に、返り値の型を指定できるようになるかつ、その型が HTML の Element であることを保証できます。

これをうまく活用したうえで、継承先のクラスで色々書いて行けばいい感じになるかなと思います。
といっても、HTML 直書きではなく Element を生成していくので少しめんどくさいかもしれませんが！

# これを継承するためには

これからの皆さんの拡張性を信じて、このクラスの継承の仕方を書いておきます。

まずこのクラス自体にはコンストラクタはないのですが、まぁめんどくさくないように継承先ではコンストラクタは書いておきましょう。
そこで UI の構築を行います。

また、その時に`const screen = this.build();`とかって呼び出すことで、HTML 上に名前空間が確保されて、後は`appendChild`とか`insertAdjacentHTML`とかで好きに書くことが出来ます。

では次回は実際に継承して、セリフを表示する UI を作ってみましょう！
ついに喋ります。