HTML-in-Canvas API オリジン トライアルの導入

Thomas Nattestad
Thomas Nattestad

ウェブ デベロッパーは長年、ウェブ上で複雑でインタラクティブ性の高いビジュアル アプリケーションを構築する際に、難しいアーキテクチャの選択を迫られてきました。豊富なセマンティック機能を備えた 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> 要素内で DOM に UI を処理させることで、次のメリットが得られます。

  • テキストのレイアウトと書式設定: CSS スタイルが適用された複数行または双方向のテキストなど、テキストのレイアウトと書式設定が簡素化されます。
  • フォーム コントロール: 豊富なカスタマイズ オプションを備えた、表現力豊かで使いやすいフォーム コントロール。
  • テキストの選択、コピー/貼り付け、右クリック: ユーザーは 3D シーン内のテキストをハイライト表示したり、コンテキスト メニューをネイティブに右クリックしたりできます。
  • テキストの選択、コピー/貼り付け、右クリック: ユーザーは 3D シーン内のテキストをハイライト表示したり、コンテキスト メニューをネイティブに右クリックしたりできます。
  • アクセシビリティ: キャンバス内にレンダリングされたコンテンツは、アクセシビリティ ツリーに公開されます。ユーザー補助システムは、通常の HTML と同様に UI を解析し、スクリーン リーダーなどのシステムに公開できます。
  • ページ内検索: ユーザーはページ内検索(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://flags/#canvas-draw-element フラグを有効にして、Chrome Canary 149 以降を使用します。他のユーザーが API を有効にできるようにするには、オリジン トライアルに登録します。

ステップ 1: 基本的な Canvas の設定

まず、layoutsubtree 属性を <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 は、デバイスキューで copyElementImageToTexture メソッドを使用します。これは copyExternalImageToTexture と同様です。

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 ピクセルのボタンが 200 倍大きく表示されます。
  • キャンバスのビューポートにマッピングします。このステップは「再スケーリング」フェーズです。単位空間の計算を元に戻して、画面上の <canvas> 要素の実際のピクセル寸法に合わせます。また、Y 軸を反転します。WebGL では上が正ですが、CSS では下が正です。
  • 最終的な変換を計算します。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 モデルに基づいて光を屈折させる 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 設計の改善にご協力いただけます。問題を報告してフィードバックを提供することもできます。

リソース