HTML-in-Canvas API memungkinkan Anda merender subtree DOM yang interaktif dan mudah diakses langsung ke konteks kanvas 2D, WebGL, atau WebGPU.
Panduan ini menguraikan konsep teknis HTML-in-Canvas yang umum di semua konteks kanvas:
- Menyiapkan kanvas untuk merender HTML
- Memahami subtree
drawable - Memahami peristiwa
paint - Memperbarui geometri elemen
- Memahami aksesibilitas HTML-in-Canvas
- Menggunakan strategi penggantian
Menyiapkan kanvas untuk merender HTML
Untuk merender elemen HTML di dalam <canvas>, konfigurasikan dua atribut berikut:
- Tetapkan
content="drawable"pada<canvas>: Tindakan ini akan mengaktifkan elemen canvas untuk menata letak node DOM turunannya, bukan hanya memperlakukannya sebagai konten tersembunyi. - Tetapkan
drawablepada elemen target: Tindakan ini menandai elemen sebagai root render independen yang memenuhi syarat untuk rendering kanvas.
<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>
Memahami subhierarki drawable
Saat menyiapkan HTML untuk dirender ke dalam kanvas, Anda harus memutuskan elemen mana yang harus digambar secara independen. Setiap elemen drawable mewakili root gambar independen dan subhierarki drawable-nya.
Sub-hierarki drawable terdiri dari elemen dengan atribut drawable dan semua turunannya, tidak termasuk turunan bertingkat yang juga membawa drawable, misalnya:
<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>
| Elemen | Batas paint | Perilaku |
|---|---|---|
#parentElement (Subtree A) |
Panel latar belakang, judul, dan paragraf | Dirender saat memanggil drawElementImage(parentElement, ...) |
#nestedChildElement (Subtree B) |
Permukaan dan teks tombol | Dikecualikan dari subhierarki #parentElement; dirender dalam panggilan drawElementImage(nestedChildElement, ...) terpisah |
drawable disusun bertingkat di dalam kartu drawable, dan masing-masing digambar oleh panggilan drawElementImage()-nya sendiri.Dengan menyusun elemen drawable, Anda dapat mempertahankan hubungan HTML semantik sekaligus memberi Anda kebebasan untuk membuat lapisan, memosisikan, atau menganimasikan elemen turunan secara independen di seluruh scene kanvas.
Memahami peristiwa paint
HTML-in-Canvas API memperkenalkan peristiwa paint. Browser otomatis mengirimkan peristiwa paint di <canvas> setiap kali tata letak, teks, atau gaya CSS berubah di salah satu turunan drawable-nya.
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);
});
Peristiwa paint mengirimkan instance CanvasPaintEvent, yang memperluas Event DOM standar dan memiliki properti changedElements yang hanya berisi turunan drawable yang berubah secara visual (misalnya, perubahan gaya, pergeseran tata letak, pengeditan teks, atau perubahan status fokus) sejak rendering terakhir.
canvas.addEventListener("paint", (event) => {
// Array of changed elements
for (const el of event.changedElements) {
console.log(`Element #${el.id} visually changed and needs repainting`);
}
});
Siklus proses frame
Perintah gambar kanvas yang dijalankan di dalam peristiwa paint akan muncul dalam update rendering saat ini, tetapi modifikasi DOM yang dibuat di dalam peristiwa paint tidak akan muncul hingga frame berikutnya:
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";
});
Kanvas bertingkat
Jika halaman Anda berisi elemen <canvas content="drawable"> bertingkat, browser akan mengirimkan peristiwa paint dalam urutan hierarki DOM terbalik (turunan diaktifkan sebelum induk). Hal ini memastikan elemen kanvas turunan dirender sebelum elemen kanvas induk menggabungkannya.
Misalnya, dalam contoh berikut, elemen nestedElement dalam harus berubah secara visual (misalnya, warna latar belakang CSS-nya berubah saat pengguna mengarahkan kursor ke elemen tersebut). Pertama, peristiwa paint diaktifkan di kanvas nestedCanvas. Pemrosesnya menghapus buffer turunan dan memanggil ctx.drawElementImage(nestedElement, ...) untuk merender status yang diperbarui. Kemudian, peristiwa paint diaktifkan di parentCanvas. Pemrosesnya menggambar wrapperElement (yang berisi kanvas nestedCanvas yang sudah diperbarui) ke dalam tampilan.
<!-- 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>
<canvas content="drawable"> disusun bertingkat, peristiwa gambar diaktifkan dalam urutan hierarki terbalik: kanvas bertingkat menggambar terlebih dahulu, sehingga kanvas induk dapat menggambar piksel terbarunya.Memicu peristiwa paint
Panggil requestPaint() untuk menjadwalkan satu peristiwa paint pada frame berikutnya, meskipun tidak ada teks atau gaya DOM yang berubah. Hal ini dapat digunakan untuk merender adegan interaktif dengan elemen bergerak, misalnya:
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();
Memperbarui geometri elemen
Saat Anda menggambar elemen HTML ke dalam kanvas, kode gambar Anda akan menentukan tempat elemen muncul dan ukurannya. Tidak seperti konten HTML biasa, posisi dan ukuran elemen di layar, yaitu geometrinya, tidak dapat ditentukan dari gaya yang dihitung saja. Browser memerlukan geometri elemen untuk melakukan hal berikut:
- Mengirim peristiwa input seperti klik, ketukan, dan gerakan pointer ke elemen yang tepat. Proses ini disebut hit testing.
- Menampilkan hasil yang akurat dari kueri tata letak, seperti
getBoundingClientRect(). - Melaporkan batas elemen ke teknologi pendukung, seperti pembesar layar.
Dalam 2D, drawElementImage() memperbarui geometri secara otomatis secara default. Argumen yang Anda teruskan ke fungsi tersebut, bersama dengan gaya yang dihitung elemen dan transformasi konteks saat ini, sudah cukup untuk menentukan tempat elemen muncul.
Dalam konteks WebGL dan WebGPU, browser tidak dapat menentukan geometri. Elemen HTML disalin ke dalam tekstur, dan program shader Anda kemudian dapat memetakan tekstur tersebut ke scene dengan cara arbitrer apa pun. Untuk memberi tahu browser tempat elemen muncul, teruskan transformasi ke updateElementGeometry().
updateElementGeometry(), browser tidak lagi mengetahui lokasi elemen HTML pada kartu 3D. Kartu terus berputar, tetapi klik masih mengarah ke tempat semula.Untuk memperbarui geometri, teruskan transformasi spasial ke metode. Pelajari cara membuat transformasi untuk berbagai konteks:
Mengelola urutan tumpukan hit-test
Secara default, setiap kali geometri elemen diperbarui, browser akan membawanya ke bagian atas stack interaksi (digambar terakhir = target klik paling atas). Anda dapat mengontrol perilaku ini menggunakan preserveHitTestOrder:
// Updates the backdrop without stealing clicks from foreground elements
canvas.updateElementGeometry(backgroundElement, {
canvasTransform: customTransform,
preserveHitTestOrder: true
});
| Opsi | Perilaku | Kasus penggunaan |
|---|---|---|
preserveHitTestOrder: false (default) |
Memindahkan elemen ke bagian atas stack hit-test. | Elemen latar depan yang harus menerima klik terlebih dahulu. |
preserveHitTestOrder: true |
Membiarkan elemen pada prioritas tumpukan saat ini. | Elemen latar belakang yang perlu diperbarui setelah elemen latar depan. |
Reset geometri
Untuk membatalkan penggambaran elemen tanpa menghapusnya dari DOM, panggil canvas.clearElementGeometry(). Tindakan ini akan mereset transformasi kanvas elemen dan menandainya sebagai di luar layar dalam hierarki aksesibilitas:
// Remove from canvas hit-testing and mark as offscreen
canvas.clearElementGeometry(childElement);
Ukuran dan unit
Saat Anda menggambar HTML ke dalam kanvas, browser beroperasi dalam tiga sistem koordinat sekaligus: piksel CSS, piksel perangkat, dan piksel petak kanvas.
Piksel CSS digunakan untuk mengukur dan memosisikan konten selama tata letak. Kemudian, browser akan menskalakannya sebesar devicePixelRatio untuk merasterisasi halaman menjadi piksel perangkat fisik. Karena piksel CSS adalah unit logis, posisi dan ukuran sering kali berakhir sebagai pecahan piksel perangkat (misalnya, 10,5 px CSS × DPR 1,5 = 15,75 px perangkat). Layar hanya dapat mewarnai seluruh piksel, sehingga browser harus me-resolve apa pun yang berada di antaranya. Hal ini dilakukan dengan dua cara:
- Snapping: Batas kotak, batas, dan offset lapisan dibulatkan ke piksel perangkat utuh terdekat agar tetap tajam.
- Anti-aliasing: Untuk glyph teks, kurva, dan tepi yang diubah, cakupan parsial dipadukan ke dalam piksel di sekitarnya sehingga tetap terlihat halus dan berada di tempat yang tepat.
<canvas> memiliki petak pikselnya sendiri, yang ditetapkan oleh atribut width dan height, yang terpisah dari piksel CSS dan piksel perangkat. Untuk menampilkannya, browser mengambil sampel ulang bitmap kanvas ke petak piksel fisik, dengan memadukan piksel kanvas yang berdekatan untuk mengisi setiap piksel perangkat. Untuk mempelajari cara browser memetakan antara CSS, kanvas, dan piksel perangkat, lihat Rendering yang sempurna piksel dengan devicePixelContentBox.
Setiap elemen HTML drawable ditata letak dalam piksel CSS. Karena konten yang digambar ke dalam kanvas dapat melalui dua konversi, dari piksel CSS ke petak kanvas, lalu dari petak kanvas ke piksel fisik, setiap langkah yang tidak sejajar akan menambahkan putaran penggabungan lain, dan hasilnya dapat terlihat buram. Untuk menghindari hal ini, buat petak kanvas sesuai dengan petak piksel fisik. Hal ini memiliki dua tujuan:
- Rendering yang konsisten: Jika kanvas dipetakan 1:1 ke piksel fisik, mengonversi piksel CSS menjadi piksel kanvas akan menghasilkan hasil yang sama dengan konversi browser dari piksel CSS menjadi piksel fisik untuk HTML reguler.
- Tidak ada penggabungan tambahan: Jika kanvas dipetakan 1:1 ke piksel fisik, mengonversinya ke piksel perangkat tidak akan menambahkan blur.
Gambar berikut menunjukkan konversi piksel yang menghasilkan konten buram. Ketiga petak tidak sejajar: Elemen kanvas berukuran 360 × 180 piksel CSS, dan ukuran kanvas adalah 300 × 150 piksel petak kanvas. Pada rasio piksel perangkat 1,5, kanvas mencakup 540 × 270 piksel perangkat. Satu piksel CSS mencakup 1,5 piksel perangkat, dan satu piksel kanvas mencakup 1,2 piksel CSS, atau 1,8 piksel perangkat (panel 1). Saat lingkaran digambar, posisi dan radiusnya berada dalam piksel kanvas, bukan piksel CSS, sehingga browser menggambarnya ke dalam bitmap kanvas 300 × 150 dan melakukan anti-alias pada tepinya (panel 2). Untuk menampilkan kanvas, browser kemudian merentangkan bitmap ini ke 540 × 270 piksel perangkat, sehingga browser harus melakukan interpolasi: setiap piksel perangkat mengambil campuran berbobot dari piksel kanvas terdekat, yang sudah berisi tepi anti-alias. Tepi tersebut dipadukan untuk kedua kalinya dan tersebar di beberapa piksel perangkat, sehingga lingkaran terlihat buram (panel 3). Hal ini dapat dihindari jika petak kanvas cocok dengan piksel perangkat 1:1. Browser kemudian akan melakukan anti-alias pada tepi hanya sekali, langsung di petak perangkat, dan menampilkan bitmap tanpa pengambilan sampel ulang, sehingga tepi tetap tajam (panel 4):
Untuk rendering yang jelas, petak kanvas harus sesuai dengan jumlah piksel perangkat yang dicakup kanvas: 540 × 270 dalam contoh sebelumnya.
Menentukan ukuran petak kanvas dengan pengamat pengubahan ukuran
Ukuran petak yang tepat bergantung pada perangkat, dan dapat berubah saat tata letak berubah, saat pengguna memperbesar halaman, atau saat pengguna memindahkan jendela ke layar lain. Jadi, daripada melakukan hardcode pada ukuran petak, biarkan browser memberi tahu Anda ukuran yang seharusnya.
Agar petak kanvas tetap sinkron dengan kanvas di layar, selalu ukur dengan ResizeObserver dan sesuaikan petak kanvas ke 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);
Mengonversi piksel CSS menjadi koordinat petak kanvas
Pengendali paint sering kali menggabungkan pengukuran DOM dengan panggilan gambar—misalnya, untuk memosisikan elemen berdasarkan ukurannya. Keduanya menggunakan unit yang berbeda:
- Piksel kanvas mendeskripsikan petak kanvas, yang ukurannya ditentukan oleh
canvas.widthdancanvas.height. Misalnya, parameterdx,dy,dwidth, dandheightdari metode gambardrawElementImage(), atau parameter API canvas lainnya, sepertifillRect()dantranslate(). - Piksel CSS mendeskripsikan halaman. Pengukuran DOM seperti
offsetWidthdanclientWidth, serta koordinat peristiwa pointer sepertioffsetX, menggunakan piksel CSS.
Berikut adalah ringkasan unit yang digunakan oleh berbagai parameter API:
| Nilai | Unit |
|---|---|
canvas.width dan canvas.height |
Piksel petak kanvas |
Parameter gambar 2D, termasuk dx, dy, dwidth, dan dheight dari drawElementImage() |
Piksel petak kanvas |
width dan height ditampilkan oleh canvas.getElementImageDefaultSize() |
Piksel petak kanvas |
Persegi panjang sumber: sx, sy, swidth, dan sheight dari drawElementImage() dan texElementSubImage2D(); sourceX, sourceY, sourceWidth, dan sourceHeight dari drawElementImageToTexture() |
Piksel CSS |
Ukuran tekstur: Opsi width dan height dari texElementSubImage2D(); size dari drawElementImageToTexture() |
Piksel tekstur. Secara default, ukuran elemen dalam piksel petak kanvas, dibulatkan ke atas |
Geometri elemen: getElementTransform(), serta opsi canvasTransform dan canvasClip dari updateElementGeometry() |
Piksel CSS |
Pengukuran DOM, seperti offsetWidth, clientWidth, getBoundingClientRect(), koordinat peristiwa pointer, dan contentBoxSize entri pengamat pengubahan ukuran |
Piksel CSS |
devicePixelContentBoxSize entri pengamat pengubahan ukuran |
Piksel perangkat |
Jika Anda menggunakan unit yang salah, ukuran konten HTML yang digambar ke kanvas mungkin tidak sesuai dengan ekspektasi Anda. Anda perlu mengonversi nilai piksel CSS menjadi piksel petak kanvas saat menggambar HTML ke dalam kanvas. Untuk melakukannya, hitung rasio antara ukuran petak kanvas dan ukuran CSS kanvas: canvas.width / cssContentWidth secara horizontal, dan canvas.height / cssContentHeight secara vertikal. Resize observer adalah tempat yang tepat untuk menghitung rasio sebagai berikut:
// 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);
Kemudian, kalikan nilai piksel CSS dengan scaleX dan scaleY sebelum meneruskannya ke metode gambar. Misalnya, untuk menempatkan elemen drawable di tengah kanvas:
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);
};
Terakhir, karena drawElementImage() menggambar kotak batas elemen, gunakan box-sizing: border-box untuk menyertakan padding dan batas dalam width dan height kartu, sehingga kartu digambar sesuai ukuran yang Anda tetapkan:
#card {
/* Make width and height include padding and border */
box-sizing: border-box;
width: 200px;
padding: 16px;
}
Memahami aksesibilitas HTML-in-Canvas
Umumnya, konten HTML <canvas content="drawable"> diekspos ke aksesibilitas dengan cara yang sama seperti DOM reguler, yang berarti konten tersebut direpresentasikan dalam hierarki aksesibilitas kecuali jika disembunyikan secara eksplisit menggunakan atribut seperti aria-hidden="true" atau inert. Namun, ada satu pengecualian utama: elemen drawable yang geometri yang dirender-nya belum ditetapkan atau diperbarui.
Elemen drawable di dalam <canvas content="drawable"> tidak memiliki geometri yang dirender jika:
- Peta ini belum dirender dengan
drawElementImage()yang memperbarui geometri secara otomatis secara default. - Geometrinya belum diperbarui secara eksplisit dengan
updateElementGeometry().
Jika elemen drawable tidak memiliki geometri yang dirender, elemen drawable ini dan subhierarki drawable-nya akan dilaporkan ke hierarki aksesibilitas tanpa informasi geometri. Hal ini berguna, misalnya, untuk memungkinkan pembaca layar mengakses konten semantik, sekaligus memberi sinyal kepada alat bantu visual (seperti pembesar) untuk memfilter elemen yang tidak digambar.
Contoh berikut mempertahankan semantik gambar, gambar, dan teks saat menggambar gambar dan gambar secara terpisah:
<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>
Strategi penggantian
HTML-in-Canvas API dikirimkan di Chrome 157, dan mungkin tidak didukung di semua browser. Untuk memastikan solusi Anda berfungsi dengan lancar bagi semua orang, periksa apakah browser mendukung API, dan gunakan solusi alternatif jika diperlukan:
export function supportsHtmlInCanvas() {
if (typeof document === 'undefined') return false;
const canvas = document.createElement('canvas');
canvas.setAttribute('content', 'drawable');
return canvas.content === 'drawable';
}
Jika API tidak didukung, bergantung pada sasaran penerapan, Anda dapat memilih salah satu alternatif berikut:
- UI alternatif
- Overlay HTML mengambang
- Library three-html-render
- html2canvas
- dom-to-image
Langkah berikutnya
- Pelajari cara merender HTML dalam konteks 2D.
- Pelajari cara merender HTML di kanvas WebGL.
- Pelajari cara merender HTML di kanvas WebGPU.
- Pelajari strategi penggantian.
- Laporkan masalah di GitHub untuk memberikan masukan.
- Lihat koleksi demo HTML-in-Canvas oleh komunitas.