Konten <canvas> reguler tidak diekspos ke fitur browser seperti pembaca layar, pengindeksan, alat terjemahan, alat bantu aksesibilitas, temukan dalam halaman, cetak, dll. Dengan HTML in canvas, Anda dapat merender DOM asli langsung di elemen kanvas. Menambahkan atribut content="drawable" ke elemen HTML <canvas> akan menata letak elemen HTML turunannya, dan atribut drawable menandai elemen yang dapat Anda render dalam konteks rendering kanvas. Anda dapat menggunakannya untuk menata gaya dan tata letak teks di kanvas, mengekspos konten kanvas ke fitur browser (seperti aksesibilitas, terjemahan, atau temukan di halaman), serta menerapkan efek 2D dan 3D ke HTML.
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);Render konten HTML ke kanvas di dalam pengendali peristiwa
canvas.onpaint. Elemen hanya dapat digambar setelah kanvas menggambarnya: panggilcanvas.requestPaint()untuk meminta peristiwapaint.
- Dalam konteks 2D, gunakan metode
drawElementImage. Fungsi ini menggambar snapshot terakhir dari elemendrawable, dan subhierarki yang dapat digambarnya, dimulai dari kotak batas elemen, sebelum transformasi CSS:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- Dalam konteks WebGL dan WebGPU, Anda menyalin elemen ke dalam tekstur. 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.
// 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;
}
- Dalam konteks WebGL, alokasikan terlebih dahulu dukungan tekstur menggunakan metode
texImage2D. Cuplikan dalam panduan ini menggunakan konteks WebGL 2 (canvas.getContext("webgl2")). Alokasikan penyimpanan tekstur dengan memanggiltexImage2D()dengannull, bukan data piksel, yang mencadangkan memori GPU dengan ukuran dan format tertentu tanpa mengupload piksel apa pun. Ikuti aturan berikut:- Ubah ukuran tekstur dari
captureElementImage(), seperti yang ditunjukkan di atas. - 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.
- Ubah ukuran tekstur dari
// 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 };
- Kemudian, dalam konteks WebGL, render konten HTML ke tekstur menggunakan metode
texElementSubImage2D. Ikuti aturan berikut:- 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 di atas.
- Teruskan
// 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);
}
};
- Dalam konteks WebGPU, gunakan metode
drawElementImageToTexture. Ikuti aturan berikut:- Buat tekstur dengan penggunaan
COPY_DSTdanRENDER_ATTACHMENT, lalu tambahkanTEXTURE_BINDINGuntuk mengambil sampelnya di shader. Penggunaan yang salah dilaporkan sebagai error validasi WebGPU, bukan sebagai pengecualian. - Teruskan ukuran tekstur sebagai
size. Tanpasize, elemen yang lebih besar akan dipangkas ke tekstur. Dengansize, elemen diskalakan untuk mengisinya. - Tekstur WebGPU tidak dapat diubah ukurannya. Buat ulang tekstur saat ukuran elemen berubah, dan buat ulang bind group yang mereferensikannya.
- Buat tekstur dengan penggunaan
// WebGPU textures can't be resized. 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);
}
};
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 menggambar ulang elemen, atau mengupload ulang teksturnya, 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) {
// 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 6
};
- Menyinkronkan geometri elemen.
Browser perlu mengetahui tempat elemen HTML digambar, sehingga posisi DOM-nya cocok dengan piksel yang digambar. Hit testing, misalnya, untuk klik dan pemilihan teks, serta aksesibilitas menggunakan geometri ini: elemen drawable yang geometrinya tidak diperbarui masih diekspos ke hierarki aksesibilitas, tetapi tanpa informasi geometri. Kanvas hanya melakukan hit test pada elemen yang digambar dengan metode drawElementImage() 2D, atau didaftarkan dengan updateElementGeometry().
Untuk kasus konteks 2D, browser akan menyinkronkan elemen secara otomatis.
drawElementImage()menyelaraskan batas hit-test DOM dan cincin fokus pembaca layar dengan koordinat yang digambar, sehingga Anda tidak perlu menetapkanstyle.transform. Untuk mengelola geometri secara manual, misalnya, saat Anda menggambar elemen yang sama lebih dari sekali, teruskan{ preserveElementGeometry: true }kedrawElementImage(), dan terapkan transformasi dengancanvas.updateElementGeometry().Untuk kasus 3D dengan WebGL atau WebGPU, kanvas tidak dapat mengetahui tempat elemen HTML digambar, jadi Anda harus memberi tahunya. Menghitung
DOMMatrixyang 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.
Kode contoh
Canvas 2D
<canvas id="canvas" content="drawable" style="width: 400px; height: 200px;">
<div id="ui-element" drawable>
<p>
This text is rendered inside the canvas but is present in the DOM tree.
</p>
<input type="email" name="email" placeholder="enter your email" />
<button type="button">Submit</button>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const uiElement = document.getElementById("ui-element");
canvas.onpaint = () => {
ctx.reset();
// Draw the HTML element at x:0, y:0. The browser syncs the DOM location
// with the drawn location.
ctx.drawElementImage(uiElement, 0, 0);
};
// Handle resizing to match device pixels
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>
Canvas WebGL
<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(), allocateElementTexture(), and uploadElementWebGL()
// are defined in step 5.
// computeCanvasTransform() is defined in step 6.
// 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>
Canvas WebGPU
<canvas id="canvas" content="drawable" style="width: 400px; height: 400px;">
<div id="ui-element" drawable>
<p>WebGPU UI Element</p>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const context = canvas.getContext("webgpu");
const uiElement = document.getElementById("ui-element");
// getElementImageSize() and ensureElementTexture() are defined in step 5.
// computeCanvasTransform() is defined in step 6.
// Setup WebGPU...
// const device = ...
const textureState = { texture: null };
canvas.onpaint = () => {
// 1. Copy HTML content to a texture that matches the element size
const size = getElementImageSize(canvas, uiElement);
if (size) {
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);
}
}
// ... Render your 3D scene here, calculating htmlElementMVP matrix ...
// 2. Sync DOM position, hit testing, and accessibility bounds (same matrix math as WebGL)
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: Tambahkan atribut
drawableke setiap elemen yang Anda gambar, termasuk turunan langsung kanvas. - WAJIB: Gunakan pengendali peristiwa
onpaintuntuk merender konten HTML ke kanvas. - WAJIB: Gunakan metode
drawElementImagedalam 2D, ataudrawElementImageToTexturedi WebGPU, untuk merender konten HTML ke kanvas. - WAJIB: Di WebGL, 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: Di WebGL, gunakan metode
texElementSubImage2Duntuk merender konten HTML ke tekstur yang telah Anda alokasikan sebelumnya dengantexImage2D(), berukuran daricaptureElementImage(). - WAJIB: Di WebGPU, tentukan ukuran tekstur dari
captureElementImage(), dan buat ulang hanya saat ukurannya berubah. Buat tekstur dengan penggunaanCOPY_DSTdanRENDER_ATTACHMENT, lalu teruskan ukuran tekstur sebagaisize. - WAJIB: Sinkronkan geometri elemen HTML dengan konten yang dirender. Dalam 2D,
drawElementImagemelakukannya secara otomatis. Di WebGL dan WebGPU, panggilcanvas.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.