Webanwendungen müssen häufig umfangreiche HTML-Inhalte wie benutzerdefinierte Dashboards, formatierte Dokumente oder interaktive Diagramme als statische Bilder oder Videoaufnahmen erfassen und exportieren. Bisher waren dafür sperrige Drittanbieterbibliotheken erforderlich, die DOM-Knoten und CSS-Eigenschaften manuell parsen, um ein visuelles Faksimile auf einem Canvas zu rekonstruieren. Dieser Ansatz ist rechenintensiv, fehleranfällig und unterstützt häufig keine modernen CSS-Layoutfunktionen. Mit der HTML-in-Canvas API können Entwickler echte DOM-Elemente direkt im Canvas-Kontext rendern. Da die native Rendering-Engine des Browsers die HTML-Unterstruktur pixelgenau darstellt, ist die Erfassung der genauen visuellen Ausgabe als Bild- oder Videostream mit integrierten Canvas-Methoden wie toDataURL(), toBlob() oder captureStream() sehr effizient.
Implementierung
Prüfen Sie, ob HTML-in-Canvas im Browser unterstützt wird:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }Initialisieren Sie den Canvas, um das Rendern von untergeordneten HTML-Elementen zu unterstützen, indem Sie dem HTML-Element
<canvas>das Attributcontent="drawable"hinzufügen. Platziere deinen HTML-Inhalt innerhalb des<canvas>-Elements und füge jedem Element, das du zeichnest, das Attributdrawablehinzu. Ein Element mitdrawableerfasst seinen Unterbaum, mit Ausnahme von verschachtelten untergeordneten Elementen, die ebenfallsdrawablesind:<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>Skalieren Sie das Canvas-Raster so, dass es dem Skalierungsfaktor des Geräts entspricht, um Unschärfe zu vermeiden:
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);Rendern Sie den HTML-Inhalt im Canvas innerhalb eines
canvas.onpaint-Event-Handlers.Verwenden Sie im 2D-Kontext die Methode
drawElementImage. Es zeichnet den letzten Snapshot einesdrawable-Elements und seines zeichenbaren Unterbaums (ohne andere zeichenbare untergeordnete Elemente) ab der Rahmenbox des Elements vor CSS-Transformationen:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- In WebGL- und WebGPU-Kontexten kopieren Sie das Element in eine Textur. Größe der Textur von
canvas.captureElementImage(element)festlegen. Die Werte fürwidthundheightwerden in Pixeln des Zeichenflächenrasters (Backing Store) angegeben. Runden Sie sie daher mitMath.ceil()auf.captureElementImage()löst einen Fehler aus, wenn das Element noch keinen Paint-Eintrag hat: Rufen SierequestPaint()auf und versuchen Sie es beim nächstenpaint-Ereignis noch einmal.
// Returns the element's size in canvas grid pixels, or null if the element
// has no paint record yet.
function getElementImageSize(canvas, element) {
let elementImage;
try {
elementImage = canvas.captureElementImage(element);
} catch (err) {
canvas.requestPaint(); // No paint record yet: retry on the next paint event.
return null;
}
const width = Math.ceil(elementImage.width);
const height = Math.ceil(elementImage.height);
elementImage.close();
return width > 0 && height > 0 ? { width, height } : null;
}
- Weisen Sie im WebGL-Kontext zuerst die Texturunterstützung mit der Methode
texImage2Dvorab zu. Die Snippets in diesem Leitfaden verwenden einen WebGL 2-Kontext (canvas.getContext("webgl2")). Weisen Sie Texturspeicher zu, indem SietexImage2D()mitnullanstelle von Pixeldaten aufrufen. Dadurch wird GPU-Speicher der angegebenen Größe und des angegebenen Formats reserviert, ohne dass Pixel hochgeladen werden. Beachten Sie dabei folgende Regeln:- Passen Sie die Größe der Textur von
captureElementImage()an, wie oben gezeigt. - Weisen Sie Ebene 0 nur beim ersten Upload oder bei einer Größenänderung zu, da die Textur durch die Neuzuweisung gelöscht wird. Veränderlichen Speicher verwenden: Die Größe von unveränderlichem
texStorage2D()-Speicher kann nicht geändert werden, wenn sich die Größe des Elements ändert.
- Passen Sie die Größe der Textur von
// Binds the texture, and allocates its storage on the first upload, and when
// the element size changes. `state` stores the allocated size.
function allocateElementTexture(gl, texture, state, width, height) {
gl.bindTexture(gl.TEXTURE_2D, texture);
if (state.width === width && state.height === height) return;
gl.texImage2D(
gl.TEXTURE_2D,
0, // Mipmap level
gl.RGBA8, // Internal format: 8 bits per RGBA channel
width, // Size in canvas grid pixels
height,
0, // Border: must be 0
gl.RGBA, // Format and type that match RGBA8
gl.UNSIGNED_BYTE,
null // null reserves GPU memory without uploading pixels
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
state.width = width;
state.height = height;
}
const texture = gl.createTexture();
const textureState = { width: 0, height: 0 };
- Rendern Sie dann im WebGL-Kontext den HTML-Inhalt mit der Methode
texElementSubImage2Din die Textur. Beachten Sie dabei folgende Regeln:- Übergeben Sie
{ width, height }in der Konfiguration. Die Kopie füllt die zugewiesene Textur aus, unabhängig davon, wie der Canvas skaliert wird. - Weisen Sie die Textur neu zu, wenn sich die Größe geändert hat, wie oben gezeigt.
- Übergeben Sie
// Uploads the element into the texture. Returns false if the element has no
// paint record yet. In that case, it's uploaded on the next paint event.
function uploadElementWebGL(gl, canvas, texture, element, state) {
const size = getElementImageSize(canvas, element);
if (!size) return false;
const { width, height } = size;
// Bind the texture, and reallocate it if the size changed (see above).
allocateElementTexture(gl, texture, state, width, height);
gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, element, { width, height });
return true;
}
canvas.onpaint = () => {
try {
uploadElementWebGL(gl, canvas, texture, uiElement, textureState);
} catch (err) {
console.error('texElementSubImage2D copy failed:', err);
}
};
- Verwenden Sie im WebGPU-Kontext die Methode
drawElementImageToTexture. Beachten Sie dabei folgende Regeln:- Erstelle die Textur mit der Verwendung
COPY_DSTundRENDER_ATTACHMENTund fügeTEXTURE_BINDINGhinzu, um sie in Shadern zu sampeln. Eine falsche Verwendung wird als WebGPU-Validierungsfehler und nicht als Ausnahme gemeldet. - Übergeben Sie die Texturgröße als
size. Ohnesizewird ein größeres Element auf die Textur zugeschnitten. Beisizewird das Element so skaliert, dass es den Bereich ausfüllt. - Die Textur wird neu erstellt, wenn sich die Elementgröße ändert, und die Bindungsgruppen, die darauf verweisen, werden neu erstellt.
- Erstelle die Textur mit der Verwendung
// Create the texture once, and recreate it
// when the element size changes.
function ensureElementTexture(device, state, width, height) {
if (state.texture?.width === width && state.texture?.height === height) {
return state.texture;
}
state.texture?.destroy();
state.texture = device.createTexture({
size: { width, height },
format: "rgba8unorm",
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
return state.texture; // Recreate the bind groups that used the previous texture.
}
const textureState = { texture: null };
canvas.onpaint = () => {
const size = getElementImageSize(canvas, uiElement);
if (!size) return;
const previousTexture = textureState.texture;
const texture = ensureElementTexture(device, textureState, size.width, size.height);
if (texture !== previousTexture) {
// New texture: create, or rebuild, the bind groups that reference it.
}
try {
device.queue.drawElementImageToTexture({ source: uiElement }, { texture, size });
} catch (err) {
console.error('drawElementImageToTexture copy failed:', err);
}
};
Wenn Sie eine requestAnimationFrame-Schleife zum Rendern der Szene verwenden, rufen Sie canvas.requestPaint() innerhalb der Schleife auf, damit der HTML-Inhalt auf dem Canvas gerendert wird. event.changedElements listet die Elemente auf, deren Rendering sich geändert hat. Achten Sie darauf, dass Sie das Element nur dann neu zeichnen oder seine Textur neu hochladen, wenn die untergeordneten HTML-Elemente aktualisiert wurden:
function render() {
// Request to update the canvas
canvas.requestPaint();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas.onpaint = (event) => {
if (event.changedElements && event.changedElements.length > 0) {
// Redraw the element, or update its texture, as shown above
}
// In WebGL and WebGPU, render the scene, and sync the element geometry as shown in step 5
};
- Synchronisieren Sie die Elementgeometrie.
Der Browser muss wissen, wo das HTML-Element gezeichnet wird, damit die DOM-Position mit den gezeichneten Pixeln übereinstimmt, z. B. damit Nutzer darauf klicken und Text eingeben können. Ein Canvas führt nur Trefferprüfungen für Elemente durch, die mit der 2D-Methode drawElementImage() gezeichnet oder mit updateElementGeometry() registriert wurden.
Im Fall des 2D-Kontexts synchronisiert der Browser das Element automatisch.
drawElementImage()richtet die DOM-Trefferprüfungsgrenzen und die Fokusringe des Screenreaders an den gezeichneten Koordinaten aus, sodass Siestyle.transformnicht festlegen müssen. Wenn Sie die Geometrie manuell verwalten möchten, z. B. wenn Sie dasselbe Element mehrmals zeichnen, übergeben Sie{ preserveElementGeometry: true }andrawElementImage()und wenden Sie Transformationen mitcanvas.updateElementGeometry()an.Im 3D-Fall mit WebGL oder WebGPU kann das Canvas nicht erkennen, wo das HTML-Element gezeichnet wird. Sie müssen es also angeben. Berechnen Sie eine
DOMMatrix, die die Rahmenbox des Elements in CSS-Pixeln dem Canvas in CSS-Pixeln zuordnet. Der Browser muss den 3D-Koordinatenraum mithilfe einer Darstellungsbereichstransformation in den CSS-Koordinatenraum umwandeln. Gehen Sie dazu so vor:- Normalisieren Sie das HTML-Element. Die Größe von HTML-Elementen wird in Pixeln angegeben (z. B. 200 px breit). WebGL behandelt Objekte jedoch normalerweise als „Einheitsquadrate“, z.B.im Bereich von -0,5 bis 0,5. Wenn Sie die Werte nicht normalisieren, wird die 200 Pixel große Schaltfläche 200-mal größer dargestellt. In diesem Schritt wird auch die Y-Achse umgedreht, da in CSS nach unten positiv ist, in WebGL jedoch nach oben.
- Konvertieren Sie die WebGL-MVP-Matrix in eine DOM-Matrix.
- Ordnen Sie die Daten dem Darstellungsbereich des Canvas zu. Dieser Schritt ist die „Neuskalierungsphase“: Die mathematischen Werte des Einheitsraums werden wieder so gestreckt, dass sie den CSS-Pixelabmessungen des
<canvas>-Elements auf dem Bildschirm entsprechen, und die Y-Achse wird wieder umgedreht. - Berechnen Sie die endgültige Transformation. Multiplizieren Sie die Matrizen in der folgenden Reihenfolge: Darstellungsbereich * MVP * Normalisierung. Wenn Sie sie zu einer endgültigen Transformation kombinieren, entsteht eine „Karte“, die dem Browser genau mitteilt, wo sich das HTML-Element befinden soll, damit es mit der 3D-Zeichnung übereinstimmt. Der Browser führt die perspektivische Teilung durch.
// Maps the element's border box (CSS pixels) to the canvas (CSS pixels), for a // quad that spans -0.5 to 0.5 in model space and is drawn with the `mvp` matrix. function computeCanvasTransform(canvas, element, mvp) { const width = element.offsetWidth; const height = element.offsetHeight; // 1. Normalize the HTML element (CSS pixels -> WebGL Model Space) const toGLModel = new DOMMatrix() // Scale pixels to 1 unit, flip Y (as in CSS it points down, and in WebGL it points up) .scale(1 / width, -1 / height, 1 / height) // Center the origin: (0,0) becomes (-width/2, -height/2) before scaling .translate(-width / 2, -height / 2); // 2. Convert WebGL MVP Matrix to DOM Matrix const mvpDOM = new DOMMatrix(Array.from(mvp)); // 3. Map to the canvas viewport, in CSS pixels const clipToCanvasViewport = new DOMMatrix() // Move center (0,0) to center of canvas .translate(canvas.clientWidth / 2, canvas.clientHeight / 2) // Scale normalized clip (-1..1) to viewport size, and flip Y back .scale(canvas.clientWidth / 2, -canvas.clientHeight / 2, canvas.clientHeight / 2); // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square) return clipToCanvasViewport.multiply(mvpDOM).multiply(toGLModel); }
Übergeben Sie dann die Transformation als canvasTransform an canvas.updateElementGeometry(). Dadurch wird das HTML-Element direkt über den gerenderten Pixeln platziert. So wird sichergestellt, dass Nutzer, die auf eine Schaltfläche klicken oder Text auswählen, tatsächlich das echte HTML-Element treffen. Der Browser verwendet die Transformation auch für die Grenzen der Barrierefreiheit des Elements, z. B. für Fokusringe von Screenreadern:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
Mit diesen Optionen können Sie die Trefferprüfung steuern:
- Mit jedem
updateElementGeometry()-Aufruf wird das Element an den Anfang der Trefferprüfungsreihenfolge verschoben. Karte/Ticket „{ preserveHitTestOrder: true }“ behält die Position. - Rufen Sie
canvas.clearElementGeometry(element)auf, um ein Element aus der Trefferprüfung zu entfernen, z. B. wenn Sie es ausblenden.
- Verwenden Sie reguläre Canvas-Exportmethoden wie
toDataURL(),toBlob()odercaptureStream(). Die exportierten Daten enthalten den gerenderten HTML-Inhalt.
Beispielcode
<body>
<canvas id="canvas" style="width: 400px; height: 200px;" content="drawable">
<input id="element" drawable>
</canvas>
<button id="download">Download Image</button>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const element = document.getElementById('element');
const download = document.getElementById('download');
canvas.onpaint = (event) => {
ctx.reset();
// Draw the element into the canvas. The browser synchronizes its
// DOM position for hit testing (typing).
ctx.drawElementImage(element, 10, 10);
};
download.onclick = () => {
// Export the canvas content as an image
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'exported-canvas.png';
link.href = dataURL;
link.click();
};
// Re-initialize canvas size on screen resize
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);
canvas.requestPaint();
});
const supportsDevicePixelContentBox =
typeof ResizeObserverEntry !== 'undefined' &&
'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);
</script>
</body>
Best Practices
- OBLIGATORISCH: Prüfen Sie die Browserunterstützung für die HTML-in-Canvas API, bevor Sie sie verwenden.
- OBLIGATORISCH: Fügen Sie dem
<canvas>-Element immer das Attributcontent="drawable"hinzu. - ERFORDERLICH: Fügen Sie jedem Element, das Sie zeichnen, das Attribut
drawablehinzu, einschließlich direkter untergeordneter Elemente des Canvas. - OBLIGATORISCH: Verwenden Sie einen
onpaint-Event-Handler, um den HTML-Inhalt auf dem Canvas zu rendern. - VERPFLICHTEND: Verwenden Sie die Methode
drawElementImagein 2D oderdrawElementImageToTexturein WebGPU, um den HTML-Inhalt auf dem Canvas zu rendern. - OBLIGATORISCH: In WebGL muss der Texturpuffer einmalig mit
texImage2D()vorab zugewiesen werden (bei der Initialisierung oder wenn sich die Elementgröße ändert). Weisen Sie die Textur nur dann neu zu, wenn sich die Größe ändert, und setzen SieTEXTURE_MIN_FILTERaufLINEAR. - ERFORDERLICH: Verwenden Sie in WebGL die Methode
texElementSubImage2D, um den HTML-Inhalt in einer Textur zu rendern, die Sie vorab mittexImage2D()zuweisen und deren Größe Sie mitcaptureElementImage()festlegen. - OBLIGATORISCH: In WebGPU muss die Größe der Textur aus
captureElementImage()stammen und die Textur darf nur neu erstellt werden, wenn sich die Größe ändert. Erstellen Sie die Textur mit der VerwendungCOPY_DSTundRENDER_ATTACHMENTund übergeben Sie die Texturgröße alssize. - OBLIGATORISCH: Synchronisieren Sie die Geometrie des HTML-Elements mit dem gerenderten Inhalt. In 2D erledigt
drawElementImagedas automatisch. Rufen Sie in WebGL und WebGPUcanvas.updateElementGeometry(element, { canvasTransform })auf. - OBLIGATORISCH: Verwenden Sie
ResizeObserver, um die Bildschirmgröße zu beobachten und die Canvas-Größe an die Gerätepixel anzupassen. - Betten Sie KEINE ursprungsübergreifenden Inhalte in einen Canvas ein, da dies nicht unterstützt wird.
- Initialisieren Sie
ResizeObserverNICHT imonpaint-Event-Handler, da dies zu Speicherlecks führen kann.
Fallback-Strategien
Die HTML-in-Canvas API wird derzeit nicht in allen modernen Browsern unterstützt. Daher ist in der Regel eine Fallback-Strategie erforderlich. Aufgrund der verbesserten Leistung dieser API sollte HTML-in-Canvas jedoch verwendet werden, wenn der Browser dies unterstützt.
Wenn HTML-Inhalte aus einem Canvas exportiert werden müssen, verwenden Sie Bibliotheken wie html2canvas, dom-to-image oder snapdom.
Wenn Sie HTML-Interaktionen Frame für Frame erfassen möchten, z. B. für das Streaming, erfassen Sie DOM-Mutationen mit Bibliotheken wie rrweb.
Alternativ können Sie eine Warnung implementieren, dass der Export von HTML-Medien im Browser nicht unterstützt wird, weil er HTML-in-Canvas nicht unterstützt.