Programy do cieniowania WebGL zapewniają zaawansowane efekty wizualne przyspieszane przez GPU, umożliwiając korzystanie z zaawansowanych funkcji, takich jak dynamiczne zniekształcenia fal, modele oświetlenia, gradacja kolorów i niestandardowe przekształcenia wierzchołków. Interfejs HTML-in-Canvas API umożliwia deweloperom stosowanie tekstur WebGL do treści HTML. Umożliwia to natywne stosowanie wysokowydajnych programów do cieniowania fragmentów i wierzchołków do w pełni interaktywnych komponentów interfejsu, takich jak przyciski, pola do wprowadzania danych i tekst sformatowany, przy zachowaniu natywnej dostępności, zaznaczania tekstu i obsługi zdarzeń DOM.
Jak to zrobić?
Sprawdź, czy przeglądarka obsługuje HTML-in-Canvas:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }Dodaj atrybut
content="drawable"do elementu HTML<canvas>.Umieść zawartość HTML w elemencie
<canvas>i dodaj atrybutdrawabledo każdego elementu, który rysujesz. Element z atrybutemdrawableprzechwytuje swoje poddrzewo, z wyjątkiem zagnieżdżonych elementów podrzędnych, które również mają atrybutdrawable.<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>Aby zapobiec rozmyciu, przeskaluj siatkę obszaru roboczego, aby dopasować ją do współczynnika skalowania urządzenia:
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);Najpierw wstępnie przydziel podkład tekstury za pomocą metody
texImage2D. Fragmenty kodu w tym przewodniku używają kontekstu WebGL 2 (canvas.getContext("webgl2")).
Najpierw przydziel pamięć tekstury, wywołując funkcję texImage2D() z null zamiast danych pikseli, co spowoduje zarezerwowanie pamięci GPU o określonym rozmiarze i formacie bez przesyłania pikseli. Przestrzegaj tych zasad:
- Rozmiar tekstury:
canvas.captureElementImage(element). Wartościwidthiheightsą podane w pikselach siatki obszaru roboczego (magazynu zapasowego), więc zaokrąglij je w górę za pomocąMath.ceil().captureElementImage()zgłasza błąd, jeśli element nie ma jeszcze rekordu renderowania: wywołajrequestPaint()i spróbuj ponownie przy następnym zdarzeniupaint. - Przydzielaj poziom 0 tylko przy pierwszym przesłaniu lub gdy zmieni się rozmiar, ponieważ ponowne przydzielenie powoduje wyczyszczenie tekstury. Używaj pamięci zmiennej: rozmiaru niezmiennej pamięci
texStorage2D()nie można zmienić, gdy zmienia się rozmiar elementu.
// 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;
}
// 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 };
Wyrenderuj zawartość HTML na teksturze w module obsługi zdarzeń
canvas.onpaintza pomocą metodytexElementSubImage2D. Przestrzegaj tych zasad:- Element można przesłać dopiero po jego narysowaniu na obszarze roboczym. Prześlij z wydarzenia
painti wywołajcanvas.requestPaint(), aby poprosić o wydarzenie. - Przekaż
{ width, height }w konfiguracji. Sprawia, że kopia wypełnia przydzieloną teksturę niezależnie od tego, jak skalowany jest obszar roboczy. - Jeśli rozmiar się zmienił, ponownie przydziel teksturę, jak pokazano w kroku 5.
- Element można przesłać dopiero po jego narysowaniu na obszarze roboczym. Prześlij z wydarzenia
// 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 (step 5).
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);
}
};
Jeśli do renderowania sceny używasz pętli requestAnimationFrame, wywołaj w niej funkcję canvas.requestPaint(), aby mieć pewność, że treść HTML zostanie wyrenderowana na elemencie canvas. event.changedElements zawiera listę elementów, których renderowanie uległo zmianie. Pamiętaj, aby ponownie przesłać teksturę tylko wtedy, gdy nastąpiła aktualizacja elementów HTML potomnych:
function render() {
// Request to update the canvas
canvas.requestPaint();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas.onpaint = (event) => {
if (event.changedElements && event.changedElements.length > 0) {
// Update the texture with uploadElementWebGL()
}
// Render the scene, and sync the element geometry as shown in step 7
};
- Zsynchronizuj geometrię elementu.
Oblicz DOMMatrix, który mapuje obramowanie elementu w pikselach CSS na obszar roboczy w pikselach CSS. Przeglądarka musi zmapować przestrzeń współrzędnych 3D na przestrzeń współrzędnych CSS za pomocą przekształcenia widocznego obszaru. Aby to zrobić:
- Znormalizuj element HTML. Rozmiary elementów HTML są podawane w pikselach (np. szerokość 200 pikseli). WebGL zwykle traktuje obiekty jako „kwadraty jednostkowe”, na przykład w zakresie od -0,5 do 0,5. Jeśli nie znormalizujesz, przycisk o rozmiarze 200 pikseli będzie wyglądał na 200 razy większy. Ten krok odwraca też oś Y, ponieważ w CSS kierunek w dół jest dodatni, a w WebGL – w górę.
- Przekształć macierz MVP WebGL w macierz DOM.
- Mapowanie na widoczny obszar obszaru roboczego. Ten krok to faza „ponownego skalowania”: rozciąga matematykę przestrzeni jednostkowej z powrotem, aby dopasować ją do wymiarów pikseli CSS elementu
<canvas>na ekranie, i odwraca oś Y. Oblicz ostateczne przekształcenie. Pomnóż macierze w kolejności: widoczny obszar * MVP * normalizacja. Połączenie ich w jedną ostateczną transformację tworzy „mapę”, która dokładnie wskazuje przeglądarce, gdzie powinien znajdować się element HTML, aby był wyrównany z rysunkiem 3D. Przeglądarka wykonuje podział perspektywy.
// 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); }
Następnie przekaż przekształcenie do canvas.updateElementGeometry() jako canvasTransform. Spowoduje to przeniesienie elementu HTML bezpośrednio na wyrenderowane piksele. Dzięki temu, gdy użytkownik kliknie przycisk lub zaznaczy tekst, faktycznie trafi na rzeczywisty element HTML. Przeglądarka używa też przekształcenia do określenia granic ułatwień dostępu elementu, na przykład w przypadku pierścieni zaznaczenia czytnika ekranu:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
Do kontrolowania testu pozycji wskaźnika służą te opcje:
- Każde wywołanie
updateElementGeometry()przenosi element na górę kolejności testu pozycji wskaźnika. Przekaż{ preserveHitTestOrder: true }, aby utrzymać pozycję. - Wywołaj
canvas.clearElementGeometry(element), aby usunąć element z testu pozycji wskaźnika, np. gdy go ukryjesz. - Przekaż
{ canvasClip },DOMRectInitw przestrzeni współrzędnychcanvasTransform, aby ograniczyć obszar testu trafień, np. gdy rysowana jest tylko część elementu.
Przykładowy kod
<canvas id="canvas" content="drawable" style="width: 400px; height: 400px;">
<div id="ui-element" drawable>
<p>WebGL UI Element</p>
<button>Action</button>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl2");
const uiElement = document.getElementById("ui-element");
// getElementImageSize() and allocateElementTexture() are defined in step 5.
// uploadElementWebGL() is defined in step 6.
// computeCanvasTransform() is defined in step 7.
// Setup WebGL texture...
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
const textureState = { width: 0, height: 0 };
canvas.onpaint = () => {
// 1. Update texture with HTML content
try {
uploadElementWebGL(gl, canvas, texture, uiElement, textureState);
} catch (err) {
console.error('texElementSubImage2D copy failed:', err);
}
// ... Render your 3D scene here, calculating htmlElementMVP matrix ...
// 2. Sync DOM position, hit testing, and accessibility bounds with 3D scene
canvas.updateElementGeometry(uiElement, {
canvasTransform: computeCanvasTransform(canvas, uiElement, htmlElementMVP),
});
};
</script>
Sprawdzone metody
- OBOWIĄZKOWO: przed użyciem interfejsu HTML-in-Canvas API sprawdź, czy przeglądarka go obsługuje.
- OBOWIĄZKOWO: zawsze dodawaj atrybut
content="drawable"do elementu<canvas>. - OBOWIĄZKOWO: ZAWSZE dodawaj atrybut
drawabledo bezpośrednich elementów podrzędnych obszaru roboczego. - OBOWIĄZKOWO: dodaj atrybut
drawabledo każdego elementu, który chcesz rysować niezależnie od elementów nadrzędnych. - OBOWIĄZKOWO: użyj modułu obsługi zdarzeń
onpaint, aby wyrenderować zawartość HTML na elemencie canvas. - WYMAGANE: wstępnie przydziel bufor tekstury za pomocą
texImage2D()(podczas inicjowania lub zmiany rozmiaru elementu). Zmieniaj przydział tekstury tylko wtedy, gdy zmienia się rozmiar, i ustawTEXTURE_MIN_FILTERnaLINEAR. - OBOWIĄZKOWO: użyj metody
texElementSubImage2D, aby wyrenderować zawartość HTML do tekstury wstępnie przydzielonej za pomocątexImage2D()o rozmiarzecaptureElementImage(). - WYMAGANE: zsynchronizuj geometrię elementu HTML z wyrenderowaną treścią, wywołując funkcję
canvas.updateElementGeometry(element, { canvasTransform }). - OBOWIĄZKOWO: użyj
ResizeObserver, aby sprawdzić rozmiar ekranu i zaktualizować rozmiar obszaru roboczego, aby pasował do pikseli urządzenia. - NIE umieszczaj treści z innych domen w obszarze roboczym, ponieważ nie jest to obsługiwane.
- NIE inicjuj
ResizeObserverw module obsługi zdarzeńonpaint, ponieważ może to prowadzić do wycieków pamięci.
Strategie zastępcze
Interfejs HTML-in-Canvas API nie jest obecnie obsługiwany we wszystkich nowoczesnych przeglądarkach, dlatego zwykle wymagana jest strategia zastępcza.
Jednak ze względu na lepszą wydajność tego interfejsu API należy używać HTML-in-Canvas, jeśli przeglądarka go obsługuje.
Strategia zastępcza zależy od przypadku użycia. Jeśli na przykład interaktywna treść HTML w elemencie canvas nie jest obsługiwana, umieść ją nad elementem canvas za pomocą CSS.
kod polyfill HTML-in-Canvas
Użyj poniższego skryptu polyfill, aby naśladować interfejs HTML-in-Canvas API w przeglądarkach, które go nie obsługują.
Zainstaluj lub umieść bibliotekę:
# Install npm install three-html-render# Embed <script src="https://cdn.jsdelivr.net/npm/three-html-render/dist/polyfill.js"></script>Uruchom metodę
installHtmlInCanvasPolyfill(), aby przetłumaczyć HTML-in-Canvas.