HTML in Canvas API は WICG で提案されている実験的な API で、Canvas 内に直接 HTML を描画できるようにするものです。現在の <canvas> 要素にはリッチテキストや HTML コンテンツを描画する標準的な方法が存在しないという課題があります。fillText() メソッドはテキストを描画するための基本的な機能を提供していますが、レイアウトやスタイリングの制御が限られており、開発者は複雑なテキスト描画を実現するためにサードパーティのライブラリや独自の実装に頼る必要がありました。その結果、アクセシビリティ上の問題やパフォーマンスの低下が生じることがありました。
参考までに、fillText() を使用してテキストを描画する例は以下のようになります。
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "16px sans-serif";
ctx.fillStyle = "#333";
// 第2引数でx座標、第3引数でy座標を指定
// 実用的にテキストを描画するためには、テキストの幅を計測して改行処理などを実装する必要がある
ctx.fillText("こんにちは", 10, 30);このように fillText() を使用してテキストを描画する場合は改行されないため、一部の文字を太字にするといったインラインスタイルを適用できない、テキストの折り返しやレイアウトの制御を自前で実装する必要があります。またアクセシビリティの観点からも問題があります。アクセシビリティツリーにテキストが含まれないため、スクリーンリーダーなどの支援技術がテキストを認識できません。さらに、テキストの選択やコピーもできないため、ユーザーエクスペリエンスが損なわれる可能性があります。
HTML in Canvas API を使用すると、ブラウザのレイアウトエンジンでレイアウト・描画した HTML を canvas に画像のように転写することで、これらの問題を解決できます。ユースケースとして凡例やラベルのようなグラフのテキスト、ゲーム内の UI テキスト、インタラクティブなコンテンツなどが挙げられます。
この記事では、HTML in Canvas API をどのように使用するのかについて説明します。
HTML in Canvas API は WICG の提案段階にある実験的な API です。現時点では Chrome Canary の chrome://flags/#canvas-draw-element フラグを有効にすることで利用できます。
HTML in Canvas API の使用方法
HTML in Canvas API は以下の 3 つの要素で構成されています。
layoutsubtree属性drawElementImage()メソッドpaintイベント
canvas 要素に layoutsubtree 属性を追加することで、canvas 内に描画したい HTML を定義できます。この段階ではまだ canvas 内には描画されません。
<canvas id="canvas" width="400" height="300" layoutsubtree>
<div id="content">
<h2>こんにちは</h2>
<p>Canvas 内の HTML です</p>
<input type="text" placeholder="入力もできます" />
</div>
</canvas>次に、drawElementImage() メソッドを呼び出すことで、canvas の子要素を描画できます。使用方法は drawImage() メソッドと似ています。戻り値の DOMMatrix オブジェクトを使用して、描画された HTML の位置やサイズを取得できます。この戻り値を使用して、描画された HTML と DOM の位置が一致するように transform を適用できます。これはヒットテストやアクセシビリティ上の問題を解決するために重要です。
const canvas = document.getElementById("canvas");
const content = document.getElementById("content");
const ctx = canvas.getContext("2d");
// content 要素を座標 (100, 0) に描画
const transform = ctx.drawElementImage(content, 100, 0);
// DOM の位置が描画された位置と一致するように transform を適用
content.style.transform = transform.toString();上記のコードを実行すると、以下のように HTML が描画されることが確認できます。もちろん <input> 要素に入力するといったインタラクションも可能です。

canvas の子要素のレンダリングが変更されたとき、paint イベントが発火します。このイベントを購読することにより、canvas 内の HTML が変更されたときに再描画できます。例えば、以下のようにボタンをクリックした時に canvas 内のテキストを変更するコードがあるとしましょう。
<canvas id="canvas" width="400" height="300" layoutsubtree>
<div id="content">
<h2>こんにちは</h2>
<p>Canvas 内の HTML です</p>
</div>
</canvas>
<button id="button">テキストを変更</button>
<script>
const content = document.getElementById("content");
const button = document.getElementById("button");
button.addEventListener("click", () => {
content.querySelector("h2").textContent = "こんにちは、世界!";
});
</script>このままでは、canvas は自動で DOM 再描画と同期されないため、canvas 内のテキストが変更されても変更が反映されません。paint イベントを購読して、canvas 内の HTML が変更されたときに再描画するようにしましょう。event.changedElements には変更された要素が渡されるため、これを drawElementImage() メソッドに渡します。
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.addEventListener("paint", (event) => {
ctx.reset();
ctx.drawElementImage(event.changedElements[0], 100, 0);
});毎フレーム描画する必要があるゲームのようなユースケースでは、requestPaint() を使用して、次のフレームで paint イベントを発火させることができます。これにより、canvas 内の HTML が変更されたときに毎フレーム再描画できます。
let t = 0;
function gameLoop() {
t += 0.01;
canvas.requestPaint();
requestAnimationFrame(gameLoop);
}WebGL との組み合わせ
WebGL コンテキストを使用している場合、3D 空間内に HTML を描画できます。WebGL コンテキストを使用している場合は drawElementImage() の代わりに texElementImage2D() メソッドを使用します。このメソッドの使用方法は texImage2D() メソッドと似ています。texElementImage2D() メソッドは drawElementImage() と異なり、描画された HTML の位置やサイズを取得するための戻り値はありません。そのため、描画された HTML と DOM の位置を一致させるためには、別途 canvas.getElementTransform() メソッドを使用して transform を取得する必要があります。
const canvas = document.getElementById("c");
const el = document.getElementById("el");
const gl = canvas.getContext("webgl");
canvas.onpaint = () => {
gl.texElementImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
el,
);
// シェーダーと同じ回転を DOMMatrix で再現
const drawTransform = new DOMMatrix([
// ...
]);
const cssTransform = canvas.getElementTransform(el, drawTransform);
el.style.transform = cssTransform.toString();
};主なユースケース
HTML in Canvas を使用したケースをいくつか紹介します。ここで紹介する例は Codex に作ってもらったものです。
1 つ目のサンプルは HTML コンテンツ全体に霧のようなエフェクトをかける例です。マウスホバーしたときに、その円の内側に HTML を再描画することで、霧の中から HTML が見えるような表現をしています。
2 つ目のサンプルはライトモードとダークモードの切り替え時に波のようなエフェクトをかける例です。中身の UI は通常の HTML として持ちつつ、アニメーション効果のみを canvas に逃がすことで、複雑なアニメーションも実装できます。
最後のサンプルは、canvas 上に実装されたゲームの例です。ゲームのロジック自体は canvas 内で完結させつつ、名前の入力フォームを HTML で実装しています。Canvas だけで入力フォームを実装すると、日本語入力やフォーカス制御、アクセシビリティ対応まで個別に作り込む必要がありますが、HTML in Canvas を使えばゲーム画面の描画は Canvas に任せつつ、入力 UI は HTML の標準機能をそのまま利用できます。
まとめ
- HTML in Canvas API を使用すると、Canvas 内に直接 HTML を描画できるようになる
- これにより、Canvas 内にテキストを配置するためにサードパーティのライブラリや独自の実装に頼る必要がなくなり、アクセシビリティの向上やパフォーマンスの改善が期待できる
- HTML in Canvas API は
layoutsubtree属性、drawElementImage()メソッド、paintイベントの 3 つの要素で構成されている - WebGL コンテキストを使用している場合は、
texElementImage2D()メソッドを使用して、3D 空間内に HTML を描画できる - HTML in Canvas API を使用することで、HTML コンテンツ全体にエフェクトをかけられる
- ライトモードとダークモードの切り替え時に波のようなエフェクトをかけたり、canvas 上に実装されたゲームの UI を HTML で実装したりできる



