Met de HTML-in-Canvas API kun je interactieve, toegankelijke DOM-subtrees rechtstreeks renderen in 2D-, WebGL- of WebGPU-canvascontexten.
In deze handleiding worden de technische concepten van HTML-in-Canvas beschreven die voor alle canvascontexten gelden:
- Je canvas voorbereiden om HTML te renderen
- Inzicht in de substructuur
drawable - Inzicht in de gebeurtenis
paint - De geometrie van een element updaten
- Inzicht in de toegankelijkheid van HTML-in-Canvas
- Terugvalstrategieën gebruiken
Je canvas voorbereiden om HTML te renderen
Als je HTML-elementen wilt weergeven in een <canvas>, stel je de volgende 2 kenmerken in:
- Stel
content="drawable"in op de<canvas>: Hiermee wordt het canvaselement ingesteld om de onderliggende DOM-nodes op te maken in plaats van ze alleen als verborgen content te behandelen. - Stel
drawablein voor doelelementen: Hiermee wordt een element gemarkeerd als een onafhankelijke paint-root die in aanmerking komt voor canvasweergave.
<canvas id="mainCanvas" width="600" height="400" content="drawable">
<div id="parentElement" class="card" drawable>
<h2>Container Title</h2>
<p>Descriptive text inside the parent element.</p>
<!-- Nested drawable: excluded from parentElement and drawn independently -->
<button id="childElement" class="btn" drawable>Action Button</button>
</div>
</canvas>
Inzicht in de tekenbare substructuur
Als je HTML voorbereidt om te worden gerenderd in een canvas, moet je beslissen welke elementen onafhankelijk moeten worden getekend. Elk drawable-element vertegenwoordigt een onafhankelijke tekenroot en de bijbehorende tekenbare substructuur.
Een tekenbare substructuur bestaat uit een element met het kenmerk drawable en alle onderliggende elementen, met uitzondering van geneste onderliggende elementen die ook drawable bevatten, bijvoorbeeld:
<canvas content="drawable">
<!-- Subtree A (Renders parent container and text) -->
<div id="parentElement" drawable>
<h2>Parent Card Heading</h2>
<p>Card body text content.</p>
<!-- Subtree B (Excluded from Subtree A and drawn separately) -->
<button id="nestedChildElement" drawable>
Clickable Button
</button>
</div>
</canvas>
| Element | Grens tekenen | Gedrag |
|---|---|---|
#parentElement (Substructuur A) |
Achtergrondvenster, kop en alinea | Weergegeven bij het aanroepen van drawElementImage(parentElement, ...) |
#nestedChildElement (Substructuur B) |
Knopoppervlak en tekst | Uitgesloten van #parentElement-substructuur; weergegeven in een afzonderlijke drawElementImage(nestedChildElement, ...)-aanroep |
drawable-knop is genest in een drawable-kaart en elke knop wordt getekend door een eigen drawElementImage()-aanroep.Door tekenbare elementen te nesten, kun je semantische HTML-relaties behouden en tegelijkertijd onderliggende elementen onafhankelijk van elkaar in lagen plaatsen, positioneren of animeren in je canvas-scène.
Inzicht in de gebeurtenis paint
De HTML-in-Canvas API introduceert de gebeurtenis paint. De browser stuurt automatisch een paint-gebeurtenis naar de <canvas> wanneer de vormgeving, tekst of CSS-stijlen veranderen in een van de drawable-afstammelingen.
canvas.addEventListener("paint", (event) => {
// Clear the viewport
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw elements individually
ctx.drawElementImage(parentElement, 20, 20);
ctx.drawElementImage(childElement, 40, 90);
});
De gebeurtenis paint verzendt een instantie van CanvasPaintEvent, die de standaard DOM Event uitbreidt en een property changedElements heeft die alleen de onderliggende elementen van drawable bevat die visueel zijn gewijzigd (bijvoorbeeld stijl- of lay-outwijzigingen, tekstbewerkingen of wijzigingen in de focusstatus) sinds de laatste weergave.
canvas.addEventListener("paint", (event) => {
// Array of changed elements
for (const el of event.changedElements) {
console.log(`Element #${el.id} visually changed and needs repainting`);
}
});
Levenscyclus van frames
Canvas-tekenopdrachten die worden uitgevoerd binnen de gebeurtenis paint, verschijnen in de huidige weergave-update, maar DOM-wijzigingen die binnen de gebeurtenis paint worden aangebracht, verschijnen pas in het volgende frame:
canvas.addEventListener("paint", () => {
// Renders in the CURRENT rendering update:
ctx.drawElementImage(parentElement, 20, 20);
// Modifying DOM styles inside paint schedules a new layout cycle;
// changes will appear in the NEXT frame:
parentElement.style.backgroundColor = "#e0f2fe";
});
Geneste canvassen
Als je pagina geneste <canvas content="drawable">-elementen bevat, stuurt de browser paint-gebeurtenissen in omgekeerde DOM-boomstructuurvolgorde (afstammelingen worden geactiveerd vóór voorouders). Zo zorg je dat onderliggende canvaselementen worden gerenderd voordat bovenliggende canvaselementen ze samenstellen.
Stel dat in het volgende voorbeeld het binnenste nestedElement-element visueel moet veranderen (bijvoorbeeld de CSS-achtergrondkleur verandert wanneer de gebruiker de muisaanwijzer erop houdt). Eerst wordt de gebeurtenis paint geactiveerd op het canvas nestedCanvas. De listener wist de onderliggende buffer en roept ctx.drawElementImage(nestedElement, ...) aan om de geüpdatete status weer te geven. Daarna wordt de gebeurtenis paint geactiveerd op parentCanvas. De listener tekent wrapperElement (met het al geüpdatete nestedCanvas-canvas) in de scène.
<!-- Outer parent canvas -->
<canvas id="parentCanvas" width="800" height="600" content="drawable">
<!-- Wrapper container for nested content -->
<div id="wrapperElement" drawable>
<!-- Nested child canvas -->
<canvas id="nestedCanvas" width="300" height="200" content="drawable">
<div id="nestedElement" class="badge" drawable>
Inner Child Content
</div>
</canvas>
</div>
</canvas>
<script>
const parentCanvas = document.getElementById("parentCanvas");
const parentCtx = parentCanvas.getContext("2d");
const nestedCanvas = document.getElementById("nestedCanvas");
const nestedCtx = nestedCanvas.getContext("2d");
const nestedElement = document.getElementById("nestedElement");
const wrapperElement = document.getElementById("wrapperElement");
// ============================================================================
// 1. Child Canvas Listener (Fires FIRST)
// ============================================================================
nestedCanvas.addEventListener("paint", (event) => {
// Clear and redraw the child canvas with the updated inner element
nestedCtx.clearRect(0, 0, nestedCanvas.width, nestedCanvas.height);
nestedCtx.drawElementImage(nestedElement, 10, 10);
console.log("1. Child nested canvas rendered fresh pixels.");
});
// ============================================================================
// 2. Parent Canvas Listener (Fires SECOND)
// ============================================================================
parentCanvas.addEventListener("paint", (event) => {
// Clear and redraw the parent canvas scene
parentCtx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);
// Draws the wrapper container (including the freshly painted nestedCanvas)
parentCtx.drawElementImage(wrapperElement, 50, 50);
console.log("2. Parent canvas composited the complete scene.");
});
</script>
<canvas content="drawable">-elementen zijn genest, worden tekengebeurtenissen geactiveerd in omgekeerde boomstructuurvolgorde: het geneste canvas tekent eerst, zodat het bovenliggende canvas de up-to-date pixels kan tekenen.Een paint-gebeurtenis activeren
Roep requestPaint() aan om één paint-gebeurtenis te plannen in het volgende frame, zelfs als er geen DOM-tekst of -stijlen zijn gewijzigd. Dit kan worden gebruikt om interactieve scènes met bewegende elementen weer te geven, bijvoorbeeld:
let animatedX = 0;
canvas.addEventListener("paint", () => {
animatedX += 1.5; // Move the element horizontally
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawElementImage(staticElement, 20, 20);
ctx.drawElementImage(movingElement, animatedX, 200);
// Request canvas paint on each rendering update for smooth animation
canvas.requestPaint();
});
canvas.requestPaint();
De geometrie van een element updaten
Als je een HTML-element in een canvas tekent, bepaalt je tekencode waar het element verschijnt en hoe groot het is. In tegenstelling tot normale HTML-content kunnen de positie en grootte van het element op het scherm, de geometrie, niet alleen worden bepaald op basis van de berekende stijl. De browser heeft de geometrie van een element nodig om het volgende te doen:
- Invoergebeurtenissen zoals klikken, tikken en aanwijzerbewegingen naar het juiste element sturen. Dit wordt hit-testing genoemd.
- Nauwkeurige resultaten retourneren voor opmaakquery's, zoals
getBoundingClientRect(). - De grenzen van het element melden aan ondersteunende technologieën, zoals vergrootglazen.
In 2D updatet drawElementImage() de geometrie standaard automatisch. De argumenten die je doorgeeft, samen met de berekende stijl van het element en de huidige transformatie van de context, zijn voldoende om te bepalen waar het element verschijnt.
In WebGL- en WebGPU-contexten kan de browser de geometrie niet bepalen. Een HTML-element wordt gekopieerd naar een textuur en je shaderprogramma kan die textuur op een willekeurige manier toewijzen aan de scène. Als je de browser wilt laten weten waar het element verschijnt, geef je een transformatie door aan updateElementGeometry().
updateElementGeometry() niet meer aanroept, weet de browser niet meer waar de HTML-elementen op de 3D-kaart zich bevinden. De kaart blijft draaien, maar klikken komen nog steeds terecht op de plek waar de kaart stond.Als je de geometrie wilt updaten, geef je een ruimtelijke transformatie door aan de methode. Meer informatie over hoe u een transformatie maakt voor verschillende contexten:
- Geometrie synchroniseren in 2D.
- Geometrie synchroniseren in WebGL.
- Geometrie synchroniseren in WebGPU.
Stapelvolgorde voor hit-test beheren
Elke keer dat de geometrie van een element wordt geüpdatet, plaatst de browser het standaard bovenaan de interactiestack (laatst getekend = bovenste klikdoel). Je kunt dit gedrag beheren met preserveHitTestOrder:
// Updates the backdrop without stealing clicks from foreground elements
canvas.updateElementGeometry(backgroundElement, {
canvasTransform: customTransform,
preserveHitTestOrder: true
});
| Optie | Gedrag | Use case |
|---|---|---|
preserveHitTestOrder: false (standaard) |
Verplaatst het element naar de bovenkant van de hit-test-stack. | Elementen op de voorgrond die als eerste klikken moeten krijgen. |
preserveHitTestOrder: true |
Laat het element op de huidige stapelprioriteit staan. | Achtergrondelementen die na de voorgrondelementen moeten worden geüpdatet. |
Geometrie resetten
Als u een element ongedaan wilt maken zonder het uit het DOM te verwijderen, roept u canvas.clearElementGeometry() aan. Hiermee wordt de canvastransformatie van het element gereset en wordt het gemarkeerd als buiten het scherm in de toegankelijkheidsstructuur:
// Remove from canvas hit-testing and mark as offscreen
canvas.clearElementGeometry(childElement);
Formaten en eenheden
Als je HTML in een canvas tekent, werkt de browser in 3 coördinatensystemen tegelijk: CSS-pixels, apparaatpixels en canvasrasterpixels.
CSS-pixels worden gebruikt om content te meten en te positioneren tijdens de opmaak. De browser schaalt ze daarna met devicePixelRatio om de pagina te rasteren in fysieke apparaatpixels. Omdat CSS-pixels een logische eenheid zijn, worden posities en formaten vaak fracties van een apparaatpixel (bijvoorbeeld 10,5 CSS-pixels × een DPR van 1,5 = 15,75 apparaatpixels). Een scherm kan alleen hele pixels kleuren, dus de browser moet alles oplossen wat ertussen valt. Dit gebeurt op 2 manieren:
- Uitlijnen: Vakgrenzen, randen en laagverschuivingen worden afgerond op de dichtstbijzijnde hele apparaatpixel, zodat ze scherp blijven.
- Anti-aliasing: Voor teksttekens, curven en getransformeerde randen wordt gedeeltelijke dekking gemengd met aangrenzende pixels, zodat ze er nog steeds vloeiend uitzien en op de juiste plaats staan.
Een <canvas> heeft een eigen pixelraster, dat wordt ingesteld door de kenmerken width en height. Dit raster staat los van zowel CSS-pixels als apparaatpixels. De browser herneemt de canvasbitmap op het fysieke pixelraster om deze weer te geven. Hierbij worden aangrenzende canvaspixels samengevoegd om elke apparaatpixel te vullen. Ga naar Pixel-perfecte weergave met devicePixelContentBox voor meer informatie over hoe de browser toewijst tussen CSS, canvas en apparaatpixels.
Alle drawable HTML-elementen zijn opgemaakt in css-pixels. Omdat content die in een canvas wordt getekend, 2 conversies kan doorlopen (van CSS-pixels naar het canvasraster en dan van het canvasraster naar fysieke pixels), voegt elke stap die niet precies overeenkomt een nieuwe mengronde toe. Het resultaat kan er wazig uitzien. Zorg dat het canvasraster overeenkomt met het fysieke pixelraster om dit te voorkomen. Dit heeft 2 doelen:
- Consistente weergave: Als het canvas 1:1 wordt toegewezen aan fysieke pixels, levert de conversie van CSS-pixels naar canvaspixels hetzelfde resultaat op als de conversie van CSS-pixels naar fysieke pixels voor normale HTML door de browser.
- Geen extra overvloeiing: Als het canvas 1:1 wordt toegewezen aan fysieke pixels, wordt er geen vervaging toegevoegd als je het converteert naar apparaatpixels.
De volgende afbeelding toont een pixelconversie die resulteert in wazige content. De 3 rasters komen niet overeen: het canvaselement is 360 × 180 CSS-pixels en het canvasformaat is 300 × 150 canvasrasterpixels. Bij een pixelratio van 1, 5 beslaat het canvas 540 × 270 apparaatpixels. Eén CSS-pixel beslaat 1,5 apparaatpixels en één canvaspixel beslaat 1,2 CSS-pixels of 1,8 apparaatpixels (deelvenster 1). Als de cirkel wordt getekend, zijn de positie en straal in canvaspixels, niet in CSS-pixels. De browser tekent de cirkel dus in de canvasbitmap van 300 × 150 en past anti-aliasing toe op de rand (deelvenster 2). De browser rekt deze bitmap vervolgens uit over 540 × 270 apparaatpixels om het canvas weer te geven. De browser moet dus interpoleren: elke apparaatpixel neemt een gewogen mix van de dichtstbijzijnde canvaspixels, die de anti-aliased rand al bevatten. De rand wordt een 2e keer gemengd en verspreidt zich over verschillende apparaatpixels, waardoor de cirkel er wazig uitziet (deelvenster 3). Dit kan worden voorkomen als het canvasraster 1:1 overeenkomt met de apparaatpixels. De browser past dan maar één keer anti-aliasing toe op de rand, rechtstreeks op het apparaatraster, en geeft de bitmap weer zonder resampling, zodat de rand scherp blijft (deelvenster 4):
Voor een scherpe weergave moet het canvasraster overeenkomen met het aantal apparaatpixels dat het canvas beslaat: 540 × 270 in het vorige voorbeeld.
Het canvasraster aanpassen met een formaatwaarnemer
De juiste rastergrootte is afhankelijk van het apparaat en kan veranderen als de vormgeving verandert, als de gebruiker de pagina inzoomt of als de gebruiker het venster naar een ander scherm verplaatst. In plaats van de rastergrootte vast te leggen in de code, laat je de browser bepalen wat de grootte moet zijn.
Als je het canvasraster synchroon wilt houden met het canvas op het scherm, geef je het altijd een grootte met een ResizeObserver en pas je het canvasraster aan naar devicePixelContentBoxSize:
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);
CSS-pixels converteren naar coördinaten van canvasraster
Een paint-handler combineert vaak DOM-metingen met tekenaanroepen, bijvoorbeeld om een element te positioneren op basis van de grootte. De 2 gebruiken verschillende eenheden:
- Canvaspixels beschrijven het canvasraster, dat wordt bepaald door
canvas.widthencanvas.height. Bijvoorbeeld de parametersdx,dy,dwidthendheightvan de tekenmethodendrawElementImage()of andere parameters van canvas-API's, zoalsfillRect()entranslate(). - Css-pixels beschrijven de pagina. DOM-metingen zoals
offsetWidthenclientWidthen coördinaten van aanwijsgebeurtenissen zoalsoffsetXzijn in CSS-pixels.
Hier is een overzicht van de eenheden die door verschillende API-parameters worden gebruikt:
| Waarde | Eenheden |
|---|---|
canvas.width en canvas.height |
Pixels van canvasraster |
Parameters voor 2D-tekeningen, waaronder dx, dy, dwidth en dheight van drawElementImage() |
Pixels van canvasraster |
width en height teruggegeven door canvas.getElementImageDefaultSize() |
Pixels van canvasraster |
Bronrechthoeken: sx, sy, swidth en sheight van drawElementImage() en texElementSubImage2D(); sourceX, sourceY, sourceWidth en sourceHeight van drawElementImageToTexture() |
CSS-pixels |
Textuurgrootten: width en height opties van texElementSubImage2D(); size van drawElementImageToTexture() |
Textuurpixels. Standaard de grootte van het element in canvasrasterpixels, naar boven afgerond |
Elementgeometrie: getElementTransform() en de opties canvasTransform en canvasClip van updateElementGeometry() |
Css-pixels |
DOM-metingen, zoals offsetWidth, clientWidth, getBoundingClientRect(), coördinaten van aanwijzergebeurtenissen en contentBoxSize van een invoer voor formaat aanpassen |
CSS-pixels |
devicePixelContentBoxSize van een invoer voor een formaatwijzigingsobservator |
Apparaatpixels |
Als u de verkeerde eenheden gebruikt, voldoet het formaat van de HTML-content die in het canvas wordt getekend misschien niet aan uw verwachtingen. Je moet de CSS-pixelwaarde converteren naar canvasrasterpixels als je HTML in het canvas tekent. Bereken hiervoor de verhouding tussen de rastergrootte van het canvas en de CSS-grootte van het canvas: canvas.width / cssContentWidth horizontaal en canvas.height / cssContentHeight verticaal. Je resize observer is een handige plek om de verhouding als volgt te berekenen:
// The number of canvas grid pixels per CSS pixel
let scaleX = 1;
let scaleY = 1;
const observer = new ResizeObserver(([entry]) => {
const dpc = entry.devicePixelContentBoxSize;
const cssContentWidth = entry.contentRect.width;
const cssContentHeight = entry.contentRect.height;
// Best practice: make the canvas grid match the physical pixel grid
if (dpc) {
canvas.width = dpc[0].inlineSize;
canvas.height = dpc[0].blockSize;
} else {
canvas.width = Math.round(cssContentWidth * window.devicePixelRatio);
canvas.height = Math.round(cssContentHeight * window.devicePixelRatio);
}
scaleX = canvas.width / cssContentWidth;
scaleY = canvas.height / cssContentHeight;
});
const supportsDevicePixelContentBox =
typeof ResizeObserverEntry !== 'undefined' &&
'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);
Vermenigvuldig daarna de css-pixelwaarden met scaleX en scaleY voordat u ze doorgeeft aan de tekenmethode. Zo centreert u bijvoorbeeld een tekenbaar element op het canvas:
const ctx = canvas.getContext('2d');
const card = document.getElementById('card');
canvas.onpaint = () => {
ctx.reset();
// offsetWidth and offsetHeight are in CSS pixels.
// Convert them to canvas grid coordinates.
const width = card.offsetWidth * scaleX;
const height = card.offsetHeight * scaleY;
const x = (canvas.width - width) / 2;
const y = (canvas.height - height) / 2;
// drawElementImage() takes canvas grid coordinates.
ctx.drawElementImage(card, x, y);
};
Omdat drawElementImage() het randvak van het element tekent, gebruik je box-sizing: border-box om opvulling en rand op te nemen in de width en height van de kaart, zodat de kaart wordt getekend met het formaat dat je hebt ingesteld:
#card {
/* Make width and height include padding and border */
box-sizing: border-box;
width: 200px;
padding: 16px;
}
Inzicht in de toegankelijkheid van HTML-in-Canvas
Over het algemeen wordt HTML-content van een <canvas content="drawable"> op dezelfde manier blootgesteld aan toegankelijkheid als gewone DOM. Dit betekent dat deze wordt weergegeven in de toegankelijkheidsstructuur, tenzij deze expliciet is verborgen met kenmerken zoals aria-hidden="true" of inert. Er is echter één belangrijke uitzondering: een drawable-element waarvan de weergegeven geometrie nog niet is vastgesteld of geüpdatet.
Een drawable-element in een <canvas content="drawable"> heeft geen gerenderde geometrie als:
- Het is nog niet gerenderd met
drawElementImage(), dat de geometrie standaard automatisch updatet. - De geometrie is niet expliciet geüpdatet met
updateElementGeometry().
Als een drawable-element geen gerenderde geometrie heeft, worden dit drawable-element en de drawable-substructuur gerapporteerd aan de toegankelijkheidsstructuur zonder geometrie-informatie. Dit is bijvoorbeeld handig om schermlezers toegang te geven tot de semantische content, terwijl visuele hulptools (zoals vergrootglazen) de niet-getekende elementen wegfilteren.
In het volgende voorbeeld blijft de semantiek van een afbeelding, afbeelding en bijschrift behouden, terwijl de afbeelding en de afbeelding afzonderlijk worden getekend:
<canvas id="canvas" width="400" height="300" content="drawable">
<figure id="figure">
<img drawable id="image" src="..." alt="image alt text" />
<figcaption drawable id="caption">A caption</figcaption>
</figure>
</canvas>
<script>
const ctx = canvas.getContext('2d');
canvas.onpaint = () => {
ctx.reset();
// Note: Because `preserveElementGeometry` defaults to false, the
// `drawElementImage` call automatically updates geometry, ensuring
// the image and caption have geometry information for accessibility.
ctx.drawElementImage(image, 0, 0);
ctx.drawElementImage(caption, 0, 200);
};
</script>
Reservestrategieën
De HTML-in-Canvas API is geleverd in Chrome 157 en wordt misschien niet in alle browsers ondersteund. Check of de browser de API ondersteunt en gebruik zo nodig een alternatieve oplossing om te zorgen dat je oplossing voor iedereen soepel werkt:
export function supportsHtmlInCanvas() {
if (typeof document === 'undefined') return false;
const canvas = document.createElement('canvas');
canvas.setAttribute('content', 'drawable');
return canvas.content === 'drawable';
}
Als de API niet wordt ondersteund, kunt u, afhankelijk van uw implementatiedoelen, een van de volgende alternatieven kiezen:
- Een alternatieve UI
- Zwevende HTML-overlays
- De bibliotheek three-html-render
- html2canvas
- dom-to-image
Volgende stappen
- Meer informatie over hoe je HTML rendert in een 2D-context.
- Meer informatie over hoe je HTML rendert in een WebGL-canvas.
- Meer informatie over hoe je HTML rendert in een WebGPU-canvas.
- Meer informatie over terugvalstrategieën.
- Dien een probleem in op GitHub om je feedback te delen.
- Ga naar de verzameling HTML-in-Canvas-demo's van de community.