Memperkenalkan uji coba origin HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Selama bertahun-tahun, developer web harus membuat pilihan arsitektur yang sulit saat membuat aplikasi visual yang kompleks dan sangat interaktif di web: apakah Anda mengandalkan DOM untuk fitur semantiknya yang kaya, atau apakah Anda merender langsung ke elemen <canvas> untuk performa grafis tingkat rendah?

Dengan HTML-in-Canvas API eksperimental baru—yang kini tersedia dalam uji coba origin—Anda tidak perlu memilih. API ini memungkinkan Anda menggambar konten DOM langsung ke tekstur kanvas 2D atau WebGL/WebGPU sambil menjaga UI tetap interaktif, dapat diakses, dan terhubung ke fitur browser favorit Anda. Dengan menggabungkan HTML dengan pemrosesan grafis tingkat rendah, Anda dapat membuat pengalaman yang sebelumnya tidak mungkin.

DOM versus Canvas

Untuk memahami kemampuan API baru ini, sebaiknya lihat kekuatan relatif DOM dan Canvas.

DOM adalah bagian penting dari UI web. DOM menawarkan solusi tata letak teks langsung, menggunakan konten yang dipahami secara semantik untuk membuat antarmuka yang kaya. Hal ini memungkinkan pengguna melakukan operasi umum di seluruh halaman web dengan lancar—hal-hal yang sering kita anggap remeh, seperti menandai teks untuk disalin, atau mengklik kanan gambar untuk menyimpannya. DOM juga terintegrasi dengan fitur browser penting: alat aksesibilitas, terjemahan, temukan di halaman, mode pembaca, ekstensi, mode gelap, zoom browser, dan isi otomatis.

Canvas (dan WebGL/WebGPU), di sisi lain, memungkinkan akses tingkat rendah untuk mendorong petak piksel untuk grafis 2D dan 3D yang sangat canggih. Game dan aplikasi web yang kompleks (seperti Google Dokumen atau Figma) memerlukan akses tingkat rendah yang berperforma tinggi ini. Karena kanvas pada dasarnya adalah petak piksel, fitur pendukung seperti teks responsif yang digunakan memerlukan logika UI kustom yang kompleks, sehingga meningkatkan ukuran paket Anda secara drastis. Yang terpenting, semua fitur browser canggih yang terintegrasi ke dalam DOM akan rusak sepenuhnya saat UI terperangkap di dalam petak piksel kanvas statis.

Keuntungan menggunakan DOM ke Canvas

HTML-in-Canvas API adalah jembatan yang memberi Anda yang terbaik dari kedua dunia. Dengan menempatkan HTML di dalam elemen <canvas> dan menyinkronkan transformasinya, Anda memastikan konten tetap sepenuhnya interaktif, dan semua integrasi browser berfungsi secara otomatis.

Berikut yang Anda dapatkan dengan mengizinkan DOM menangani UI di dalam elemen <canvas>:

  • Tata letak dan pemformatan teks: Tata letak dan pemformatan teks yang disederhanakan, termasuk teks multibaris atau dua arah dengan gaya CSS yang diterapkan.
  • Kontrol formulir: Kontrol formulir yang ekspresif dan lebih mudah digunakan dengan opsi penyesuaian yang luas.
  • Pemilihan teks, salin/tempel, dan klik kanan: Pengguna dapat menandai teks di dalam scene 3D Anda, atau menu konteks klik kanan secara native.
  • Pemilihan teks, salin/tempel, dan klik kanan: Pengguna dapat menandai teks di dalam scene 3D Anda, atau menu konteks klik kanan secara native.
  • Aksesibilitas: Konten yang dirender di dalam kanvas ditampilkan ke hierarki aksesibilitas. Sistem aksesibilitas dapat mengurai UI seperti HTML normal, dan menampilkannya ke sistem seperti pembaca layar.
  • Find-in-page: Pengguna dapat menggunakan temukan di halaman (Ctrl/Cmd+F) untuk menelusuri teks, dan browser akan menandainya langsung dalam tekstur WebGL Anda.
  • Find-in-page: Pengguna dapat menggunakan temukan di halaman (Ctrl/Cmd+F) untuk menelusuri teks, dan browser akan menandainya langsung dalam tekstur WebGL Anda.
  • Dapat diindeks dan dapat dihubungkan dengan agen AI: Crawler web dan agen AI dapat mengindeks dan membaca teks yang dirender ke dalam scene 2D dan 3D Anda dengan lancar.
  • Integrasi ekstensi: Ekstensi browser berfungsi secara native. Misalnya, ekstensi penggantian teks akan otomatis memperbarui teks yang dirender pada mesh 3D Anda.
  • Integrasi DevTools: Anda dapat memeriksa konten kanvas, termasuk elemen UI WebGL/WebGPU langsung di Chrome DevTools. Sesuaikan gaya CSS di pemeriksa, dan lihat gaya tersebut langsung diperbarui pada tekstur 3D.

Kasus penggunaan tingkat tinggi

API ini membuka potensi luar biasa di beberapa domain:

  • Aplikasi berbasis kanvas besar: Aplikasi web yang berat seperti Google Dokumen, Miro, atau Figma kini dapat merender komponen UI aplikasi yang kompleks secara native ke dalam ruang kerja berbasis kanvas, sehingga meningkatkan aksesibilitas dan mengurangi bobot paket.
  • Scene dan game 3D: Situs pemasaran, pengalaman WebXR imersif, dan game web kini dapat menempatkan UI web yang sepenuhnya interaktif ke dalam scene 3D—seperti buku 3D yang menggunakan teks DOM asli, atau terminal dalam game yang secara native mendukung penyalinan dan penempelan.

Cara menggunakan API

Penggunaan API terjadi dalam tiga fase: Menyiapkan kanvas, merender ke dalam kanvas, dan memperbarui transformasi CSS sehingga browser mengetahui lokasi elemen secara fisik di layar.

Prasyarat

HTML-in-Canvas API berada dalam uji coba origin di Chrome 148 hingga 150. Untuk mengujinya di situs Anda, gunakan Chrome Canary 149 atau yang lebih baru dengan flag chrome://flags/#canvas-draw-element diaktifkan. Untuk mengaktifkan API bagi pengguna lain, daftar ke Uji Coba Origin.

Langkah 1: Penyiapan Canvas dasar

Pertama, tambahkan atribut layoutsubtree ke tag <canvas>. Hal ini membuat browser mengetahui konten yang disarangkan di dalam kanvas, mempersiapkannya untuk ditampilkan di dalam kanvas, dan menampilkannya ke hierarki aksesibilitas.

<canvas id="canvas" style="width: 200px; height: 200px;" layoutsubtree>
  <div id="form_element">
    <label for="name">Name:</label> <input id="name" type="text">
  </div>
</canvas>

Mengukur petak kanvas

Untuk menghindari konten yang dirender menjadi buram, pastikan untuk mengukur petak kanvas agar sesuai dengan faktor skala perangkat.

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);

Langkah 2: Rendering

Untuk konteks 2D, gunakan metode drawElementImage. Lakukan hal ini di dalam peristiwa paint, yang dipicu setiap kali elemen digambar ulang—misalnya, selama penyorotan teks atau input pengguna. Penting untuk memperbarui transformasi CSS elemen dengan nilai yang ditampilkan agar interaktivitas terus berfungsi.

const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');

canvas.onpaint = () => {
  ctx.reset();

  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Use the transform returned later on...
};

Merender dengan WebGL

Untuk WebGL, Anda menggunakan texElementImage2D. Fungsinya mirip dengan texImage2D, tetapi menggunakan elemen DOM sebagai sumber.

canvas.onpaint = () => {
  if (gl.texElementImage2D) {
    gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
  }
};

Merender dengan WebGPU

WebGPU menggunakan metode copyElementImageToTexture pada antrean perangkat, yang analog dengan copyExternalImageToTexture:

canvas.onpaint = () => {
  root.device.queue.copyElementImageToTexture(
    valueElement,
    { texture: targetTexture }
  );
};

Langkah 3: Memperbarui transformasi CSS

Setelah merender elemen ke dalam kanvas, Anda harus memberi tahu browser tentang lokasinya. Hal ini memastikan sinkronisasi spasial antara kanvas dan tata letak DOM. Hal ini penting agar browser dapat memetakan zona peristiwa dengan benar—seperti tempat pengguna mengklik atau mengarahkan kursor—dengan tempat elemen dirender.

Untuk kasus konteks 2D, terapkan transformasi yang ditampilkan oleh panggilan rendering ke .style.transform property:

const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');

canvas.onpaint = () => {
  ctx.reset();
  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Sync the DOM location with the drawn location
  form_element.style.transform = transform.toString();
};

Dengan WebGL atau WebGPU, lokasi elemen di layar bergantung pada cara tekstur output digunakan oleh kode shader, dan tidak dapat disimpulkan dari konteks rendering kanvas. Namun, jika program shader Anda menggunakan proyeksi tampilan model umum untuk menggambar tekstur, Anda dapat menggunakan fungsi praktis baru element.getElementTransform() untuk menghitung transformasi yang dapat digunakan dengan cara yang sama seperti nilai yang ditampilkan dari drawElementImage(). Untuk memfasilitasi hal ini, Anda harus melakukan hal berikut:

  • Mengonversi Matriks WebGL MVP ke Matriks DOM.
  • Menormalkan elemen HTML. Ukuran elemen HTML dalam piksel (misalnya, lebar 200 piksel). Namun, WebGL biasanya memperlakukan objek sebagai "kotak unit", misalnya, mulai dari 0 hingga 1. Jika tidak dinormalkan, tombol 200 piksel akan terlihat 200 kali lebih besar.
  • Memetakan ke area pandang kanvas. Langkah ini adalah fase "penskalaan ulang": langkah ini memperluas matematika ruang unit kembali agar sesuai dengan dimensi piksel sebenarnya dari elemen <canvas> di layar. Langkah ini juga membalik sumbu Y, karena di WebGL, atas adalah positif, tetapi di CSS, bawah adalah positif.
  • Menghitung transformasi akhir. Kalikan matriks dalam urutan: Viewport * MVP * Normalization. Menggabungkannya menjadi satu transformasi akhir akan menghasilkan "peta" yang memberi tahu browser dengan tepat tempat lapisan elemen HTML tersebut harus berada agar selaras dengan gambar 3D.
  • Menerapkan transformasi ke elemen HTML. Tindakan ini memindahkan lapisan elemen HTML agar berada tepat di atas piksel yang dirender. Hal ini memastikan bahwa saat pengguna mengklik tombol atau memilih teks, mereka akan mengklik elemen HTML yang sebenarnya.
if (canvas.getElementTransform) {
  // 1. Convert WebGL MVP Matrix to DOM Matrix
  const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));

  // 2. Normalize the HTML element (pixels -> 1x1 unit square)
  const width = targetHTMLElement.offsetWidth;
  const height = targetHTMLElement.offsetHeight;

  const cssToUnitSpace = new DOMMatrix()
    .scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y
    .translate(-width / 2, -height / 2); // Center the element

  // 3. Map to the canvas viewport
  const clipToCanvasViewport = new DOMMatrix()
    .translate(canvas.width / 2, canvas.height / 2) // Move origin to center
    .scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions

  // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)
  const screenSpaceTransform = clipToCanvasViewport
      .multiply(mvpDOM)
      .multiply(cssToUnitSpace);

  // 5. Apply to the transform
  const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
  if (computedTransform) {
    targetHTMLElement.style.transform = computedTransform.toString();
  }
}

Dukungan library dan framework

Beberapa library populer telah mengirimkan dukungan untuk fitur HTML-in-Canvas.

Three.js

Memperbarui matriks secara manual dapat membosankan, itulah sebabnya framework sudah mulai digunakan. Three.js memiliki dukungan eksperimental menggunakan THREE.HTMLTexture baru:

const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

PlayCanvas

PlayCanvas juga mendukung HTML-in-Canvas menggunakan Texture API:

// Wait for the 'paint' event to set the source
canvas.addEventListener('paint', () => {
    htmlTexture.setSource(htmlElement);
}, { once: true });
canvas.requestPaint();

// Keep up to date
canvas.addEventListener('paint', onPaintUpload);

const material = new pc.StandardMaterial();
material.diffuseMap = htmlTexture;
material.update();

Demo

Sebelum mencoba demo, pastikan lingkungan Anda dikonfigurasi dengan benar.

Ada beberapa demo yang berfungsi sebagai referensi untuk menggunakan API. Kita sudah melihat solusi kreatif dari komunitas, mulai dari buku 3D yang dapat diterjemahkan hingga elemen UI yang membiaskan melalui shader kaca:

  • Buku 3D: Buku 3D yang dirender WebGL yang menggunakan tata letak HTML untuk halamannya. Pengguna dapat mengganti font dengan CSS. Karena berbasis DOM, terjemahan bawaan berfungsi secara instan, dan agen AI dapat mengekstrak teks dengan lebih mudah.
  • UI 3D interaktif: Slider jelly WebGPU yang membiaskan cahaya berdasarkan model 3D yang mendasarinya, sekaligus merespons atribut langkah HTML standar <input type="range">.
  • Tekstur animasi: Billboard 3D dinamis yang merender pensil SVG animasi menggunakan DOM langsung ke tekstur WebGL tanpa memerlukan loop animasi kustom.
  • Overlay refraktif: Lapisan tipografi interaktif yang terdistorsi oleh kursor 3D yang bergerak, tetapi sepenuhnya dapat dipilih dan ditelusuri menggunakan temukan di halaman.

Lihat koleksi demo yang dibuat oleh komunitas. Jika Anda ingin demo HTML-in-Canvas ditampilkan dalam koleksi ini, buat permintaan pull untuk menambahkannya.

Batasan

Meskipun canggih, API ini memiliki beberapa batasan yang disadari:

  • Konten lintas origin: Karena alasan keamanan dan privasi, API ini tidak berfungsi dengan konten iframe lintas origin.
  • Scrolling thread utama: HTML-in-canvas digambar dengan JavaScript, yang berarti scrolling dan animasi tidak dapat diperbarui secara independen dari JavaScript, seperti yang dapat dilakukan di luar kanvas. Developer harus mempertimbangkan dengan cermat karakteristik performa dalam menempatkan konten scrolling di dalam kanvas dibandingkan dengan membuat seluruh kanvas di-scroll.

Masukan

Jika Anda bereksperimen dengan HTML-in-Canvas API, kami ingin mendengar pendapat Anda. Anda dapat mendaftar ke uji coba origin untuk mengaktifkan fitur di situs Anda saat fitur tersebut berada dalam fase eksperimental untuk membantu kami membentuk desain API. Anda juga dapat mengajukan masalah untuk memberikan masukan.

Resource