Przedstawiamy testowanie origin interfejsu HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Od lat twórcy stron internetowych muszą podejmować trudne decyzje architektoniczne podczas tworzenia złożonych, wysoce interaktywnych aplikacji wizualnych w internecie. Czy powinni korzystać z DOM ze względu na jego bogate funkcje semantyczne, czy też renderować bezpośrednio do elementu <canvas>, aby uzyskać wydajność grafiki niskiego poziomu?

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 teksturze WebGL/WebGPU przy zachowaniu interaktywności i dostępności interfejsu oraz połączenia z ulubionymi funkcjami przeglądarki. Łącząc HTML z przetwarzaniem grafiki niskiego poziomu, możesz tworzyć funkcje, 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 internetowego. Oferuje gotowe rozwiązania do układu tekstu, które wykorzystują treści rozumiane semantycznie do tworzenia bogatych interfejsów. Umożliwia to użytkownikom bezproblemowe wykonywanie typowych operacji na stronach internetowych – czynności, które często uważamy za oczywiste, np. zaznaczanie tekstu w celu skopiowania lub klikanie prawym przyciskiem myszy obrazu w celu zapisania go. DOM jest też zintegrowany 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 kolei umożliwia dostęp niskiego poziomu do siatki pikseli w celu tworzenia zaawansowanej grafiki 2D i 3D. Gry i złożone aplikacje internetowe (takie jak Dokumenty Google czy Figma) wymagają wydajnego dostępu niskiego poziomu. Obszar roboczy jest w zasadzie siatką pikseli, więc obsługa funkcji takich jak tekst responsywny wymagała złożonej logiki niestandardowego interfejsu, co znacznie zwiększało rozmiar pakietu. Co ważne, wszystkie zaawansowane funkcje przeglądarki zintegrowane z DOM całkowicie przestają działać, gdy interfejs jest uwięziony w statycznej siatce pikseli obszaru rysowania.

Zalety przeniesienia DOM do Canvas

Interfejs HTML w Canvas to pomost, który łączy zalety obu tych rozwiązań. Umieszczając kod HTML w elemencie <canvas> i synchronizując jego przekształcenie, masz pewność, że treść pozostanie w pełni interaktywna, a wszystkie integracje przeglądarki będą działać automatycznie.

Oto co zyskujesz, gdy pozwalasz DOM zarządzać interfejsem 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 formularzem: ekspresyjne i łatwiejsze w użyciu elementy sterujące formularzem z rozbudowanymi opcjami dostosowywania.
  • Zaznaczanie tekstu, kopiowanie i wklejanie oraz klikanie prawym przyciskiem myszy: użytkownicy mogą zaznaczać tekst w scenach 3D lub korzystać z menu kontekstowych, klikając prawym przyciskiem myszy.
  • Ułatwienia dostępu: treści renderowane w obszarze roboczym są udostępniane w drzewie ułatwień dostępu. Systemy ułatwień dostępu mogą analizować interfejs w taki sam sposób jak zwykły kod HTML i udostępniać go systemom takim jak czytniki ekranu.
  • Znajdowanie 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.
  • Indeksowanie 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 zastępujące tekst automatycznie aktualizuje tekst renderowany na siatkach 3D.
  • Integracja z Narzędziami deweloperskimi: możesz sprawdzać zawartość obszaru rysowania, w tym elementy interfejsu WebGL/WebGPU, bezpośrednio w Narzędziach deweloperskich w Chrome. Dostosuj styl CSS w inspektorze i obserwuj, jak natychmiast aktualizuje się na teksturze 3D.

Częste korzystanie

Ten interfejs API otwiera niesamowite możliwości w wielu obszarach:

  • Aplikacje oparte na dużym obszarze roboczym: zaawansowane aplikacje internetowe mogą teraz renderować złożone komponenty interfejsu aplikacji natywnie w obszarach roboczych opartych na obszarze roboczym, co zwiększa dostępność i zmniejsza rozmiar pakietu.
  • Sceny i gry 3D: witryny marketingowe, immersyjne środowiska WebXR i gry internetowe mogą teraz umieszczać w scenach 3D w pełni interaktywne interfejsy internetowe, 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 fazach: konfigurowanie obszaru roboczego, renderowanie w obszarze roboczym i aktualizowanie przekształcenia CSS, aby przeglądarka wiedziała, gdzie element fizycznie znajduje się na ekranie.

Wymagania wstępne

Interfejs HTML-in-Canvas jest dostępny w ramach wersji próbnej 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 programie Origin Trial.

Krok 1. Podstawowa konfiguracja Canvas

Najpierw dodaj atrybut layoutsubtree do tagu <canvas>. Dzięki temu przeglądarka wie, że w elemencie canvas znajdują się treści, i przygotowuje je do wyświetlenia w tym elemencie oraz udostępnia je 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>

Zmień rozmiar siatki obszaru roboczego

Aby uniknąć rozmycia renderowanych treści, dopasuj rozmiar siatki obszaru roboczego do współczynnika skali 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, musisz zaktualizować przekształcenie CSS elementu za pomocą wartości zwrotnej.

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 funkcji 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ądzeń, analogicznie do copyExternalImageToTexture:

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

Krok 3. Zaktualizuj przekształcenie CSS

Po wyrenderowaniu elementu na obszarze roboczym musisz poinformować przeglądarkę o jego lokalizacji. Zapewnia to synchronizację przestrzenną między obszarem rysowania a układem DOM. Jest to ważne, aby przeglądarka mogła prawidłowo mapować strefę zdarzenia – np. miejsce, w którym użytkownik kliknie lub najedzie 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 położenie elementu na ekranie zależy od tego, jak tekstura wyjściowa jest używana przez kod shadera, i nie można go wywnioskować z kontekstu renderowania płótna. Jeśli jednak program cieniujący używa typowego modelu widoku projekcji do rysowania tekstury, możesz użyć nowej funkcji element.getElementTransform(), aby obliczyć przekształcenie, które można wykorzystać w taki sam sposób jak wartość zwracaną 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. 200 pikseli szerokości). WebGL traktuje jednak obiekty jako „kwadraty jednostkowe”, np. w zakresie od 0 do 1. Jeśli nie przeprowadzisz normalizacji, przycisk o rozmiarze 200 pikseli będzie wyglądać na 200 razy większy.
  • Mapuj na widoczny obszar obszaru roboczego. Ten krok to faza „skalowania”. Rozciąga on obliczenia dotyczące przestrzeni jednostkowej, aby dopasować je do rzeczywistych wymiarów w pikselach elementu <canvas> na ekranie. Odwraca też oś Y, ponieważ w WebGL kierunek do góry jest dodatni, a w CSS kierunek w dół jest dodatni.
  • Oblicz ostateczną transformację. Pomnóż macierze w odpowiedniej kolejności: Viewport * MVP * Normalization. połączenie ich w jedną transformację końcową tworzy „mapę”, która dokładnie określa, gdzie 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 wybierze tekst, trafi w prawdziwy 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 w obszarze rysowania.

Three.js

Ręczne aktualizowanie macierzy może być żmudne, dlatego platformy już zaczynają je wdrażać. Three.js ma eksperymentalne wsparcie 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 w Canvas za pomocą interfejsu API tekstur:

// 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 mogą służyć jako punkt odniesienia podczas korzystania z interfejsu API. W społeczności widać już kreatywne rozwiązania, od książek 3D z możliwością tłumaczenia po elementy interfejsu, które załamują się przez shadery szkła:

  • Książka 3D: książka 3D renderowana w WebGL, która wykorzystuje układ HTML dla swoich stron. Użytkownicy mogą zamieniać czcionki za pomocą CSS. Wbudowane tłumaczenie działa natychmiast, ponieważ jest oparte na DOM, a agenty AI mogą wyodrębniać tekst z mniejszą złożonością.
  • Interaktywne interfejsy 3D: suwak z galaretką w 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 w teksturze WebGL bez konieczności używania niestandardowej pętli animacji.
  • Nakładki refrakcyjne: interaktywna warstwa typograficzna zniekształcona przez ruchomy kursor 3D, ale w pełni wybieralna i umożliwiająca wyszukiwanie za pomocą funkcji „Znajdź na stronie”.

Zapoznaj się z kolekcją wersji demonstracyjnych utworzonych przez społeczność. Jeśli chcesz, aby Twoja wersja demonstracyjna HTML w Canvas została uwzględniona w tej kolekcji, utwórz żądanie pull, aby ją dodać.

Ograniczenia

Ten interfejs API jest bardzo przydatny, ale ma pewne ograniczenia:

  • Treści z innej domeny: ze względu na bezpieczeństwo i prywatność interfejs API nie działa z treściami elementu iframe pochodzącymi z innej domeny.
  • Przewijanie w głównym wątku: HTML w obszarze roboczym jest rysowany za pomocą JavaScriptu, co oznacza, że przewijanie i animacje nie mogą być aktualizowane niezależnie od JavaScriptu, tak jak poza obszarem roboczym. Deweloperzy powinni dokładnie rozważyć charakterystykę wydajności umieszczania przewijanej treści w obszarze Canvas w porównaniu z przewijaniem całego obszaru Canvas.

Prześlij opinię

Jeśli testujesz interfejs HTML-in-Canvas API, chcemy poznać Twoją opinię. Możesz zarejestrować się w testach origin, aby włączyć tę funkcję w swojej witrynie, gdy jest ona w fazie eksperymentalnej. Pomoże nam to w opracowaniu projektu interfejsu API. Możesz też zgłosić problem, aby przesłać opinię.

Zasoby