Shader WebGL memberikan efek visual yang didukung GPU yang canggih, sehingga memungkinkan kemampuan lanjutan seperti distorsi riak dinamis, model pencahayaan, gradasi warna, dan transformasi vertex kustom. HTML-in-Canvas API memungkinkan developer menerapkan tekstur WebGL ke konten HTML. Hal ini memungkinkan penerapan shader fragmen dan vertex berperforma tinggi secara native ke komponen UI yang sepenuhnya interaktif, seperti tombol, kolom input, dan rich text, sekaligus mempertahankan aksesibilitas native, pemilihan teks, dan penanganan peristiwa DOM.
Cara menerapkan
Periksa apakah HTML-in-Canvas didukung di browser:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }Tambahkan atribut
content="drawable"ke elemen HTML<canvas>.Tempatkan konten HTML Anda di dalam elemen
<canvas>, dan tambahkan atributdrawableke setiap elemen yang Anda gambar. Elemen dengandrawablemenangkap subhierarkinya, kecuali turunan bertingkat yang juga merupakandrawable.<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>Skalakan petak kanvas agar sesuai dengan faktor skala perangkat untuk mencegah keburaman:
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);Alokasikan terlebih dahulu dukungan tekstur menggunakan metode
texImage2D. Cuplikan dalam panduan ini menggunakan konteks WebGL 2 (canvas.getContext("webgl2")).
Alokasikan penyimpanan tekstur terlebih dahulu dengan memanggil texImage2D() dengan null, bukan data piksel, yang mencadangkan memori GPU dengan ukuran dan format yang ditentukan tanpa mengupload piksel apa pun. Ikuti aturan berikut:
- Ukur tekstur dari
canvas.captureElementImage(element).widthdanheight-nya berada dalam piksel petak kanvas (backing store), jadi bulatkan denganMath.ceil().captureElementImage()menampilkan error jika elemen belum memiliki data gambar: panggilrequestPaint(), dan coba lagi pada peristiwapaintberikutnya. - Alokasikan level 0 hanya pada upload pertama, atau saat ukuran berubah, karena alokasi ulang akan menghapus tekstur. Gunakan penyimpanan yang dapat diubah: penyimpanan
texStorage2D()yang tidak dapat diubah tidak dapat diubah ukurannya saat elemen berubah ukuran.
// 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 };
Render konten HTML ke tekstur di dalam pengendali peristiwa
canvas.onpaintmenggunakan metodetexElementSubImage2D. Ikuti aturan berikut:- Elemen hanya dapat diupload setelah kanvas menggambarnya. Upload dari peristiwa
paint, dan panggilcanvas.requestPaint()untuk memintanya. - Teruskan
{ width, height }dalam konfigurasi. Hal ini membuat salinan mengisi tekstur yang dialokasikan, terlepas dari cara kanvas diskalakan. - Alokasikan ulang tekstur jika ukurannya berubah, seperti yang ditunjukkan pada langkah 5.
- Elemen hanya dapat diupload setelah kanvas menggambarnya. Upload dari peristiwa
// 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);
}
};
Saat menggunakan loop requestAnimationFrame untuk merender tampilan, panggil canvas.requestPaint() dalam loop untuk memastikan konten HTML dirender ke kanvas. event.changedElements mencantumkan elemen yang rendering-nya berubah. Pastikan Anda hanya mengupload ulang tekstur jika ada update pada elemen HTML turunan:
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
};
- Menyinkronkan geometri elemen.
Menghitung DOMMatrix yang memetakan kotak batas elemen, dalam piksel CSS, ke kanvas, dalam piksel CSS. Browser perlu memetakan dari ruang koordinat 3D ke ruang koordinat CSS menggunakan transformasi area pandang. Untuk memfasilitasi hal ini, lakukan hal berikut:
- Menormalisasi elemen HTML. Elemen HTML diukur dalam piksel (misalnya, lebar 200 px). Namun, WebGL biasanya memperlakukan objek sebagai "persegi satuan", misalnya, berkisar dari -0,5 hingga 0,5. Jika Anda tidak menormalisasi, tombol 200 px akan terlihat 200 kali lebih besar. Langkah ini juga membalik sumbu Y, karena di CSS, arah ke bawah bernilai positif, tetapi di WebGL, arah ke atas bernilai positif.
- Mengonversi Matriks MVP WebGL menjadi Matriks DOM.
- Petakan ke area pandang kanvas. Langkah ini adalah fase "penskalaan ulang": langkah ini memperluas matematika ruang unit tersebut kembali agar sesuai dengan dimensi piksel CSS elemen
<canvas>Anda di layar, dan membalik sumbu Y kembali. Menghitung transformasi akhir. Kalikan matriks secara berurutan: Area pandang * MVP * Normalisasi. Menggabungkannya menjadi satu transformasi akhir akan menghasilkan "peta" yang memberi tahu browser tempat elemen HTML tersebut harus diletakkan agar sejajar dengan gambar 3D. Browser melakukan pembagian perspektif.
// 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); }
Kemudian, teruskan transformasi ke canvas.updateElementGeometry() sebagai canvasTransform. Tindakan ini akan memindahkan elemen HTML agar berada tepat di atas piksel yang dirender. Hal ini memastikan bahwa saat pengguna mengklik tombol atau memilih teks, mereka benar-benar mengklik elemen HTML yang sebenarnya. Browser juga menggunakan transformasi untuk batas aksesibilitas elemen, misalnya, untuk cincin fokus pembaca layar:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
Gunakan opsi ini untuk mengontrol hit testing:
- Setiap panggilan
updateElementGeometry()akan memindahkan elemen ke bagian atas urutan hit-testing. Pass{ preserveHitTestOrder: true }untuk mempertahankan posisinya. - Panggil
canvas.clearElementGeometry(element)untuk menghapus elemen dari hit testing, misalnya, saat Anda menyembunyikannya. - Teruskan
{ canvasClip },DOMRectInitdalam ruang koordinatcanvasTransform, untuk membatasi area yang dapat diuji tekan, misalnya, saat hanya sebagian elemen yang digambar.
Kode contoh
<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>
Praktik Terbaik
- WAJIB: Periksa dukungan browser untuk HTML-in-Canvas API sebelum menggunakannya.
- WAJIB: Selalu tambahkan atribut
content="drawable"ke elemen<canvas>. - WAJIB: SELALU tambahkan atribut
drawableke turunan langsung kanvas. - WAJIB: Tambahkan atribut
drawableke setiap elemen yang ingin Anda gambar secara independen dari elemen induknya. - WAJIB: Gunakan pengendali peristiwa
onpaintuntuk merender konten HTML ke kanvas. - WAJIB: Alokasikan buffer tekstur terlebih dahulu dengan
texImage2D()satu kali (saat diinisialisasi atau saat ukuran elemen berubah). Alokasikan ulang tekstur hanya saat ukuran berubah, dan tetapkanTEXTURE_MIN_FILTERkeLINEAR. - WAJIB: Gunakan metode
texElementSubImage2Duntuk merender konten HTML ke tekstur yang telah Anda alokasikan sebelumnya dengantexImage2D(), berukuran daricaptureElementImage(). - WAJIB: Sinkronkan geometri elemen HTML dengan konten yang dirender dengan memanggil
canvas.updateElementGeometry(element, { canvasTransform }). - WAJIB: Gunakan
ResizeObserveruntuk mengamati ukuran layar dan memperbarui ukuran kanvas agar sesuai dengan piksel perangkat. - JANGAN menyematkan konten lintas origin di kanvas, karena tidak didukung.
- JANGAN melakukan inisialisasi
ResizeObserverdalam pengendali peristiwaonpaint, karena dapat menyebabkan kebocoran memori.
Strategi penggantian
HTML-in-Canvas API saat ini tidak didukung di semua browser modern, sehingga strategi penggantian biasanya diperlukan.
Namun, mengingat manfaat peningkatan performa API ini, HTML-in-Canvas harus digunakan jika browser mendukungnya.
Strategi penggantian bergantung pada kasus penggunaan. Misalnya, untuk konten HTML interaktif di kanvas, jika HTML-in-Canvas tidak didukung, tempatkan konten HTML di atas kanvas menggunakan CSS.
Polyfill HTML-in-Canvas
Gunakan skrip polyfill berikut untuk meniru HTML-in-Canvas API di browser yang tidak mendukungnya.
Instal atau sematkan library:
# Install npm install three-html-render# Embed <script src="https://cdn.jsdelivr.net/npm/three-html-render/dist/polyfill.js"></script>Jalankan metode
installHtmlInCanvasPolyfill()untuk menerjemahkan HTML-in-Canvas.