Ứng dụng web thường cần chụp và xuất nội dung HTML đa dạng (chẳng hạn như trang tổng quan tuỳ chỉnh, tài liệu được tạo kiểu hoặc biểu đồ tương tác) dưới dạng hình ảnh tĩnh hoặc bản ghi video. Trước đây, để làm được điều này, bạn cần có các thư viện bên thứ ba cồng kềnh, phân tích cú pháp các nút DOM và thuộc tính CSS theo cách thủ công để tạo lại bản sao trực quan trên canvas. Phương pháp này tốn nhiều tài nguyên tính toán, dễ xảy ra lỗi và thường không hỗ trợ các tính năng bố cục CSS hiện đại. Với HTML-in-Canvas API, nhà phát triển có thể kết xuất các phần tử DOM thực trực tiếp vào ngữ cảnh canvas. Vì công cụ kết xuất gốc của trình duyệt vẽ cây con HTML với độ chính xác đến từng pixel, nên việc ghi lại kết quả hình ảnh chính xác dưới dạng hình ảnh hoặc luồng video sẽ rất hiệu quả khi sử dụng các phương thức canvas tích hợp sẵn như toDataURL(), toBlob() hoặc captureStream().
Cách triển khai
Kiểm tra xem trình duyệt có hỗ trợ HTML-in-Canvas hay không:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }Khởi chạy canvas để hỗ trợ việc kết xuất các phần tử HTML con bằng cách thêm thuộc tính
content="drawable"vào phần tử HTML<canvas>. Đặt nội dung HTML của bạn vào bên trong phần tử<canvas>và thêm thuộc tínhdrawablevào mọi phần tử mà bạn vẽ. Một phần tử códrawablesẽ thu thập cây con của nó, ngoại trừ các phần tử con lồng nhau cũng làdrawable:<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>Điều chỉnh tỷ lệ lưới canvas cho phù hợp với hệ số tỷ lệ của thiết bị để tránh bị mờ:
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);Hiển thị nội dung HTML cho canvas bên trong trình xử lý sự kiện
canvas.onpaint.Trong ngữ cảnh 2D, hãy sử dụng phương thức
drawElementImage. Hàm này vẽ ảnh chụp nhanh gần đây nhất của một phần tửdrawablevà cây con có thể vẽ của phần tử đó (không bao gồm các phần tử con có thể vẽ khác), bắt đầu từ hộp đường viền của phần tử, trước khi biến đổi CSS:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- Trong bối cảnh WebGL và WebGPU, bạn sao chép phần tử vào một kết cấu. Định cỡ hoạ tiết từ
canvas.captureElementImage(element).widthvàheightcủa nó nằm trong pixel lưới canvas (kho lưu trữ sao lưu), vì vậy, hãy làm tròn chúng bằngMath.ceil().captureElementImage()sẽ gửi nếu phần tử chưa có bản ghi vẽ: gọirequestPaint()và thử lại trong sự kiệnpainttiếp theo.
// Returns the element's size in canvas grid pixels, or null if the element
// has no paint record yet.
function getElementImageSize(canvas, element) {
let elementImage;
try {
elementImage = canvas.captureElementImage(element);
} catch (err) {
canvas.requestPaint(); // No paint record yet: retry on the next paint event.
return null;
}
const width = Math.ceil(elementImage.width);
const height = Math.ceil(elementImage.height);
elementImage.close();
return width > 0 && height > 0 ? { width, height } : null;
}
- Trong ngữ cảnh WebGL, trước tiên, hãy phân bổ trước kết cấu sao lưu bằng phương thức
texImage2D. Các đoạn mã trong hướng dẫn này sử dụng ngữ cảnh WebGL 2 (canvas.getContext("webgl2")). Phân bổ bộ nhớ kết cấu bằng cách gọitexImage2D()bằngnullthay vì dữ liệu pixel, giúp dự trữ bộ nhớ GPU có kích thước và định dạng nhất định mà không cần tải bất kỳ pixel nào lên. Hãy tuân theo các quy tắc sau:- Định cỡ hoạ tiết từ
captureElementImage(), như minh hoạ ở trên. - Chỉ phân bổ cấp 0 trong lần tải lên đầu tiên hoặc khi kích thước thay đổi, vì việc phân bổ lại sẽ xoá kết cấu. Sử dụng bộ nhớ có thể thay đổi: không thể đổi kích thước bộ nhớ
texStorage2D()không thể thay đổi khi phần tử thay đổi kích thước.
- Định cỡ hoạ tiết từ
// Binds the texture, and allocates its storage on the first upload, and when
// the element size changes. `state` stores the allocated size.
function allocateElementTexture(gl, texture, state, width, height) {
gl.bindTexture(gl.TEXTURE_2D, texture);
if (state.width === width && state.height === height) return;
gl.texImage2D(
gl.TEXTURE_2D,
0, // Mipmap level
gl.RGBA8, // Internal format: 8 bits per RGBA channel
width, // Size in canvas grid pixels
height,
0, // Border: must be 0
gl.RGBA, // Format and type that match RGBA8
gl.UNSIGNED_BYTE,
null // null reserves GPU memory without uploading pixels
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
state.width = width;
state.height = height;
}
const texture = gl.createTexture();
const textureState = { width: 0, height: 0 };
- Sau đó, trong bối cảnh WebGL, hãy kết xuất nội dung HTML vào kết cấu bằng phương thức
texElementSubImage2D. Hãy tuân theo các quy tắc sau:- Truyền
{ width, height }vào cấu hình. Thao tác này giúp bản sao lấp đầy kết cấu được phân bổ bất kể canvas được điều chỉnh theo tỷ lệ như thế nào. - Phân bổ lại kết cấu nếu kích thước thay đổi, như minh hoạ ở trên.
- Truyền
// Uploads the element into the texture. Returns false if the element has no
// paint record yet. In that case, it's uploaded on the next paint event.
function uploadElementWebGL(gl, canvas, texture, element, state) {
const size = getElementImageSize(canvas, element);
if (!size) return false;
const { width, height } = size;
// Bind the texture, and reallocate it if the size changed (see above).
allocateElementTexture(gl, texture, state, width, height);
gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, element, { width, height });
return true;
}
canvas.onpaint = () => {
try {
uploadElementWebGL(gl, canvas, texture, uiElement, textureState);
} catch (err) {
console.error('texElementSubImage2D copy failed:', err);
}
};
- Trong ngữ cảnh WebGPU, hãy sử dụng phương thức
drawElementImageToTexture. Hãy tuân theo các quy tắc sau:- Tạo hoạ tiết bằng cách sử dụng
COPY_DSTvàRENDER_ATTACHMENT, đồng thời thêmTEXTURE_BINDINGđể lấy mẫu trong chương trình đổ bóng. Cách sử dụng sai sẽ được báo cáo là lỗi xác thực WebGPU, chứ không phải là ngoại lệ. - Truyền kích thước hoạ tiết dưới dạng
size. Nếu không cósize, thì một phần tử lớn hơn sẽ được cắt bớt cho vừa với hoạ tiết. Vớisize, phần tử được điều chỉnh theo tỷ lệ để lấp đầy phần tử đó. - Tạo lại hoạ tiết khi kích thước phần tử thay đổi và tạo lại các nhóm liên kết tham chiếu đến hoạ tiết đó.
- Tạo hoạ tiết bằng cách sử dụng
// Create the texture once, and recreate it
// when the element size changes.
function ensureElementTexture(device, state, width, height) {
if (state.texture?.width === width && state.texture?.height === height) {
return state.texture;
}
state.texture?.destroy();
state.texture = device.createTexture({
size: { width, height },
format: "rgba8unorm",
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
return state.texture; // Recreate the bind groups that used the previous texture.
}
const textureState = { texture: null };
canvas.onpaint = () => {
const size = getElementImageSize(canvas, uiElement);
if (!size) return;
const previousTexture = textureState.texture;
const texture = ensureElementTexture(device, textureState, size.width, size.height);
if (texture !== previousTexture) {
// New texture: create, or rebuild, the bind groups that reference it.
}
try {
device.queue.drawElementImageToTexture({ source: uiElement }, { texture, size });
} catch (err) {
console.error('drawElementImageToTexture copy failed:', err);
}
};
Khi sử dụng vòng lặp requestAnimationFrame để kết xuất cảnh, hãy gọi canvas.requestPaint() trong vòng lặp để đảm bảo rằng nội dung HTML được kết xuất vào canvas. event.changedElements liệt kê các phần tử có hoạt động kết xuất đã thay đổi. Đảm bảo rằng bạn chỉ vẽ lại phần tử hoặc tải lại kết cấu của phần tử nếu có bản cập nhật cho các phần tử HTML con:
function render() {
// Request to update the canvas
canvas.requestPaint();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas.onpaint = (event) => {
if (event.changedElements && event.changedElements.length > 0) {
// Redraw the element, or update its texture, as shown above
}
// In WebGL and WebGPU, render the scene, and sync the element geometry as shown in step 5
};
- Đồng bộ hoá hình học của phần tử.
Trình duyệt cần biết vị trí vẽ phần tử HTML để vị trí DOM của phần tử đó khớp với các pixel được vẽ, chẳng hạn như để người dùng có thể nhấp và nhập vào phần tử đó. Canvas chỉ thử nghiệm nhấn các phần tử được vẽ bằng phương thức 2D drawElementImage() hoặc được đăng ký bằng updateElementGeometry().
Đối với trường hợp ngữ cảnh 2D, trình duyệt sẽ tự động đồng bộ hoá phần tử.
drawElementImage()điều chỉnh ranh giới thử nghiệm nhấn DOM và vòng lấy tiêu điểm của trình đọc màn hình theo toạ độ được vẽ, vì vậy, bạn không cần đặtstyle.transform. Để quản lý hình học theo cách thủ công, ví dụ: khi bạn vẽ cùng một phần tử nhiều lần, hãy truyền{ preserveElementGeometry: true }đếndrawElementImage()và áp dụng các phép biến đổi bằngcanvas.updateElementGeometry().Đối với trường hợp 3D có WebGL hoặc WebGPU, canvas không thể biết phần tử HTML được vẽ ở đâu, vì vậy, bạn phải cho canvas biết. Tính toán một
DOMMatrixliên kết hộp viền của phần tử (tính bằng pixel CSS) với canvas (tính bằng pixel CSS). Trình duyệt cần liên kết từ không gian toạ độ 3D vào không gian toạ độ CSS bằng cách sử dụng phép biến đổi khung nhìn. Để hỗ trợ việc này, hãy làm như sau:- Chuẩn hoá phần tử HTML. Các phần tử HTML được định kích thước 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,5 đến 0, 5. Nếu bạn không chuẩn hoá, nút 200px của bạn sẽ trông lớn hơn 200 lần. Bước này cũng lật trục Y, vì trong CSS, hướng xuống là dương, nhưng trong WebGL, hướng lên là dương.
- Chuyển đổi Ma trận MVP WebGL thành Ma trận DOM.
- Ánh xạ tới khung nhìn canvas. Bước này là giai đoạn "điều chỉnh lại tỷ lệ": bước này kéo giãn toán học không gian đơn vị đó ra để khớp với kích thước pixel CSS của phần tử
<canvas>trên màn hình và lật lại trục Y. - Tính toán phép biến đổi cuối cùng. Nhân các ma trận theo thứ tự: Khung nhìn * MVP * Chuẩn hoá. Việc kết hợp các phép biến đổi 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 phần tử HTML đó để căn chỉnh với bản vẽ 3D. Trình duyệt thực hiện phép chia phối cảnh.
// Maps the element's border box (CSS pixels) to the canvas (CSS pixels), for a // quad that spans -0.5 to 0.5 in model space and is drawn with the `mvp` matrix. function computeCanvasTransform(canvas, element, mvp) { const width = element.offsetWidth; const height = element.offsetHeight; // 1. Normalize the HTML element (CSS pixels -> WebGL Model Space) const toGLModel = new DOMMatrix() // Scale pixels to 1 unit, flip Y (as in CSS it points down, and in WebGL it points up) .scale(1 / width, -1 / height, 1 / height) // Center the origin: (0,0) becomes (-width/2, -height/2) before scaling .translate(-width / 2, -height / 2); // 2. Convert WebGL MVP Matrix to DOM Matrix const mvpDOM = new DOMMatrix(Array.from(mvp)); // 3. Map to the canvas viewport, in CSS pixels const clipToCanvasViewport = new DOMMatrix() // Move center (0,0) to center of canvas .translate(canvas.clientWidth / 2, canvas.clientHeight / 2) // Scale normalized clip (-1..1) to viewport size, and flip Y back .scale(canvas.clientWidth / 2, -canvas.clientHeight / 2, canvas.clientHeight / 2); // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square) return clipToCanvasViewport.multiply(mvpDOM).multiply(toGLModel); }
Sau đó, truyền phép biến đổi đến canvas.updateElementGeometry() dưới dạng canvasTransform. Thao tác này sẽ di chuyển phần tử HTML để đặt trực tiếp lên 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ọ thực sự đang nhấp vào phần tử HTML thực. Trình duyệt cũng sử dụng phép biến đổi cho các ranh giới hỗ trợ tiếp cận của phần tử, ví dụ: cho vòng lấy tiêu điểm của trình đọc màn hình:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
Sử dụng các tuỳ chọn này để kiểm soát thử nghiệm nhấn:
- Mỗi lệnh gọi
updateElementGeometry()sẽ di chuyển phần tử lên đầu thứ tự thử nghiệm nhấn. Thẻ/vé{ preserveHitTestOrder: true }sẽ giữ nguyên vị trí. - Gọi
canvas.clearElementGeometry(element)để xoá một phần tử khỏi thử nghiệm nhấn, ví dụ: khi bạn ẩn phần tử đó.
- Sử dụng các phương thức xuất canvas thông thường như
toDataURL(),toBlob()hoặccaptureStream(). Dữ liệu được xuất sẽ bao gồm nội dung HTML được kết xuất.
Mã mẫu
<body>
<canvas id="canvas" style="width: 400px; height: 200px;" content="drawable">
<input id="element" drawable>
</canvas>
<button id="download">Download Image</button>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const element = document.getElementById('element');
const download = document.getElementById('download');
canvas.onpaint = (event) => {
ctx.reset();
// Draw the element into the canvas. The browser synchronizes its
// DOM position for hit testing (typing).
ctx.drawElementImage(element, 10, 10);
};
download.onclick = () => {
// Export the canvas content as an image
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'exported-canvas.png';
link.href = dataURL;
link.click();
};
// Re-initialize canvas size on screen resize
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);
canvas.requestPaint();
});
const supportsDevicePixelContentBox =
typeof ResizeObserverEntry !== 'undefined' &&
'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);
</script>
</body>
Các phương pháp hay nhất
- BẮT BUỘC: Kiểm tra khả năng hỗ trợ của trình duyệt đối với API HTML-in-Canvas trước khi sử dụng.
- BẮT BUỘC: Luôn thêm thuộc tính
content="drawable"vào phần tử<canvas>. - BẮT BUỘC: Thêm thuộc tính
drawablevào mọi phần tử mà bạn vẽ, bao gồm cả các phần tử con trực tiếp của canvas. - BẮT BUỘC: Sử dụng trình xử lý sự kiện
onpaintđể hiển thị nội dung HTML trên canvas. - BẮT BUỘC: Sử dụng phương thức
drawElementImagetrong 2D hoặcdrawElementImageToTexturetrong WebGPU để kết xuất nội dung HTML vào canvas. - BẮT BUỘC: Trong WebGL, hãy phân bổ trước vùng đệm kết cấu bằng
texImage2D()một lần (khi vùng đệm được khởi chạy hoặc khi kích thước phần tử thay đổi). Chỉ phân bổ lại kết cấu khi kích thước thay đổi và đặtTEXTURE_MIN_FILTERthànhLINEAR. - BẮT BUỘC: Trong WebGL, hãy sử dụng phương thức
texElementSubImage2Dđể kết xuất nội dung HTML thành một hoạ tiết mà bạn phân bổ trước bằngtexImage2D(), có kích thước từcaptureElementImage(). - BẮT BUỘC: Trong WebGPU, hãy định kích thước hoạ tiết từ
captureElementImage()và chỉ tạo lại hoạ tiết khi kích thước thay đổi. Tạo kết cấu bằng cách sử dụngCOPY_DSTvàRENDER_ATTACHMENT, đồng thời truyền kích thước kết cấu dưới dạngsize. - BẮT BUỘC: Đồng bộ hoá hình học của phần tử HTML với nội dung được kết xuất. Trong 2D,
drawElementImagesẽ tự động thực hiện việc này. Trong WebGL và WebGPU, hãy gọicanvas.updateElementGeometry(element, { canvasTransform }). - BẮT BUỘC: Sử dụng
ResizeObserverđể quan sát kích thước màn hình và cập nhật kích thước canvas cho phù hợp với pixel của thiết bị. - KHÔNG nhúng nội dung trên nhiều nguồn gốc vào canvas vì tính năng này không được hỗ trợ.
- KHÔNG khởi động
ResizeObservertrong trình xử lý sự kiệnonpaintvì việc này có thể dẫn đến rò rỉ bộ nhớ.
Chiến lược dự phòng
API HTML-in-Canvas hiện không được hỗ trợ trong tất cả các trình duyệt hiện đại, do đó, bạn thường cần có chiến lược dự phòng. Tuy nhiên, do lợi ích về hiệu suất được cải thiện của API này, bạn nên sử dụng HTML-in-Canvas nếu trình duyệt hỗ trợ.
Đối với trường hợp sử dụng cần xuất nội dung HTML từ canvas, hãy sử dụng các thư viện như html2canvas, dom-to-image hoặc snapdom.
Để ghi lại từng khung hình của các hoạt động tương tác HTML (ví dụ: để phát trực tuyến), hãy ghi lại các đột biến DOM bằng cách sử dụng các thư viện như rrweb.
Ngoài ra, hãy triển khai cảnh báo rằng trình duyệt không hỗ trợ tính năng xuất nội dung nghe nhìn HTML vì trình duyệt không hỗ trợ HTML-in-Canvas.