Trong nhiều năm, các nhà phát triển web đã phải đưa ra một lựa chọn khó khăn về kiến trúc khi xây dựng các ứng dụng trực quan phức tạp, có tính tương tác cao trên web: bạn có dựa vào DOM vì các tính năng ngữ nghĩa phong phú của nó hay bạn kết xuất trực tiếp vào phần tử <canvas> để có hiệu suất đồ hoạ cấp thấp?
Với HTML-in-Canvas API thử nghiệm mới (hiện có trong bản dùng thử theo nguyên gốc), bạn không cần phải chọn. API này cho phép bạn vẽ nội dung DOM trực tiếp vào canvas 2D hoặc một hoạ tiết WebGL/WebGPU trong khi vẫn giữ cho giao diện người dùng có thể tương tác, dễ truy cập và kết nối với các tính năng trình duyệt mà bạn yêu thích. Bằng cách kết hợp HTML với quy trình xử lý đồ hoạ cấp thấp, bạn có thể tạo ra những trải nghiệm mà trước đây không thể thực hiện được.
DOM so với Canvas
Để hiểu được sức mạnh của API mới này, bạn nên xem xét các điểm mạnh tương đối của cả DOM và Canvas.
DOM là thành phần chính của giao diện người dùng web. Thư viện này cung cấp các giải pháp bố cục văn bản ngay lập tức, sử dụng nội dung được hiểu theo ngữ nghĩa để tạo giao diện phong phú. Điều này cho phép người dùng thực hiện các thao tác phổ biến trên các trang web một cách liền mạch – những việc mà chúng ta thường coi là đương nhiên, chẳng hạn như đánh dấu văn bản để sao chép hoặc nhấp chuột phải vào hình ảnh để lưu. DOM cũng tích hợp với các tính năng thiết yếu của trình duyệt: công cụ hỗ trợ tiếp cận, dịch, tìm kiếm trên trang, chế độ đọc, tiện ích, chế độ tối, tính năng thu phóng trình duyệt và tự động điền.
Mặt khác, Canvas (và WebGL/WebGPU) cho phép truy cập ở cấp độ thấp để điều khiển một lưới gồm các pixel cho đồ hoạ 2D và 3D có độ phức tạp cao. Các trò chơi và ứng dụng web phức tạp (như Google Tài liệu hoặc Figma) yêu cầu quyền truy cập hiệu suất cao, cấp thấp này. Vì canvas về cơ bản là một lưới pixel, nên các tính năng hỗ trợ như văn bản thích ứng thường đòi hỏi logic giao diện người dùng tuỳ chỉnh phức tạp, làm tăng đáng kể kích thước gói của bạn. Điều quan trọng là tất cả các tính năng mạnh mẽ của trình duyệt được tích hợp vào DOM sẽ hoàn toàn bị gián đoạn khi giao diện người dùng bị mắc kẹt bên trong một lưới pixel canvas tĩnh.
Ưu điểm của việc đưa DOM vào Canvas
HTML-in-Canvas API là cầu nối giúp bạn tận dụng tối đa cả hai thế giới. Bằng cách đặt HTML bên trong phần tử <canvas> và đồng bộ hoá phép biến đổi của phần tử đó, bạn đảm bảo nội dung vẫn hoàn toàn có tính tương tác và tất cả các tính năng tích hợp trình duyệt đều hoạt động tự động.
Sau đây là những gì bạn nhận được khi cho phép DOM xử lý giao diện người dùng của bạn trong phần tử <canvas>:
- Bố cục và định dạng văn bản: Bố cục và định dạng văn bản đơn giản, bao gồm cả văn bản nhiều dòng hoặc hai chiều có áp dụng kiểu CSS.
- Các chế độ kiểm soát biểu mẫu: Các chế độ kiểm soát biểu mẫu dễ sử dụng và có nhiều lựa chọn tuỳ chỉnh.
- Chọn văn bản, sao chép/dán và nhấp chuột phải: Người dùng có thể làm nổi bật văn bản bên trong cảnh 3D hoặc nhấp chuột phải vào trình đơn ngữ cảnh một cách tự nhiên.
- Chọn văn bản, sao chép/dán và nhấp chuột phải: Người dùng có thể làm nổi bật văn bản bên trong cảnh 3D hoặc nhấp chuột phải vào trình đơn ngữ cảnh một cách tự nhiên.
- Khả năng hỗ trợ tiếp cận: Nội dung được kết xuất bên trong canvas sẽ được hiển thị trong cây hỗ trợ tiếp cận. Các hệ thống hỗ trợ tiếp cận có thể phân tích cú pháp giao diện người dùng như cách chúng phân tích cú pháp HTML thông thường và hiển thị giao diện người dùng đó cho các hệ thống như trình đọc màn hình.
- Find-in-page: Người dùng có thể sử dụng tính năng tìm nội dung trên trang (Ctrl/Cmd+F) để tìm kiếm văn bản và trình duyệt sẽ làm nổi bật văn bản đó ngay trong các hoạ tiết WebGL của bạn.
- Find-in-page: Người dùng có thể sử dụng tính năng tìm nội dung trên trang (Ctrl/Cmd+F) để tìm kiếm văn bản và trình duyệt sẽ làm nổi bật văn bản đó ngay trong các hoạ tiết WebGL của bạn.
- Khả năng được lập chỉ mục và giao diện có thể tương tác với tác nhân AI: Trình thu thập dữ liệu web và tác nhân AI có thể lập chỉ mục và đọc văn bản được kết xuất vào cảnh 2D và 3D một cách liền mạch.
- Tích hợp tiện ích: Tiện ích trên trình duyệt hoạt động một cách tự nhiên. Ví dụ: tiện ích thay thế văn bản sẽ tự động cập nhật văn bản được kết xuất trên các lưới 3D của bạn.
- Tích hợp DevTools: Bạn có thể kiểm tra nội dung canvas, bao gồm cả các phần tử giao diện người dùng WebGL/WebGPU ngay trong Chrome DevTools. Điều chỉnh một kiểu CSS trong trình kiểm tra và xem kiểu đó cập nhật ngay lập tức trên hoạ tiết 3D!
Các trường hợp sử dụng cấp cao
API này mở ra tiềm năng to lớn trên nhiều lĩnh vực:
- Các ứng dụng dựa trên canvas lớn: Các ứng dụng web có dung lượng lớn như Google Tài liệu, Miro hoặc Figma hiện có thể kết xuất các thành phần giao diện người dùng ứng dụng phức tạp một cách tự nhiên vào không gian làm việc dựa trên canvas, giúp cải thiện khả năng tiếp cận và giảm dung lượng gói.
- Cảnh và trò chơi 3D: Các trang web tiếp thị, trải nghiệm WebXR sống động và trò chơi trên web hiện có thể đặt giao diện người dùng web có thể tương tác đầy đủ vào cảnh 3D, chẳng hạn như một cuốn sách 3D sử dụng văn bản DOM thực hoặc một thiết bị đầu cuối trong trò chơi hỗ trợ nguyên bản việc sao chép và dán.
Cách sử dụng API
Việc sử dụng API diễn ra theo 3 giai đoạn: Thiết lập canvas, kết xuất vào canvas và cập nhật biến đổi CSS để trình duyệt biết vị trí thực của phần tử trên màn hình.
Điều kiện tiên quyết
HTML-in-Canvas API đang trong giai đoạn dùng thử theo nguyên gốc trong Chrome 148 đến 150. Để thử nghiệm tính năng này trên trang web của bạn, hãy sử dụng Chrome Canary 149 trở lên và bật cờ chrome://flags/#canvas-draw-element. Để bật API cho những người dùng khác, hãy đăng ký Origin Trial.
Bước 1: Thiết lập Canvas cơ bản
Trước tiên, hãy thêm thuộc tính layoutsubtree vào thẻ <canvas>. Điều này giúp trình duyệt nhận biết nội dung được lồng bên trong canvas, chuẩn bị nội dung đó để hiển thị bên trong canvas và hiển thị nội dung đó cho các cây hỗ trợ tiếp cận.
<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>
Điều chỉnh kích thước lưới trên khung vẽ
Để tránh nội dung được kết xuất bị mờ, hãy đảm bảo bạn điều chỉnh kích thước lưới canvas sao cho phù hợp với hệ số tỷ lệ của thiết bị.
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);
Bước 2: Kết xuất
Đối với ngữ cảnh 2D, hãy sử dụng phương thức drawElementImage. Hãy thực hiện việc này trong sự kiện paint. Sự kiện này sẽ kích hoạt bất cứ khi nào phần tử vẽ lại, chẳng hạn như trong quá trình đánh dấu văn bản hoặc hoạt động đầu vào của người dùng. Điều quan trọng là bạn phải cập nhật biến đổi CSS của phần tử bằng giá trị trả về để hoạt động tương tác tiếp tục diễn ra.
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...
};
Kết xuất bằng WebGL
Đối với WebGL, bạn sẽ sử dụng texElementImage2D. Hàm này hoạt động tương tự như texImage2D, nhưng lấy phần tử DOM làm nguồn.
canvas.onpaint = () => {
if (gl.texElementImage2D) {
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
}
};
Kết xuất bằng WebGPU
WebGPU sử dụng phương thức copyElementImageToTexture trên hàng đợi thiết bị, tương tự như copyExternalImageToTexture:
canvas.onpaint = () => {
root.device.queue.copyElementImageToTexture(
valueElement,
{ texture: targetTexture }
);
};
Bước 3: Cập nhật biến đổi CSS
Giờ đây, sau khi kết xuất phần tử vào canvas, bạn sẽ cần cập nhật trình duyệt về vị trí của phần tử đó. Điều này đảm bảo quá trình đồng bộ hoá không gian giữa canvas và bố cục của DOM. Điều này rất quan trọng để trình duyệt có thể ánh xạ chính xác vùng sự kiện (chẳng hạn như vị trí chính xác mà người dùng nhấp hoặc di chuột) với vị trí mà phần tử được kết xuất.
Đối với trường hợp ngữ cảnh 2D, hãy áp dụng phép biến đổi do lệnh gọi kết xuất trả về cho .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();
};
Với WebGL hoặc WebGPU, vị trí của một phần tử trên màn hình phụ thuộc vào cách mã đổ bóng sử dụng kết cấu đầu ra và không thể suy ra từ ngữ cảnh kết xuất canvas. Tuy nhiên, nếu chương trình đổ bóng của bạn sử dụng một phép chiếu chế độ xem mô hình thông thường để vẽ hoạ tiết, thì bạn có thể sử dụng hàm tiện ích mới element.getElementTransform() để tính toán một phép biến đổi có thể được dùng theo cách tương tự như giá trị trả về từ drawElementImage(). Để tạo điều kiện thuận lợi cho việc này, bạn cần làm như sau:
- Chuyển đổi MVP Matrix WebGL thành DOM Matrix.
- Chuẩn hoá phần tử HTML. Các phần tử HTML có kích thước tính bằng pixel (ví dụ: rộng 200px). Tuy nhiên, WebGL thường coi các đối tượng là "hình vuông đơn vị", chẳng hạn như từ 0 đến 1. Nếu bạn không chuẩn hoá, nút 200px sẽ trông lớn hơn 200 lần.
- Ánh xạ đến khung hiển thị canvas. Bước này là giai đoạn "điều chỉnh tỷ lệ": bước này kéo dài phép tính không gian đơn vị đó để khớp với kích thước pixel thực tế của phần tử
<canvas>trên màn hình. Thao tác này cũng lật trục Y, vì trong WebGL, hướng lên là dương, nhưng trong CSS, hướng xuống là dương. - Tính toán phép biến đổi cuối cùng. Nhân các ma trận theo thứ tự:
Viewport * MVP * Normalization.Kết hợp các ma trận này thành một phép biến đổi cuối cùng sẽ tạo ra một "bản đồ" cho trình duyệt biết chính xác vị trí của lớp phần tử HTML đó để căn chỉnh với bản vẽ 3D. - Áp dụng phép biến đổi cho phần tử HTML. Thao tác này sẽ di chuyển lớp phần tử HTML để nằm ngay trên các pixel được kết xuất. Điều này đảm bảo rằng khi người dùng nhấp vào một nút hoặc chọn văn bản, họ sẽ nhấp vào phần tử HTML thực.
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();
}
}
Hỗ trợ thư viện và khung
Một số thư viện phổ biến đã hỗ trợ tính năng HTML trong Canvas.
Three.js
Việc cập nhật ma trận theo cách thủ công có thể tốn nhiều thời gian, đó là lý do tại sao các khung đã bắt đầu được sử dụng. Three.js có hỗ trợ thử nghiệm bằng THREE.HTMLTexture mới:
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 cũng hỗ trợ HTML trong Canvas bằng API kết cấu của họ:
// 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();
Bản minh hoạ
Trước khi dùng thử các bản minh hoạ, hãy đảm bảo rằng môi trường của bạn đã được định cấu hình đúng cách.
Có một số bản minh hoạ đóng vai trò là tài liệu tham khảo để sử dụng API. Chúng tôi đã thấy những giải pháp sáng tạo từ cộng đồng, từ sách 3D có thể dịch đến các phần tử giao diện người dùng khúc xạ qua trình đổ bóng kính:
- Sách 3D: Một cuốn sách 3D được kết xuất bằng WebGL, sử dụng bố cục HTML cho các trang của sách. Người dùng có thể thay đổi phông chữ bằng CSS. Vì dựa trên DOM, nên tính năng dịch tích hợp hoạt động ngay lập tức và các tác nhân AI có thể trích xuất văn bản mà không quá phức tạp.
- Giao diện người dùng 3D tương tác: Một thanh trượt dạng thạch rau câu WebGPU khúc xạ ánh sáng dựa trên mô hình 3D cơ bản, đồng thời vẫn phản hồi các thuộc tính bước
<input type="range">HTML tiêu chuẩn. - Kết cấu động: Một biển quảng cáo 3D động kết xuất một bút chì SVG động bằng cách sử dụng DOM trực tiếp vào một kết cấu WebGL mà không cần vòng lặp hoạt ảnh tuỳ chỉnh.
- Lớp phủ khúc xạ: Một lớp kiểu chữ tương tác bị biến dạng bởi con trỏ 3D chuyển động, nhưng vẫn có thể chọn và tìm kiếm hoàn toàn bằng tính năng tìm kiếm trên trang.
Hãy xem bộ sưu tập bản minh hoạ do cộng đồng tạo. Nếu bạn muốn bản minh hoạ HTML trong Canvas xuất hiện trong bộ sưu tập này, hãy tạo một yêu cầu kéo để thêm bản minh hoạ đó.
Các điểm hạn chế
Mặc dù mạnh mẽ, nhưng API này có một số hạn chế có chủ ý:
- Nội dung trên nhiều nguồn: Vì lý do bảo mật và quyền riêng tư, API này không hoạt động với nội dung iframe trên nhiều nguồn.
- Thao tác cuộn trên luồng chính: HTML trong canvas được vẽ bằng JavaScript, tức là thao tác cuộn và ảnh động không thể cập nhật độc lập với JavaScript, như khi ở bên ngoài canvas. Nhà phát triển nên cân nhắc kỹ lưỡng các đặc điểm hiệu suất của việc đặt nội dung có thể cuộn bên trong canvas so với việc cuộn toàn bộ canvas.
Phản hồi
Nếu bạn đang thử nghiệm HTML-in-Canvas API, chúng tôi rất mong nhận được phản hồi của bạn! Bạn có thể đăng ký bản dùng thử theo nguyên gốc để bật tính năng này trên trang web của mình trong giai đoạn thử nghiệm nhằm giúp chúng tôi định hình thiết kế API. Bạn cũng có thể báo cáo vấn đề để gửi ý kiến phản hồi.
Tài nguyên
- Hỗ trợ HTML trong Canvas trong Three.js
- HTML trong Canvas trong bản minh hoạ Three.js
- Hỗ trợ HTML trong Canvas trong PlayCanvas: Tài liệu dành cho nhà phát triển
- HTML trong Canvas trong bản minh hoạ PlayCanvas
- HTML trong Canvas: Giải thích
- Hướng dẫn về web hiện đại cho các công cụ viết mã AI cho HTML trong Canvas
- Bản minh hoạ Chrome.dev cho HTML trong Canvas
- Bộ sưu tập bản minh hoạ HTML trong Canvas tuyệt vời do cộng đồng tạo