Ursprungstest der HTML-in-Canvas API

Thomas Nattestad
Thomas Nattestad

Jahrelang mussten Webentwickler bei der Entwicklung komplexer, hochinteraktiver visueller Anwendungen im Web eine schwierige architektonische Entscheidung treffen: Sollten sie sich auf das DOM mit seinen umfangreichen semantischen Funktionen verlassen oder direkt auf das <canvas>-Element rendern, um eine leistungsstarke Grafik zu erzielen?

Mit der neuen experimentellen HTML-in-Canvas API, die jetzt als Ursprungstest verfügbar ist, müssen Sie sich nicht entscheiden. Mit dieser API können Sie DOM-Inhalte direkt in ein 2D-Canvas oder eine WebGL-/WebGPU-Textur zeichnen und gleichzeitig dafür sorgen, dass die Benutzeroberfläche interaktiv und zugänglich bleibt und mit Ihren bevorzugten Browserfunktionen verknüpft ist. Durch die Kombination von HTML mit der Verarbeitung von Grafiken auf niedriger Ebene können Sie Inhalte erstellen, die bisher nicht möglich waren.

DOM im Vergleich zu Canvas

Um das Potenzial 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, bei denen semantisch verstandene Inhalte verwendet werden, um ansprechende 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 markieren oder mit der rechten Maustaste auf ein Bild klicken, um es zu speichern. Das DOM ist auch in wichtige Browserfunktionen integriert: Bedienungshilfen, Übersetzen, Auf der 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 hochentwickelte 2D- und 3D-Grafiken zu steuern. Spiele und komplexe Web-Apps wie Google Docs oder Figma benötigen diesen leistungsstarken Low-Level-Zugriff. Da der Canvas im Grunde ein Raster aus Pixeln ist, waren für die Unterstützung von Funktionen wie responsivem Text komplexe benutzerdefinierte UI-Logik erforderlich, was die Bundle-Größe drastisch erhöht hat. Alle leistungsstarken Browserfunktionen, die in das DOM integriert sind, funktionieren nicht mehr, wenn die Benutzeroberfläche in einem statischen Canvas-Pixelraster eingeschlossen ist.

Vorteile der DOM-Integration in Canvas

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

Wenn Sie das DOM Ihre Benutzeroberfläche in einem <canvas>-Element verarbeiten lassen, haben Sie folgende Vorteile:

  • Textlayout und ‑formatierung:Vereinfachtes Textlayout und ‑formatierung, einschließlich mehrzeiligen oder bidirektionalen Texts mit angewendeten CSS-Stilen.
  • Formularsteuerelemente:Ausdrucksstarke und benutzerfreundliche Formularsteuerelemente mit umfangreichen Anpassungsoptionen.
  • Textauswahl, Kopieren/Einfügen und Rechtsklick:Nutzer können Text in Ihren 3D-Szenen markieren oder Kontextmenüs per Rechtsklick aufrufen.
  • Textauswahl, Kopieren/Einfügen und Rechtsklick:Nutzer können Text in Ihren 3D-Szenen markieren oder Kontextmenüs per Rechtsklick aufrufen.
  • Barrierefreiheit:Inhalte, die im Canvas gerendert werden, sind im Barrierefreiheitsbaum verfügbar. Bedienungshilfen können die Benutzeroberfläche wie normales HTML parsen und für Systeme wie Screenreader verfügbar machen.
  • Find-in-page::Nutzer können mit der Funktion „In-Page-Suche“ (Strg/Cmd+F) nach Text suchen. Der Browser hebt ihn dann direkt in Ihren WebGL-Texturen hervor.
  • Find-in-page::Nutzer können mit der Funktion „In-Page-Suche“ (Strg/Cmd+F) nach Text suchen. Der Browser hebt ihn dann direkt in Ihren WebGL-Texturen hervor.
  • Indexierbarkeit und Schnittstelle für KI-Agenten:Webcrawler und KI-Agenten können den in Ihren 2D- und 3D-Szenen gerenderten Text nahtlos indexieren und lesen.
  • Erweiterungsintegration:Browsererweiterungen funktionieren nativ. Eine Erweiterung zum Ersetzen von Text aktualisiert beispielsweise automatisch den Text, der auf Ihren 3D-Meshes gerendert wird.
  • Integration der Entwicklertools: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 zu, wie er sofort auf die 3D-Textur angewendet wird.

Allgemeine Anwendungsfälle

Diese API bietet unglaubliches Potenzial in verschiedenen Bereichen:

  • Große Canvas-basierte Anwendungen:Schwergewichtige Web-Apps wie Google Docs, Miro oder Figma können jetzt komplexe UI-Komponenten nativ in ihren Canvas-basierten Arbeitsbereichen rendern. Das verbessert die Barrierefreiheit und reduziert das Bundle-Gewicht.
  • 3D-Szenen und ‑Spiele:Auf Marketingwebsites, in immersiven WebXR-Erlebnissen und in Webspielen kann jetzt eine vollständig interaktive Web-Benutzeroberfläche in 3D-Szenen platziert werden, z. B. ein 3D-Buch, in dem echter DOM-Text verwendet wird, 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: Einrichten des Canvas, Rendern in den Canvas und Aktualisieren der CSS-Transformation, 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 in einem Ursprungstest. Wenn Sie die Funktion auf Ihrer Website testen möchten, verwenden Sie Chrome Canary 149 oder höher mit aktiviertem chrome://flags/#canvas-draw-element-Flag. Wenn Sie die API für andere Nutzer aktivieren möchten, registrieren Sie sich für den Ursprungstest.

Schritt 1: Canvas einrichten

Fügen Sie zuerst das Attribut layoutsubtree zu Ihrem <canvas>-Tag hinzu. Dadurch wird der Browser auf die im Canvas verschachtelten Inhalte aufmerksam gemacht. Er bereitet sie für die Anzeige im Canvas vor und macht sie für Barrierefreiheitsbäume verfügbar.

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

Damit die gerenderte Grafik nicht verschwommen ist, muss die Größe des Canvas-Rasters dem Skalierungsfaktor des Geräts entsprechen.

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 drawElementImage-Methode. Führen Sie diese Schritte innerhalb des Ereignisses paint aus, das immer dann ausgelöst wird, wenn das Element neu gezeichnet wird, z. B. beim Markieren von Text oder bei Nutzereingaben. 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. Sie 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 auf dem Canvas gerendert haben, müssen Sie den Browser darüber informieren, wo es sich befindet. So wird die räumliche Synchronisierung zwischen dem Canvas und dem Layout des DOM sichergestellt. Das ist wichtig, damit der Browser die Ereigniszone (also die Stelle, an der der Nutzer klickt oder den Mauszeiger platziert) korrekt dem gerenderten Element zuordnen kann.

Wenden Sie im 2D-Kontext die vom Rendering-Aufruf zurückgegebene Transformation auf .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 Shader-Code verwendet wird. Sie kann nicht aus dem Canvas-Renderingkontext abgeleitet werden. Wenn in Ihrem Shader-Programm jedoch eine typische Modell-Ansichts-Projektion zum Zeichnen der Textur verwendet wird, können Sie mit der neuen Convenience-Funktion element.getElementTransform() eine Transformation 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 angegeben (z. B. 200 Pixel breit). In WebGL werden Objekte jedoch in der Regel als „Einheitsquadrate“ behandelt, die beispielsweise von 0 bis 1 reichen. Wenn Sie die Normalisierung nicht durchführen, wird Ihr 200 px großer Button 200-mal größer dargestellt.
  • Dem Darstellungsbereich des Canvas zuordnen: In diesem Schritt wird die „Reskalierung“ durchgeführt: Die Berechnungen im Einheitsraum werden wieder so angepasst, dass sie den tatsächlichen Pixelabmessungen des <canvas>-Elements auf dem Bildschirm entsprechen. Außerdem wird die Y-Achse umgekehrt, da in WebGL „up“ positiv ist, in CSS aber „down“.
  • Endgültige Transformation berechnen Multiplizieren Sie die Matrizen in der richtigen Reihenfolge: Viewport * MVP * Normalization. Wenn Sie sie in einer endgültigen Transformation kombinieren, entsteht eine „Karte“, die dem Browser genau sagt, wo die HTML-Elementebene platziert werden soll, damit sie mit der 3D-Zeichnung übereinstimmt.
  • Wenden Sie die Transformation auf das HTML-Element an. Dadurch wird die HTML-Elementebene direkt über den gerenderten Pixeln platziert. So wird sichergestellt, dass Nutzer, wenn sie auf eine Schaltfläche klicken oder Text auswählen, 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 von Bibliotheken und Frameworks

Einige der beliebtesten Bibliotheken unterstützen die Funktion „HTML-in-Canvas“ bereits.

Three.js

Das manuelle Aktualisieren von Matrizen kann mühsam sein. Deshalb werden Frameworks bereits integriert. 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 auch HTML-in-Canvas über die 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();

Demos

Bevor Sie die Demos ausprobieren, müssen Sie sicherstellen, dass Ihre Umgebung richtig konfiguriert ist.

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

  • Das 3D-Buch: Ein mit WebGL gerendertes 3D-Buch, für dessen Seiten das HTML-Layout verwendet wird. Nutzer können Schriftarten mit CSS tauschen. Da die integrierte Übersetzung DOM-basiert ist, funktioniert sie sofort und KI-Agents können den Text mit weniger Komplexität extrahieren.
  • Interaktive 3D-Benutzeroberflächen: Ein WebGPU-Schieberegler, der Licht basierend auf einem zugrunde liegenden 3D-Modell bricht und gleichzeitig auf Standard-HTML-<input type="range">-Schrittattribute reagiert.
  • Animierte Texturen: Eine dynamische 3D-Billboard-Darstellung eines animierten SVG-Stifts, der das DOM direkt in eine WebGL-Textur rendert, ohne dass eine benutzerdefinierte Animationsschleife erforderlich ist.
  • Brechende Overlays: Eine interaktive Typografieebene, die durch einen sich bewegenden 3D-Cursor verzerrt wird, aber dennoch vollständig auswählbar und mit der Funktion „Auf Seite suchen“ durchsuchbar ist.

Sehen Sie sich die von der Community erstellten Demos an. Wenn Sie möchten, dass Ihre HTML-in-Canvas-Demo in dieser Sammlung enthalten ist, 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 iframe-Inhalten aus anderen Quellen.
  • Scrollen im Hauptthread:HTML-in-Canvas wird mit JavaScript gezeichnet. Das bedeutet, dass sich Scrollen und Animationen nicht unabhängig von JavaScript aktualisieren lassen, wie es außerhalb von Canvas möglich ist. Entwickler sollten die Leistungsmerkmale von scrollendem Inhalt in einem Canvas im Vergleich zum Scrollen des gesamten Canvas sorgfältig abwägen.

Feedback

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

Ressourcen