# UI表示の設計

## 文字表示の設計指針

3Dアプリケーションの最大の魅力は、ダイナミックな視覚体験にあります。しかし、その体験を最大限に引き出すためには、描画した3Dオブジェクトに加えて、適切な「情報の提示」を組み合わせることが重要です。

利用者に操作方法を伝え、現在の状態を明示し、読み込み時の状況や診断結果を丁寧に説明する。あるいは、物語を伝える会話やチュートリアルを表示させる。これらの要素が揃うことで、アプリケーションは初めて「使いやすい道具」や「没入感のある体験」へと進化します。

一見すると、これらはすべて「文字を画面に出す」という単一の機能に思えるかもしれません。しかし、表示したい情報の性質によって、求められる機能や更新頻度は大きく異なります。

- 毎フレーム変動するスコアやFPS
- 画面端に配置する簡潔な操作説明
- 日本語を含む複数行のヘルプ
- 読み込み失敗時のエラーメッセージ
- 詳細な診断レポート（diagnostics report）
- ボタンや選択肢を伴うブリーフィング
- 開発者や解析ツールに共有するための状態記録
- タッチ環境でキー入力を代替する画面ボタン

これらを単一の仕組みですべて管理しようとすると、ユーザーインターフェイス（UI）が煩雑になり、視認性が著しく低下します。一方で、用途ごとにクラスを増やしすぎると、「どのAPIを使用すべきか」という判断に迷うことになります。

そこで本章では、`webg` における文字表示を「表示したい情報の性質」に基づいて整理していきます。API名を暗記するのではなく、「その情報は短いHUDなのか」「じっくり読ませる文章なのか」「操作を伴うパネルなのか」「記録として残すべきものなのか」という判断基準を明確にすることを目指します。

## 表示経路の全体像

`webg` のUI表示は、大きく分けて次の3つの系統に分類されます。

- **キャンバス HUD (`Text`, `Message`)**
  短いASCII文字を、3Dシーンと同じキャンバスに直接描画します。
- **DOM オーバーレイ (`OverlayPanel`, `CommandPalette`)**
  日本語、長文、ボタン、選択肢、スクロール付きパネルなどをシーン上に重ねて表示します。
- **記録 / 開発補助 (`Diagnostics`, `DebugDock`)**
  内部状態をレポート化し、開発者や解析ツールに共有可能な形式で管理します。

![UI 部品選択表](fig14_01_ui_component_selector.png)

これらに加え、`Touch` は画面上にボタンを表示しますが、これは文字表示のためのAPIではなく「入力API」です。見た目は `OverlayPanel` や `CommandPalette` のボタンに似ていますが、目的はキー入力の代替であるため、詳細は後の章で詳しく解説します。

具体的なAPI選択の判断基準は以下の通りです。

- **`Message` を選択する場合**
  スコア、残機、FPS、簡潔な状態表示など。キャンバス HUD として軽量であり、毎フレームの更新に適しています。
- **`OverlayPanel` を選択する場合**
  日本語の操作説明、ヘルプ、エラー、長文など。DOMベースであるため、UTF-8、可変幅フォント、スクロール、ボタンを自在に扱えます。
- **`OverlayPanel` + コントローラーを選択する場合**
  ボタン待機、選択肢の提示、ブリーフィングなど。表示（UI）と進行制御（ロジック）を分離して管理したい場合に適しています。
- **`CommandPalette` を選択する場合**
  キャンバス上で一時的に開く操作メニューや、簡易的な設定変更など。特定の操作で呼び出し、多様な操作部品をコンパクトにまとめられます。
- **`Diagnostics` / `DebugDock` を選択する場合**
  調査ログや、解析ツールに渡す状態記録など。単なる表示ではなく、「記録」と「共有」を目的としています。
- **`Touch` を選択する場合**
  タッチ操作インターフェースなど。キーボード入力の代替という「入力機能」としての役割を担います。

## HUDとDOMの基礎知識

ここで、UI設計の鍵となる「HUD」と「DOM」という2つの概念について整理しておきましょう。

HUD（Heads-Up Display：ヘッドアップディスプレイ）は、もともと航空機のコクピットなどで、パイロットが視線を正面から外さずに情報を確認できるよう、フロントガラスなどの視界に情報を投影するシステムのことです。コンピューターグラフィックスやゲームにおけるHUDも同様の目的で利用されます。ユーザーが3Dシーンという「主役」から視線を外さず、スコアや体力、FPS（フレームレート）などの補助的な情報をリアルタイムに確認できるよう、画面の最前面に重ねて表示するインターフェースを指します。

一方、DOM（Document Object Model）は、ブラウザがHTML文書をJavaScriptから操作できるようにしたツリー構造です。HTMLの `<div>` や `<button>` といった要素は、ブラウザ内部でDOMノードとして管理されており、JavaScriptを通じて内容の書き換えやスタイルの変更、クリックイベントなどの接続が可能です。

`webg` の3DシーンはWebGPUの `canvas` に描画されます。この `canvas` はピクセル単位で描画する領域であり、3Dモデルや背景、そして「Canvas HUD」はこの描画結果の一部として表示されます。対して、説明文やボタン、エラーパネルのようなUIは、`canvas` の外側または上側にHTML要素として配置できます。この経路を、本章では「DOMオーバーレイ」と呼びます。

Canvas HUD と DOMオーバーレイは、どちらもユーザーには「画面に重なっている情報」に見えますが、内部的な仕組みは大きく異なります。Canvas HUD はWebGPUの描画フローに含まれるため、3Dシーンと同じタイミングで極めて軽量に描画できます。その代わり、標準の文字描画は短いASCII表示を前提としており、日本語の組版や可変幅フォント、テキストの選択、スクロール、ボタン操作などの機能は持ちません。

対して DOMオーバーレイはブラウザのHTML表示そのものであるため、日本語や長文、ボタン、スクロール、コピー可能なテキスト、入力フォームなどを自然に扱うことができます。

この違いを理解すれば、「短い状態表示は Canvas HUD」「読むための説明や操作UIは DOM」という役割分担が明確になります。HUDとオーバーレイは、単に見え方の上下関係で分けるのではなく、描画フローと利用目的の違いに基づいて選択してください。

## 軽量な状態表示を担う `Message`

`Message` は、キャンバス上に簡潔な文字列を重ねるためのHUD管理クラスです。文字グリッドを直接操作するローレベル（低レイヤー）な `Text` クラスをラップし、より使いやすく設計されたハイレベル（高レイヤー）なAPIとなっています。`id`、`anchor`（配置基準点）、複数行ブロックといった概念を備えており、通常のサンプルでは `app.message.setLines()` を使用するのが基本となります。

例えば、現在のスコアと経過時間を左上に表示する場合は、次のように記述します。

```js
app.message.setLines("status", [
  `score=${score}`,
  `time=${elapsedSec.toFixed(1)} sec`
], {
  anchor: "top-left",
  x: 0,
  y: 0
});
```

操作ガイドを画面下側に配置する場合は、別の `id` を指定して別のブロックとして作成します。

```js
app.message.setLines("guide", [
  "Drag: orbit camera",
  "Wheel: zoom",
  "R: reset camera"
], {
  anchor: "bottom-left",
  x: 0,
  y: -2,
  width: 36,
  wrap: true
});
```

`Message` は、短く直感的に理解でき、常時表示されていても視界を妨げない情報に適しています。スコアや残機、現在のモード、デバッグモードのON/OFF、カメラ距離、選択中の座標などが典型的な例です。

一方で、以下のような情報は `Message` には適していません。

- 日本語による詳細な複数行説明
- 長文のエラーメッセージ
- スタックトレースや詳細な診断レポート
- ボタンや選択肢を伴うインタラクティブなUI
- スクロールして閲覧する必要がある文章

`Message` は標準のフォントアトラスと文字グリッドを前提とした軽量なHUDです。日本語や一般的なUTF-8文章を適切に表示したい場合は、DOMオーバーレイへと移行しましょう。

## 読ませる情報を構成する `OverlayPanel`

`OverlayPanel` は、シーン上に重ねて表示するDOMパネルです。日本語の説明、ヘルプ、エラー表示、長文レポート、ブリーフィング、ボタン、選択肢などを一元的に扱います。

ここで重要なのは、`OverlayPanel` が「ヘルプ専用」や「エラー専用」のクラスではないという点です。`OverlayPanel` はあくまで汎用的な「表示基盤」であり、用途による違いはオプション（option）によって表現します。

最小限のヘルプパネルは、次のように構成します。

```js
app.showOverlayPanel({
  id: "help",
  title: "Help",
  lines: [
    "Drag: orbit camera",
    "Wheel: zoom",
    "H: hide help"
  ],
  anchor: "top-left",
  format: "plain",
  collapsible: true,
  closable: false
});
```

本文の指定には、単一の文字列を渡す `text` と、行配列を渡す `lines` があります。これらを同時に指定すると表示内容が曖昧になるため、`OverlayPanel` ではエラーとして処理されます。

```js
app.showOverlayPanel({
  id: "note",
  title: "Note",
  text: "1 つの文字列として渡す場合はこちらを使用します",
  anchor: "bottom-left"
});
```

エラーログのように、空白や改行などの整形を保持したい場合は、`format: "pre"` を指定します。

```js
app.showOverlayPanel({
  id: "load-error",
  title: "Load Error",
  text: error.message,
  anchor: "bottom-right",
  format: "pre",
  scrollY: true,
  closable: true,
  showCloseButton: true,
  maxHeight: "40vh"
});
```

このように、ヘルプもエラーもレポートも、すべて同一の `OverlayPanel` で表現可能です。開発者が習得すべきはクラス名ではなく、`anchor`、`format`、`scrollY`、`collapsible`、`buttons`、`choices` といった具体的なオプションの役割です。

## 配置基準とドックの回避

DOMオーバーレイを設計する際、最初に検討すべきは「配置場所」です。3Dシーンの中央はユーザーが最も注視する領域であるため、パネルを中央に固定すると重要な視覚情報を遮ることになります。

`OverlayPanel` では、以下の `anchor` を提供しています。

| anchor | 主な用途 |
| :--- | :--- |
| `top-left` | 操作説明・補助ヘルプ |
| `top-center` | 一時的な案内 |
| `top-right` | ステータス・補助情報 |
| `middle-left` | 左側メニュー |
| `middle-center` | モーダル・ダイアログ |
| `middle-right` | 右側インスペクター |
| `bottom-left` | ブリーフィング・ダイアログ |
| `bottom-center` | 操作待ち・プロンプト |
| `bottom-right` | ログ・エラー・レポート |

さらに `offsetX` と `offsetY` を指定することで、基準点からの余白を精密に調整できます。

```js
app.showOverlayPanel({
  id: "briefing",
  title: "Mission",
  lines: ["Alpha が beacon を回収します"],
  anchor: "bottom-left",
  offsetX: 16,
  offsetY: 16,
  width: 360
});
```

また、右側に `DebugDock` を表示している場合、右寄せパネルがドックと重なる可能性があります。そのため、`OverlayPanel` には `avoidDebugDock: true` というオプションが用意されており、これを有効にすることでドックの幅を自動的に回避して配置されます。

## ヘルプ機能とプリセットの活用

多くのサンプルでは、操作説明を「折りたたみ可能なパネル」として配置したいケースがあります。本ライブラリでは、ヘルプ機能を専用APIとして提供するのではなく、`OverlayPanel` のオプションの組み合わせで表現します。

直接記述する場合は、以下のようになります。

```js
app.showOverlayPanel({
  id: "runtime-help",
  title: "Help",
  lines: [
    "Drag: orbit",
    "Wheel: zoom",
    "R: reset"
  ],
  anchor: "top-left",
  collapsible: true,
  collapsed: false,
  closable: false,
  showCollapseButton: true,
  collapseLabelExpanded: "Hide Help",
  collapseLabelCollapsed: "Show Help"
});
```

この記述を簡略化したい場合は、`OverlayPanelPresets.js` のヘルパー関数を利用しましょう。

```js
import { buildHelpPanelOptions } from "../../webg/OverlayPanelPresets.js";

app.showOverlayPanel(buildHelpPanelOptions({
  id: "help",
  lines: [
    "Drag: orbit",
    "Wheel: zoom",
    "R: reset"
  ],
  anchor: "top-left"
}));
```

ここで重要なのは、`buildHelpPanelOptions()` は新しいUIクラスを生成しているのではなく、単に適切なオプションオブジェクトを組み立てるヘルパーであるということです。実際に表示を担うのはあくまで `OverlayPanel` です。

## エラー表示と診断レポートの使い分け

エラーメッセージや診断情報の全文は、HUDに表示すべきではありません。HUDは流動的な現在値の表示には適していますが、ユーザーが精読すべき文章には不向きだからです。

エラー表示は、次のように `OverlayPanel` で構成します。

```js
app.showOverlayPanel({
  id: "start-error",
  title: "Start Error",
  text: error.message,
  anchor: "bottom-right",
  format: "pre",
  scrollY: true,
  closable: true,
  showCloseButton: true,
  maxHeight: "40vh"
});
```

同様に、`OverlayPanelPresets.js` に用意されたエラー用ヘルパーを利用することも可能です。

```js
import { buildErrorPanelOptions } from "../../webg/OverlayPanelPresets.js";

app.showOverlayPanel(buildErrorPanelOptions(error, {
  id: "load-error",
  title: "Load Error"
}));
```

ただし、エラーを表示するだけでなく、開発者や解析ツールに共有するための情報として残したい場合は、`Diagnostics` にレポートとして記録することを優先してください。`OverlayPanel` は「閲覧するための場所」であり、`Diagnostics` は「状態を記録するための場所」であるという役割分担を意識しましょう。

## ブリーフィングの進行制御

会話やチュートリアル、ミッションブリーフィングのようなUIは、単なる表示だけでなく「進行制御」を伴います。「次へ進む」「選択肢を選ぶ」「分岐する」「閉じた後にゲームを再開する」といったルールが必要になるためです。

`OverlayPanel` はボタンや選択肢のアクションを返しますが、文章のキュー（待ち行列）や分岐ルール自体は保持しません。これは意図的な設計による分離です。表示部品に進行制御まで含めてしまうと、ゲーム固有のルールが基盤側に混入し、APIの汎用性が損なわれるためです。

最小限のブリーフィングコントローラーは、サンプル側で次のように実装できます。

```js
const briefingEntries = [
  {
    title: "Mission",
    lines: [
      "Alpha が beacon を回収します",
      "goal が開いたら脱出してください"
    ]
  },
  {
    title: "Controls",
    lines: [
      "Arrow keys: move",
      "Enter: next"
    ]
  }
];

let briefingIndex = 0;

function showBriefing() {
  const entry = briefingEntries[briefingIndex];
  app.showOverlayPanel({
    id: "briefing",
    title: entry.title,
    lines: entry.lines,
    anchor: "bottom-left",
    width: 420,
    buttons: [
      { id: "next", label: "Next", kind: "primary" },
      { id: "close", label: "Close", kind: "secondary" }
    ],
    onAction: ({ actionId }) => {
      if (actionId === "close") {
        app.hideOverlayPanel("briefing");
        return;
      }
      briefingIndex += 1;
      if (briefingIndex >= briefingEntries.length) {
        app.hideOverlayPanel("briefing");
      } else {
        showBriefing();
      }
    }
  });
}
```

この構成により、`OverlayPanel` は表示のみを担当し、文章の順序や分岐ロジックはアプリケーション側で完全に制御できます。

## モーダル設定とシーンの一時停止

`OverlayPanel` には `modal` と `pauseScene` という重要なオプションがあります。

`modal` は、DOM上の操作をパネルに集中させるための設定です。背後のUIを操作させたくない確認ダイアログや、開始前のブリーフィングに使用します。

`pauseScene` は、アプリケーション側に「このパネルが表示されている間はシーンの進行を停止すべきである」という状態を伝えるフラグです。なお、`OverlayPanel` 自体が自動的にゲームループを停止させるわけではありません。

```js
app.showOverlayPanel({
  id: "confirm",
  title: "Start?",
  lines: ["この設定で開始しますか"],
  anchor: "middle-center",
  modal: true,
  pauseScene: true,
  buttons: [
    { id: "start", label: "Start", kind: "primary" },
    { id: "cancel", label: "Cancel", kind: "secondary" }
  ],
  onAction: ({ actionId }) => {
    if (actionId === "start") {
      app.hideOverlayPanel("confirm");
      startGame();
    }
  }
});
```

更新ループ側では、必要に応じてこの状態を参照して処理をスキップします。

```js
app.start({
  onUpdate: ({ deltaSec }) => {
    const confirm = app.getOverlayPanel("confirm");
    const state = confirm?.getState?.();
    if (state?.visible && state.pauseScene) {
      return false;
    }

    updateGame(deltaSec);
    return false;
  }
});
```

この設計により、UI部品が勝手にゲーム時間を止めることを避け、シーンを停止させるかどうかの判断をアプリケーション側の設計に委ねることができます。

## 診断情報の記録と共有フロー

3Dアプリケーションでは、画面上の表示だけでは把握できない内部状態が多く存在します。カメラの正確な向き、読み込まれた形状の総数、現在のシェーダー設定、入力の反応状況などは、単にHUDに表示させるだけでは後からの共有や再現が困難です。

そのため、`webg` では `Diagnostics` を「正本」として扱います。まずレポートを作成し、そのレポートを必要に応じて `DebugDock`、コンソール、コピー用テキスト、JSON、`OverlayPanel` などへ出力するフローを推奨しています。

`DebugDock` はPC向けの開発補助表示であり、右側にコントロール、診断レポート、プローブ状態などを集約することで、キャンバスHUDを情報で埋め尽くすことなく詳細な状態を監視できます。

情報を整理すると、配置は以下のようになります。

- **利用者が瞬時に把握すべき現在値** $\rightarrow$ `Message`
- **利用者が精読すべき説明文** $\rightarrow$ `OverlayPanel`
- **開発者や解析ツールに共有する状態** $\rightarrow$ `Diagnostics`
- **開発中に常時監視したい詳細情報** $\rightarrow$ `DebugDock`

この分離は、特にAIを活用したデバッグにおいて重要です。画面上の文字を増やすのではなく、再現可能なレポートとして状態を渡せるようにしておくことで、問題の切り分けが飛躍的に容易になります。

## 入力インターフェースとしての `Touch`

`Touch` はDOMにボタンを表示しますが、その役割は文字表示ではありません。`Touch` は、キーボードの代わりに `arrowleft`、`space`、`enter`、`r` などのキー状態やアクションを発生させる「入力API」です。

したがって、「ボタンが必要だから」という理由で全てを `Touch` に寄せるのではなく、役割で明確に使い分けてください。

- **タッチ操作で移動、決定、リセットを入力したい** $\rightarrow$ `Touch`
- **シーン上に説明や選択肢を表示したい** $\rightarrow$ `OverlayPanel`
- **キャンバス上で一時的に開くコマンドや軽い設定を置きたい** $\rightarrow$ `CommandPalette`
- **現在値を簡潔に表示したい** $\rightarrow$ `Message`

この区別を明確にすることで、入力処理と表示処理が混在することを防げます。

## UI実装時の判断フロー

新しいサンプルを構築する際は、以下の手順で検討すると整理しやすくなります。

1. **情報の性質を判断する**: 短い現在値か、読ませる文章か。
2. **短いASCIIの現在値なら**: `Message` に配置する。
3. **日本語、長文、ボタン、選択肢が必要なら**: `OverlayPanel` に配置する。
4. **進行制御（分岐や順序）が必要なら**: サンプル側にコントローラーを実装する。
5. **キャンバス上で低頻度の操作や軽い設定を出したいなら**: `CommandPalette` に配置する。
6. **調査や共有が必要な情報なら**: `Diagnostics` にレポートとして記録する。
7. **タッチ操作が必要なら**: `Touch` を入力APIとして追加する。

例えば、ローダー系のサンプルでは、「読み込み中の短い状態 $\rightarrow$ `Message`」、「読み込み失敗の全文 $\rightarrow$ `OverlayPanel`」、「読み込み対象や統計値 $\rightarrow$ `Diagnostics`」と分けることで、非常に見通しの良いUIになります。

## まとめ

本章では、`webg` における文字表示の選択基準を整理しました。

- 短い ASCII HUD $\rightarrow$ `Message`
- 日本語、長文、ヘルプ、エラー、ボタン付き説明 $\rightarrow$ `OverlayPanel`
- 定型オプションの簡略化 $\rightarrow$ `OverlayPanelPresets`
- キャンバス上の一時的なコマンド、toggle、stepper、select $\rightarrow$ `CommandPalette`
- 会話やブリーフィングの進行 $\rightarrow$ サンプル側コントローラー
- 調査記録 $\rightarrow$ `Diagnostics` / `DebugDock`
- タッチ操作 $\rightarrow$ `Touch`

この分類により、UI表示を「どのクラスを使うか」という視点ではなく、「どの情報をどの表示面に配置するか」という設計視点で考えることができます。次章では、この分担が内部的にどのように実装されているかを、キャンバスHUDとDOMオーバーレイの構造から詳しく見ていきましょう。
