تحتاج تطبيقات الويب بشكل متكرر إلى التقاط محتوى HTML غني وتصديره، مثل لوحات البيانات المخصّصة أو المستندات ذات الأنماط أو الرسوم البيانية التفاعلية، كصور ثابتة أو تسجيلات فيديو. في السابق، كان تحقيق ذلك يتطلّب استخدام مكتبات ضخمة تابعة لجهات خارجية تحلّل يدويًا عُقد DOM وخصائص CSS لإعادة إنشاء نسخة طبق الأصل مرئية على لوحة رسم. هذا النهج مكلف من الناحية الحسابية، وعرضة للأخطاء، وغالبًا ما يفشل في توفير ميزات تنسيق CSS الحديثة. باستخدام واجهة برمجة التطبيقات HTML-in-Canvas، يمكن للمطوّرين عرض عناصر DOM حقيقية مباشرةً في سياق لوحة الرسم. بما أنّ محرّك العرض الأصلي للمتصفّح يعرض شجرة HTML الفرعية بدقة عالية، فإنّ التقاط الناتج المرئي الدقيق كصورة أو بث فيديو يكون فعالاً للغاية باستخدام طرق لوحة الرسم المضمّنة، مثل toDataURL() أو toBlob() أو captureStream().
كيفية الإعداد
تحقَّق مما إذا كان المتصفّح يتيح استخدام HTML-in-Canvas:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }ابدأ اللوحة لإتاحة عرض عناصر HTML التابعة من خلال إضافة السمة
content="drawable"إلى عنصر HTML<canvas>. أضِف محتوى HTML داخل العنصر<canvas>، وأضِف السمةdrawableإلى كل عنصر ترسمه. يلتقط العنصر الذي يتضمّن السمةdrawableشجرته الفرعية، باستثناء العناصر التابعة المدمجة التي تتضمّن أيضًا السمةdrawable:<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>اضبط شبكة لوحة الرسم لتتطابق مع عامل قياس الجهاز من أجل منع التمويه:
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 على اللوحة داخل معالج الأحداث
canvas.onpaint.في سياق ثنائي الأبعاد، استخدِم طريقة
drawElementImage. وهي ترسم آخر لقطة لعنصرdrawableوشجرته الفرعية القابلة للرسم (باستثناء العناصر الثانوية الأخرى القابلة للرسم)، بدءًا من مربّع حدود العنصر، قبل تحويلات CSS:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- في سياقات WebGL وWebGPU، يمكنك نسخ العنصر إلى مادة. يمكنك تحديد حجم الزخرفة من
canvas.captureElementImage(element). تكون السمتانwidthوheightبوحدات بكسل شبكة لوحة الرسم (مخزن احتياطي)، لذا يجب تقريبهما باستخدامMath.ceil(). يتم عرضcaptureElementImage()إذا لم يتوفّر للعنصر سجلّ رسم بعد: يمكنك استدعاءrequestPaint()وإعادة المحاولة في حدثpaintالتالي.
// 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، يمكنك تخصيص دعم الزخرفة مسبقًا أولاً باستخدام طريقة
texImage2D. تستخدم المقتطفات في هذا الدليل سياق WebGL 2 (canvas.getContext("webgl2")). يمكنك تخصيص مساحة تخزين الزخرفة من خلال استدعاءtexImage2D()باستخدامnullبدلاً من بيانات وحدات البكسل، ما يؤدي إلى حجز ذاكرة وحدة معالجة الرسومات بالحجم والتنسيق المحدّدَين بدون تحميل أي وحدات بكسل. اتّبِع القواعد التالية:- اضبط حجم الزخرفة من
captureElementImage()، كما هو موضّح أعلاه. - يجب تخصيص المستوى 0 فقط عند التحميل الأول أو عند تغيير الحجم، لأنّ إعادة التخصيص تؤدي إلى محو الزخرفة. استخدِم مساحة تخزين قابلة للتغيير: لا يمكن تغيير حجم مساحة التخزين غير القابلة للتغيير
texStorage2D()عند تغيير حجم العنصر.
- اضبط حجم الزخرفة من
// 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 };
- بعد ذلك، في سياق WebGL، اعرض محتوى HTML على النسيج باستخدام طريقة
texElementSubImage2D. اتّبِع القواعد التالية:- مرِّر
{ width, height }في الإعدادات. ويؤدي ذلك إلى ملء النسخة بالبنية المخصّصة بغض النظر عن كيفية تغيير حجم لوحة الرسم. - أعِد تخصيص النسيج إذا تغيّر الحجم، كما هو موضّح أعلاه.
- مرِّر
// 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، استخدِم طريقة
drawElementImageToTexture. اتّبِع القواعد التالية:- أنشئ المادة باستخدام
COPY_DSTوRENDER_ATTACHMENT، وأضِفTEXTURE_BINDINGلأخذ عيّنات منها في أدوات التظليل. يتم الإبلاغ عن الاستخدام الخاطئ كخطأ في التحقّق من WebGPU، وليس كاستثناء. - يجب ضبط حجم الزخرفة على
size. بدونsize، يتم اقتصاص عنصر أكبر حجمًا ليتناسب مع الهيئة. باستخدامsize، يتم تغيير حجم العنصر لملئه. - أعِد إنشاء الزخرفة عند تغيير حجم العنصر، وأعِد إنشاء مجموعات الربط التي تشير إليها.
- أنشئ المادة باستخدام
// 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);
}
};
عند استخدام حلقة تكرارية requestAnimationFrame لعرض المشهد، يمكنك استدعاء canvas.requestPaint() داخل الحلقة لضمان عرض محتوى HTML على اللوحة. تعرض السمة event.changedElements العناصر التي تغيّر عرضها. تأكَّد من إعادة رسم العنصر فقط أو إعادة تحميل بنيته في حال تم تعديل عناصر HTML التابعة:
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
};
- مزامنة هندسة العناصر
يحتاج المتصفّح إلى معرفة مكان رسم عنصر HTML، لكي يتطابق موضعه في نموذج DOM مع وحدات البكسل المرسومة، على سبيل المثال، لكي يتمكّن المستخدمون من النقر عليه والكتابة فيه. يختبر عنصر canvas مؤشر الماوس فقط على العناصر التي يتم رسمها باستخدام طريقة drawElementImage() ثنائية الأبعاد أو المسجّلة باستخدام updateElementGeometry().
بالنسبة إلى حالة السياق الثنائي الأبعاد، يزامن المتصفّح العنصر تلقائيًا. تتم محاذاة حدود اختبار مؤشر الماوس في نموذج DOM وحلقات تركيز قارئ الشاشة مع الإحداثيات المرسومة في
drawElementImage()، لذا لا حاجة إلى ضبطstyle.transform. لإدارة الشكل الهندسي يدويًا، على سبيل المثال، عند رسم العنصر نفسه أكثر من مرة، يمكنك تمرير{ preserveElementGeometry: true }إلىdrawElementImage()، وتطبيق عمليات التحويل باستخدامcanvas.updateElementGeometry().بالنسبة إلى الحالة الثلاثية الأبعاد مع WebGL أو WebGPU، لا يمكن للوحة الرسم معرفة مكان رسم عنصر HTML، لذا عليك إخبارها بذلك. احتسِب
DOMMatrixيربط مربّع حدود العنصر، بوحدات بكسل CSS، بلوحة العرض، بوحدات بكسل CSS. يحتاج المتصفّح إلى الربط بين مساحة الإحداثيات الثلاثية الأبعاد ومساحة إحداثيات CSS باستخدام تحويل إطار العرض. لتسهيل ذلك، اتّبِع الخطوات التالية:- تطبيع عنصر HTML. يتم تحديد حجم عناصر HTML بالبكسل (على سبيل المثال، عرض 200 بكسل). ومع ذلك، يتعامل WebGL عادةً مع العناصر على أنّها "مربّعات وحدات"، على سبيل المثال، تتراوح من -0.5 إلى 0.5. إذا لم تقم بالتسوية، فسيظهر الزر الذي يبلغ حجمه 200 بكسل أكبر بمقدار 200 مرة. تعمل هذه الخطوة أيضًا على قلب المحور Y، لأنّه في CSS، يكون الاتجاه لأسفل موجبًا، ولكن في WebGL، يكون الاتجاه لأعلى موجبًا.
- تحويل مصفوفة WebGL MVP إلى مصفوفة DOM
- الربط بإطار عرض اللوحة هذه الخطوة هي مرحلة "إعادة تغيير الحجم": فهي تمدد حسابات مساحة الوحدة مرة أخرى لتتطابق مع أبعاد بكسل CSS لعنصر
<canvas>على الشاشة، وتقلب المحور Y مرة أخرى. - احسب التحويل النهائي. اضرب المصفوفات بالترتيب: إطار العرض * MVP * التسوية. وعند دمجها في تحويل نهائي واحد، يتم إنشاء "خريطة" تخبر المتصفّح بالمكان الذي يجب أن يظهر فيه عنصر HTML بالضبط ليتوافق مع الرسم الثلاثي الأبعاد. ينفّذ المتصفّح عملية القسمة المنظورية.
// 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); }
بعد ذلك، مرِّر التحويل إلى canvas.updateElementGeometry() على أنّه canvasTransform. يؤدي ذلك إلى نقل عنصر HTML ليكون فوق وحدات البكسل المعروضة مباشرةً. يضمن ذلك أنّه عندما ينقر المستخدم على زر أو يختار نصًا، فإنّه ينقر فعليًا على عنصر HTML الحقيقي. يستخدم المتصفّح أيضًا التحويل لحدود تسهيل الاستخدام للعنصر، على سبيل المثال، لحلقات تركيز قارئ الشاشة:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
استخدِم هذه الخيارات للتحكّم في اختبار مؤشر الماوس:
- يؤدي كل استدعاء لـ
updateElementGeometry()إلى نقل العنصر إلى أعلى ترتيب اختبار مؤشر الماوس. تجاوز "{ preserveHitTestOrder: true }" للحفاظ على مركزه. - يمكنك استدعاء
canvas.clearElementGeometry(element)لإزالة عنصر من اختبار مؤشر الماوس، على سبيل المثال، عند إخفائه.
- استخدِم طرق التصدير العادية في Canvas، مثل
toDataURL()أوtoBlob()أوcaptureStream(). ستتضمّن البيانات التي يتم تصديرها محتوى HTML المعروض.
مثال على الرمز
<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>
أفضل الممارسات
- إجراء إلزامي: تحقَّق من توافق المتصفّح مع واجهة HTML-in-Canvas API قبل استخدامها.
- إلزامي: أضِف دائمًا السمة
content="drawable"إلى العنصر<canvas>. - إجراء إلزامي: أضِف السمة
drawableإلى كل عنصر ترسمه، بما في ذلك العناصر الفرعية المباشرة للّوحة. - إلزامي: استخدِم معالج أحداث
onpaintلعرض محتوى HTML على اللوحة. - إجراء إلزامي: استخدِم طريقة
drawElementImageفي البُعد الثنائي أوdrawElementImageToTextureفي WebGPU لعرض محتوى HTML على اللوحة. - إجراء إلزامي: في WebGL، يجب تخصيص مخزن مؤقت للزخرفة مسبقًا باستخدام
texImage2D()مرة واحدة (عند الإعداد أو عند تغيير حجم العنصر). يجب إعادة تخصيص الزخرفة فقط عند تغيير الحجم، وضبطTEXTURE_MIN_FILTERعلىLINEAR. - إلزامي: في WebGL، استخدِم طريقة
texElementSubImage2Dلعرض محتوى HTML في مادة يتم تخصيصها مسبقًا باستخدامtexImage2D()، بحجم منcaptureElementImage(). - إجراء إلزامي: في WebGPU، يجب تحديد حجم النسيج من
captureElementImage()، وإعادة إنشائه فقط عند تغيير الحجم. أنشئ النسيج باستخدامCOPY_DSTوRENDER_ATTACHMENT، وأدخِل حجم النسيج على أنّهsize. - إجراء إلزامي: يجب مزامنة هندسة عنصر HTML مع المحتوى المعروض. في الوضع الثنائي الأبعاد، يتم تنفيذ ذلك تلقائيًا من خلال "
drawElementImage". في WebGL وWebGPU، يمكنك استدعاءcanvas.updateElementGeometry(element, { canvasTransform }). - إجراء إلزامي: استخدِم
ResizeObserverلمراقبة حجم الشاشة وتعديل حجم اللوحة ليتطابق مع وحدات بكسل الجهاز. - لا تضمِّن محتوى متعدّد المصادر في لوحة رسم، لأنّ ذلك غير متاح.
- لا تبدأ
ResizeObserverضمن معالِج الأحداثonpaint، لأنّ ذلك قد يؤدي إلى تسرُّب الذاكرة.
استراتيجيات احتياطية
لا تتوفّر واجهة HTML-in-Canvas API حاليًا في جميع المتصفّحات الحديثة، لذا عادةً ما تكون هناك حاجة إلى استراتيجية احتياطية. ومع ذلك، نظرًا لمزايا الأداء المحسّنة لواجهة برمجة التطبيقات هذه، يجب استخدام HTML-in-Canvas إذا كان المتصفّح يتيح ذلك.
في حال الحاجة إلى تصدير محتوى HTML من لوحة رسم، استخدِم مكتبات مثل html2canvas أو dom-to-image أو snapdom.
لتسجيل تفاعلات HTML إطارًا تلو الآخر، على سبيل المثال، للبث، يمكنك تسجيل تغييرات نموذج DOM باستخدام مكتبات مثل rrweb.
بدلاً من ذلك، يمكنك تنفيذ تحذير يفيد بأنّ تصدير وسائط HTML غير متاح في المتصفّح لأنّه لا يتيح استخدام HTML-in-Canvas.