HTML-in-Canvas: عرض عناصر DOM حقيقية داخل Canvas باستخدام واجهة drawElement الجديدة

منصة الويب اكتسبت قوة خارقة. مقترح WICG HTML-in-Canvas — المتاح الآن خلف علم في Chromium — يتيح لك عرض عناصر HTML حقيقية وتفاعلية مباشرة داخل <canvas>. نماذج، نصوص، لوحات معلومات، أدوات واجهة مستخدم كاملة — مرسومة في سياقات 2D أو WebGL مع الحفاظ على إمكانية الوصول الكاملة لـ DOM.
لا مزيد من حيل html2canvas. لا مزيد من حلول foreignObject SVG. عرض HTML أصلي من الدرجة الأولى داخل canvas.
في هذا الدليل، ستبني 4 عروض توضيحية تُظهر ما تجعله هذه الواجهة ممكنًا — وتتعلم البنية المعمارية خلفها.
المتطلبات الأساسية
- Node.js 18+
- Next.js 14+ (App Router)
- Chrome Canary مع تفعيل
chrome://flags/#canvas-draw-element - معرفة أساسية بواجهة Canvas API
المشكلة التي تحلها
حتى الآن، كان عرض محتوى HTML داخل canvas يتطلب حلولاً بديلة مؤلمة:
| الطريقة | القيود |
|---|---|
html2canvas | يعيد عرض DOM كصورة نقطية — لا تفاعلية، عرض غير دقيق |
foreignObject في SVG | canvas ملوث، مشاكل CORS، لا دعم لـ WebGL |
| رسم Canvas يدوي | تفقد كل CSS والوصولية والتدويل وتنسيق النص |
| نص WebGL | يحتاج أطالس خطوط، لا تنسيقات معقدة، عبء ضخم |
واجهة HTML-in-Canvas تحل كل هذا بالسماح لمحرك عرض المتصفح برسم عناصر DOM في سياق canvas.
الواجهة: ثلاث بدائيات
1. سمة layoutsubtree
<canvas layoutsubtree width="800" height="600">
<!-- هذه العناصر الأبناء هي عناصر DOM حقيقية -->
<div class="card">
<h2>أنا عنوان حقيقي</h2>
<button>أنا زر حقيقي</button>
</div>
</canvas>سمة layoutsubtree تخبر المتصفح: "رتّب أبنائي كعناصر DOM عادية، لكن اجعلهم أيضًا متاحين للرسم في canvas." الأبناء يحصلون على:
- سياق تراص
- سلوك كتلة حاوية
- احتواء رسم
- اختبار نقر كامل وإمكانية وصول
2. drawElement() لـ Canvas ثنائي الأبعاد
const ctx = canvas.getContext('2d');
const transform = ctx.drawElement(childElement, x, y, { width, height });
childElement.style.transform = transform;هذا يرسم العنصر الابن — بما في ذلك جميع أنماط CSS والعناصر الزائفة والظلال والتنسيق — في canvas عند الموضع (x, y). التحويل المُعاد يوائم موضع DOM مع الموضع المرسوم ليعمل التعامل مع الأحداث بشكل صحيح.
3. texElement2D() لـ WebGL
const gl = canvas.getContext('webgl2');
gl.texElement2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, childElement);هذا يرفع عرض العنصر كنسيج WebGL — مما يتيح تحويلات ثلاثية الأبعاد وإضاءة ومعالجة لاحقة وتأثيرات shader على محتوى HTML حقيقي.
الإعداد
npx create-next-app@latest html-in-canvas-demo --typescript --tailwind --app
cd html-in-canvas-demoالعرض 1: رسم HTML في Canvas (أساسي)
أبسط حالة استخدام — أخذ عنصر HTML ورسمه في canvas ثنائي الأبعاد.
// app/basic/page.tsx
'use client';
import { useEffect, useRef } from 'react';
export default function BasicDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const card = cardRef.current;
if (!canvas || !card) return;
const ctx = canvas.getContext('2d')!;
canvas.width = 800;
canvas.height = 500;
// حدث paint يُطلق عندما يتغير الأبناء
canvas.addEventListener('paint', () => {
ctx.clearRect(0, 0, 800, 500);
// رسم خلفية داكنة
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, 800, 500);
// رسم عنصر HTML البطاقة في الموضع (200, 100)
// drawElement يعرض شجرة DOM المُنسقة بالكامل
const transform = ctx.drawElement(card, 200, 100, {
width: 400,
height: 300,
});
// تطبيق التحويل ليصل النقر للمكان الصحيح
card.style.transform = transform;
});
}, []);
return (
<canvas ref={canvasRef} layoutsubtree width={800} height={500}>
{/* هذا عنصر DOM حقيقي داخل canvas */}
<div
ref={cardRef}
className="rounded-xl bg-slate-800 p-8 text-white shadow-2xl"
>
<h2 className="text-2xl font-bold mb-4">مرحبًا من DOM</h2>
<p className="text-slate-300 mb-6">
هذه البطاقة عنصر HTML حقيقي — مع تنسيق CSS كامل
وإمكانية الوصول والتعامل مع الأحداث — مرسومة في canvas.
</p>
<button
className="rounded-lg bg-sky-500 px-6 py-2 font-semibold
hover:bg-sky-400 transition-colors"
onClick={() => alert('تم النقر على الزر! الأحداث تعمل.')}
>
انقر هنا
</button>
</div>
</canvas>
);
}ما يحدث:
- سمة
layoutsubtreeتخبر Chrome بترتيب<div>كعنصر DOM عادي ctx.drawElement()يعرضه في canvas عند(200, 100)بالأبعاد المحددةtransformالمُعاد يُطبق على العنصر ليتطابق النقر مع الموضع الصحيح- حدث
paintيُطلق كلما تغير العنصر الابن — لا حاجة لاستطلاع يدوي
العرض 2: نموذج تفاعلي في Canvas
نماذج حقيقية — مع حالات التركيز والتنقل بلوحة المفاتيح والتحقق — داخل canvas.
// app/form/page.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
export default function FormDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
const canvas = canvasRef.current;
const form = formRef.current;
if (!canvas || !form) return;
const ctx = canvas.getContext('2d')!;
canvas.width = 900;
canvas.height = 600;
canvas.addEventListener('paint', () => {
ctx.clearRect(0, 0, 900, 600);
// تدرج الخلفية
const grad = ctx.createLinearGradient(0, 0, 900, 600);
grad.addColorStop(0, '#0f172a');
grad.addColorStop(1, '#1e1b4b');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 900, 600);
// دوائر زخرفية
ctx.beginPath();
ctx.arc(700, 100, 200, 0, Math.PI * 2);
ctx.fillStyle = '#38bdf820';
ctx.fill();
ctx.beginPath();
ctx.arc(200, 500, 150, 0, Math.PI * 2);
ctx.fillStyle = '#a78bfa15';
ctx.fill();
// رسم النموذج في المنتصف
const transform = ctx.drawElement(form, 250, 80, {
width: 400,
height: 440,
});
form.style.transform = transform;
});
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
setTimeout(() => setSubmitted(false), 2000);
};
return (
<canvas ref={canvasRef} layoutsubtree width={900} height={600}>
<form
ref={formRef}
onSubmit={handleSubmit}
className="rounded-2xl bg-slate-800/90 backdrop-blur p-8 shadow-2xl
border border-slate-700"
>
<h2 className="text-xl font-bold text-white mb-6">تواصل معنا</h2>
<label className="block mb-4">
<span className="text-sm text-slate-400">الاسم</span>
<input
type="text"
required
className="mt-1 w-full rounded-lg bg-slate-900 border border-slate-600
px-4 py-2.5 text-white focus:border-sky-400
focus:ring-2 focus:ring-sky-400/30 outline-none"
placeholder="اسمك"
/>
</label>
<label className="block mb-4">
<span className="text-sm text-slate-400">البريد الإلكتروني</span>
<input
type="email"
required
className="mt-1 w-full rounded-lg bg-slate-900 border border-slate-600
px-4 py-2.5 text-white focus:border-sky-400
focus:ring-2 focus:ring-sky-400/30 outline-none"
placeholder="you@example.com"
/>
</label>
<label className="block mb-6">
<span className="text-sm text-slate-400">الرسالة</span>
<textarea
rows={4}
className="mt-1 w-full rounded-lg bg-slate-900 border border-slate-600
px-4 py-2.5 text-white focus:border-sky-400
focus:ring-2 focus:ring-sky-400/30 outline-none resize-none"
placeholder="كيف يمكننا مساعدتك؟"
/>
</label>
<button
type="submit"
className={`w-full rounded-lg py-3 font-semibold transition-all ${
submitted
? 'bg-green-500 text-white'
: 'bg-sky-500 text-white hover:bg-sky-400'
}`}
>
{submitted ? 'تم الإرسال!' : 'إرسال الرسالة'}
</button>
</form>
</canvas>
);
}لماذا هذا مهم: النموذج عنصر DOM حقيقي. قارئات الشاشة تراه. مفتاح Tab يعمل. الملء التلقائي للمتصفح يعمل. مدراء كلمات المرور يعملون. كل ذلك معروض في canvas مع خلفيات زخرفية مخصصة ستكون مستحيلة مع DOM وحده.
العرض 3: أدوات لوحة المعلومات في Canvas
اجمع عدة أدوات HTML في canvas واحد — مع تركيب مسرّع بواسطة GPU.
// app/dashboard/page.tsx
'use client';
import { useEffect, useRef } from 'react';
function StatCard({ title, value, change, color }: {
title: string; value: string; change: string; color: string;
}) {
return (
<div className="rounded-xl bg-slate-800 p-5 border border-slate-700">
<p className="text-sm text-slate-400">{title}</p>
<p className="text-2xl font-bold text-white mt-1">{value}</p>
<p className={`text-sm font-medium mt-2`} style={{ color }}>
{change}
</p>
</div>
);
}
export default function DashboardDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const card1Ref = useRef<HTMLDivElement>(null);
const card2Ref = useRef<HTMLDivElement>(null);
const card3Ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d')!;
canvas.width = 900;
canvas.height = 500;
const cards = [card1Ref, card2Ref, card3Ref];
canvas.addEventListener('paint', () => {
ctx.clearRect(0, 0, 900, 500);
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, 900, 500);
// العنوان مرسوم بواجهة Canvas API
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 24px Inter, system-ui, sans-serif';
ctx.fillText('لوحة التحليلات', 30, 40);
// رسم كل بطاقة إحصائيات في مواضع مختلفة
const positions = [
{ x: 30, y: 70 },
{ x: 310, y: 70 },
{ x: 590, y: 70 },
];
cards.forEach((ref, i) => {
if (ref.current) {
const t = ctx.drawElement(ref.current, positions[i].x, positions[i].y, {
width: 250,
height: 120,
});
ref.current.style.transform = t;
}
});
// رسم رسم بياني بواجهة Canvas API تحت بطاقات HTML
drawChart(ctx, 30, 220, 840, 250);
});
}, []);
return (
<canvas ref={canvasRef} layoutsubtree width={900} height={500}>
<div ref={card1Ref}>
<StatCard title="الإيرادات" value="$12,847" change="+12.5%" color="#4ade80" />
</div>
<div ref={card2Ref}>
<StatCard title="المستخدمون" value="3,429" change="+8.2%" color="#38bdf8" />
</div>
<div ref={card3Ref}>
<StatCard title="الطلبات" value="842" change="+23.1%" color="#a78bfa" />
</div>
</canvas>
);
}
function drawChart(
ctx: CanvasRenderingContext2D,
x: number, y: number, w: number, h: number
) {
// خلفية الرسم البياني
ctx.fillStyle = '#1e293b';
ctx.beginPath();
ctx.roundRect(x, y, w, h, 12);
ctx.fill();
// خط الرسم البياني
const points = [40, 65, 45, 80, 60, 90, 75, 95, 70, 100, 85, 110];
ctx.beginPath();
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 2.5;
points.forEach((p, i) => {
const px = x + 30 + (i / (points.length - 1)) * (w - 60);
const py = y + h - 30 - (p / 120) * (h - 60);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.stroke();
// تعبئة بالتدرج
const lastPx = x + 30 + ((points.length - 1) / (points.length - 1)) * (w - 60);
ctx.lineTo(lastPx, y + h - 30);
ctx.lineTo(x + 30, y + h - 30);
ctx.closePath();
const grad = ctx.createLinearGradient(0, y, 0, y + h);
grad.addColorStop(0, '#38bdf830');
grad.addColorStop(1, '#38bdf805');
ctx.fillStyle = grad;
ctx.fill();
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 14px Inter, system-ui';
ctx.fillText('الإيرادات عبر الزمن', x + 16, y + 28);
}القوة هنا: بطاقات الإحصائيات HTML تمنحك تنسيق CSS كامل وحالات التمرير وإمكانية الوصول — بينما الرسم البياني أسفلها مرسوم بواجهة Canvas API للعرض الدقيق بالبكسل. حدث paint يحافظ على تزامن كل شيء. سطح عرض واحد، أفضل ما في العالمين.
العرض 4: HTML كنسيج WebGL (بطاقة ثلاثية الأبعاد)
البدائية الأكثر إثارة — ارفع عنصر HTML كنسيج WebGL وطبّق عليه تحويلات ثلاثية الأبعاد.
// app/3d-card/page.tsx
'use client';
import { useEffect, useRef } from 'react';
export default function ThreeDCardDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const profileRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const profile = profileRef.current;
if (!canvas || !profile) return;
const gl = canvas.getContext('webgl2')!;
canvas.width = 800;
canvas.height = 600;
// إنشاء نسيج من عنصر HTML
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// رفع عنصر HTML كنسيج
// texElement2D يعرض الإخراج المُنسق الكامل للعنصر
gl.texElement2D(
gl.TEXTURE_2D, // الهدف
0, // المستوى
gl.RGBA, // التنسيق الداخلي
gl.RGBA, // التنسيق
gl.UNSIGNED_BYTE, // النوع
profile // عنصر HTML
);
// معاملات النسيج
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// الآن استخدم هذا النسيج في مشهد WebGL الخاص بك
// طبّق تحويلات المنظور والإضاءة والانعكاسات...
// محتوى HTML يصبح كائن ثلاثي الأبعاد من الدرجة الأولى
renderScene(gl, texture);
}, []);
return (
<canvas ref={canvasRef} layoutsubtree width={800} height={600}>
<div
ref={profileRef}
className="w-72 rounded-2xl bg-gradient-to-b from-slate-800 to-slate-900
p-8 text-center border border-slate-700 shadow-2xl"
>
<div className="w-20 h-20 mx-auto rounded-full bg-gradient-to-br
from-sky-400 to-violet-500 flex items-center
justify-center text-3xl font-bold text-white mb-4">
N
</div>
<h3 className="text-xl font-bold text-white">وكيل نقطة</h3>
<p className="text-sky-400 text-sm mt-1">وكالة تطوير ذكاء اصطناعي</p>
<div className="flex justify-around mt-6 text-center">
<div>
<p className="text-lg font-bold text-white">127</p>
<p className="text-xs text-slate-400">مشروع</p>
</div>
<div>
<p className="text-lg font-bold text-white">48</p>
<p className="text-xs text-slate-400">عميل</p>
</div>
<div>
<p className="text-lg font-bold text-white">2.4k</p>
<p className="text-xs text-slate-400">نجمة</p>
</div>
</div>
<button className="mt-6 w-full rounded-lg bg-gradient-to-r
from-sky-500 to-violet-500 py-2.5 font-semibold
text-white hover:opacity-90 transition">
عرض الملف الشخصي
</button>
</div>
</canvas>
);
}
function renderScene(gl: WebGL2RenderingContext, texture: WebGLTexture | null) {
// Vertex shader لبطاقة دوّارة
const vsSource = `#version 300 es
in vec4 aPosition;
in vec2 aTexCoord;
uniform mat4 uProjection;
uniform mat4 uModelView;
out vec2 vTexCoord;
void main() {
gl_Position = uProjection * uModelView * aPosition;
vTexCoord = aTexCoord;
}
`;
// Fragment shader — يطبق نسيج HTML
const fsSource = `#version 300 es
precision highp float;
in vec2 vTexCoord;
uniform sampler2D uTexture;
out vec4 fragColor;
void main() {
fragColor = texture(uTexture, vTexCoord);
}
`;
// ... إعداد WebGL القياسي: تجميع shaders، إنشاء البرنامج،
// إعداد هندسة البطاقة، تحريك الدوران مع requestAnimationFrame
// الفكرة الأساسية: النسيج هو الإخراج المعروض لعنصر HTML
}ما يتيحه texElement2D:
- بطاقات منتجات ثلاثية الأبعاد بمحتوى HTML حقيقي
- تأثيرات نص بالمنظور مع طباعة CSS كاملة
- انتقالات مبنية على shaders بين عروض HTML
- واجهات واقع مختلط مع HTML في مشاهد WebXR
كيف يعمل حدث paint
حدث paint هو آلية المزامنة. يُطلق عندما يتغير أي ابن لـ canvas — إعادة عرض، رسوم متحركة، تغييرات أنماط.
canvas.addEventListener('paint', () => {
// لقطة من عرض جميع الأبناء تُلتقط
// قبل إطلاق هذا الحدث. عندما تستدعي drawElement()
// هنا، يستخدم تلك اللقطة — لا مسار عرض إضافي.
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawElement(myElement, 0, 0);
});هذا يعني:
- لا عرض مزدوج — المتصفح يلتقط بكسلات الأبناء في نفس مسار التركيب
- لا
requestAnimationFrameيدوي — حدث paint يُطلق في الوقت المناسب - تجميع تلقائي — تغييرات متعددة في الأبناء تطلق حدث paint واحد
دعم المتصفحات (أبريل 2026)
| المتصفح | الحالة |
|---|---|
| Chrome Canary | خلف علم #canvas-draw-element |
| Chrome Stable | متوقع الربع الثالث 2026 |
| Firefox | قيد الدراسة |
| Safari | لا إشارة حتى الآن |
للاستخدام في الإنتاج اليوم، ستحتاج استراتيجية تحسين تدريجي:
function CanvasWithFallback({ children }: { children: React.ReactNode }) {
const supportsDrawElement = typeof HTMLCanvasElement !== 'undefined'
&& 'drawElement' in CanvasRenderingContext2D.prototype;
if (!supportsDrawElement) {
// عرض كـ DOM عادي
return <div className="canvas-fallback">{children}</div>;
}
return (
<canvas layoutsubtree width={800} height={600}>
{children}
</canvas>
);
}ما بنيته
| العرض | الواجهة المستخدمة | القدرة |
|---|---|---|
| HTML في Canvas | drawElement() | عرض أي HTML مُنسق في canvas ثنائي الأبعاد |
| نموذج تفاعلي | drawElement() + أحداث | نموذج كامل مع تركيز وتحقق ووصولية |
| لوحة معلومات | drawElement() + Canvas API | مزج أدوات HTML مع رسوم canvas |
| بطاقة ملف شخصي 3D | texElement2D() | محتوى HTML كنسيج WebGL مع تحويلات ثلاثية الأبعاد |
لماذا HTML-in-Canvas يغير كل شيء
قبل هذه الواجهة، كان canvas و DOM عالمين منفصلين. كنت إما:
- تستخدم DOM وتتخلى عن أداء/تأثيرات canvas
- تستخدم canvas وتتخلى عن الوصولية والتدويل وتنسيق النص والأحداث
- تستخدم
html2canvasوتحصل على لقطة شاشة معطلة
HTML-in-Canvas يسد الفجوة. محرك عرض المتصفح يرسم عناصر DOM في canvas — بدقة كاملة وتفاعلية كاملة وتركيب مسرّع بواسطة GPU.
هذا يتيح فئة جديدة من تطبيقات الويب:
- أدوات إبداعية (مثل Figma/Canva) مع تحرير نص حقيقي داخل canvas
- واجهات ألعاب مع قوائم HTML متاحة مركبة على مشاهد WebGL
- تصور البيانات مع تلميحات وتسميات HTML داخل رسوم canvas
- أدوات العروض مع انتقالات ثلاثية الأبعاد بين شرائح HTML
تبني تطبيقًا كثيف الاستخدام لـ canvas؟ وكلاؤنا يُنتجون Next.js للإنتاج مع Canvas متقدم وWebGL وبرمجة إبداعية — 45$/ساعة، مع إشراف بشري. احجز مكالمة مجانية
ماذا تبني بعد ذلك؟
- محرر مثل Figma مع تحرير نص حقيقي داخل كائنات canvas
- محرر صور مع لوحات تحكم HTML معروضة في مساحة عمل canvas
- معرض أعمال ثلاثي الأبعاد مع بطاقات ملفات شخصية HTML عائمة في مشهد WebGL
- لوحة بيانات تفاعلية تمزج رسوم D3 مع أدوات إحصائيات HTML
واجهة HTML-in-Canvas هي الجسر المفقود بين DOM و canvas. انتهت أيام الاختيار بين أحدهما أو الآخر.
تحتاج مساعدة في بناء منتج يعتمد على canvas؟ من الأدوات الإبداعية إلى لوحات البيانات، وكلاؤنا بالذكاء الاصطناعي يتعاملون مع كود Canvas/WebGL المعقد بينما تركز أنت على المنتج. تحدث مع وكيل
قراءات إضافية
- بناء تأثيرات نصية مذهلة مع Pretext و Next.js
- مقترح WICG HTML-in-Canvas
- حالة Chromium: HTML-in-Canvas
Canvas أعطانا البكسلات. DOM أعطانا البنية. HTML-in-Canvas يعطينا كليهما — في نفس الوقت، في نفس العنصر.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

الدليل التفصيلي لتثبيت وهيكلة تطبيقك في Next.js لأداء أمثل
الدليل التفصيلي لتثبيت وهيكلة تطبيقك في Next.js لأداء أمثل: عزز تطبيق Next.js الخاص بك باستخدام هذا الدليل الشامل حول التثبيت وأفضل الممارسات لهيكلة مشروعك لتحقيق الأداء الأمثل.

بناء تطبيق ذكاء اصطناعي محادثي مع Next.js
تعلم كيفية بناء تطبيق ويب يتيح محادثات صوتية في الوقت الفعلي مع وكلاء الذكاء الاصطناعي باستخدام Next.js وElevenLabs.

بناء تطبيق متعدد المستأجرين مع Next.js
تعلم كيفية بناء تطبيق متعدد المستأجرين كامل باستخدام Next.js وVercel والتقنيات الحديثة الأخرى.