Ra mắt bản dùng thử theo nguyên gốc HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Trong nhiều năm, nhà phát triển web đã phải đưa ra 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 nên dựa vào DOM để tận dụng các tính năng ngữ nghĩa phong phú hay 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 một khung vẽ 2D hoặc một kết cấu WebGL/WebGPU trong khi vẫn giữ cho giao diện người dùng có thể tương tác, hỗ trợ tiếp cận và được kết nối với các tính năng trình duyệt yêu thích của bạn. Bằng cách kết hợp HTML với quá 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. DOM cung cấp các giải pháp bố cục văn bản ngay khi bạn sử dụng, bằng cách sử dụng nội dung được hiểu theo ngữ nghĩa để tạo ra các 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 thao tá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 trên trang, chế độ đọc, tiện ích, chế độ tối, thu phóng trình duyệt và tự động điền.

Canvas (và WebGL/WebGPU), mặt khác, cho phép truy cập ở cấp thấp để điều khiển một lưới pixel cho đồ hoạ 2D và 3D nâng 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ì khung vẽ về cơ bản là một lưới pixel, nên việc hỗ trợ các tính năng như văn bản thích ứng từng yêu cầu 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 đều bị hỏng hoàn toàn khi giao diện người dùng bị mắc kẹt bên trong lưới pixel tĩnh của khung vẽ.

Ưu điểm của việc đưa DOM vào Canvas

HTML-in-Canvas API là cầu nối mang lại cho bạn những lợi ích tốt nhất của cả hai. Bằng cách đặt HTML bên trong phần tử <canvas> và đồng bộ hoá quá trình chuyể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 của trình duyệt đều hoạt động tự động.

Sau đây là những lợi ích bạn nhận được khi cho phép DOM xử lý giao diện người dùng 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ế độ điều khiển biểu mẫu: Các chế độ điều khiển biểu mẫu biểu cảm và dễ sử dụng hơn với 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ể đánh dấu văn bản bên trong cảnh 3D hoặc nhấp chuột phải vào trình đơn theo 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ể đánh dấu văn bản bên trong cảnh 3D hoặc nhấp chuột phải vào trình đơn theo ngữ cảnh một cách tự nhiên.
  • Hỗ trợ tiếp cận: Nội dung được kết xuất bên trong khung vẽ sẽ được hiển thị cho 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ư 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.
  • Tìm trên trang: Người dùng có thể sử dụng tính năng tìm trên trang (Ctrl/Cmd+F) để tìm văn bản và trình duyệt sẽ đánh dấu văn bản đó trực tiếp trong kết cấu WebGL của bạn.
  • Khả năng 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 thông tin trên 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: Các 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 khung vẽ, bao gồm cả các phần tử giao diện người dùng WebGL/WebGPU trực tiếp trong Chrome DevTools. Hãy điều chỉnh kiểu CSS trong trình kiểm tra và xem kiểu đó cập nhật ngay lập tức trên kết cấu 3D!

Các trường hợp sử dụng nâng cao

API này mở ra tiềm năng đáng kinh ngạc trên nhiều miền:

  • Các ứng dụng lớn dựa trên khung vẽ: 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 khung vẽ, cải thiện khả năng hỗ trợ 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 hoàn toàn có tính tương tác 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ợ sao chép và dán một cách tự nhiê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 khung vẽ, kết xuất vào khung vẽ và cập nhật quá trình chuyể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 API này trên trang web của bạn, hãy sử dụng Chrome Canary 149 trở lên với cờ chrome://flags/#canvas-draw-element được bật. Để bật API này cho những người dùng khác, hãy đăng ký Bản dùng thử theo nguyên gốc.

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 khung vẽ, chuẩn bị để hiển thị nội dung đó bên trong khung vẽ và hiển thị nội dung đó cho 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 khung vẽ

Để tránh nội dung được kết xuất bị mờ, hãy đảm bảo điều chỉnh kích thước lưới khung vẽ 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 bên 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 – ví dụ: 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. Bạn cần cập nhật quá trình chuyển đổi CSS của phần tử bằng giá trị trả về để tính tương tác tiếp tục hoạt động.

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ử dụng texElementImage2D. Phương thức 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 quá trình chuyển đổi CSS

Bây giờ, sau khi đã kết xuất phần tử vào khung vẽ, bạn phải cập nhật cho trình duyệt biết vị trí của phần tử đó. Điều này giúp đảm bảo quá trình đồng bộ hoá không gian giữa khung vẽ và bố cục của DOM. Điều này rất quan trọng để trình duyệt có thể liên kết 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í kết xuất phần tử.

Đối với trường hợp ngữ cảnh 2D, hãy áp dụng quá trình chuyể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í trên màn hình của một phần tử phụ thuộc vào cách mã trình đổ 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 khung vẽ. Tuy nhiên, nếu chương trình đổ bóng của bạn sử dụng mô hình chiếu chế độ xem thông thường để vẽ hoạ tiết, thì bạn có thể sử dụng hàm tiện lợi mới element.getElementTransform() để tính toán phép biến đổi có thể được sử 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 Ma trận MVP WebGL thành Ma trận DOM.
  • 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 200 pixel). Tuy nhiên, WebGL thường coi các đối tượng là "hình vuông đơn vị", ví dụ: từ 0 đến 1. Nếu bạn không chuẩn hoá, nút 200 pixel sẽ trông lớn hơn 200 lần.
  • Liên kết với khung nhìn khung vẽ. Bước này là giai đoạn "điều chỉnh tỷ lệ". Bước này sẽ kéo dài phép toán 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. Bướ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 quá trình biến đổi cuối cùng. Nhân các ma trận theo thứ tự: Viewport * MVP * Normalization. Việc 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 "ánh xạ" cho trình duyệt biết chính xác vị trí lớp phần tử HTML đó cần nằm để căn chỉnh với bản vẽ 3D.
  • Áp dụng quá trình 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 trực tiếp 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-in-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 các khung đã bắt đầu hỗ trợ. Three.js có hỗ trợ thử nghiệm bằng cách sử dụ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-in-Canvas bằng cách sử dụ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 môi trường của bạn được định cấu hình đúng cách.

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 các giải pháp sáng tạo từ cộng đồng, từ sách 3D có thể dịch được đế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: Sách 3D được kết xuất bằng WebGL sử dụng bố cục HTML cho các trang. Người dùng có thể đổ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ột cách đơn giản hơn.
  • Giao diện người dùng 3D tương tác: Thanh trượt thạch 3D 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 HTML <input type="range"> tiêu chuẩn.
  • Kết cấu động: Bảng quảng cáo 3D động kết xuất bút chì SVG động bằng cách sử dụng DOM trực tiếp vào kết cấu WebGL mà không cần vòng lặp ảnh động tuỳ chỉnh.
  • Lớp phủ khúc xạ: Lớp kiểu chữ tương tác bị biến dạng bởi con trỏ 3D đang di chuyển, nhưng có thể chọn và tìm kiếm hoàn toàn bằng tính năng tì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-in-Canvas của mình được giới thiệu trong bộ sưu tập này, hãy tạo 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ố điểm hạn chế:

  • Nội dung trên nhiều nguồn gốc: 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 gốc.
  • Cuộn luồng chính: HTML-in-canvas được vẽ bằng JavaScript, nghĩa là thao tác cuộn và ảnh động không thể cập nhật độc lập với JavaScript, như bên ngoài khung vẽ. Nhà phát triển nên cân nhắc kỹ các đặc điểm hiệu suất của việc đặt nội dung cuộn bên trong khung vẽ so với việc cuộn toàn bộ khung vẽ.

Phản hồi

Nếu bạn đang thử nghiệm HTML-in-Canvas API, chúng tôi rất muốn nghe ý kiến 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 đề để cung cấp ý kiến phản hồi.

Tài nguyên