多年來,網頁程式開發人員在網路上建構複雜、高度互動的視覺應用程式時,必須做出艱難的架構選擇:應該依賴 DOM 的豐富語意功能,還是直接轉譯至 <canvas> 元素,以獲得低階圖形效能?
有了全新的實驗性 HTML-in-Canvas API (目前在來源試用階段),您就不必再做選擇。這個 API 可讓您直接在 2D 畫布或 WebGL/WebGPU 紋理中繪製 DOM 內容,同時保持 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> 元素中,並同步其轉換,就能確保內容保持完整互動,且所有瀏覽器整合功能都會自動運作。
讓 DOM 處理 <canvas> 元素中的 UI,可帶來以下好處:
- 文字版面配置和格式:簡化文字版面配置和格式,包括套用 CSS 樣式的多行或雙向文字。
- 表單控制項:更具表現力且更易於使用的表單控制項,提供多種自訂選項。
- 選取文字、複製/貼上和按一下滑鼠右鍵:使用者可以醒目顯示 3D 場景中的文字,或按一下滑鼠右鍵開啟原生內容選單。
- 無障礙功能:在畫布中算繪的內容會顯示在無障礙樹狀結構中。無障礙系統可以像剖析一般 HTML 一樣剖析 UI,並將其提供給螢幕閱讀器等系統。
- 網頁內搜尋:使用者可以透過網頁內搜尋功能 (Ctrl/Cmd+F) 搜尋文字,瀏覽器會直接在 WebGL 紋理中醒目顯示該文字。
- 可索引性且可與 AI 代理互動:網路檢索器和 AI 代理可順暢建立索引,並讀取 2D 和 3D 場景中算繪的文字。
- 擴充功能整合:瀏覽器擴充功能可原生運作。舉例來說,文字替換擴充功能會自動更新 3D 網格上顯示的文字。
- 開發人員工具整合:您可以直接在 Chrome 開發人員工具中檢查畫布內容,包括 WebGL/WebGPU UI 元素。在檢查器中調整 CSS 樣式,3D 紋理就會立即更新!
一般應用情況
這項 API 可在多個領域發揮驚人潛力:
- 大型 Canvas 應用程式:重量級網頁應用程式現在可以將複雜的應用程式 UI 元件,原生轉譯至 Canvas 驅動的工作區,提升無障礙程度並減輕套件重量。
- 3D 場景和遊戲:行銷網站、沉浸式 WebXR 體驗和網頁遊戲現在可以將完全可互動的網頁 UI 放入 3D 場景,例如使用實際 DOM 文字的 3D 書籍,或原生支援複製和貼上的遊戲內終端機。
如何使用 API
使用 API 的過程分為三個階段:設定畫布、在畫布中算繪,以及更新 CSS 轉換,讓瀏覽器知道元素在畫面上的實際位置。
必要條件
HTML-in-Canvas API 正在 Chrome 148 至 150 版進行來源試用。如要在網站上測試,請使用 Chrome Canary 149 以上版本,並啟用 chrome://flags/#canvas-draw-element 旗標。如要為其他使用者啟用 API,請註冊來源試用。
Step 1: Basic Canvas setup
首先,請在 <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 會在裝置佇列上使用 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>元素的實際像素尺寸。此外,由於 WebGL 的 Y 軸向上為正,CSS 則是向下為正,因此 Y 軸也會翻轉。 - 計算最終轉換。依序乘以矩陣:
Viewport * MVP * Normalization.將矩陣合併為最終轉換,會產生「地圖」,告訴瀏覽器 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 在 Canvas 中嵌入 HTML:
// 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:WebGPU 果凍滑桿會根據基礎 3D 模型折射光線,同時仍會回應標準 HTML
<input type="range">步驟屬性。 - 動畫紋理:動態 3D 廣告看板,使用 DOM 將動畫 SVG 鉛筆直接算繪至 WebGL 紋理,不需要自訂動畫迴圈。
- 折射疊加層:互動式字體排版層,會隨著移動的 3D 游標扭曲,但仍可使用「在網頁中尋找」功能選取及搜尋。
歡迎查看社群建立的示範集合。如要將 HTML-in-Canvas 示範加入這個集合,請建立提取要求。
限制
雖然 API 功能強大,但有一些刻意設定的限制:
- 跨來源內容:基於安全和隱私考量,API 不支援跨來源 iframe 內容。
- 主執行緒捲動:HTML-in-canvas 是使用 JavaScript 繪製,因此捲動和動畫無法像在畫布外一樣,獨立於 JavaScript 更新。開發人員應仔細考量效能特性,決定要將捲動內容放在畫布內,還是讓整個畫布捲動。
意見回饋
如果您正在試用 HTML-in-Canvas API,歡迎與我們分享意見!您可以註冊來源試用,在實驗階段啟用網站上的功能,協助我們調整 API 設計。您也可以提交問題,提供任何意見回饋。