長年、ウェブ デベロッパーは、ウェブ上で複雑でインタラクティブなビジュアル アプリケーションを構築する際に、難しいアーキテクチャ上の選択を迫られてきました。DOM の豊富なセマンティック機能を利用するか、低レベルのグラフィック パフォーマンスのために <canvas> 要素に直接レンダリングするかです。
新しい試験運用版の HTML-in-Canvas API(現在オリジン トライアルで利用可能)を使用すると、どちらかを選択する必要がなくなります。この API を使用すると、UI の操作性、アクセシビリティ、お気に入りのブラウザ機能との連携を維持しながら、DOM コンテンツを 2D キャンバスまたは WebGL/WebGPU テクスチャに直接描画できます。HTML と低レベルのグラフィック処理を組み合わせることで、これまで不可能だったエクスペリエンスを作成できます。
DOM と Canvas
この新しい API の威力を理解するには、DOM と Canvas の相対的な強みを把握することが重要です。
DOM はウェブ UI の基本です。セマンティックに理解されたコンテンツを使用してリッチ インターフェースを作成し、テキスト レイアウト ソリューションをすぐに利用できます。これにより、ユーザーはウェブページ全体で一般的な操作をシームレスに実行できます。たとえば、コピーするテキストをハイライト表示したり、画像を右クリックして保存したりといった、普段当たり前のように行っている操作です。DOM は、ユーザー補助ツール、翻訳、ページ内検索、リーダー モード、拡張機能、ダークモード、ブラウザのズーム、自動入力などの重要なブラウザ機能とも統合されています。
一方、Canvas(および WebGL/WebGPU)では、高度な 2D および 3D グラフィックのピクセル グリッドを駆動するための低レベル アクセスが可能です。ゲームや複雑なウェブアプリ(Google ドキュメントや Figma など)では、このパフォーマンスの高い低レベル アクセスが必要です。キャンバスは基本的にピクセルのグリッドであるため、レスポンシブ テキストなどの機能をサポートするには、複雑なカスタム UI ロジックが必要になり、バンドルサイズが大幅に増加していました。重要なのは、UI が静的なキャンバスのピクセル グリッド内に閉じ込められると、DOM に統合された強力なブラウザ機能がすべて完全に機能しなくなることです。
DOM を Canvas に取り込むメリット
HTML-in-Canvas API は、両方のメリットを活かすための橋渡しとなります。HTML を <canvas> 要素内に配置し、その変換を同期することで、コンテンツの完全なインタラクティブ性が維持され、すべてのブラウザ統合が自動的に機能します。
<canvas> 要素内の UI を DOM に処理させることで、次のメリットが得られます。
- テキストのレイアウトと書式設定: CSS スタイルが適用された複数行または双方向のテキストなど、テキストのレイアウトと書式設定を簡略化します。
- フォーム コントロール: 表現力豊かで使いやすいフォーム コントロール。カスタマイズ オプションも豊富です。
- テキストの選択、コピー/ペースト、右クリック: 3D シーン内のテキストをハイライト表示したり、コンテキスト メニューを右クリックしたりできます。
- テキストの選択、コピー/ペースト、右クリック: 3D シーン内のテキストをハイライト表示したり、コンテキスト メニューを右クリックしたりできます。
- アクセシビリティ: キャンバス内でレンダリングされたコンテンツは、アクセシビリティ ツリーに公開されます。ユーザー補助システムは、通常の HTML と同様に UI を解析し、スクリーン リーダーなどのシステムに公開できます。
- Find-in-page: ユーザーはページ内検索(Ctrl/Cmd + F)を使用してテキストを検索できます。ブラウザは WebGL テクスチャ内でテキストを直接ハイライト表示します。
- Find-in-page: ユーザーはページ内検索(Ctrl/Cmd + F)を使用してテキストを検索できます。ブラウザは WebGL テクスチャ内でテキストを直接ハイライト表示します。
- インデックス登録可能性と AI エージェントのインターフェース: ウェブ クローラーと AI エージェントは、2D シーンと 3D シーンにレンダリングされたテキストをシームレスにインデックス登録して読み取ることができます。
- 拡張機能の統合: ブラウザの拡張機能がネイティブに動作します。たとえば、テキスト置換拡張機能は、3D メッシュでレンダリングされたテキストを自動的に更新します。
- DevTools の統合: Chrome DevTools で、WebGL/WebGPU UI 要素を含むキャンバス コンテンツを直接検査できます。インスペクタで CSS スタイルを調整すると、3D テクスチャにすぐに反映されます。
ユースケースの概要
この API は、複数のドメインにわたって大きな可能性を秘めています。
- キャンバスベースの大型アプリケーション: Google ドキュメント、Miro、Figma などのヘビーウェイトのウェブアプリで、複雑なアプリケーション UI コンポーネントをキャンバス駆動のワークスペースにネイティブにレンダリングできるようになり、アクセシビリティが向上し、バンドルの重さが軽減されます。
- 3D シーンとゲーム: マーケティング サイト、没入型 WebXR エクスペリエンス、ウェブゲームで、完全にインタラクティブなウェブ UI を 3D シーンに配置できるようになりました。たとえば、実際の DOM テキストを使用する 3D ブックや、コピー&ペーストをネイティブにサポートするゲーム内ターミナルなどです。
API の使用にあたっての注意事項
API の使用は、キャンバスの設定、キャンバスへのレンダリング、ブラウザが画面上の要素の物理的な位置を認識できるように CSS 変換を更新する、という 3 つのフェーズで行われます。
前提条件
HTML-in-Canvas API は、Chrome 148 ~ 150 でオリジン トライアルを実施しています。サイトでテストするには、Chrome Canary 149 以降で chrome://flags/#canvas-draw-element フラグを有効にします。他のユーザーが API を利用できるようにするには、オリジン トライアルに登録します。
ステップ 1: 基本的なキャンバスの設定
まず、<canvas> タグに layoutsubtree 属性を追加します。これにより、ブラウザはキャンバス内にネストされたコンテンツを認識し、キャンバス内に表示する準備を整え、アクセシビリティ ツリーに公開します。
<canvas id="canvas" style="width: 200px; height: 200px;" layoutsubtree>
<div id="form_element">
<label for="name">Name:</label> <input id="name" type="text">
</div>
</canvas>
キャンバス グリッドのサイズを変更する
レンダリングされたコンテンツがぼやけないようにするには、デバイスのスケール ファクタに合わせてキャンバス グリッドのサイズを設定してください。
const observer = new ResizeObserver(([entry]) => {
const dpc = entry.devicePixelContentBoxSize;
canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);
canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);
});
const supportsDevicePixelContentBox =
typeof ResizeObserverEntry !== 'undefined' &&
'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);
ステップ 2: レンダリング
2D コンテキストの場合は、drawElementImage メソッドを使用します。これは、要素が再描画されるたびにトリガーされる paint イベント内で行います(テキストのハイライト表示やユーザー入力など)。インタラクティビティが引き続き機能するように、要素の CSS 変換を戻り値で更新することが重要です。
const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
let transform = ctx.drawElementImage(form_element, 0, 0);
// Use the transform returned later on...
};
WebGL でレンダリングする
WebGL の場合は、texElementImage2D を使用します。texImage2D と同様に機能しますが、ソースとして DOM 要素を受け取ります。
canvas.onpaint = () => {
if (gl.texElementImage2D) {
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
}
};
WebGPU でレンダリングする
WebGPU は、copyExternalImageToTexture と同様に、デバイスキューで copyElementImageToTexture メソッドを使用します。
canvas.onpaint = () => {
root.device.queue.copyElementImageToTexture(
valueElement,
{ texture: targetTexture }
);
};
ステップ 3: CSS 変換を更新する
要素をキャンバスにレンダリングしたら、その要素の位置をブラウザに通知する必要があります。これにより、キャンバスと DOM のレイアウト間の空間同期が保証されます。これは、ブラウザがイベント ゾーン(ユーザーがクリックまたはホバーした場所)と要素がレンダリングされた場所を正しくマッピングするために重要です。
2D コンテキストの場合、レンダリング呼び出しから返された変換を .style.transform property に適用します。
const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
let transform = ctx.drawElementImage(form_element, 0, 0);
// Sync the DOM location with the drawn location
form_element.style.transform = transform.toString();
};
WebGL または WebGPU では、要素の画面上の位置は、シェーダーコードによる出力テクスチャの使用方法によって決まり、キャンバス レンダリング コンテキストから推測することはできません。ただし、シェーダー プログラムで一般的なモデルビュー投影を使用してテクスチャを描画する場合は、新しい便利な関数 element.getElementTransform() を使用して、drawElementImage() の戻り値と同じ方法で使用できる変換を計算できます。そのためには、次の操作を行います。
- WebGL の MVP 行列を DOM 行列に変換します。
- HTML 要素を正規化します。HTML 要素のサイズはピクセル単位で指定します(幅 200 ピクセルなど)。ただし、WebGL では通常、オブジェクトは 0 ~ 1 の範囲の「単位正方形」として扱われます。正規化しないと、200 px のボタンは 200 倍大きく表示されます。
- キャンバスのビューポートにマッピングします。このステップは「リスケーリング」フェーズです。このフェーズでは、単位空間の計算を元に戻して、画面上の
<canvas>要素の実際のピクセル寸法に合わせます。また、WebGL では上方向が正の方向ですが、CSS では下方向が正の方向であるため、Y 軸を反転させます。 - 最終変換を計算します。行列を
Viewport * MVP * Normalization.の順序で乗算します。これらを 1 つの最終的な変換に結合すると、3D 描画に合わせて HTML 要素レイヤを配置する正確な位置をブラウザに伝える「地図」が生成されます。 - HTML 要素に変換を適用します。これにより、HTML 要素レイヤがレンダリングされたピクセルの真上に移動します。これにより、ユーザーがボタンをクリックしたりテキストを選択したりしたときに、実際の HTML 要素がヒットするようになります。
if (canvas.getElementTransform) {
// 1. Convert WebGL MVP Matrix to DOM Matrix
const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));
// 2. Normalize the HTML element (pixels -> 1x1 unit square)
const width = targetHTMLElement.offsetWidth;
const height = targetHTMLElement.offsetHeight;
const cssToUnitSpace = new DOMMatrix()
.scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y
.translate(-width / 2, -height / 2); // Center the element
// 3. Map to the canvas viewport
const clipToCanvasViewport = new DOMMatrix()
.translate(canvas.width / 2, canvas.height / 2) // Move origin to center
.scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions
// 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)
const screenSpaceTransform = clipToCanvasViewport
.multiply(mvpDOM)
.multiply(cssToUnitSpace);
// 5. Apply to the transform
const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
if (computedTransform) {
targetHTMLElement.style.transform = computedTransform.toString();
}
}
ライブラリとフレームワークのサポート
人気のライブラリの一部では、HTML-in-Canvas 機能のサポートがすでに提供されています。
Three.js
マトリックスを手動で更新するのは面倒なため、フレームワークはすでにこの機能に対応しています。Three.js には、新しい THREE.HTMLTexture を使用した試験運用版のサポートがあります。
const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
PlayCanvas
PlayCanvas は、テクスチャ API を使用した HTML-in-Canvas もサポートしています。
// Wait for the 'paint' event to set the source
canvas.addEventListener('paint', () => {
htmlTexture.setSource(htmlElement);
}, { once: true });
canvas.requestPaint();
// Keep up to date
canvas.addEventListener('paint', onPaintUpload);
const material = new pc.StandardMaterial();
material.diffuseMap = htmlTexture;
material.update();
デモ
デモを試す前に、環境が適切に構成されていることを確認してください。
API の使用方法の参考となるデモがいくつかあります。すでに、翻訳可能な 3D 書籍からガラス シェーダーで屈折する UI 要素まで、コミュニティからさまざまなクリエイティブなソリューションが生まれています。
- 3D の書籍: ページに HTML レイアウトを使用する WebGL レンダリングの 3D の書籍。ユーザーは CSS でフォントを切り替えることができます。DOM ベースであるため、組み込みの翻訳は瞬時に機能し、AI エージェントは複雑さを軽減してテキストを抽出できます。
- インタラクティブな 3D UI: 基盤となる 3D モデルに基づいて光を屈折させながら、標準の HTML
<input type="range">ステップ属性にも対応する WebGPU ゼリー スライダー。 - アニメーション テクスチャ: カスタム アニメーション ループを必要とせずに、DOM を使用してアニメーション SVG 鉛筆を WebGL テクスチャに直接レンダリングする動的 3D ビルボード。
- 屈折オーバーレイ: 3D カーソルの動きによって歪められるインタラクティブなタイポグラフィ レイヤ。ページ内検索を使用して完全に選択可能で検索可能。
コミュニティが作成したデモのコレクションをご覧ください。このコレクションに HTML-in-Canvas デモを掲載したい場合は、pull リクエストを作成して追加してください。
制限事項
この API は強力ですが、いくつかの制限があります。
- クロスオリジン コンテンツ: セキュリティとプライバシー上の理由から、この API はクロスオリジン iframe コンテンツでは動作しません。
- メインスレッドのスクロール: HTML-in-canvas は JavaScript で描画されるため、キャンバス外のように、スクロールやアニメーションを JavaScript とは独立して更新することはできません。デベロッパーは、スクロール コンテンツをキャンバス内に配置する場合と、キャンバス全体をスクロールする場合のパフォーマンス特性を慎重に検討する必要があります。
フィードバック
HTML-in-Canvas API を試している場合は、ぜひご意見をお聞かせください。オリジン トライアルに登録すると、試験運用中のサイトでこの機能を有効にして、API 設計の改善にご協力いただけます。問題を報告してフィードバックを提供することもできます。