HTML-in-Canvas: Các khái niệm kỹ thuật

API HTML-in-Canvas cho phép bạn kết xuất các cây con DOM có thể truy cập và tương tác trực tiếp vào ngữ cảnh canvas 2D, WebGL hoặc WebGPU.

Hướng dẫn này trình bày các khái niệm kỹ thuật về HTML trong Canvas, vốn phổ biến trong mọi ngữ cảnh canvas:

Chuẩn bị canvas để hiển thị HTML

Để hiển thị các phần tử HTML bên trong <canvas>, hãy định cấu hình hai thuộc tính sau:

  • Đặt content="drawable" trên <canvas>: Thao tác này sẽ chọn phần tử canvas để bố trí các nút DOM con thay vì chỉ coi chúng là nội dung bị ẩn.
  • Đặt drawable trên các phần tử mục tiêu: Thao tác này đánh dấu một phần tử là gốc vẽ độc lập đủ điều kiện để kết xuất canvas.
<canvas id="mainCanvas" width="600" height="400" content="drawable">
  <div id="parentElement" class="card" drawable>
    <h2>Container Title</h2>
    <p>Descriptive text inside the parent element.</p>
    <!-- Nested drawable: excluded from parentElement and drawn independently -->
    <button id="childElement" class="btn" drawable>Action Button</button>
  </div>
</canvas>

Tìm hiểu cây con có thể vẽ

Khi chuẩn bị HTML để hiển thị vào canvas, bạn cần quyết định phần tử nào sẽ được vẽ độc lập. Mỗi phần tử drawable đại diện cho một gốc vẽ độc lập và cây con có thể vẽ của nó.

Cây con có thể vẽ bao gồm một phần tử có thuộc tính drawable và tất cả các phần tử con của phần tử đó, ngoại trừ mọi phần tử con lồng nhau cũng có thuộc tính drawable, ví dụ:

<canvas content="drawable">
  <!-- Subtree A (Renders parent container and text) -->
  <div id="parentElement" drawable>
    <h2>Parent Card Heading</h2>
    <p>Card body text content.</p>
    <!-- Subtree B (Excluded from Subtree A and drawn separately) -->
    <button id="nestedChildElement" drawable>
      Clickable Button
    </button>
  </div>
</canvas>
Phần tử Vẽ ranh giới Hành vi
#parentElement (Cây con A) Bảng điều khiển, tiêu đề và đoạn văn bản nền Được hiển thị khi gọi drawElementImage(parentElement, ...)
#nestedChildElement (Cây con B) Bề mặt và văn bản của nút Bị loại trừ khỏi cây con #parentElement; được kết xuất trong một lệnh gọi drawElementImage(nestedChildElement, ...) riêng biệt
Nút drawable được lồng bên trong thẻ drawable và mỗi nút được vẽ bằng lệnh gọi drawElementImage() riêng.

Việc lồng các phần tử có thể vẽ cho phép bạn duy trì các mối quan hệ HTML có ngữ nghĩa, đồng thời cho phép bạn tự do phân lớp, định vị hoặc tạo ảnh động cho các phần tử con một cách độc lập trên cảnh canvas.

Tìm hiểu về sự kiện paint

HTML-in-Canvas API giới thiệu sự kiện paint. Trình duyệt sẽ tự động gửi sự kiện paint trên <canvas> bất cứ khi nào bố cục, văn bản hoặc kiểu CSS thay đổi trên bất kỳ thành phần con drawable nào của trình duyệt.

canvas.addEventListener("paint", (event) => {
  // Clear the viewport
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Redraw elements individually
  ctx.drawElementImage(parentElement, 20, 20);
  ctx.drawElementImage(childElement, 40, 90);
});

Sự kiện paint gửi một thực thể của CanvasPaintEvent, mở rộng DOM Event tiêu chuẩn và có thuộc tính changedElements chỉ chứa các thành phần con drawable đã thay đổi về mặt hình ảnh (ví dụ: thay đổi kiểu, thay đổi bố cục, chỉnh sửa văn bản hoặc thay đổi trạng thái tiêu điểm) kể từ lần vẽ gần nhất.

canvas.addEventListener("paint", (event) => {
  // Array of changed elements
  for (const el of event.changedElements) {
    console.log(`Element #${el.id} visually changed and needs repainting`);
  }
});
Trình duyệt sẽ kích hoạt thao tác vẽ trên canvas bất cứ khi nào quá trình kết xuất của một thành phần con có thể vẽ thay đổi.

Vòng đời khung hình

Các lệnh vẽ canvas được thực thi bên trong sự kiện paint sẽ xuất hiện trong bản cập nhật kết xuất hiện tại, nhưng các nội dung sửa đổi DOM được thực hiện bên trong sự kiện paint sẽ không xuất hiện cho đến khung hình tiếp theo:

canvas.addEventListener("paint", () => {
  // Renders in the CURRENT rendering update:
  ctx.drawElementImage(parentElement, 20, 20);

  // Modifying DOM styles inside paint schedules a new layout cycle;
  // changes will appear in the NEXT frame:
  parentElement.style.backgroundColor = "#e0f2fe";
});

Canvas lồng nhau

Nếu trang của bạn chứa các phần tử <canvas content="drawable"> lồng nhau, thì trình duyệt sẽ gửi các sự kiện paint theo thứ tự ngược lại của cây DOM (phần tử con kích hoạt trước phần tử mẹ). Điều này đảm bảo các phần tử canvas con hiển thị trước khi các phần tử canvas mẹ kết hợp chúng.

Giả sử trong ví dụ sau, phần tử nestedElement bên trong phải thay đổi về mặt hình ảnh (ví dụ: màu nền CSS của phần tử này thay đổi khi người dùng giữ con trỏ trên phần tử đó). Trước tiên, sự kiện paint kích hoạt trên canvas nestedCanvas. Trình nghe của nó sẽ xoá vùng đệm con và gọi ctx.drawElementImage(nestedElement, ...) để kết xuất trạng thái đã cập nhật. Sau đó, sự kiện paint sẽ kích hoạt trên parentCanvas. Trình nghe của nó sẽ vẽ wrapperElement (chứa canvas nestedCanvas đã cập nhật) vào cảnh.

<!-- Outer parent canvas -->
<canvas id="parentCanvas" width="800" height="600" content="drawable">
  <!-- Wrapper container for nested content -->
  <div id="wrapperElement" drawable>

    <!-- Nested child canvas -->
    <canvas id="nestedCanvas" width="300" height="200" content="drawable">
      <div id="nestedElement" class="badge" drawable>
        Inner Child Content
      </div>
    </canvas>

  </div>
</canvas>

<script>
const parentCanvas = document.getElementById("parentCanvas");
const parentCtx = parentCanvas.getContext("2d");

const nestedCanvas = document.getElementById("nestedCanvas");
const nestedCtx = nestedCanvas.getContext("2d");

const nestedElement = document.getElementById("nestedElement");
const wrapperElement = document.getElementById("wrapperElement");

// ============================================================================
// 1. Child Canvas Listener (Fires FIRST)
// ============================================================================
nestedCanvas.addEventListener("paint", (event) => {
  // Clear and redraw the child canvas with the updated inner element
  nestedCtx.clearRect(0, 0, nestedCanvas.width, nestedCanvas.height);
  nestedCtx.drawElementImage(nestedElement, 10, 10);

  console.log("1. Child nested canvas rendered fresh pixels.");
});

// ============================================================================
// 2. Parent Canvas Listener (Fires SECOND)
// ============================================================================
parentCanvas.addEventListener("paint", (event) => {
  // Clear and redraw the parent canvas scene
  parentCtx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);

  // Draws the wrapper container (including the freshly painted nestedCanvas)
  parentCtx.drawElementImage(wrapperElement, 50, 50);

  console.log("2. Parent canvas composited the complete scene.");
});
</script>
Khi các phần tử <canvas content="drawable"> được lồng, các sự kiện vẽ sẽ kích hoạt theo thứ tự cây đảo ngược: canvas lồng nhau vẽ trước, vì vậy canvas mẹ có thể vẽ các pixel mới nhất.

Kích hoạt sự kiện paint

Gọi requestPaint() để lên lịch cho một sự kiện paint duy nhất trên khung hình tiếp theo, ngay cả khi không có văn bản hoặc kiểu DOM nào thay đổi. Bạn có thể dùng tính năng này để kết xuất cảnh tương tác có các phần tử chuyển động, ví dụ:

let animatedX = 0;

canvas.addEventListener("paint", () => {
  animatedX += 1.5; // Move the element horizontally
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawElementImage(staticElement, 20, 20);
  ctx.drawElementImage(movingElement, animatedX, 200);
  // Request canvas paint on each rendering update for smooth animation
  canvas.requestPaint();
});

canvas.requestPaint();

Cập nhật hình học của một phần tử

Khi bạn vẽ một phần tử HTML vào canvas, mã vẽ của bạn sẽ quyết định vị trí và kích thước của phần tử đó. Không giống như nội dung HTML thông thường, bạn không thể xác định vị trí và kích thước trên màn hình của phần tử (hình học) chỉ dựa trên kiểu được tính toán của phần tử đó. Trình duyệt cần có hình học của một phần tử để làm những việc sau:

  • Gửi các sự kiện đầu vào như lượt nhấp, lượt nhấn và chuyển động của con trỏ đến đúng phần tử. Quá trình này được gọi là thử nghiệm nhấn.
  • Trả về kết quả chính xác từ các truy vấn bố cục, chẳng hạn như getBoundingClientRect().
  • Báo cáo ranh giới của phần tử cho các công nghệ hỗ trợ, chẳng hạn như trình phóng to màn hình.

Trong chế độ 2D, drawElementImage() sẽ tự động cập nhật hình học theo mặc định. Các đối số bạn truyền vào, cùng với kiểu được tính toán của phần tử và phép biến đổi hiện tại của ngữ cảnh, là đủ để xác định vị trí phần tử xuất hiện.

Trong bối cảnh WebGL và WebGPU, trình duyệt không thể xác định hình học. Một phần tử HTML được sao chép vào một hoạ tiết, sau đó chương trình đổ bóng của bạn có thể liên kết hoạ tiết đó với cảnh theo bất kỳ cách nào tuỳ ý. Để cho trình duyệt biết vị trí xuất hiện của phần tử, hãy truyền một phép biến đổi đến updateElementGeometry().

Khi mã ngừng gọi updateElementGeometry(), trình duyệt sẽ không còn biết vị trí của các phần tử HTML trên thẻ 3D. Thẻ vẫn tiếp tục quay, nhưng lượt nhấp vẫn được tính cho vị trí cũ của thẻ.

Để cập nhật hình học, hãy truyền một phép biến đổi không gian vào phương thức. Tìm hiểu cách tạo phép biến đổi cho nhiều ngữ cảnh:

Quản lý thứ tự xếp chồng thử nghiệm nhấn

Theo mặc định, mỗi khi hình học của một phần tử được cập nhật, trình duyệt sẽ đưa phần tử đó lên đầu ngăn xếp tương tác (được vẽ sau cùng = mục tiêu nhấp chuột trên cùng). Bạn có thể kiểm soát hành vi này bằng cách sử dụng preserveHitTestOrder:

// Updates the backdrop without stealing clicks from foreground elements
canvas.updateElementGeometry(backgroundElement, {
  canvasTransform: customTransform,
  preserveHitTestOrder: true
});
Phương thức Hành vi Trường hợp sử dụng
preserveHitTestOrder: false (mặc định) Di chuyển phần tử lên đầu ngăn xếp thử nghiệm nhấn. Các thành phần trên nền trước nên nhận được lượt nhấp trước tiên.
preserveHitTestOrder: true Giữ nguyên mức độ ưu tiên hiện tại của phần tử trong ngăn xếp. Các phần tử nền cần được cập nhật sau các phần tử nền trước.

Đặt lại hình học

Để huỷ vẽ một phần tử mà không xoá phần tử đó khỏi DOM, hãy gọi canvas.clearElementGeometry(). Thao tác này sẽ đặt lại phép biến đổi canvas của phần tử và đánh dấu phần tử đó là nằm ngoài màn hình trong cây hỗ trợ tiếp cận:

// Remove from canvas hit-testing and mark as offscreen
canvas.clearElementGeometry(childElement);

Kích thước và đơn vị

Khi bạn vẽ HTML vào canvas, trình duyệt sẽ hoạt động đồng thời trong ba hệ toạ độ: pixel CSS, pixel thiết bị và pixel lưới canvas.

Pixel CSS được dùng để đo lường và định vị nội dung trong bố cục. Sau đó, trình duyệt sẽ điều chỉnh tỷ lệ theo devicePixelRatio để tạo điểm ảnh cho trang thành các pixel của thiết bị thực. Vì pixel CSS là một đơn vị logic, nên vị trí và kích thước thường là phân số của pixel thiết bị (ví dụ: 10,5 pixel CSS × DPR là 1,5 = 15,75 pixel thiết bị). Màn hình chỉ có thể tô màu toàn bộ pixel, vì vậy, trình duyệt phải phân giải mọi thứ nằm giữa các pixel đó. Việc này được thực hiện theo hai cách:

  • Căn chỉnh: Đường ranh giới, đường viền và độ lệch lớp của hộp được làm tròn đến pixel thiết bị nguyên gần nhất để giữ được độ sắc nét.
  • Khử răng cưa: Đối với ký tự văn bản, đường cong và cạnh được biến đổi, phạm vi bao phủ một phần sẽ được kết hợp vào các pixel lân cận để chúng vẫn trông mượt mà và ở đúng vị trí.

<canvas> có lưới pixel riêng, được đặt theo các thuộc tính width và height, tách biệt với cả pixel CSS và pixel thiết bị. Để hiển thị, trình duyệt sẽ lấy mẫu lại bitmap canvas trên lưới pixel thực, kết hợp các pixel canvas lân cận để lấp đầy từng pixel của thiết bị. Để tìm hiểu cách trình duyệt liên kết giữa CSS, canvas và pixel thiết bị, hãy xem bài viết Hiển thị chính xác đến từng pixel bằng devicePixelContentBox.

Mọi phần tử HTML drawable đều được bố trí theo pixel CSS. Vì nội dung được vẽ vào canvas có thể trải qua hai lượt chuyển đổi, từ pixel CSS sang lưới canvas rồi từ lưới canvas sang pixel vật lý, nên mỗi bước không khớp chính xác sẽ thêm một vòng kết hợp khác và kết quả có thể trông mờ. Để tránh điều này, hãy đặt lưới canvas khớp với lưới pixel vật lý. Việc này có hai mục đích:

  • Hiển thị nhất quán: Nếu canvas ánh xạ 1:1 với pixel vật lý, thì việc chuyển đổi pixel CSS thành pixel canvas sẽ cho ra kết quả giống như việc trình duyệt chuyển đổi pixel CSS thành pixel vật lý cho HTML thông thường.
  • Không pha trộn thêm: Nếu canvas liên kết 1:1 với pixel vật lý, thì việc chuyển đổi canvas sang pixel thiết bị sẽ không làm mờ thêm.

Hình ảnh sau đây minh hoạ một lượt chuyển đổi pixel dẫn đến nội dung bị mờ. Ba lưới không khớp nhau: Phần tử canvas là 360 × 180 CSS px và kích thước canvas là 300 × 150 pixel lưới canvas. Ở tỷ lệ pixel thiết bị là 1,5, canvas bao phủ 540 × 270 pixel thiết bị. Một pixel CSS trải dài 1,5 pixel thiết bị và một pixel canvas trải dài 1,2 pixel CSS hoặc 1,8 pixel thiết bị (bảng điều khiển 1). Khi hình tròn được vẽ, vị trí và bán kính của hình tròn sẽ được tính bằng pixel canvas chứ không phải pixel CSS, vì vậy, trình duyệt sẽ vẽ hình tròn đó vào bitmap canvas 300 × 150 và khử răng cưa cho cạnh (bảng điều khiển 2). Để hiển thị canvas, trình duyệt sẽ kéo giãn bitmap này trên 540 × 270 pixel thiết bị, vì vậy, trình duyệt phải nội suy: mỗi pixel thiết bị sẽ lấy một hỗn hợp có trọng số của các pixel canvas gần nhất, vốn đã chứa cạnh khử răng cưa. Đường viền được pha trộn lần thứ hai và trải rộng trên nhiều pixel của thiết bị, vì vậy, hình tròn trông mờ (bảng điều khiển 3). Bạn có thể tránh được điều này nếu lưới canvas khớp với pixel của thiết bị theo tỷ lệ 1:1. Sau đó, trình duyệt sẽ khử răng cưa cho cạnh chỉ một lần, ngay trên lưới thiết bị và hiển thị bitmap mà không cần lấy mẫu lại, nhờ đó cạnh vẫn sắc nét (bảng 4):

Biểu đồ gồm 4 bảng so sánh lưới pixel cho một cạnh cong: 1. Lưới CSS (360 x 180), canvas (300 x 150) và thiết bị (540 x 270) không khớp nhau. 2. Đường cong được khử răng cưa trên lưới canvas thô 300 x 150. 3. Bitmap đó được nội suy vào lưới thiết bị 540 x 270, khử răng cưa lần thứ hai và làm mờ cạnh. 4. Một khung vẽ 540 x 270 khớp với lưới thiết bị theo tỷ lệ 1:1, được khử răng cưa một lần để có cạnh sắc nét.
Cách CSS, canvas và lưới pixel của thiết bị không khớp nhau gây ra hiện tượng khử răng cưa kép (bảng 1–3), so với lưới canvas-to-device 1:1 (bảng 4).

Để hiển thị rõ nét, lưới canvas phải khớp với số lượng pixel thiết bị mà canvas bao phủ: 540 × 270 trong ví dụ trước.

So sánh song song một thẻ HTML có nội dung &quot;Hello from the DOM!&quot; Thẻ này là HTML được vẽ vào canvas.' Bên trái: được vẽ trên lưới canvas 300 x 150 mặc định, trông mờ. Bên phải: được vẽ trên lưới canvas 800 x 400 có kích thước bằng ResizeObserver, trông sắc nét.
Cùng một canvas, 400 x 200 pixel CSS, trên màn hình có DPR là 1,5. Bên trái: lưới canvas 400 x 200. Bên phải: được điều chỉnh theo DPR, lưới 600 x 300.

Định kích thước lưới canvas bằng trình quan sát đổi kích thước

Kích thước lưới phù hợp phụ thuộc vào thiết bị và có thể thay đổi khi bố cục thay đổi, khi người dùng thu phóng trang hoặc khi người dùng di chuyển cửa sổ sang một màn hình khác. Vì vậy, thay vì cố định giá trị kích thước lưới, hãy để trình duyệt cho bạn biết kích thước đó nên là bao nhiêu.

Để giữ cho lưới canvas đồng bộ với canvas trên màn hình, hãy luôn định kích thước lưới bằng ResizeObserver và điều chỉnh lưới canvas thành devicePixelContentBoxSize:

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);

Chuyển đổi pixel CSS thành toạ độ lưới canvas

Trình xử lý paint thường kết hợp các phép đo DOM với hàm gọi vẽ, chẳng hạn như để định vị một phần tử dựa trên kích thước của phần tử đó. Hai loại này sử dụng các đơn vị khác nhau:

  • Pixel canvas mô tả lưới canvas, được định kích thước bằng canvas.width và canvas.height. Ví dụ: các tham số dx, dy, dwidth và dheight của phương thức vẽ drawElementImage() hoặc các tham số API canvas khác, chẳng hạn như fillRect() và translate().
  • Pixel CSS mô tả trang. Các phép đo DOM như offsetWidth và clientWidth, cũng như toạ độ sự kiện con trỏ như offsetX, đều được tính bằng pixel CSS.

Sau đây là thông tin tổng quan về các đơn vị mà nhiều tham số API sử dụng:

Giá trị Đơn vị
canvas.width và canvas.height Pixel lưới canvas
Tham số bản vẽ 2D, bao gồm dx, dy, dwidth và dheight của drawElementImage() Pixel lưới canvas
width và height được trả về bởi canvas.getElementImageDefaultSize() Pixel lưới canvas
Hình chữ nhật nguồn: sx, sy, swidth và sheight của drawElementImage() và texElementSubImage2D(); sourceX, sourceY, sourceWidth và sourceHeight của drawElementImageToTexture() Pixel CSS
Kích thước hoạ tiết: width và height lựa chọn trong số texElementSubImage2D(); size trong số drawElementImageToTexture() Pixel kết cấu. Theo mặc định, kích thước của phần tử tính bằng pixel lưới canvas, được làm tròn lên
Hình học phần tử: getElementTransform(), cũng như các tuỳ chọn canvasTransform và canvasClip của updateElementGeometry() Pixel CSS
Các phép đo DOM, chẳng hạn như offsetWidth, clientWidth, getBoundingClientRect(), toạ độ sự kiện con trỏ và contentBoxSize của một mục nhập trình quan sát đổi kích thước Pixel CSS
devicePixelContentBoxSize của một mục nhập trình quan sát đổi kích thước Pixel của thiết bị

Nếu bạn sử dụng sai đơn vị, kích thước của nội dung HTML được vẽ vào canvas có thể không như bạn mong đợi. Bạn cần chuyển đổi giá trị pixel CSS thành pixel lưới canvas khi vẽ HTML vào canvas. Để làm việc này, hãy tính tỷ lệ giữa kích thước lưới canvas và kích thước CSS của canvas: canvas.width / cssContentWidth theo chiều ngang và canvas.height / cssContentHeight theo chiều dọc. Trình quan sát đổi kích thước là một nơi thuận tiện để tính tỷ lệ như sau:

// The number of canvas grid pixels per CSS pixel
let scaleX = 1;
let scaleY = 1;

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  const cssContentWidth = entry.contentRect.width;
  const cssContentHeight = entry.contentRect.height;

  // Best practice: make the canvas grid match the physical pixel grid
  if (dpc) {
    canvas.width = dpc[0].inlineSize;
    canvas.height = dpc[0].blockSize;
  } else {
    canvas.width = Math.round(cssContentWidth * window.devicePixelRatio);
    canvas.height = Math.round(cssContentHeight * window.devicePixelRatio);
  }

  scaleX = canvas.width / cssContentWidth;
  scaleY = canvas.height / cssContentHeight;
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

Sau đó, hãy nhân các giá trị pixel CSS với scaleX và scaleY trước khi truyền các giá trị đó vào phương thức vẽ. Ví dụ: để căn giữa một phần tử có thể vẽ trong canvas:

const ctx = canvas.getContext('2d');
const card = document.getElementById('card');

canvas.onpaint = () => {
  ctx.reset();

  // offsetWidth and offsetHeight are in CSS pixels.
  // Convert them to canvas grid coordinates.
  const width = card.offsetWidth * scaleX;
  const height = card.offsetHeight * scaleY;
  const x = (canvas.width - width) / 2;
  const y = (canvas.height - height) / 2;

  // drawElementImage() takes canvas grid coordinates.
  ctx.drawElementImage(card, x, y);
};

Cuối cùng, vì drawElementImage() vẽ hộp đường viền của phần tử, hãy sử dụng box-sizing: border-box để đưa khoảng đệm và đường viền vào width và height của thẻ, nhờ đó thẻ được vẽ theo kích thước bạn đặt:

#card {
  /* Make width and height include padding and border */
  box-sizing: border-box;
  width: 200px;
  padding: 16px;
}

Tìm hiểu về khả năng hỗ trợ tiếp cận của HTML-in-Canvas

Nhìn chung, nội dung HTML của <canvas content="drawable"> được hiển thị cho tính năng hỗ trợ tiếp cận theo cách tương tự như DOM thông thường, tức là nội dung này được biểu thị trong cây hỗ trợ tiếp cận, trừ phi bị ẩn rõ ràng bằng các thuộc tính như aria-hidden="true" hoặc inert. Tuy nhiên, có một ngoại lệ quan trọng: phần tử drawable có hình học được kết xuất chưa được thiết lập hoặc cập nhật.

Phần tử drawable bên trong <canvas content="drawable"> thiếu hình học được kết xuất nếu:

  • Nó chưa được kết xuất bằng drawElementImage(), theo mặc định, lớp này sẽ tự động cập nhật hình học.
  • Hình học của đối tượng này chưa được cập nhật rõ ràng bằng updateElementGeometry().

Nếu một phần tử drawable thiếu hình học được kết xuất, thì phần tử drawable này và cây con drawable của phần tử đó sẽ được báo cáo cho cây hỗ trợ tiếp cận mà không có thông tin hình học. Ví dụ: điều này giúp trình đọc màn hình truy cập vào nội dung ngữ nghĩa, đồng thời báo hiệu cho các công cụ hỗ trợ hình ảnh (như kính lúp) lọc ra các phần tử chưa được vẽ.

Ví dụ sau đây giữ nguyên ngữ nghĩa của hình, ảnh và chú thích trong khi vẽ hình và ảnh riêng biệt:

<canvas id="canvas" width="400" height="300" content="drawable">
  <figure id="figure">
    <img drawable id="image" src="..." alt="image alt text" />
    <figcaption drawable id="caption">A caption</figcaption>
  </figure>
</canvas>
<script>
  const ctx = canvas.getContext('2d');

  canvas.onpaint = () => {
    ctx.reset();
    // Note: Because `preserveElementGeometry` defaults to false, the
    // `drawElementImage` call automatically updates geometry, ensuring
    // the image and caption have geometry information for accessibility.
    ctx.drawElementImage(image, 0, 0);
    ctx.drawElementImage(caption, 0, 200);
  };
</script>

Chiến lược dự phòng

HTML-in-Canvas API được cung cấp trong Chrome 157 và có thể không được hỗ trợ trong một số trình duyệt. Để đảm bảo giải pháp của bạn hoạt động trơn tru cho mọi người, hãy kiểm tra xem trình duyệt có hỗ trợ API hay không và quay lại giải pháp thay thế nếu cần:

export function supportsHtmlInCanvas() {
  if (typeof document === 'undefined') return false;
  const canvas = document.createElement('canvas');
  canvas.setAttribute('content', 'drawable');
  return canvas.content === 'drawable';
}

Nếu API không được hỗ trợ, thì tuỳ thuộc vào mục tiêu triển khai, bạn có thể chọn một trong các phương án thay thế sau:

Các bước tiếp theo