長年にわたり、ウェブ デベロッパーは、ウェブ上で複雑でインタラクティブなビジュアル アプリケーションを構築する際に、アーキテクチャの選択に苦労してきました。DOM の豊富なセマンティック機能に頼るべきか、それとも <canvas> 要素に直接レンダリングして低レベルのグラフィック パフォーマンスを実現すべきか。
新しい試験運用版の HTML-in-Canvas API(オリジン トライアルで利用可能)を使用すると、どちらかを選択する必要がなくなります。この API を使用すると、DOM コンテンツを 2D キャンバスまたは WebGL / WebGPU テクスチャに直接描画できます。UI のインタラクティブ性とユーザー補助機能は維持され、お気に入りのブラウザ機能に接続できます。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> 要素内で DOM に UI を処理させることで、次のようなメリットが得られます。
- テキストのレイアウトと書式設定: テキストのレイアウトと書式設定が簡素化されました。CSS スタイルが適用された複数行テキストや双方向テキストも含まれます。
- フォーム コントロール: 表現力豊かで使いやすいフォーム コントロール。幅広いカスタマイズ オプションが用意されています。
- テキストの選択、コピー / 貼り付け、右クリック: ユーザーは、3D シーン内のテキストをハイライト表示したり、右クリックのコンテキスト メニューをネイティブに使用したりできます。
- ユーザー補助: キャンバス内にレンダリングされたコンテンツは、ユーザー補助ツリーに公開されます。ユーザー補助システムは、通常の HTML と同様に UI を解析し、スクリーン リーダーなどのシステムに公開できます。
- ページ内検索: ユーザーはページ内検索(Ctrl/Cmd+F)を使用してテキストを検索でき、ブラウザは WebGL テクスチャ内で直接テキストをハイライト表示します。
- インデックス登録可能で AI エージェントがインターフェース可能: ウェブ クローラーと AI エージェントは、2D および 3D シーンにレンダリングされたテキストをシームレスにインデックス登録して読み取ることができます。
- 拡張機能の統合: ブラウザ拡張機能はネイティブに動作します。たとえば、テキスト置換拡張機能は、3D メッシュにレンダリングされたテキストを自動的に更新します。
- DevTools の統合: WebGL / WebGPU UI 要素を含むキャンバス コンテンツを Chrome DevTools で直接検査できます。インスペクタで CSS スタイルを調整すると、3D テクスチャにすぐに反映されます。
ユースケースの概要
この API は、複数のドメインにわたって大きな可能性を秘めています。
- 大規模なキャンバスベースのアプリケーション: 重量級のウェブアプリでも、複雑なアプリケーション 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 の基本設定
まず、<canvas> タグに layoutsubtree 属性を追加します。これにより、ブラウザは canvas 内にネストされたコンテンツを認識し、canvas 内に表示する準備を整え、ユーザー補助ツリーに公開します。
<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 要素のサイズはピクセル単位で指定します(例: 幅 200px)。しかし、WebGL では通常、オブジェクトを「単位正方形」(0 ~ 1 の範囲など)として扱います。正規化しないと、200 ピクセルのボタンが 200 倍の大きさで表示されます。
- キャンバスのビューポートにマッピングする。このステップは「再スケーリング」フェーズです。この関数は、単位空間の計算を画面上の
<canvas>要素の実際のピクセル寸法に合わせて引き伸ばします。また、WebGL では上方向が正、CSS では下方向が正となるため、Y 軸を反転させています。 - 最終的な変換を計算する。行列を
Viewport * MVP * Normalization.の順に掛け合わせます。これらを 1 つの最終的な変換に結合すると、HTML 要素レイヤを 3D 描画に合わせるために配置する必要がある正確な位置をブラウザに伝える「マップ」が生成されます。 - 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 ブック: WebGL でレンダリングされた 3D ブック。ページのレイアウトには HTML が使用されています。ユーザーは CSS でフォントを入れ替えることができます。DOM ベースであるため、組み込みの翻訳が即座に機能し、AI エージェントは複雑な処理を必要とせずにテキストを抽出できます。
- インタラクティブな 3D UI: 基盤となる 3D モデルに基づいて光を屈折させる WebGPU ゼリースライダー。標準の HTML
<input type="range">ステップ属性にも応答します。 - アニメーション テクスチャ: カスタム アニメーション ループを必要とせずに、DOM を使用してアニメーション化された SVG 鉛筆を WebGL テクスチャに直接レンダリングする動的な 3D ビルボード。
- 屈折オーバーレイ: 移動する 3D カーソルによって歪むインタラクティブなタイポグラフィ レイヤ。ページ内検索を使用して完全に選択、検索できます。
コミュニティが作成したデモのコレクションをご覧ください。HTML-in-Canvas のデモをこのコレクションに掲載したい場合は、pull リクエストを作成して追加してください。
制限事項
この API は強力ですが、意図的にいくつかの制限が設けられています。
- クロスオリジン コンテンツ: セキュリティとプライバシー上の理由により、API はクロスオリジンの iframe コンテンツでは機能しません。
- メインスレッドのスクロール: HTML-in-canvas は JavaScript で描画されるため、スクロールやアニメーションを JavaScript とは独立して更新することはできません(キャンバス外では可能です)。デベロッパーは、スクロールするコンテンツをキャンバス内に配置する場合と、キャンバス全体をスクロールする場合のパフォーマンス特性を慎重に検討する必要があります。
フィードバック
HTML-in-Canvas API を試用されている方は、ぜひご意見をお聞かせください!オリジン トライアルに登録すると、試験運用段階の機能をサイトで有効にして、API 設計の改善にご協力いただけます。問題を報告してフィードバックを提供することもできます。