Przedstawiamy testowanie origin interfejsu HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Od lat deweloperzy stron internetowych muszą podejmować trudne decyzje architektoniczne 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ć wydajność grafiki na niskim poziomie?

Dzięki nowemu eksperymentalnemu interfejsowi HTML-in-Canvas API dostępnemu teraz w ramach wersji próbnej origin nie musisz wybierać. Ten interfejs API umożliwia rysowanie treści DOM bezpośrednio na płótnie 2D lub teksturze WebGL/WebGPU przy zachowaniu interaktywności, dostępności i połączenia z ulubionymi funkcjami przeglądarki. Łącząc HTML z przetwarzaniem grafiki na niskim poziomie, możesz tworzyć rozwiązania, które wcześniej były niemożliwe.

DOM a Canvas

Aby zrozumieć możliwości tego nowego interfejsu API, warto przyjrzeć się względnym zaletom DOM i Canvas.

DOM to podstawa interfejsu użytkownika w internecie. Oferuje gotowe rozwiązania do układu tekstu, wykorzystując treści zrozumiałe semantycznie do tworzenia bogatych interfejsów. Umożliwia to użytkownikom płynne wykonywanie typowych operacji na stronach internetowych – czynności, które często uważamy za oczywiste, np. zaznaczanie tekstu w celu skopiowania go lub klikanie obrazu prawym przyciskiem myszy, aby go zapisać. DOM integruje się też z najważniejszymi funkcjami przeglądarki: narzędziami ułatwień dostępu, tłumaczeniem, wyszukiwaniem na stronie, trybem czytania, rozszerzeniami, trybem ciemnym, powiększeniem przeglądarki i autouzupełnianiem.

Canvas (i WebGL/WebGPU) z drugiej strony umożliwia dostęp na niskim poziomie do siatki pikseli, co pozwala na tworzenie zaawansowanej grafiki 2D i 3D. Gry i złożone aplikacje internetowe (takie jak Dokumenty Google czy Figma) wymagają tego wydajnego dostępu na niskim poziomie. Ponieważ płótno jest zasadniczo siatką pikseli, obsługa funkcji takich jak tekst responsywny wymagała złożonej logiki interfejsu użytkownika, co znacznie zwiększało rozmiar pakietu. Co najważniejsze, wszystkie zaawansowane funkcje przeglądarki zintegrowane z DOM przestają działać, gdy interfejs użytkownika jest uwięziony w statycznej siatce pikseli płótna.

Zalety przeniesienia DOM do Canvas

Interfejs HTML-in-Canvas API to pomost, który zapewnia to, co najlepsze w obu światach. Umieszczając HTML w elemencie <canvas> i synchronizując jego transformację, zapewniasz, że treść pozostanie w pełni interaktywna, a wszystkie integracje z przeglądarką będą działać automatycznie.

Oto co zyskujesz, gdy DOM obsługuje interfejs użytkownika w elemencie <canvas>:

  • Układ i formatowanie tekstu: uproszczony układ i formatowanie tekstu, w tym tekst wielowierszowy lub dwukierunkowy z zastosowanymi stylami CSS.
  • Elementy sterujące formularza: ekspresyjne i łatwiejsze w użyciu elementy sterujące formularza z rozbudowanymi opcjami dostosowywania.
  • Zaznaczanie tekstu, kopiowanie i wklejanie oraz klikanie prawym przyciskiem myszy: użytkownicy mogą zaznaczać tekst w scenach 3D lub natywnie klikać prawym przyciskiem myszy menu kontekstowe.
  • Zaznaczanie tekstu, kopiowanie i wklejanie oraz klikanie prawym przyciskiem myszy: użytkownicy mogą zaznaczać tekst w scenach 3D lub natywnie klikać prawym przyciskiem myszy menu kontekstowe.
  • Ułatwienia dostępu: treści renderowane na płótnie są udostępniane w drzewie ułatwień dostępu. Systemy ułatwień dostępu mogą analizować interfejs użytkownika tak jak zwykły kod HTML i udostępniać go systemom takim jak czytniki ekranu.
  • Find-in-page: użytkownicy mogą używać wyszukiwania na stronie (Ctrl/Cmd+F), aby wyszukiwać tekst, a przeglądarka będzie go wyróżniać bezpośrednio w teksturach WebGL.
  • Find-in-page: użytkownicy mogą używać wyszukiwania na stronie (Ctrl/Cmd+F), aby wyszukiwać tekst, a przeglądarka będzie go wyróżniać bezpośrednio w teksturach WebGL.
  • Indeksowalność i interfejs agenta AI: Roboty skanujące i agenci AI mogą bezproblemowo indeksować i odczytywać tekst renderowany w scenach 2D i 3D.
  • Integracja z rozszerzeniami: rozszerzenia przeglądarki działają natywnie. Na przykład rozszerzenie do zastępowania tekstu automatycznie zaktualizuje tekst renderowany na siatkach 3D.
  • Integracja z Narzędziami deweloperskimi: możesz sprawdzać zawartość płótna, w tym elementy interfejsu WebGL/WebGPU, bezpośrednio w Narzędziach deweloperskich w Chrome. Dostosuj styl CSS w inspektorze i zobacz, jak natychmiast aktualizuje się na teksturze 3D.

Częste korzystanie

Ten interfejs API otwiera ogromne możliwości w kilku obszarach:

  • Duże aplikacje oparte na płótnie: zaawansowane aplikacje internetowe, takie jak Dokumenty Google, Miro czy Figma, mogą teraz renderować złożone komponenty interfejsu aplikacji natywnie w swoich obszarach roboczych opartych na płótnie, co zwiększa dostępność i zmniejsza rozmiar pakietu.
  • Sceny i gry 3D: witryny marketingowe, wciągające rozwiązania WebXR i gry internetowe mogą teraz umieszczać w scenach 3D w pełni interaktywny interfejs użytkownika, np. książkę 3D, która używa prawdziwego 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: konfiguracja obszaru roboczego, renderowanie na obszarze roboczym i aktualizowanie transformacji CSS, aby przeglądarka wiedziała, gdzie element znajduje się fizycznie na ekranie.

Wymagania wstępne

Interfejs HTML-in-Canvas API jest dostępny w ramach wersji próbnej origin w Chrome w wersjach od 148 do 150. Aby go przetestować 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 płótna

Najpierw dodaj atrybut layoutsubtree do tagu <canvas>. Dzięki temu przeglądarka będzie wiedzieć o treści zagnieżdżonej w płótnie, przygotowując ją do wyświetlenia na płótnie i udostępniając ją w drzewach 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 płótna

Aby uniknąć rozmycia renderowanej treści, ustaw rozmiar siatki płótna tak, aby 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 zdarzeniu paint, które jest wywoływane za każdym razem, gdy element jest ponownie rysowany, np. podczas wyróżniania tekstu lub danych wejściowych użytkownika. Aby interaktywność nadal działała, konieczne jest zaktualizowanie transformacji 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 jak 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);
  }
};

Renderowanie za pomocą WebGPU

WebGPU używa metody copyElementImageToTexture w kolejce urządzenia, analogicznej do copyExternalImageToTexture:

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

Krok 3. Aktualizowanie transformacji CSS

Po wyrenderowaniu elementu na płótnie musisz poinformować przeglądarkę o jego lokalizacji. Zapewnia to synchronizację przestrzenną między płótnem a układem DOM. Jest to ważne, aby przeglądarka mogła prawidłowo mapować strefę zdarzeń – np. miejsce, w którym użytkownik kliknie lub najedzie kursorem – z miejscem, w którym element jest renderowany.

W przypadku kontekstu 2D zastosuj transformację zwróconą przez wywołanie renderowania do .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 shadera, i nie można jej wywnioskować z kontekstu renderowania płótna. Jeśli jednak program shadera używa typowego modelu widoku projekcji do rysowania tekstury, możesz użyć nowej funkcji pomocniczej element.getElementTransform(), aby obliczyć transformację, której można użyć w taki sam sposób jak wartości zwracanej przez drawElementImage(). Aby to ułatwić, musisz wykonać te czynności:

  • Konwertowanie macierzy WebGL MVP na macierz DOM.
  • Normalizowanie elementu HTML. Rozmiar elementów HTML jest określany w pikselach (np. szerokość 200 pikseli). WebGL zwykle traktuje jednak obiekty jako „kwadraty jednostkowe”, np. w zakresie od 0 do 1. Jeśli nie znormalizujesz, przycisk o szerokości 200 pikseli będzie wyglądać 200 razy większy.
  • Mapowanie na widoczny obszar płótna. Ten krok to faza "skalowania": rozciąga on obliczenia w przestrzeni jednostkowej, aby odpowiadały rzeczywistym wymiarom pikseli elementu <canvas> na ekranie. Odwraca też oś Y, ponieważ w WebGL kierunek do góry jest dodatni, a w CSS kierunek do dołu jest dodatni.
  • Obliczanie ostatecznej transformacji. Pomnóż macierze w kolejności: Viewport * MVP * Normalization. Połączenie ich w jedną ostateczną transformację tworzy "mapę", która informuje przeglądarkę, gdzie dokładnie powinna znajdować się warstwa elementu HTML, aby była wyrównana z rysunkiem 3D.
  • Stosowanie transformacji do elementu HTML. Powoduje to przesunięcie warstwy elementu HTML tak, aby znajdowała się bezpośrednio nad renderowanymi pikselami. Dzięki temu, gdy użytkownik kliknie przycisk lub zaznaczy tekst, trafi w 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 frameworków

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ą to robić. Three.js ma eksperymentalną obsługę za pomocą nowego elementu 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 prezentacje, upewnij się, że środowisko jest prawidłowo skonfigurowane.

Dostępnych jest kilka prezentacji, które służą jako odniesienie do korzystania z interfejsu API. Widzimy już kreatywne rozwiązania od społeczności, od książek 3D z możliwością tłumaczenia po elementy interfejsu użytkownika, które załamują światło przez shadery szkła:

  • Książka 3D: książka 3D renderowana za pomocą WebGL, która używa układu HTML na stronach. Użytkownicy mogą zmieniać czcionki za pomocą CSS. Ponieważ jest oparta na DOM, wbudowane tłumaczenie działa natychmiast, a agenci AI mogą wyodrębniać tekst z mniejszą złożonością.
  • Interaktywne interfejsy 3D: suwak galaretki WebGPU, który załamuje światło na podstawie modelu 3D, a jednocześnie reaguje na standardowe atrybuty kroku HTML <input type="range">.
  • Animowane tekstury: dynamiczny billboard 3D renderujący animowany ołówek SVG za pomocą DOM bezpośrednio do tekstury WebGL bez konieczności używania niestandardowej pętli animacji.
  • Nakładki załamujące światło: interaktywna warstwa typografii zniekształcona przez ruchomy kursor 3D, ale w pełni możliwa do zaznaczenia i wyszukania za pomocą wyszukiwania na stronie.

Zapoznaj się z kolekcją prezentacji utworzonych przez społeczność. Jeśli chcesz, aby Twoja prezentacja HTML-in-Canvas znalazła się w tej kolekcji, utwórz żądanie pull, aby ją dodać.

Ograniczenia

Ten interfejs API jest bardzo skuteczny, ale ma kilka świadomych ograniczeń:

  • Treści z innych domen: ze względów bezpieczeństwa i prywatności interfejs API nie działa z treściami iframe z innych domen.
  • Przewijanie w wątku głównym: HTML-in-Canvas jest rysowany za pomocą JavaScriptu, co oznacza, że przewijanie i animacje nie mogą być aktualizowane niezależnie od JavaScriptu, tak jak poza płótnem. Deweloperzy powinni dokładnie rozważyć charakterystykę wydajności umieszczania przewijanej treści w płótnie w porównaniu z przewijaniem całego płótna.

Prześlij opinię

Jeśli eksperymentujesz z interfejsem HTML-in-Canvas API, chcemy poznać 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, co pomoże nam w kształtowaniu projektu interfejsu API. Możesz też zgłosić problem, aby przesłać opinię.

Zasoby