Ursprungstest der HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Jahrelang mussten Webentwickler bei der Entwicklung komplexer, hochgradig interaktiver visueller Anwendungen im Web eine schwierige architektonische Entscheidung treffen: Sollten sie das DOM wegen seiner umfangreichen semantischen Funktionen nutzen oder direkt in das <canvas> Element rendern, um eine hohe Grafikleistung zu erzielen?

Mit der neuen experimentellen HTML-in-Canvas API , die jetzt im Ursprungstest verfügbar ist, müssen Sie sich nicht mehr entscheiden. Mit dieser API können Sie DOM-Inhalte direkt in ein 2D-Canvas oder eine WebGL-/WebGPU-Textur zeichnen, während die UI interaktiv und zugänglich bleibt und mit Ihren bevorzugten Browserfunktionen verknüpft ist. Durch die Kombination von HTML mit der Grafikverarbeitung auf niedriger Ebene können Sie Erlebnisse schaffen, die bisher nicht möglich waren.

DOM im Vergleich zu Canvas

Um die Leistungsfähigkeit dieser neuen API zu verstehen, ist es hilfreich, sich die relativen Stärken von DOM und Canvas anzusehen.

Das DOM ist das Herzstück der Web-UI. Es bietet sofort einsatzbereite Lösungen für das Textlayout und verwendet semantisch verstandene Inhalte, um umfangreiche Benutzeroberflächen zu erstellen. So können Nutzer gängige Vorgänge auf Webseiten nahtlos ausführen – Dinge, die wir oft für selbstverständlich halten, z. B. Text zum Kopieren hervorheben oder mit der rechten Maustaste auf ein Bild klicken, um es zu speichern. Das DOM ist auch in wichtige Browserfunktionen integriert: Bedienungshilfen, Übersetzen, Auf Seite suchen, Lesemodus, Erweiterungen, Dark Mode, Browserzoom und Autofill.

Canvas (und WebGL/WebGPU) hingegen ermöglicht den Zugriff auf niedriger Ebene, um ein Raster von Pixeln für hochmoderne 2D- und 3D-Grafiken zu steuern. Spiele und komplexe Webanwendungen wie Google Docs oder Figma erfordern diesen leistungsstarken Zugriff auf niedriger Ebene. Da das Canvas im Grunde ein Raster von Pixeln ist, erforderte die Unterstützung von Funktionen wie responsivem Text bisher eine komplexe benutzerdefinierte UI-Logik, was die Bundlegröße drastisch erhöhte. Vor allem aber funktionieren alle leistungsstarken Browserfunktionen, die in das DOM integriert sind, nicht mehr, wenn die UI in einem statischen Canvas-Pixelraster eingeschlossen ist.

Vorteile der Übertragung des DOM in Canvas

Die HTML-in-Canvas API ist die Brücke, die Ihnen das Beste aus beiden Welten bietet. Wenn Sie HTML in das <canvas> Element einfügen und die Transformation synchronisieren, bleiben die Inhalte vollständig interaktiv und alle Browserintegrationen funktionieren automatisch.

Wenn Sie die UI im <canvas>-Element vom DOM verarbeiten lassen, erhalten Sie Folgendes:

  • Textlayout und -formatierung: Vereinfachtes Textlayout und vereinfachte Textformatierung, einschließlich mehrzeiligem oder bidirektionalem Text mit angewendeten CSS-Stilen.
  • Formularsteuerelemente: Ausdrucksstarke und benutzerfreundlichere Formularsteuerelemente mit umfangreichen Anpassungsoptionen.
  • Textauswahl, Kopieren/Einfügen und Rechtsklick: Nutzer können Text in Ihren 3D-Szenen hervorheben oder Kontextmenüs mit der rechten Maustaste aufrufen.
  • Barrierefreiheit: Inhalte, die im Canvas gerendert werden, sind für den Barrierefreiheitsbaum verfügbar. Systeme für Bedienungshilfen können die UI wie normales HTML parsen und für Systeme wie Screenreader verfügbar machen.
  • Auf Seite suchen: Nutzer können mit der Funktion „Auf Seite suchen“ (Strg/Befehlstaste+F) nach Text suchen und der Browser hebt ihn direkt in Ihren WebGL-Texturen hervor.
  • Indexierbarkeit und Schnittstelle für KI-Agenten: Webcrawler und KI-Agenten können den in Ihre 2D- und 3D-Szenen gerenderten Text nahtlos indexieren und lesen.
  • Erweiterungsintegration: Browsererweiterungen funktionieren nativ. Eine Erweiterung zur Textersetzung aktualisiert beispielsweise automatisch den Text, der auf Ihren 3D-Meshes gerendert wird.
  • DevTools-Integration: Sie können Ihre Canvas-Inhalte, einschließlich WebGL-/WebGPU-UI-Elemente, direkt in den Chrome-Entwicklertools untersuchen. Passen Sie einen CSS-Stil im Inspector an und sehen Sie, wie er sofort in der 3D-Textur aktualisiert wird.

Allgemeine Anwendungsfälle

Diese API bietet ein unglaubliches Potenzial in verschiedenen Bereichen:

  • Umfangreiche Canvas-basierte Anwendungen: Umfangreiche Webanwendungen wie Google Docs, Miro oder Figma können jetzt komplexe UI-Komponenten von Anwendungen nativ in ihren Canvas-basierten Arbeitsbereichen rendern, was die Barrierefreiheit verbessert und die Bundlegröße reduziert.
  • 3D-Szenen und -Spiele: Auf Marketingwebsites, in immersiven WebXR-Erlebnissen und in Webspielen kann jetzt eine vollständig interaktive Web-UI in 3D-Szenen platziert werden – z. B. ein 3D-Buch mit echtem DOM-Text oder ein Terminal im Spiel, das das Kopieren und Einfügen nativ unterstützt.

Verwendung der API

Die Verwendung der API erfolgt in drei Phasen: Canvas einrichten, in das Canvas rendern und die CSS-Transformation aktualisieren, damit der Browser weiß, wo sich das Element physisch auf dem Bildschirm befindet.

Vorbereitung

Die HTML-in-Canvas API befindet sich in Chrome 148 bis 150 im Ursprungstest. Wenn Sie sie auf Ihrer Website testen möchten, verwenden Sie Chrome Canary 149 oder höher mit aktiviertem Flag chrome://flags/#canvas-draw-element. Wenn Sie die API für andere Nutzer aktivieren möchten, registrieren Sie sich für den Ursprungstest.

Schritt 1: Einfache Canvas-Einrichtung

Fügen Sie zuerst das layoutsubtree Attribut zu Ihrem <canvas> Tag hinzu. Dadurch werden dem Browser die im Canvas verschachtelten Inhalte bekannt gemacht, damit sie im Canvas angezeigt werden können und für Barrierefreiheitssysteme verfügbar sind.

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

Canvas-Rastergröße festlegen

Damit die gerenderten Inhalte nicht verschwommen sind, müssen Sie die Größe des Canvas-Rasters so anpassen, dass sie dem Skalierungsfaktor des Geräts entspricht.

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

Schritt 2: Rendern

Verwenden Sie für einen 2D-Kontext die Methode drawElementImage. Führen Sie dies im paint-Ereignis aus, das immer ausgelöst wird, wenn das Element neu gezeichnet wird – z. B. beim Hervorheben von Text oder bei der Nutzereingabe. Es ist wichtig, die CSS-Transformation des Elements mit dem Rückgabewert zu aktualisieren, damit die Interaktivität weiterhin funktioniert.

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

Mit WebGL rendern

Für WebGL verwenden Sie texElementImage2D. Es funktioniert ähnlich wie texImage2D, verwendet aber das DOM-Element als Quelle.

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

Mit WebGPU rendern

WebGPU verwendet die Methode copyElementImageToTexture in der Geräte-Queue, analog zu copyExternalImageToTexture:

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

Schritt 3: CSS-Transformation aktualisieren

Nachdem Sie das Element in das Canvas gerendert haben, müssen Sie den Browser darüber informieren, wo es sich befindet. Dadurch wird die räumliche Synchronisierung zwischen dem Canvas und dem Layout des DOM sichergestellt. Das ist wichtig, damit der Browser die Ereigniszone – also die genaue Stelle, an der der Nutzer klickt oder den Mauszeiger platziert – korrekt der Stelle zuordnen kann, an der das Element gerendert wird.

Wenden Sie im Fall des 2D-Kontexts die vom Rendering-Aufruf zurückgegebene Transformation auf die .style.transform property an:

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

Bei WebGL oder WebGPU hängt die Position eines Elements auf dem Bildschirm davon ab, wie die Ausgabetextur vom Shadercode verwendet wird. Sie kann nicht aus dem Canvas-Rendering-Kontext abgeleitet werden. Wenn Ihr Shaderprogramm jedoch eine typische Modell-Ansicht-Projektion verwendet, um die Textur zu zeichnen, können Sie die neue Hilfsfunktion element.getElementTransform() verwenden, um eine Transformation zu berechnen, die auf dieselbe Weise wie der Rückgabewert von drawElementImage() verwendet werden kann. Dazu müssen Sie Folgendes tun:

  • WebGL MVP-Matrix in DOM-Matrix konvertieren.
  • HTML-Element normalisieren. HTML-Elemente werden in Pixeln dimensioniert (z. B. 200 px breit). WebGL behandelt Objekte jedoch normalerweise als „Einheitsquadrate“, z. B. im Bereich von 0 bis 1. Wenn Sie nicht normalisieren, sieht Ihre 200-Pixel-Schaltfläche 200-mal größer aus.
  • Canvas-Darstellungsbereich zuordnen. Dieser Schritt ist die Phase der „Neuskalierung“. Dabei wird die Mathematik des Einheitsraums so angepasst, dass sie den tatsächlichen Pixelabmessungen Ihres <canvas> Elements auf dem Bildschirm entspricht. Außerdem wird die Y-Achse umgekehrt, da in WebGL „nach oben“ positiv ist, in CSS jedoch „nach unten“.
  • Endgültige Transformation berechnen. Multiplizieren Sie die Matrizen in der folgenden Reihenfolge: Viewport * MVP * Normalization. Wenn Sie sie zu einer endgültigen Transformation kombinieren, entsteht eine „Karte“, die dem Browser genau sagt, wo sich diese HTML-Elementebene befinden sollte, um mit der 3D-Zeichnung übereinzustimmen.
  • Transformation auf das HTML-Element anwenden. Dadurch wird die HTML-Elementebene direkt über die gerenderten Pixel verschoben. So wird sichergestellt, dass Nutzer beim Klicken auf eine Schaltfläche oder beim Auswählen von Text das tatsächliche HTML-Element treffen.
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();
  }
}

Unterstützung für Bibliotheken und Frameworks

Einige der beliebten Bibliotheken unterstützen bereits die HTML-in-Canvas-Funktion.

Three.js

Das manuelle Aktualisieren von Matrizen kann mühsam sein. Deshalb unterstützen Frameworks die Funktion bereits. Three.js bietet experimentelle Unterstützung mit dem neuen 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 unterstützt HTML-in-Canvas auch mit der Textur-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();

Demos

Bevor Sie die Demos ausprobieren, müssen Sie Ihre Umgebung richtig konfigurieren.

Es gibt mehrere Demos, die als Referenz für die Verwendung der API dienen. Wir sehen bereits kreative Lösungen aus der Community, von übersetzbaren 3D-Büchern bis hin zu UI-Elementen, die durch Glasshader gebrochen werden:

  • Das 3D-Buch: Ein mit WebGL gerendertes 3D-Buch, das für seine Seiten das HTML-Layout verwendet. Nutzer können Schriftarten mit CSS austauschen. Da es auf dem DOM basiert, funktioniert die integrierte Übersetzung sofort und KI-Agenten können den Text einfacher extrahieren.
  • Interaktive 3D-UIs: Ein WebGPU-Jelly-Slider, der Licht basierend auf einem zugrunde liegenden 3D-Modell bricht, aber weiterhin auf die Schrittattribute des Standard-HTML-Elements <input type="range"> reagiert.
  • Animierte Texturen: Eine dynamische 3D-Werbetafel, auf der ein animierter SVG-Stift mit dem DOM direkt in eine WebGL-Textur gerendert wird, ohne dass eine benutzerdefinierte Animationsschleife erforderlich ist.
  • Brechende Overlays: Eine interaktive Typografieebene, die von einem sich bewegenden 3D-Cursor verzerrt wird, aber mit der Funktion „Auf Seite suchen“ vollständig ausgewählt und durchsucht werden kann.

Sehen Sie sich die Sammlung von Demos an, die von der Community erstellt wurden. Wenn Ihre HTML-in-Canvas-Demo in dieser Sammlung enthalten sein soll, erstellen Sie eine Pull-Anfrage, um sie hinzuzufügen.

Beschränkungen

Die API ist zwar leistungsstark, hat aber einige bewusste Einschränkungen:

  • Inhalte aus anderen Quellen: Aus Sicherheits- und Datenschutzgründen funktioniert die API nicht mit Inhalten aus anderen Quellen in iFrames.
  • Scrolling im Hauptthread: HTML-in-Canvas wird mit JavaScript gezeichnet. Das bedeutet, dass Scrolling und Animationen nicht unabhängig von JavaScript aktualisiert werden können, wie es außerhalb von Canvas möglich ist. Entwickler sollten die Leistungsmerkmale des Scrollens von Inhalten in Canvas im Vergleich zum Scrollen des gesamten Canvas sorgfältig abwägen.

Feedback

Wenn Sie mit der HTML-in-Canvas API experimentieren, freuen wir uns über Ihr Feedback. Sie können sich für den Ursprungstest registrieren, um die Funktion auf Ihrer Website zu aktivieren, während sie sich in der Testphase befindet. So können Sie uns helfen, das API-Design zu verbessern. Sie können auch ein Problem melden, um Feedback zu geben.

Ressourcen