Od lat twórcy stron internetowych musieli dokonywać trudnego wyboru architektonicznego podczas tworzenia złożonych, wysoce interaktywnych aplikacji wizualnych w internecie. Czy powinni polegać na DOM ze względu na jego bogate funkcje semantyczne, czy też renderować bezpośrednio do elementu <canvas>, aby uzyskać niskopoziomową wydajność grafiki?
Dzięki nowemu eksperymentalnemu interfejsowi HTML-in-Canvas API, który jest już dostępny w ramach testowania origin, nie musisz wybierać. Ten interfejs API umożliwia rysowanie treści DOM bezpośrednio na płótnie 2D lub w teksturze WebGL/WebGPU, zachowując interaktywność i dostępność interfejsu oraz jego połączenie z ulubionymi funkcjami przeglądarki. Połączenie HTML z niskopoziomowym przetwarzaniem grafiki pozwala tworzyć treści, które wcześniej były niemożliwe do zrealizowania.
DOM a Canvas
Aby zrozumieć możliwości tego nowego interfejsu API, warto przyjrzeć się względnym zaletom zarówno DOM, jak i Canvas.
DOM jest podstawą interfejsu internetowego. Zapewnia gotowe rozwiązania do układu tekstu, wykorzystując semantycznie zrozumiałe treści do tworzenia rozbudowanych interfejsów. Dzięki temu użytkownicy mogą bezproblemowo wykonywać typowe operacje na stronach internetowych – takie, które często bierzemy za pewnik, np. zaznaczanie tekstu do skopiowania lub klikanie obrazu prawym przyciskiem myszy, aby go zapisać. DOM integruje się też z podstawowymi funkcjami przeglądarki: narzędziami ułatwień dostępu, tłumaczeniem, wyszukiwaniem na stronie, trybem czytania, rozszerzeniami, trybem ciemnym, powiększeniem i autouzupełnianiem.
Canvas (oraz WebGL/WebGPU) umożliwia natomiast dostęp niskiego poziomu do siatki pikseli, co pozwala na tworzenie bardzo zaawansowanej grafiki 2D i 3D. Gry i złożone aplikacje internetowe (takie jak Dokumenty Google czy Figma) wymagają takiego wydajnego dostępu niskiego poziomu. Ponieważ obszar roboczy jest zasadniczo siatką pikseli, obsługa funkcji takich jak tekst elastyczny wymagała wcześniej złożonej niestandardowej logiki interfejsu, co drastycznie zwiększało rozmiar pakietu. Co najważniejsze, wszystkie zaawansowane funkcje przeglądarki zintegrowane z DOM przestają działać, gdy interfejs jest uwięziony w statycznej siatce pikseli elementu canvas.
Zalety przeniesienia DOM do Canvas
Interfejs HTML-in-Canvas API to most, który pozwala połączyć zalety obu tych rozwiązań. Umieszczając kod HTML w elemencie <canvas> i synchronizując jego przekształcenie, zapewniasz pełną interaktywność treści i automatyczne działanie wszystkich integracji przeglądarki.
Oto co zyskujesz, gdy DOM obsługuje interfejs w elemencie <canvas>:
- Układ i formatowanie tekstu: uproszczony układ i formatowanie tekstu, w tym tekst wielowierszowy lub dwukierunkowy ze stylami CSS.
- Elementy sterujące formularza: wyraziste i łatwiejsze w użyciu elementy sterujące formularza z rozbudowanymi opcjami dostosowywania.
- Zaznaczanie tekstu, kopiowanie/wklejanie i kliknięcie prawym przyciskiem myszy: użytkownicy mogą zaznaczać tekst w scenach 3D lub klikać prawym przyciskiem myszy menu kontekstowe.
- Ułatwienia dostępu: treści renderowane w elemencie canvas są widoczne w drzewie ułatwień dostępu. Systemy ułatwień dostępu mogą analizować interfejs tak samo jak zwykły kod HTML i udostępniać go systemom takim jak czytniki ekranu.
- Znajdź na stronie: użytkownicy mogą używać funkcji znajdowania na stronie (Ctrl/Cmd+F), aby wyszukiwać tekst, a przeglądarka wyróżnia go bezpośrednio w teksturach WebGL.
- Możliwość indeksowania i interfejs agenta AI: roboty indeksujące i agenty AI mogą bezproblemowo indeksować i odczytywać tekst renderowany w scenach 2D i 3D.
- Integracja rozszerzeń: rozszerzenia do przeglądarki działają natywnie. Na przykład rozszerzenie do zastępowania tekstu automatycznie aktualizuje tekst renderowany na siatkach 3D.
- Integracja z Narzędziami deweloperskimi: możesz sprawdzać zawartość obszaru roboczego, w tym elementy interfejsu WebGL/WebGPU, bezpośrednio w Narzędziach deweloperskich w Chrome. Zmień styl CSS w inspektorze i zobacz, jak natychmiast aktualizuje się on na teksturze 3D.
Częste korzystanie
Ten interfejs API zapewnia niesamowite możliwości w kilku obszarach:
- Duże aplikacje oparte na elemencie canvas: rozbudowane aplikacje internetowe mogą teraz renderować złożone komponenty interfejsu aplikacji natywnie w obszarach roboczych opartych na elemencie canvas, co poprawia dostępność i zmniejsza rozmiar pakietu.
- Sceny i gry 3D: witryny marketingowe, immersyjne środowiska WebXR i gry przeglądarkowe mogą teraz umieszczać w scenach 3D w pełni interaktywny interfejs internetowy, np. książkę 3D, która używa rzeczywistego tekstu DOM, lub terminal w grze, który natywnie obsługuje kopiowanie i wklejanie.
Jak korzystać z interfejsu API
Korzystanie z interfejsu API odbywa się w 3 etapach: skonfigurowanie obszaru roboczego, renderowanie w obszarze roboczym i aktualizowanie przekształcenia CSS, aby przeglądarka wiedziała, gdzie element znajduje się na ekranie.
Wymagania wstępne
Interfejs HTML-in-Canvas API jest dostępny w ramach testowania origin w Chrome 148–150. Aby przetestować tę funkcję w swojej witrynie, użyj Chrome Canary w wersji 149 lub nowszej z włączoną flagą chrome://flags/#canvas-draw-element. Aby włączyć interfejs API dla innych użytkowników, zarejestruj się w wersji próbnej origin.
Krok 1. Podstawowa konfiguracja Canvas
Najpierw dodaj atrybut layoutsubtree do tagu <canvas>. Dzięki temu przeglądarka wie o treściach zagnieżdżonych w elemencie canvas, przygotowuje je do wyświetlenia w tym elemencie i udostępnia drzewom ułatwień dostępu.
<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>
Ustawianie rozmiaru siatki obszaru roboczego
Aby uniknąć rozmycia renderowanych treści, upewnij się, że rozmiar siatki obszaru roboczego odpowiada współczynnikowi skalowania urządzenia.
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);
Krok 2. Renderowanie
W przypadku kontekstu 2D użyj metody drawElementImage. Zrób to w ramach zdarzenia paint, które jest wywoływane za każdym razem, gdy element jest ponownie renderowany, np. podczas zaznaczania tekstu lub wprowadzania danych przez użytkownika. Aby interaktywność nadal działała, musisz zaktualizować przekształcenie CSS elementu za pomocą wartości zwracanej.
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...
};
Renderowanie za pomocą WebGL
W przypadku WebGL używasz texElementImage2D. Działa podobnie do texImage2D, ale jako źródło przyjmuje element DOM.
canvas.onpaint = () => {
if (gl.texElementImage2D) {
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
}
};
Renderuj za pomocą WebGPU
WebGPU używa metody copyElementImageToTexture w kolejce urządzenia, analogicznie do copyExternalImageToTexture:
canvas.onpaint = () => {
root.device.queue.copyElementImageToTexture(
valueElement,
{ texture: targetTexture }
);
};
Krok 3. Zaktualizuj przekształcenie CSS
Po wyrenderowaniu elementu w obszarze roboczym musisz zaktualizować przeglądarkę, aby wiedziała, gdzie się on znajduje. Zapewnia to synchronizację przestrzenną między obszarem roboczym a układem DOM. Jest to ważne, aby przeglądarka mogła prawidłowo zmapować strefę zdarzenia, np. miejsce, w którym użytkownik klika lub najeżdża kursorem, z miejscem, w którym element jest renderowany.
W przypadku kontekstu 2D zastosuj przekształcenie zwrócone przez wywołanie renderowania do elementu .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();
};
W przypadku WebGL lub WebGPU lokalizacja elementu na ekranie zależy od tego, jak tekstura wyjściowa jest używana przez kod cieniowania, i nie można jej wywnioskować z kontekstu renderowania obszaru roboczego. Jeśli jednak program do cieniowania używa typowej projekcji widoku modelu do rysowania tekstury, możesz użyć nowej funkcji element.getElementTransform(), aby obliczyć przekształcenie, którego można użyć w taki sam sposób jak wartości zwracanej przez funkcję drawElementImage(). Aby to ułatwić, wykonaj te czynności:
- Przekształć macierz MVP WebGL w macierz DOM.
- Znormalizuj element HTML. Rozmiary elementów HTML są podawane w pikselach (np. szerokość 200 pikseli). WebGL zwykle traktuje obiekty jako „kwadraty jednostkowe”, na przykład w zakresie od 0 do 1. Jeśli nie znormalizujesz, przycisk o rozmiarze 200 pikseli będzie wyglądał na 200 razy większy.
- Mapowanie na widoczny obszar obszaru roboczego Ten krok to faza „ponownego skalowania”. Rozciąga te obliczenia w przestrzeni jednostkowej, aby dopasować je do rzeczywistych wymiarów pikseli elementu
<canvas>na ekranie. Odwraca też oś Y, ponieważ w WebGL kierunek w górę jest dodatni, a w CSS – w dół. - Oblicz ostateczne przekształcenie. Pomnóż macierze w kolejności:
Viewport * MVP * Normalization.. Połączenie ich w jedną ostateczną transformację daje „mapę”, która wskazuje przeglądarce, gdzie dokładnie powinna znajdować się warstwa elementu HTML, aby była wyrównana z rysunkiem 3D. - Zastosuj przekształcenie do elementu HTML. Spowoduje to przeniesienie warstwy elementu HTML bezpośrednio na wyrenderowane piksele. Dzięki temu, gdy użytkownik kliknie przycisk lub zaznaczy tekst, trafi na rzeczywisty element HTML.
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();
}
}
Obsługa bibliotek i platform
Niektóre popularne biblioteki obsługują już funkcję HTML-in-Canvas.
Three.js
Ręczne aktualizowanie macierzy może być żmudne, dlatego platformy już zaczynają je stosować. Three.js ma eksperymentalną obsługę za pomocą nowego THREE.HTMLTexture:
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 obsługuje też HTML-in-Canvas za pomocą interfejsu 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();
Prezentacje
Zanim wypróbujesz wersje demonstracyjne, upewnij się, że Twoje środowisko jest prawidłowo skonfigurowane.
Dostępnych jest kilka wersji demonstracyjnych, które służą jako materiały referencyjne dotyczące korzystania z interfejsu API. Już teraz widzimy kreatywne rozwiązania od społeczności, od książek 3D, które można tłumaczyć, po elementy interfejsu, które załamują się przez shadery szkła:
- Książka 3D: książka 3D renderowana w technologii WebGL, która używa układu HTML na swoich stronach. Użytkownicy mogą zamieniać czcionki za pomocą CSS. Ponieważ jest oparta na modelu DOM, wbudowane tłumaczenie działa natychmiast, a agenty AI mogą wyodrębniać tekst z mniejszą złożonością.
- Interaktywne interfejsy 3D: suwak żelowy WebGPU, który załamuje światło na podstawie modelu 3D, a jednocześnie reaguje na standardowe atrybuty kroku
<input type="range">HTML. - Animowane tekstury: dynamiczny billboard 3D renderujący animowany ołówek SVG za pomocą DOM bezpośrednio w teksturze WebGL bez konieczności stosowania niestandardowej pętli animacji.
- Refractive overlays: interaktywna warstwa typograficzna zniekształcona przez ruchomy kursor 3D, którą można w pełni zaznaczać i przeszukiwać za pomocą funkcji wyszukiwania na stronie.
Zapoznaj się z kolekcją wersji demonstracyjnych utworzonych przez społeczność. Jeśli chcesz, aby Twoja wersja demonstracyjna HTML-in-Canvas została uwzględniona w tej kolekcji, utwórz żądanie pull, aby ją dodać.
Ograniczenia
Mimo że interfejs API jest bardzo skuteczny, ma kilka ograniczeń:
- Treści z innych domen: ze względów bezpieczeństwa i prywatności interfejs API nie działa z treściami w elementach iframe z innych domen.
- Przewijanie w głównym wątku: kod HTML w elemencie canvas jest rysowany za pomocą JavaScriptu, co oznacza, że przewijanie i animacje nie mogą być aktualizowane niezależnie od JavaScriptu, tak jak poza elementem canvas. Deweloperzy powinni dokładnie rozważyć charakterystykę wydajności umieszczania przewijanych treści w obszarze roboczym w porównaniu z przewijaniem całego obszaru roboczego.
Prześlij opinię
Jeśli eksperymentujesz z interfejsem HTML-in-Canvas API, chętnie poznamy Twoją opinię. Możesz zarejestrować się w wersji próbnej origin, aby włączyć tę funkcję w swojej witrynie, gdy jest ona w fazie eksperymentalnej, i pomóc nam w projektowaniu interfejsu API. Możesz też zgłosić problem, aby przesłać opinię.
Zasoby
- Obsługa HTML-in-Canvas w Three.js
- HTML-in-Canvas in Three.js demo
- Obsługa HTML-in-Canvas w PlayCanvas: dokumentacja dla deweloperów
- Prezentacja HTML-in-Canvas w PlayCanvas
- HTML-in-Canvas: Explainer
- Przewodnik po współczesnych technologiach internetowych dotyczący narzędzi do kodowania AI w przypadku HTML-in-Canvas
- Prezentacje Chrome.dev dotyczące HTML-in-Canvas
- Awesome HTML-in-Canvas demo collection by the community