تتيح واجهة HTML-in-Canvas API عرض نموذج DOM حقيقي مباشرةً داخل عنصر لوحة الرسم. عند تطبيق السمة content="drawable" على سياقات العرض الثلاثي الأبعاد، مثل WebGL أو WebGPU أو Three.js، وإضافة السمة drawable إلى عناصر HTML الفرعية، يمكن عرض هذه العناصر بسلاسة في المشهد الثلاثي الأبعاد. والأهم من ذلك، بما أنّ عناصر HTML تظل جزءًا من شجرة تنسيق DOM النشطة، فإنّها تحتفظ بالتفاعل الكامل، ما يتيح للمستخدمين النقر على الأزرار واختيار النص وتفعيل حالات التركيز بشكلٍ أصلي بدون الحاجة إلى استخدام تقنية Raycasting المعقّدة أو معالجة الأحداث المخصّصة.
كيفية الإعداد
WebGL وWebGPU
عند استخدام WebGL أو WebGPU، اتّبِع الخطوات التالية:
تحقَّق مما إذا كان المتصفّح يتيح استخدام HTML-in-Canvas:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }ابدأ بإعداد
<canvas>لإتاحة عناصر 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. لا يمكن تحميل عنصر إلا بعد أن ترسمه اللوحة: يمكنك استدعاءcanvas.requestPaint()لطلب حدثpaint.
يمكنك تحديد حجم الزخرفة من 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، يتم تغيير حجم العنصر لملئه. - لا يمكن تغيير حجم زخارف WebGPU. أعِد إنشاء الزخرفة عند تغيير حجم العنصر، وأعِد إنشاء مجموعات الربط التي تشير إليها.
- أنشئ المادة باستخدام
// WebGPU textures can't be resized. 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) {
// Update the texture as shown above
}
// Render the scene, and sync the element geometry as shown in step 5
};
- مزامنة هندسة العناصر
احتسِب 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)لإزالة عنصر من اختبار مؤشر الماوس، على سبيل المثال، عند إخفائه.
Three.js
تحقَّق مما إذا كان المتصفّح يتيح استخدام HTML-in-Canvas:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }أنشئ شكل هندسي ومادة مخصّصَين لمحتوى HTML.
مرِّر عنصر DOM إلى THREE.HTMLTexture. يضيف المعالج العنصر إلى لوحة العرض، ويضبط السمات المطلوبة:
material.map = new THREE.HTMLTexture(element); mesh = new THREE.Mesh( geometry, material ); scene.add( mesh );سجِّل الشبكة باستخدام
InteractionManagerلجعل عنصر HTML تفاعليًا. ويحافظ على محاذاة العنصر مع الشبكة، ويزامن هندسة العنصر مع اللوحة، بحيث تصل النقرات وتحديد النص إلى عنصر HTML الحقيقي. استدعِinteractions.update()في حلقة الحركة قبل العرض:
import { InteractionManager } from 'three/addons/interaction/InteractionManager.js';
const interactions = new InteractionManager();
interactions.connect( renderer, camera );
interactions.add( mesh );
// In the animation loop
interactions.update();
renderer.render( scene, camera );
مثال على الرمز
لوحة WebGL
<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(), allocateElementTexture(), and uploadElementWebGL()
// are defined in step 4.
// computeCanvasTransform() is defined in step 5.
// 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>
لوحة WebGPU
<canvas id="canvas" content="drawable" style="width: 400px; height: 400px;">
<div id="ui-element" drawable>
<p>WebGPU UI Element</p>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const context = canvas.getContext("webgpu");
const uiElement = document.getElementById("ui-element");
// getElementImageSize() and ensureElementTexture() are defined in step 4.
// computeCanvasTransform() is defined in step 5.
// Setup WebGPU...
// const device = ...
const textureState = { texture: null };
canvas.onpaint = () => {
// 1. Copy HTML content to a texture that matches the element size
const size = getElementImageSize(canvas, uiElement);
if (size) {
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);
}
}
// ... Render your 3D scene here, calculating htmlElementMVP matrix ...
// 2. Sync DOM position, hit testing, and accessibility bounds (same matrix math as WebGL)
canvas.updateElementGeometry(uiElement, {
canvasTransform: computeCanvasTransform(canvas, uiElement, htmlElementMVP),
});
};
</script>
Three.js
// 1. Initialize Three.js camera, scene, renderer, mesh, interactions;
// 2. Ensure HTML-in-Canvas feature support
if (!('content' in HTMLCanvasElement.prototype)) {
// Use a fallback strategy
}
// 3. Initialize the source HTML DOM element
const element = document.createElement('div');
element.innerHTML = '<h1>Hello World</h1>';
// 4. Create geometry and material
const geometry = new RoundedBoxGeometry( 100, 100, 100, 10, 10 );
const material = new THREE.MeshStandardMaterial( { roughness: 0, metalness: 0.5 } );
// 5. Pass the DOM element into THREE.HTMLTexture
material.map = new THREE.HTMLTexture(element);
mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
// 6. Make the HTML element interactive
interactions = new InteractionManager();
interactions.connect( renderer, camera );
interactions.add( mesh );
// 7. Render Loop
function animate() {
// Keep the HTML element aligned with the mesh, so it receives input
interactions.update();
renderer.render(scene, camera);
}
أفضل الممارسات
- إجراء إلزامي: تحقَّق من توافق المتصفّح مع واجهة HTML-in-Canvas API قبل استخدامها.
- إلزامي: عند استخدام WebGL أو WebGPU، أضِف دائمًا السمة
content="drawable"إلى العنصر<canvas>. - إلزامي: عند استخدام WebGL أو WebGPU، أضِف السمة
drawableإلى كل عنصر ترسمه، بما في ذلك العناصر الفرعية المباشرة للوحة الرسم. - إلزامي: عند استخدام WebGL أو WebGPU، استخدِم معالج أحداث
onpaintلعرض محتوى HTML على اللوحة. - إلزامي: عند استخدام WebGL، يجب تخصيص مخزن النسيج المؤقت مسبقًا باستخدام
texImage2D()مرة واحدة (عند الإعداد أو عند تغيير حجم العنصر). يجب إعادة تخصيص الزخرفة فقط عند تغيير الحجم، وضبطTEXTURE_MIN_FILTERعلىLINEAR. - إلزامي: عند استخدام WebGL، استخدِم طريقة
texElementSubImage2Dلعرض محتوى HTML على مادة تحدّدها مسبقًا باستخدامtexImage2D()، بحجم منcaptureElementImage(). - إلزامي: عند استخدام WebGPU، استخدِم طريقة
drawElementImageToTextureلعرض محتوى HTML على اللوحة. يمكنك تحديد حجم الزخرفة منcaptureElementImage()، وإعادة إنشائها فقط عند تغيير الحجم. أنشئ النسيج باستخدامCOPY_DSTوRENDER_ATTACHMENT، وأدخِل حجم النسيج على أنّهsize. - إجراء إلزامي: عند استخدام WebGL أو WebGPU، يجب مزامنة شكل عنصر HTML مع المحتوى المعروض من خلال استدعاء
canvas.updateElementGeometry(element, { canvasTransform }). - إلزامي: عند استخدام Three.js، يجب تسجيل الشبكة باستخدام
InteractionManagerواستدعاءinteractions.update()في حلقة الصور المتحركة، لكي يتلقّى عنصر HTML الإدخال. - إلزامي: يجب مراقبة حجم الشاشة وتعديل حجم لوحة الرسم ليتطابق مع وحدات بكسل الجهاز، على سبيل المثال، باستخدام
ResizeObserver. - لا تضمِّن محتوى متعدّد المصادر في لوحة رسم، لأنّ ذلك غير متاح.
- لا تبدأ
ResizeObserverضمن معالِج الأحداثonpaint، لأنّ ذلك قد يؤدي إلى تسرُّب الذاكرة.
استراتيجيات احتياطية
لا تتوفّر واجهة HTML-in-Canvas API حاليًا في جميع المتصفّحات الحديثة، لذا عادةً ما تكون هناك حاجة إلى استراتيجية احتياطية.
ومع ذلك، نظرًا لمزايا الأداء المحسّنة لواجهة برمجة التطبيقات هذه، يجب استخدام HTML-in-Canvas إذا كان المتصفّح يتيح ذلك.
تعتمد استراتيجية الاحتياط على حالة الاستخدام. على سبيل المثال، بالنسبة إلى محتوى HTML التفاعلي في لوحة الرسم، إذا لم يكن HTML-in-Canvas متوافقًا، يمكنك وضع محتوى HTML في أعلى لوحة الرسم باستخدام CSS.
HTML-in-Canvas polyfill
استخدِم نص polyfill البرمجي التالي لمحاكاة واجهة برمجة التطبيقات HTML-in-Canvas في المتصفحات التي لا تتيح استخدامها.
ثبِّت المكتبة أو ضمِّنها:
# Install npm install three-html-render# Embed <script src="https://cdn.jsdelivr.net/npm/three-html-render/dist/polyfill.js"></script>شغِّل طريقة
installHtmlInCanvasPolyfill()لترجمة HTML-in-Canvas.