Trình đổ bóng WebGL cung cấp các hiệu ứng hình ảnh mạnh mẽ được tăng tốc bằng GPU, cho phép các tính năng nâng cao như biến dạng gợn sóng động, mô hình ánh sáng, phân loại màu và biến đổi đỉnh tuỳ chỉnh. API HTML-in-Canvas cho phép nhà phát triển áp dụng kết cấu WebGL cho nội dung HTML. Điều này cho phép áp dụng trình đổ bóng mảnh và đỉnh hiệu suất cao cho các thành phần giao diện người dùng tương tác đầy đủ, chẳng hạn như nút, trường nhập dữ liệu và văn bản đa dạng thức, đồng thời vẫn giữ lại khả năng hỗ trợ tiếp cận gốc, lựa chọn văn bản và xử lý sự kiện DOM.
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 }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 phần tử đó, 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);Trước tiên, hãy phân bổ trước lớp nền kết cấ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")).
Trước tiên, hãy phân bổ bộ nhớ kết cấu bằng cách gọi texImage2D() bằng null thay vì dữ liệu pixel. Thao tác này sẽ dự trữ bộ nhớ GPU có kích thước và định dạng nhất định mà không tải bất kỳ pixel nào lên. Hãy tuân thủ các quy tắc sau:
- Đị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. - 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.
// 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;
}
// 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 };
Hiển thị nội dung HTML cho hoạ tiết bên trong trình xử lý sự kiện
canvas.onpaintbằng phương thứctexElementSubImage2D. Hãy tuân thủ các quy tắc sau:- Bạn chỉ có thể tải một phần tử lên sau khi canvas đã vẽ phần tử đó. Tải lên từ sự kiện
paintvà gọicanvas.requestPaint()để yêu cầu một sự kiện. - 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 hoạ tiết nếu kích thước thay đổi, như trong bước 5.
- Bạn chỉ có thể tải một phần tử lên sau khi canvas đã vẽ phần tử đó. Tải lên từ sự kiệ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 (step 5).
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);
}
};
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. Hãy đảm bảo rằng bạn chỉ tải lại kết cấu lên nếu có nội dung 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) {
// Update the texture with uploadElementWebGL()
}
// Render the scene, and sync the element geometry as shown in step 7
};
- Đồng bộ hoá hình học của phần tử.
Tính toán một DOMMatrix liê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ử đó. - Truyền
{ canvasClip }, mộtDOMRectInittrong không gian toạ độ củacanvasTransform, để giới hạn vùng có thể kiểm tra nhấn, ví dụ: khi chỉ một phần của phần tử được vẽ.
Mã mẫu
<canvas id="canvas" content="drawable" style="width: 400px; height: 400px;">
<div id="ui-element" drawable>
<p>WebGL UI Element</p>
<button>Action</button>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl2");
const uiElement = document.getElementById("ui-element");
// getElementImageSize() and allocateElementTexture() are defined in step 5.
// uploadElementWebGL() is defined in step 6.
// computeCanvasTransform() is defined in step 7.
// Setup WebGL texture...
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
const textureState = { width: 0, height: 0 };
canvas.onpaint = () => {
// 1. Update texture with HTML content
try {
uploadElementWebGL(gl, canvas, texture, uiElement, textureState);
} catch (err) {
console.error('texElementSubImage2D copy failed:', err);
}
// ... Render your 3D scene here, calculating htmlElementMVP matrix ...
// 2. Sync DOM position, hit testing, and accessibility bounds with 3D scene
canvas.updateElementGeometry(uiElement, {
canvasTransform: computeCanvasTransform(canvas, uiElement, htmlElementMVP),
});
};
</script>
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: LUÔN thêm thuộc tính
drawablevào các phần tử con trực tiếp của canvas. - BẮT BUỘC: Thêm thuộc tính
drawablevào mọi phần tử mà bạn muốn có thể vẽ độc lập với các phần tử mẹ. - 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: Phân bổ trước vùng đệm kết cấu bằng
texImage2D()một lần (khi đượ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: 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: Đồng bộ hoá hình dạng của phần tử HTML với nội dung được kết xuất bằng cách gọi
canvas.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ợ.
Chiến lược dự phòng phụ thuộc vào trường hợp sử dụng. Ví dụ: đối với nội dung HTML tương tác trong canvas, nếu HTML-in-Canvas không được hỗ trợ, hãy đặt nội dung HTML lên trên canvas bằng CSS.
Polyfill HTML-in-Canvas
Sử dụng tập lệnh polyfill sau đây để mô phỏng HTML-in-Canvas API trong các trình duyệt không hỗ trợ API này.
Cài đặt hoặc nhúng thư viện:
# Install npm install three-html-render# Embed <script src="https://cdn.jsdelivr.net/npm/three-html-render/dist/polyfill.js"></script>Chạy phương thức
installHtmlInCanvasPolyfill()để dịch HTML-in-Canvas.