Kennismaking met de origin trial van de HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Jarenlang moesten webontwikkelaars een moeilijke architectonische keuze maken als ze complexe, zeer interactieve visuele apps op het web bouwden. Moeten ze vertrouwen op het DOM vanwege de uitgebreide semantische functies of rechtstreeks renderen naar het <canvas>-element voor grafische prestaties op laag niveau?

Met de nieuwe experimentele HTML-in-Canvas API, die nu beschikbaar is in de origin trial, hoeft u niet te kiezen. Met deze API kun je DOM-content rechtstreeks tekenen in een 2D-canvas of een WebGL/WebGPU-textuur, terwijl de UI interactief en toegankelijk blijft en gekoppeld is aan je favoriete browserfuncties. Door HTML te combineren met grafische verwerking op laag niveau, kun je functionaliteit maken die eerder onmogelijk was.

Het DOM versus Canvas

Als je de kracht van deze nieuwe API wilt begrijpen, is het handig om naar de relatieve sterke punten van zowel het DOM als het canvas te kijken.

Het DOM is de basis van web-UI. Het biedt kant-en-klare oplossingen voor tekstindeling, waarbij semantisch begrepen content wordt gebruikt om uitgebreide interfaces te maken. Zo kunnen gebruikers naadloos algemene bewerkingen uitvoeren op webpagina's. Dit zijn dingen die we vaak als vanzelfsprekend beschouwen, zoals tekst markeren om te kopiëren of met de rechtermuisknop op een afbeelding klikken om deze op te slaan. Het DOM is ook geïntegreerd met essentiële browserfuncties: toegankelijkheidstools, vertalen, zoeken op pagina, leesmodus, extensies, donkere modus, browserzoom en automatisch invullen.

Canvas (en WebGL/WebGPU) biedt daarentegen toegang op laag niveau om een raster van pixels aan te sturen voor zeer geavanceerde 2D- en 3D-afbeeldingen. Games en complexe web-apps (zoals Google Documenten of Figma) vereisen deze krachtige toegang op laag niveau. Omdat het canvas in feite een raster van pixels is, vereisten ondersteunende functies zoals responsieve tekst complexe aangepaste UI-logica, waardoor de bundelgrootte aanzienlijk toenam. Het is cruciaal dat alle krachtige browserfuncties die in het DOM zijn geïntegreerd, helemaal niet meer werken als de UI is opgesloten in een statisch canvaspixelraster.

De voordelen van het DOM naar Canvas brengen

De HTML-in-Canvas API is de brug die u het beste van beide werelden biedt. Als je HTML in het element <canvas> plaatst en de transformatie synchroniseert, zorg je dat de content volledig interactief blijft en dat alle browserintegraties automatisch werken.

Dit zijn de voordelen als je de DOM je UI laat verwerken in een <canvas>-element:

  • Tekstopmaak en -indeling: Vereenvoudigde tekstopmaak en -indeling, waaronder tekst met meerdere regels of bidirectionele tekst met toegepaste css-stijlen.
  • Formulierbediening: Expressieve en gebruiksvriendelijkere formulierbediening met uitgebreide aanpassingsopties.
  • Tekst selecteren, kopiëren/plakken en met de rechtermuisknop klikken: Gebruikers kunnen tekst in je 3D-scènes markeren of contextmenu's openen met de rechtermuisknop.
  • Toegankelijkheid: Content die in het canvas wordt gerenderd, wordt blootgesteld aan de toegankelijkheidsstructuur. Toegankelijkheidssystemen kunnen de UI parseren zoals ze normale HTML parseren en deze beschikbaar maken voor systemen zoals schermlezers.
  • Zoeken op pagina: Gebruikers kunnen zoeken op pagina (Ctrl/Cmd+F) om naar tekst te zoeken. De browser markeert deze rechtstreeks in je WebGL-texturen.
  • Indexeerbaar en geschikt voor AI-agents: Webcrawlers en AI-agents kunnen de tekst die in je 2D- en 3D-scènes wordt weergegeven, naadloos indexeren en lezen.
  • Extensie-integratie: Browserextensies werken systeemeigen. Een extensie voor tekstvervanging updatet bijvoorbeeld automatisch de tekst die wordt weergegeven op je 3D-meshes.
  • DevTools-integratie: Je kunt je canvascontent, waaronder WebGL/WebGPU UI-elementen, rechtstreeks in Chrome DevTools inspecteren. Pas een CSS-stijl aan in de inspector en zie hoe deze meteen wordt geüpdatet in de 3D-textuur.

Toepassingen op hoog niveau

Deze API biedt ongelooflijke mogelijkheden op verschillende gebieden:

  • Grote apps op basis van canvas: Zware web-apps kunnen nu complexe UI-componenten van apps systeemeigen renderen in hun op canvas gebaseerde werkruimten, waardoor de toegankelijkheid wordt verbeterd en het bundelgewicht wordt verminderd.
  • 3D-scènes en -games: Marketingwebsites, meeslepende WebXR-functionaliteit en webgames kunnen nu volledig interactieve web-UI in 3D-scènes plaatsen, zoals een 3D-boek dat echte DOM-tekst gebruikt of een terminal in de game die kopiëren en plakken native ondersteunt.

De API gebruiken

Het gebruik van de API bestaat uit 3 fasen: het canvas instellen, renderen op het canvas en de CSS-transformatie updaten zodat de browser weet waar het element fysiek op het scherm staat.

Vereisten

De HTML-in-Canvas API bevindt zich in de origin trial in Chrome 148 tot en met 150. Als je het op je site wilt testen, gebruik je Chrome Canary 149 of hoger met de flag chrome://flags/#canvas-draw-element aan. Als je de API wilt aanzetten voor andere gebruikers, registreer je je voor de Origin Trial.

Stap 1: Basisinstellingen voor Canvas

Voeg eerst het kenmerk layoutsubtree toe aan uw tag <canvas>. Hierdoor weet de browser dat de content in het canvas is genesteld, wordt de content voorbereid om in het canvas te worden getoond en wordt de content beschikbaar gemaakt voor toegankelijkheidsstructuren.

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

Het canvasraster aanpassen

Als u wilt voorkomen dat de gerenderde content wazig wordt, moet u het canvasraster zo aanpassen dat het overeenkomt met de schaalfactor van het apparaat.

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

Stap 2: Renderen

Gebruik voor een 2D-context de drawElementImage-methode. Doe dit in de gebeurtenis paint, die wordt geactiveerd als het element opnieuw wordt getekend, bijvoorbeeld tijdens tekstmarkering of gebruikersinvoer. Het is essentieel dat u de CSS-transformatie van het element updatet met de retourwaarde, zodat de interactieve elementen blijven werken.

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...
};

Renderen met WebGL

Voor WebGL gebruik je texElementImage2D. De functie werkt vergelijkbaar met texImage2D, maar gebruikt het DOM-element als bron.

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

Renderen met WebGPU

WebGPU gebruikt de methode copyElementImageToTexture in de apparaatwachtrij, vergelijkbaar met copyExternalImageToTexture:

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

Stap 3: Update de CSS-transformatie

Nu je het element op het canvas hebt gerenderd, moet je de browser updaten over de locatie van het element. Dit zorgt voor ruimtelijke synchronisatie tussen het canvas en de DOM-indeling. Dit is belangrijk zodat de browser de gebeurteniszone (bijvoorbeeld waar de gebruiker precies klikt of de cursor plaatst) correct kan toewijzen aan waar het element wordt gerenderd.

Voor de 2D-contextcase past u de transformatie die door de weergave-aanroep wordt geretourneerd toe op de .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();
};

Met WebGL of WebGPU hangt de locatie van een element op het scherm af van hoe de uitvoertextuur wordt gebruikt door shadercode. Deze kan niet worden afgeleid uit de canvasweergavecontext. Als je shaderprogramma een typische modelweergaveprojectie gebruikt om de textuur te tekenen, kun je de nieuwe hulpfunctie element.getElementTransform() gebruiken om een transformatie te berekenen die op dezelfde manier kan worden gebruikt als de retourwaarde van drawElementImage(). Doe het volgende om dit mogelijk te maken:

  • Converteer de MVP-matrix van WebGL naar de DOM-matrix.
  • Normaliseer het HTML-element. HTML-elementen worden gedimensioneerd in pixels (bijvoorbeeld 200 pixels breed). WebGL behandelt objecten meestal als 'eenheidsvierkanten', bijvoorbeeld van 0 tot 1. Als je niet normaliseert, lijkt je knop van 200 px 200 keer groter.
  • Toewijzen aan het kijkvenster van het canvas Deze stap is de fase voor het opnieuw schalen. Het strekt die eenheidsruimteberekening weer uit zodat deze overeenkomt met de werkelijke pixelafmetingen van je <canvas>-element op het scherm. Ook wordt de Y-as omgedraaid, omdat in WebGL omhoog positief is, maar in CSS omlaag positief is.
  • Bereken de uiteindelijke transformatie. Vermenigvuldig de matrices in de juiste volgorde: Viewport * MVP * Normalization. Als je ze combineert tot één uiteindelijke transformatie, krijg je een 'kaart' die de browser precies vertelt waar die HTML-elementlaag moet worden geplaatst om te worden uitgelijnd met de 3D-tekening.
  • Pas de transformatie toe op het HTML-element. Hierdoor komt de laag van het HTML-element rechtstreeks boven de gerenderde pixels te staan. Zo zorgt u dat een gebruiker op het echte HTML-element klikt als die op een knop klikt of tekst selecteert.
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();
  }
}

Ondersteuning voor bibliotheken en frameworks

Sommige populaire bibliotheken bieden al ondersteuning voor de functie HTML in Canvas.

Three.js

Matrixen handmatig updaten kan vervelend zijn. Daarom maken frameworks er al gebruik van. Three.js heeft experimentele ondersteuning met de nieuwe 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 ondersteunt ook HTML-in-Canvas via de textuur-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's

Zorg voordat je de demo's uitprobeert dat je omgeving juist is ingesteld.

Er zijn verschillende demo's die als referentie dienen voor het gebruik van de API. We zien al creatieve oplossingen van de community, van vertaalbare 3D-boeken tot UI-elementen die worden gebroken door glasshaders:

  • Het 3D-boek: Een WebGL-weergegeven 3D-boek dat een HTML-indeling gebruikt voor de pagina's. Gebruikers kunnen lettertypen wisselen met CSS. Omdat het op DOM gebaseerd is, werkt de ingebouwde vertaling meteen en kunnen AI-agents de tekst met minder complexiteit extraheren.
  • Interactieve 3D-UI's: Een WebGPU-jellyschuifregelaar die licht breekt op basis van een onderliggend 3D-model, terwijl deze nog steeds reageert op standaard HTML-<input type="range">stapkenmerken.
  • Geanimeerde texturen: Een dynamisch 3D-billboard dat een geanimeerde SVG-potlood rendert met het DOM rechtstreeks in een WebGL-textuur zonder dat er een aangepaste animatielus nodig is.
  • Brekende overlays: Een interactieve typografielaag die wordt vervormd door een bewegende 3D-cursor, maar die volledig kan worden geselecteerd en doorzocht met Zoeken op pagina.

Check de verzameling demo's die door de community is gemaakt. Als je wilt dat je HTML-in-Canvas-demo in deze collectie wordt getoond, maak je een pull-verzoek om deze toe te voegen.

Beperkingen

De API is krachtig, maar heeft een paar bewuste beperkingen:

  • Cross-origin content: Om beveiligings- en privacyredenen werkt de API niet met cross-origin iframe-content.
  • Scrollen in de hoofdthread: HTML in canvas wordt getekend met JavaScript. Dit betekent dat scrollen en animaties niet onafhankelijk van JavaScript kunnen worden geüpdatet, zoals buiten het canvas. Ontwikkelaars moeten zorgvuldig overwegen of ze scrollende content in een canvas willen plaatsen of dat ze het hele canvas willen laten scrollen.

Feedback

Als je experimenteert met de HTML-in-Canvas API, horen we graag van je. Je kunt je aanmelden voor de origin trial om de functie op je site aan te zetten terwijl deze in de experimentele fase is. Zo help je ons het API-ontwerp vorm te geven. Je kunt ook een probleem indienen om feedback te geven.

Bronnen