Web uygulamalarının genellikle özelleştirilmiş kontrol panelleri, stilli dokümanlar veya etkileşimli grafikler gibi zengin HTML içeriklerini yakalayıp statik resimler ya da video kayıtları olarak dışa aktarması gerekir. Geçmişte bunu başarmak için DOM düğümlerini ve CSS özelliklerini manuel olarak ayrıştırarak tuval üzerinde görsel bir faksı yeniden oluşturan büyük üçüncü taraf kitaplıkları gerekiyordu. Bu yaklaşım, hesaplama açısından maliyetlidir, hataya açıktır ve genellikle modern CSS düzeni özelliklerini desteklemez. HTML-in-Canvas API ile geliştiriciler, gerçek DOM öğelerini doğrudan tuval bağlamında oluşturabilir. Tarayıcının yerel oluşturma motoru, HTML alt ağacını piksel düzeyinde doğrulukla boyadığından, tam görsel çıktıyı resim veya video akışı olarak yakalamak, toDataURL(), toBlob() veya captureStream() gibi yerleşik tuval yöntemleri kullanılarak son derece verimli bir şekilde yapılabilir.
Nasıl uygulanır?
Tarayıcıda HTML-in-Canvas'ın desteklenip desteklenmediğini kontrol edin:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }<canvas>HTML öğesinecontent="drawable"özelliğini ekleyerek alt HTML öğelerinin oluşturulmasını desteklemek için tuvali ilk kullanıma hazırlayın. HTML içeriğinizi<canvas>öğesinin içine yerleştirin ve çizdiğiniz her öğeyedrawableözelliğini ekleyin.drawableözelliğine sahip bir öğe, kendisi dedrawableolan iç içe yerleştirilmiş alt öğeler hariç olmak üzere alt ağacını yakalar:<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>Bulanıklığı önlemek için tuval ızgaranızı cihazın ölçeklendirme faktörüne uyacak şekilde ölçeklendirin:
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);HTML içeriğini, bir
canvas.onpaintetkinlik işleyicinin içindeki tuvalde oluşturun.2D bağlamda
drawElementImageyöntemini kullanın. CSS dönüşümlerinden önce, öğenin kenarlık kutusundan başlayarak birdrawableöğesinin son anlık görüntüsünü ve çizilebilir alt ağacını (diğer çizilebilir alt öğeler hariç) çizer:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- WebGL ve WebGPU bağlamlarında öğeyi bir dokuya kopyalarsınız. Dokuyu
canvas.captureElementImage(element)olarak boyutlandırın.widthveheightdeğerleri, tuval ızgarası (yedekleme deposu) piksel cinsinden olduğundanMath.ceil()ile yukarı yuvarlayın. Öğenin henüz boyama kaydı yoksacaptureElementImage()hata verir:requestPaint()yöntemini çağırın ve bir sonrakipaintetkinliğinde tekrar deneyin.
// 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;
}
- WebGL bağlamında, önce
texImage2Dyöntemini kullanarak doku desteğini önceden tahsis edin. Bu kılavuzdaki snippet'ler WebGL 2 bağlamını (canvas.getContext("webgl2")) kullanır. Piksel verileri yerinenulliletexImage2D()yöntemini çağırarak doku depolama alanı ayırın. Bu yöntem, piksel yüklemeden belirli boyut ve biçimdeki GPU belleğini ayırır. Aşağıdaki kurallara uyun:- Yukarıda gösterildiği gibi dokuyu
captureElementImage()boyutuna getirin. - Yeniden tahsis etme işlemi dokuyu temizlediğinden, 0. seviyeyi yalnızca ilk yüklemede veya boyut değiştiğinde tahsis edin. Değişebilir depolama alanı kullanın: Değişmez
texStorage2D()depolama alanı, öğenin boyutu değiştiğinde yeniden boyutlandırılamaz.
- Yukarıda gösterildiği gibi dokuyu
// 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 };
- Ardından, WebGL bağlamında
texElementSubImage2Dyöntemini kullanarak HTML içeriğini dokuya dönüştürün. Aşağıdaki kurallara uyun:- Yapılandırmada
{ width, height }değerini iletin. Tuvalin nasıl ölçeklendirildiğine bakılmaksızın kopyanın ayrılan dokuyu doldurmasını sağlar. - Yukarıda gösterildiği gibi, boyut değiştiyse dokuyu yeniden tahsis edin.
- Yapılandırmada
// 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);
}
};
- WebGPU bağlamında
drawElementImageToTextureyöntemini kullanın. Aşağıdaki kurallara uyun:- Dokuyu
COPY_DSTveRENDER_ATTACHMENTkullanımıyla oluşturun ve gölgelendiricilerde örneklemek içinTEXTURE_BINDINGekleyin. Yanlış kullanım, istisna olarak değil, WebGPU doğrulama hatası olarak bildirilir. - Doku boyutunu
sizeolarak iletin.sizeolmadan, daha büyük bir öğe dokuya kırpılır.sizeile öğe, doldurulacak şekilde ölçeklendirilir. - Öğe boyutu değiştiğinde dokuyu yeniden oluşturun ve dokuya referans veren bağlama gruplarını yeniden oluşturun.
- Dokuyu
// 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);
}
};
Sahneyi oluşturmak için requestAnimationFrame döngüsü kullanırken HTML içeriğinin tuvalde oluşturulmasını sağlamak için döngü içinde canvas.requestPaint() yöntemini çağırın. event.changedElements, oluşturma işlemi değişen öğeleri listeler. Alt HTML öğelerinde bir güncelleme yapıldıysa yalnızca öğeyi yeniden çizdiğinizden veya dokusunu yeniden yüklediğinizden emin olun:
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
};
- Öğe geometrisini senkronize edin.
Tarayıcının, HTML öğesinin nerede çizildiğini bilmesi gerekir. Böylece DOM konumu, çizilen pikselleriyle eşleşir ve kullanıcılar öğeyi tıklayıp içine yazabilir. Tuval, yalnızca 2D drawElementImage() yöntemiyle çizilen veya updateElementGeometry() ile kaydedilen öğeleri isabet testine tabi tutar.
2D bağlam durumunda tarayıcı, öğeyi otomatik olarak senkronize eder.
drawElementImage(), DOM isabet testi sınırlarını ve ekran okuyucu odak halkalarını çizilen koordinatlarla hizalar. Bu nedenlestyle.transformdeğerini ayarlamanız gerekmez. Geometriyi manuel olarak yönetmek için (örneğin, aynı öğeyi birden fazla kez çizdiğinizde){ preserveElementGeometry: true }değerinidrawElementImage()değerine iletin vecanvas.updateElementGeometry()ile dönüşümleri uygulayın.WebGL veya WebGPU ile 3D durumunda, tuval HTML öğesinin nerede çizildiğini anlayamaz. Bu nedenle, bunu sizin belirtmeniz gerekir. Öğenin kenarlık kutusunu CSS pikseli cinsinden tuvale (CSS pikseli cinsinden) eşleyen bir
DOMMatrixhesaplayın. Tarayıcının, görüntü alanı dönüşümü kullanarak 3D koordinat alanından CSS koordinat alanına eşleme yapması gerekir. Bunu kolaylaştırmak için aşağıdakileri yapın:- HTML öğesini normalleştirin. HTML öğeleri piksel cinsinden boyutlandırılır (ör. 200 piksel genişlik). Ancak WebGL, nesneleri genellikle "birim kareler" olarak ele alır (ör. -0,5 ile 0,5 arasında). Normalleştirme yapmazsanız 200 piksellik düğmeniz 200 kat daha büyük görünür. Bu adımda Y ekseni de ters çevrilir. CSS'de aşağı yön pozitifken WebGL'de yukarı yön pozitiftir.
- WebGL MVP matrisini DOM matrisine dönüştürün.
- Tuval görüntü alanıyla eşleştirin. Bu adım, "yeniden ölçeklendirme" aşamasıdır: Bu birim uzay matematiğini ekrandaki
<canvas>öğenizin CSS piksel boyutlarıyla eşleşecek şekilde yeniden genişletir ve Y eksenini geri çevirir. - Nihai dönüşümü hesaplayın. Matrisleri şu sırayla çarpın: Görüntü alanı * MVP * Normalleştirme. Bunları tek bir nihai dönüşümde birleştirmek, tarayıcıya HTML öğesinin 3D çizimle hizalanması için tam olarak nerede bulunması gerektiğini söyleyen bir "harita" oluşturur. Tarayıcı, perspektif bölme işlemini gerçekleştirir.
// 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); }
Ardından dönüşümü canvas.updateElementGeometry() öğesine canvasTransform olarak iletin. Bu işlem, HTML öğesini doğrudan oluşturulan piksellerinin üzerine taşır. Bu sayede, kullanıcı bir düğmeyi tıkladığında veya metin seçtiğinde gerçek HTML öğesine ulaşır. Tarayıcı, öğenin erişilebilirlik sınırları için de dönüşümü kullanır (ör. ekran okuyucu odak halkaları için):
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
İsabet testini kontrol etmek için aşağıdaki seçenekleri kullanın:
- Her
updateElementGeometry()çağrısı, öğeyi isabet testi sırasının en üstüne taşır.{ preserveHitTestOrder: true }kartı, konumunu koruyacak. - Bir öğeyi isabet testinden kaldırmak için (ör. gizlediğinizde)
canvas.clearElementGeometry(element)yöntemini çağırın.
toDataURL(),toBlob()veyacaptureStream()gibi normal tuval dışa aktarma yöntemlerini kullanın. Dışa aktarılan veriler, oluşturulan HTML içeriğini içerir.
Örnek kod
<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>
En İyi Uygulamalar
- ZORUNLU: HTML-in-Canvas API'yi kullanmadan önce tarayıcı desteğini kontrol edin.
- ZORUNLU:
<canvas>öğesine her zamancontent="drawable"özelliğini ekleyin. - ZORUNLU: Tuvalin doğrudan alt öğeleri de dahil olmak üzere çizdiğiniz her öğeye
drawableözelliğini ekleyin. - ZORUNLU: HTML içeriğini tuvalde oluşturmak için bir
onpaintetkinlik işleyici kullanın. - ZORUNLU: HTML içeriğini tuvalde oluşturmak için 2D'de
drawElementImageyöntemini veya WebGPU'dadrawElementImageToTextureyöntemini kullanın. - ZORUNLU: WebGL'de, doku arabelleğini
texImage2D()ile bir kez önceden tahsis edin (başlatıldığında veya öğe boyutu değiştiğinde). Dokuyu yalnızca boyut değiştiğinde yeniden tahsis edin veTEXTURE_MIN_FILTERdeğeriniLINEARolarak ayarlayın. - ZORUNLU: WebGL'de, HTML içeriğini
texImage2D()ile önceden tahsis ettiğiniz vecaptureElementImage()boyutunda bir dokuya dönüştürmek içintexElementSubImage2Dyöntemini kullanın. - ZORUNLU: WebGPU'da dokuyu
captureElementImage()boyutuna getirin ve yalnızca boyut değiştiğinde yeniden oluşturun. DokuyuCOPY_DSTveRENDER_ATTACHMENTkullanımıyla oluşturun ve doku boyutunusizeolarak iletin. - ZORUNLU: HTML öğesinin geometrisini, oluşturulan içerikle senkronize edin. 2D'de
drawElementImagebunu otomatik olarak yapar. WebGL ve WebGPU'dacanvas.updateElementGeometry(element, { canvasTransform })yöntemini çağırın. - ZORUNLU: Ekran boyutunu gözlemlemek ve tuval boyutunu cihaz pikselleriyle eşleşecek şekilde güncellemek için
ResizeObserverkullanın. - Desteklenmediği için çapraz kaynaklı içerikleri tuvale YERLEŞTİRMEYİN.
- Bellek sızıntılarına yol açabileceğinden
ResizeObserveröğesinionpaintetkinlik işleyicisinde ilk kullanıma hazırlamayın.
Yedek stratejiler
HTML-in-Canvas API şu anda tüm modern tarayıcılarda desteklenmediğinden genellikle bir yedek strateji gerekir. Ancak bu API'nin sağladığı performans avantajları göz önüne alındığında, tarayıcı destekliyorsa HTML-in-Canvas kullanılmalıdır.
HTML içeriğinin bir tuvalden dışa aktarılması gereken kullanım alanları için html2canvas, dom-to-image veya snapdom gibi kitaplıkları kullanın.
HTML etkileşimlerini kare kare yakalamak için (ör. akış için) rrweb gibi kitaplıkları kullanarak DOM mutasyonlarını yakalayın.
Alternatif olarak, tarayıcıda HTML-in-Canvas desteklenmediği için HTML medya dışa aktarma işleminin desteklenmediğini belirten bir uyarı uygulayın.