توفّر برامج تظليل WebGL تأثيرات مرئية فعّالة ومسرَّعة بواسطة وحدة معالجة الرسومات، ما يتيح إمكانات متقدّمة مثل تشويهات التموّج الديناميكية ونماذج الإضاءة وتدرّج الألوان وتحويلات الرؤوس المخصّصة. تتيح واجهة HTML-in-Canvas API للمطوّرين تطبيق زخارف WebGL على محتوى HTML. يتيح ذلك تطبيق برامج تظليل الأجزاء والرؤوس العالية الأداء بشكلٍ أصلي على مكوّنات واجهة المستخدم التفاعلية بالكامل، مثل الأزرار وحقول الإدخال والنصوص المنسّقة، مع الاحتفاظ بميزات تسهيل الاستخدام الأصلية وتحديد النص ومعالجة أحداث DOM.
كيفية الإعداد
تحقَّق مما إذا كان المتصفّح يتيح استخدام HTML-in-Canvas:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }أضِف السمة
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);يجب تخصيص خلفية النسيج مسبقًا باستخدام طريقة
texImage2D. تستخدم المقتطفات في هذا الدليل سياق WebGL 2 (canvas.getContext("webgl2")).
خصِّص مساحة تخزين الزخرفة أولاً من خلال استدعاء texImage2D() باستخدام null بدلاً من بيانات وحدات البكسل، ما يؤدي إلى حجز ذاكرة وحدة معالجة الرسومات بالحجم والتنسيق المحدّدَين بدون تحميل أي وحدات بكسل. اتّبِع القواعد التالية:
- يمكنك تحديد حجم الزخرفة من
canvas.captureElementImage(element). تكون السمتانwidthوheightبوحدات بكسل شبكة لوحة الرسم (مخزن احتياطي)، لذا يجب تقريبهما باستخدامMath.ceil(). يتم عرضcaptureElementImage()إذا لم يتوفّر للعنصر سجلّ رسم بعد: يمكنك استدعاءrequestPaint()وإعادة المحاولة في حدثpaintالتالي. - يجب تخصيص المستوى 0 فقط عند التحميل الأول أو عند تغيير الحجم، لأنّ إعادة التخصيص تؤدي إلى محو الزخرفة. استخدِم مساحة تخزين قابلة للتغيير: لا يمكن تغيير حجم مساحة التخزين غير القابلة للتغيير
texStorage2D()عند تغيير حجم العنصر.
// 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 };
اعرض محتوى HTML على النسيج داخل معالج أحداث
canvas.onpaintباستخدام طريقةtexElementSubImage2D. اتّبِع القواعد التالية:- لا يمكن تحميل عنصر إلا بعد أن ترسمه لوحة الرسم. يمكنك تحميلها من حدث "
paint"، والاتصال بالرقم canvas.requestPaint()لطلب واحدة. - مرِّر
{ width, height }في الإعدادات. ويؤدي ذلك إلى ملء النسخة بالبنية المخصّصة بغض النظر عن كيفية تغيير حجم لوحة الرسم. - أعِد تخصيص الزخرفة إذا تغيّر الحجم، كما هو موضّح في الخطوة 5.
- لا يمكن تحميل عنصر إلا بعد أن ترسمه لوحة الرسم. يمكنك تحميلها من حدث "
// 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);
}
};
عند استخدام حلقة تكرارية 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 with uploadElementWebGL()
}
// Render the scene, and sync the element geometry as shown in step 7
};
- مزامنة هندسة العناصر
احتسِب 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)لإزالة عنصر من اختبار مؤشر الماوس، على سبيل المثال، عند إخفائه. - يجب تمرير
{ canvasClip }، وهوDOMRectInitفي مساحة الإحداثيات فيcanvasTransform، للحدّ من المنطقة القابلة للاختبار، على سبيل المثال، عند رسم جزء فقط من العنصر.
مثال على الرمز
<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>
أفضل الممارسات
- إجراء إلزامي: تحقَّق من توافق المتصفّح مع واجهة HTML-in-Canvas API قبل استخدامها.
- إلزامي: أضِف دائمًا السمة
content="drawable"إلى العنصر<canvas>. - إلزامي: أضِف دائمًا السمة
drawableإلى العناصر الفرعية المباشرة للّوحة. - إجراء إلزامي: أضِف السمة
drawableإلى كل عنصر تريد أن تتمكّن من رسمه بشكل مستقل عن عناصره الرئيسية. - إلزامي: استخدِم معالج أحداث
onpaintلعرض محتوى HTML على اللوحة. - إلزامي: يجب تخصيص مخزن النسيج المؤقت مسبقًا باستخدام
texImage2D()مرة واحدة (عند الإعداد أو عند تغيير حجم العنصر). يجب إعادة تخصيص الزخرفة فقط عند تغيير الحجم، وضبطTEXTURE_MIN_FILTERعلىLINEAR. - إلزامي: استخدِم طريقة
texElementSubImage2Dلعرض محتوى HTML على مادة يتم تخصيصها مسبقًا باستخدامtexImage2D()، بحجم منcaptureElementImage(). - إجراء إلزامي: عليك مزامنة شكل عنصر HTML مع المحتوى المعروض من خلال استدعاء
canvas.updateElementGeometry(element, { canvasTransform }). - إجراء إلزامي: استخدِم
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.