Web-apps moeten vaak uitgebreide HTML-content, zoals aangepaste dashboards, opgemaakte documenten of interactieve diagrammen, vastleggen en exporteren als statische afbeeldingen of video-opnamen. In het verleden waren hiervoor grote bibliotheken van derden nodig die DOM-nodes en CSS-eigenschappen handmatig parseren om een visuele facsimile op een canvas te reconstrueren. Deze aanpak is rekenkundig duur, foutgevoelig en ondersteunt vaak geen moderne CSS-opmaakfuncties. Met de HTML-in-Canvas API kunnen ontwikkelaars echte DOM-elementen rechtstreeks in de canvascontext renderen. Omdat de systeemeigen weergave-engine van de browser de HTML-substructuur met pixelperfecte nauwkeurigheid tekent, is het vastleggen van de exacte visuele uitvoer als afbeelding of videostream zeer efficiënt met ingebouwde canvasmethoden zoals toDataURL(), toBlob() of captureStream().
Hoe wordt dit geïmplementeerd
Controleer of HTML-in-Canvas wordt ondersteund in de browser:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }Initialiseer het canvas om de weergave van onderliggende HTML-elementen te ondersteunen door het kenmerk
content="drawable"toe te voegen aan het HTML-element<canvas>. Plaats uw HTML-content in het element<canvas>en voeg het kenmerkdrawabletoe aan elk element dat u tekent. Een element metdrawablelegt de substructuur vast, behalve geneste onderliggende elementen die ookdrawablezijn:<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>Schaal je canvasraster zodat het overeenkomt met de schaalfactor van het apparaat om wazigheid te voorkomen:
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);Geef de HTML-content weer op het canvas in een
canvas.onpaint-gebeurtenishandler.Gebruik in een 2D-context de methode
drawElementImage. Het tekent de laatste momentopname van eendrawable-element en de tekenbare substructuur (met uitzondering van andere tekenbare onderliggende elementen), beginnend bij het randvak van het element, vóór CSS-transformaties:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- In WebGL- en WebGPU-contexten kopieer je het element naar een textuur. Pas de grootte van de textuur aan vanaf
canvas.captureElementImage(element). Dewidthenheightzijn in canvasrasterpixels (backing store), dus rond ze naar boven af metMath.ceil().captureElementImage()genereert een fout als het element nog geen tekenrecord heeft: roeprequestPaint()aan en probeer het opnieuw bij de volgendepaint-gebeurtenis.
// 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;
}
- Wijs in de WebGL-context eerst de textuurbacking vooraf toe met de methode
texImage2D. De fragmenten in deze handleiding gebruiken een WebGL 2-context (canvas.getContext("webgl2")). Wijs textuuropslag toe doortexImage2D()aan te roepen metnullin plaats van pixelgegevens. Hiermee wordt GPU-geheugen van de opgegeven grootte en indeling gereserveerd zonder pixels te uploaden. Volg deze regels:- Pas de grootte van de textuur aan vanaf
captureElementImage(), zoals hierboven getoond. - Wijs niveau 0 alleen toe bij de eerste upload of als de grootte verandert, omdat de textuur wordt gewist als je deze opnieuw toewijst. Gebruik veranderlijke opslag: de grootte van onveranderlijke
texStorage2D()-opslag kan niet worden aangepast als de grootte van het element verandert.
- Pas de grootte van de textuur aan vanaf
// 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 };
- Geef daarna in de WebGL-context de HTML-content weer in de textuur met de methode
texElementSubImage2D. Volg deze regels:- Geef
{ width, height }door in de configuratie. De kopie vult de toegewezen textuur, ongeacht hoe het canvas wordt geschaald. - Wijs de textuur opnieuw toe als de grootte is gewijzigd, zoals hierboven getoond.
- Geef
// 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);
}
};
- Gebruik in de WebGPU-context de
drawElementImageToTexture-methode. Volg deze regels:- Maak de textuur met
COPY_DST- enRENDER_ATTACHMENT-gebruik en voegTEXTURE_BINDINGtoe om deze te samplen in shaders. Onjuist gebruik wordt gerapporteerd als een WebGPU-validatiefout, niet als een uitzondering. - Geef de textuurgrootte door als
size. Zondersizewordt een groter element bijgesneden tot de textuur. Metsizewordt het element geschaald om het te vullen. - Maak de textuur opnieuw als de elementgrootte verandert en maak de bindingsgroepen die ernaar verwijzen opnieuw.
- Maak de textuur met
// 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);
}
};
Als je een requestAnimationFrame-lus gebruikt om de scène te renderen, roep je canvas.requestPaint() aan binnen de lus om te zorgen dat de HTML-content wordt gerenderd naar het canvas. event.changedElements vermeldt de elementen waarvan de weergave is gewijzigd. Zorg dat u het element alleen opnieuw tekent of de textuur opnieuw uploadt als er een update is geweest voor de onderliggende HTML-elementen:
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
};
- Synchroniseer de elementgeometrie.
De browser moet weten waar het HTML-element wordt getekend, zodat de DOM-positie overeenkomt met de getekende pixels, bijvoorbeeld zodat gebruikers erop kunnen klikken en erin kunnen typen. Een canvas voert alleen hit-tests uit voor de elementen die zijn getekend met de 2D-methode drawElementImage() of zijn geregistreerd met updateElementGeometry().
In het geval van 2D-context synchroniseert de browser het element automatisch.
drawElementImage()lijnt de DOM-hit-testgrenzen en de focusringen van de schermlezer uit met de getekende coördinaten, zodat jestyle.transformniet hoeft in te stellen. Als je de geometrie handmatig wilt beheren, bijvoorbeeld als je hetzelfde element meerdere keren tekent, geef je{ preserveElementGeometry: true }door aandrawElementImage()en pas je transformaties toe metcanvas.updateElementGeometry().In het 3D-geval met WebGL of WebGPU kan het canvas niet bepalen waar het HTML-element wordt getekend. Je moet dit dus zelf aangeven. Bereken een
DOMMatrixdie het randvak van het element, in CSS-pixels, toewijst aan het canvas, in CSS-pixels. De browser moet de 3D-coördinatenruimte toewijzen aan de CSS-coördinatenruimte met een viewport-transformatie. Doe hiervoor het volgende:- Normaliseer het HTML-element. HTML-elementen worden gemeten in pixels (bijvoorbeeld 200 pixels breed). WebGL behandelt objecten meestal als eenheidsvierkanten, bijvoorbeeld van -0,5 tot 0,5. Als je niet normaliseert, ziet je knop van 200 px er 200 keer groter uit. In deze stap wordt ook de Y-as omgedraaid, omdat in CSS omlaag positief is, maar in WebGL omhoog positief is.
- Zet de WebGL MVP-matrix om in een DOM-matrix.
- Toewijzen aan het kijkvenster van het canvas. Deze stap is de fase van opnieuw schalen: de wiskunde van de eenheidsruimte wordt weer uitgerekt zodat deze overeenkomt met de CSS-pixelafmetingen van je
<canvas>-element op het scherm en de Y-as wordt weer omgedraaid. - Bereken de uiteindelijke transformatie. Vermenigvuldig de matrices in de volgorde: Viewport * MVP * Normalization. Als je ze combineert tot één uiteindelijke transformatie, krijg je een 'kaart' die de browser precies vertelt waar dat HTML-element moet staan om uit te lijnen met de 3D-tekening. De browser voert de perspectiefdeling uit.
// 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); }
Geef de transformatie daarna door aan canvas.updateElementGeometry() als canvasTransform. Hiermee wordt het HTML-element rechtstreeks boven de gerenderde pixels geplaatst. Zo weet u zeker dat wanneer een gebruiker op een knop klikt of tekst selecteert, deze ook echt het echte HTML-element raakt. De browser gebruikt de transformatie ook voor de toegankelijkheidsgrenzen van het element, bijvoorbeeld voor focusringen van schermlezers:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
Gebruik deze opties om hit-tests te beheren:
- Elke
updateElementGeometry()-aanroep verplaatst het element naar de bovenkant van de hit-testvolgorde. Kaart{ preserveHitTestOrder: true }behoudt de positie. - Roep
canvas.clearElementGeometry(element)aan om een element uit de hit-test te verwijderen, bijvoorbeeld als je het verbergt.
- Gebruik reguliere exportmethoden voor canvas, zoals
toDataURL(),toBlob()ofcaptureStream(). De geëxporteerde gegevens bevatten de gerenderde HTML-content.
Voorbeeldcode
<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>
Praktische tips
- VERPLICHT: Controleer de browserondersteuning voor de HTML-in-Canvas API voordat u deze gebruikt.
- VERPLICHT: Voeg altijd het kenmerk
content="drawable"toe aan het element<canvas>. - VERPLICHT: Voeg het kenmerk
drawabletoe aan elk element dat u tekent, inclusief directe onderliggende elementen van het canvas. - VERPLICHT: Gebruik een
onpaint-gebeurtenisverwerker om de HTML-content op het canvas te renderen. - VERPLICHT: Gebruik de methode
drawElementImagein 2D ofdrawElementImageToTexturein WebGPU om de HTML-content weer te geven op het canvas. - VERPLICHT: Wijs in WebGL de textuurbuffer vooraf één keer toe met
texImage2D()(wanneer deze wordt geïnitialiseerd of wanneer de elementgrootte verandert). Wijs de textuur alleen opnieuw toe als de grootte verandert en stelTEXTURE_MIN_FILTERin opLINEAR. - VERPLICHT: Gebruik in WebGL de methode
texElementSubImage2Dom de HTML-content te renderen naar een textuur die u vooraf toewijst mettexImage2D(), met de grootte vancaptureElementImage(). - VERPLICHT: Geef in WebGPU de textuur de grootte van
captureElementImage()en maak deze alleen opnieuw als de grootte verandert. Maak de textuur metCOPY_DST- enRENDER_ATTACHMENT-gebruik en geef de textuurgrootte door alssize. - VERPLICHT: Synchroniseer de geometrie van het HTML-element met de gerenderde content. In 2D doet
drawElementImagedit automatisch. Roepcanvas.updateElementGeometry(element, { canvasTransform })aan in WebGL en WebGPU. - VERPLICHT: Gebruik
ResizeObserverom de schermgrootte te observeren en de canvasgrootte te updaten zodat deze overeenkomt met de apparaatpixels. - Sluit GEEN cross-origin content in een canvas in, want dat wordt niet ondersteund.
- Initialiseer
ResizeObserverNIET binnen de gebeurtenishandleronpaint, omdat dit kan leiden tot geheugenlekken.
Reservestrategieën
De HTML-in-Canvas API wordt op dit moment niet in alle moderne browsers ondersteund. Daarom is meestal een reservestrategie vereist. Vanwege de verbeterde prestaties van deze API moet HTML-in-Canvas worden gebruikt als de browser dit ondersteunt.
Als je HTML-content wilt exporteren vanuit een canvas, gebruik je bibliotheken zoals html2canvas, dom-to-image of snapdom.
Als je HTML-interacties frame voor frame wilt vastleggen, bijvoorbeeld voor streaming, leg je DOM-mutaties vast met bibliotheken zoals rrweb.
Je kunt ook een waarschuwing implementeren dat HTML-media-export niet wordt ondersteund in de browser omdat deze geen HTML-in-Canvas ondersteunt.