أساسيات JavaScript

أساسيات واجهة Canvas

45 دقيقة الدرس 54 من 60

مقدمة لعنصر Canvas

يوفر عنصر HTML <canvas> سطحا قابلا للرسم في المتصفح يمكنك التحكم فيه بالكامل من خلال JavaScript. على عكس عناصر HTML الأخرى التي يعرضها محرك المتصفح، فإن Canvas هو خريطة نقطية فارغة ترسم عليها بكسل ببكسل باستخدام واجهة رسم قوية. يُستخدم لعرض الرسوم البيانية والمخططات وإنشاء رسوميات الألعاب وبناء محررات الصور وتوليد المؤثرات المرئية وإنشاء الرسوم المتحركة التفاعلية.

عنصر Canvas نفسه مجرد حاوية. يتم كل الرسم من خلال سياق العرض -- كائن يوفر الطرق والخصائص للرسم. السياق الأكثر استخداما هو سياق العرض ثنائي الأبعاد، الذي يوفر مجموعة غنية من الطرق لرسم الأشكال والنصوص والصور والمسارات.

إعداد عنصر Canvas

لاستخدام Canvas، أضف أولا عنصر <canvas> إلى HTML مع سمات width وheight صريحة. تحدد هذه السمات دقة سطح الرسم الفعلية بالبكسلات. هذا يختلف عن عرض وارتفاع CSS، اللذين يتحكمان فقط في كيفية عرض Canvas على الصفحة. إذا حددت حجم Canvas فقط من خلال CSS، يكون سطح الرسم الافتراضي 300x150 بكسل ثم يتم تحجيمه، مما ينتج رسومات ضبابية.

مثال: إعداد Canvas في HTML

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>أساسيات Canvas</title>
    <style>
        canvas {
            border: 2px solid #333;
            display: block;
            margin: 20px auto;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="800" height="600">
        متصفحك لا يدعم عنصر canvas.
    </canvas>

    <script src="canvas.js"></script>
</body>
</html>
ملاحظة: النص بين وسمي فتح وإغلاق <canvas> هو محتوى بديل يُعرض فقط عندما لا يدعم المتصفح Canvas. جميع المتصفحات الحديثة تدعم Canvas، لكن تضمين محتوى بديل ممارسة جيدة لإمكانية الوصول.

الحصول على سياق 2D

لبدء الرسم، تحتاج إلى الحصول على مرجع لعنصر Canvas ثم الحصول على سياق العرض ثنائي الأبعاد الخاص به. طريقة getContext('2d') تُرجع كائن CanvasRenderingContext2D الذي يوفر جميع طرق الرسم التي ستستخدمها طوال هذا الدرس.

مثال: الحصول على سياق 2D

// الحصول على عنصر canvas
const canvas = document.getElementById('myCanvas');

// التحقق مما إذا كان canvas مدعوما
if (!canvas.getContext) {
    console.error('Canvas غير مدعوم في هذا المتصفح');
}

// الحصول على سياق العرض ثنائي الأبعاد
const ctx = canvas.getContext('2d');

// الآن يمكنك استخدام ctx للرسم على canvas
console.log('حجم Canvas:', canvas.width, 'x', canvas.height);
console.log('السياق جاهز:', ctx !== null);

فهم نظام الإحداثيات

يستخدم Canvas نظام إحداثيات حيث نقطة الأصل (0, 0) في الزاوية العلوية اليسرى. يزداد المحور x نحو اليمين، ويزداد المحور y نحو الأسفل. هذا يختلف عن النظام الإحداثي الرياضي حيث يزداد y نحو الأعلى. كل بكسل على Canvas له إحداثيات (x, y) فريدة. بالنسبة لـ Canvas بحجم 800x600، تتراوح قيم x الصالحة من 0 إلى 799 وقيم y الصالحة من 0 إلى 599.

مثال: تصور نظام الإحداثيات

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// رسم محاور الإحداثيات
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;

// المحور X
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(canvas.width, 0);
ctx.stroke();

// المحور Y
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, canvas.height);
ctx.stroke();

// تحديد بعض النقاط بإحداثياتها
ctx.fillStyle = '#333';
ctx.font = '14px monospace';

const points = [
    { x: 50, y: 50 },
    { x: 200, y: 100 },
    { x: 400, y: 300 },
    { x: 600, y: 200 }
];

points.forEach(function(point) {
    // رسم نقطة عند كل موضع
    ctx.beginPath();
    ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
    ctx.fill();

    // تسمية النقطة بإحداثياتها
    ctx.fillText('(' + point.x + ', ' + point.y + ')', point.x + 10, point.y - 10);
});

رسم المستطيلات

المستطيلات هي الشكل البدائي الوحيد المدعوم مباشرة بواسطة واجهة Canvas من خلال طرق مخصصة. هناك ثلاث طرق للمستطيلات: fillRect() ترسم مستطيلا مملوءا، وstrokeRect() ترسم حدود مستطيل، وclearRect() تمسح منطقة مستطيلة وتجعلها شفافة تماما. كل طريقة تأخذ أربع معلمات: موضع x وموضع y والعرض والارتفاع.

مثال: رسم المستطيلات

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// رسم مستطيل مملوء (لون صلب)
ctx.fillStyle = '#3498db';
ctx.fillRect(50, 50, 200, 120);

// رسم مستطيل بحدود (حدود فقط)
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.strokeRect(300, 50, 200, 120);

// رسم مستطيل مملوء ثم مسح جزء منه
ctx.fillStyle = '#2ecc71';
ctx.fillRect(50, 220, 200, 120);
// مسح ثقب في المستطيل الأخضر
ctx.clearRect(90, 250, 120, 60);

// الجمع بين الملء والحدود على نفس المستطيل
ctx.fillStyle = '#f39c12';
ctx.strokeStyle = '#e67e22';
ctx.lineWidth = 4;
ctx.fillRect(300, 220, 200, 120);
ctx.strokeRect(300, 220, 200, 120);

// رسم شبكة من المستطيلات الصغيرة
for (let row = 0; row < 5; row++) {
    for (let col = 0; col < 10; col++) {
        const hue = (row * 10 + col) * 7.2;
        ctx.fillStyle = 'hsl(' + hue + ', 70%, 60%)';
        ctx.fillRect(50 + col * 55, 400 + row * 35, 50, 30);
    }
}

رسم المسارات: الخطوط والأشكال المعقدة

جميع الأشكال بخلاف المستطيلات تُرسم باستخدام المسارات. المسار هو تسلسل من النقاط المتصلة بخطوط أو أقواس أو منحنيات. تبني المسار خطوة بخطوة باستخدام سلسلة من الأوامر، ثم تملأ أو ترسم حدود المسار لجعله مرئيا. طرق المسار الرئيسية هي: beginPath() تبدأ مسارا جديدا، moveTo(x, y) تنقل القلم دون رسم، lineTo(x, y) ترسم خطا مستقيما إلى النقطة المحددة، closePath() ترسم خطا للعودة إلى بداية المسار الفرعي الحالي، fill() تملأ المسار بنمط الملء الحالي، وstroke() ترسم حدود المسار بنمط الحدود الحالي.

مثال: رسم الخطوط والمثلثات

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// رسم خط بسيط
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(250, 50);
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke();

// رسم مثلث باستخدام lineTo و closePath
ctx.beginPath();
ctx.moveTo(350, 120);   // الرأس العلوي
ctx.lineTo(280, 220);   // الرأس السفلي الأيسر
ctx.lineTo(420, 220);   // الرأس السفلي الأيمن
ctx.closePath();         // يغلق العودة إلى البداية (350, 120)
ctx.fillStyle = '#9b59b6';
ctx.fill();
ctx.strokeStyle = '#8e44ad';
ctx.lineWidth = 3;
ctx.stroke();

// رسم خط متعرج
ctx.beginPath();
ctx.moveTo(50, 300);
for (let i = 1; i <= 8; i++) {
    const x = 50 + i * 60;
    const y = i % 2 === 0 ? 300 : 360;
    ctx.lineTo(x, y);
}
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.stroke();

// رسم شكل نجمة
function drawStar(ctx, cx, cy, outerRadius, innerRadius, points) {
    ctx.beginPath();
    for (let i = 0; i < points * 2; i++) {
        const radius = i % 2 === 0 ? outerRadius : innerRadius;
        const angle = (i * Math.PI) / points - Math.PI / 2;
        const x = cx + radius * Math.cos(angle);
        const y = cy + radius * Math.sin(angle);
        if (i === 0) {
            ctx.moveTo(x, y);
        } else {
            ctx.lineTo(x, y);
        }
    }
    ctx.closePath();
}

drawStar(ctx, 600, 160, 80, 35, 5);
ctx.fillStyle = '#f1c40f';
ctx.fill();
ctx.strokeStyle = '#f39c12';
ctx.lineWidth = 2;
ctx.stroke();

رسم الأقواس والدوائر

ترسم طريقة arc() قوسا دائريا أو دائرة كاملة. تأخذ ست معلمات: إحداثيات المركز x وy ونصف القطر وزاوية البداية وزاوية النهاية (كلاهما بالراديان) وقيمة بولية اختيارية لاتجاه عكس عقارب الساعة. الدائرة الكاملة تبدأ من 0 وتنتهي عند 2 * Math.PI (حوالي 6.2832 راديان، وهو يساوي 360 درجة).

مثال: رسم الدوائر والأقواس

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// رسم دائرة مملوءة
ctx.beginPath();
ctx.arc(150, 150, 80, 0, Math.PI * 2);
ctx.fillStyle = '#3498db';
ctx.fill();

// رسم حدود دائرة
ctx.beginPath();
ctx.arc(380, 150, 80, 0, Math.PI * 2);
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 4;
ctx.stroke();

// رسم نصف دائرة
ctx.beginPath();
ctx.arc(600, 150, 80, 0, Math.PI); // 0 إلى PI = نصف دائرة
ctx.fillStyle = '#2ecc71';
ctx.fill();

// رسم ربع دائرة (شريحة دائرية)
ctx.beginPath();
ctx.moveTo(150, 380); // الانتقال إلى المركز
ctx.arc(150, 380, 80, 0, Math.PI / 2);
ctx.closePath();
ctx.fillStyle = '#f39c12';
ctx.fill();
ctx.strokeStyle = '#e67e22';
ctx.lineWidth = 2;
ctx.stroke();

// رسم شكل باك مان
ctx.beginPath();
ctx.arc(380, 380, 80, 0.3, Math.PI * 2 - 0.3);
ctx.lineTo(380, 380);
ctx.closePath();
ctx.fillStyle = '#f1c40f';
ctx.fill();

// رسم دوائر متحدة المركز (هدف)
var colors = ['#e74c3c', '#ffffff', '#e74c3c', '#ffffff', '#e74c3c'];
for (let i = colors.length - 1; i >= 0; i--) {
    ctx.beginPath();
    ctx.arc(600, 380, 20 * (i + 1), 0, Math.PI * 2);
    ctx.fillStyle = colors[i];
    ctx.fill();
}
نصيحة احترافية: تذكر أن Canvas يستخدم الراديان وليس الدرجات. لتحويل الدرجات إلى راديان، اضرب في Math.PI / 180. مثلا، 90 درجة تساوي 90 * Math.PI / 180 وهو Math.PI / 2. دالة مساعدة شائعة هي function degreesToRadians(deg) { return deg * Math.PI / 180; }.

منحنيات بيزيه

للمنحنيات السلسة والانسيابية، يوفر Canvas نوعين من منحنيات بيزيه. منحنى بيزيه التربيعي يستخدم نقطة تحكم واحدة ويُرسم باستخدام quadraticCurveTo(cpx, cpy, x, y). منحنى بيزيه التكعيبي يستخدم نقطتي تحكم لأشكال أكثر تعقيدا ويُرسم باستخدام bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y). كلتا الطريقتين ترسمان منحنى من موضع القلم الحالي إلى نقطة النهاية (x, y)، مع تأثير نقاط التحكم على شكل المنحنى.

مثال: رسم منحنيات بيزيه

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// منحنى بيزيه تربيعي
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.quadraticCurveTo(250, 50, 450, 200); // نقطة التحكم عند (250, 50)
ctx.strokeStyle = '#3498db';
ctx.lineWidth = 3;
ctx.stroke();

// تصور نقطة التحكم
ctx.beginPath();
ctx.arc(250, 50, 5, 0, Math.PI * 2);
ctx.fillStyle = '#e74c3c';
ctx.fill();
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.lineTo(250, 50);
ctx.lineTo(450, 200);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke();
ctx.setLineDash([]); // إعادة تعيين نمط التقطيع

// منحنى بيزيه تكعيبي
ctx.beginPath();
ctx.moveTo(50, 400);
ctx.bezierCurveTo(150, 280, 350, 520, 450, 400);
ctx.strokeStyle = '#2ecc71';
ctx.lineWidth = 3;
ctx.stroke();

// رسم موجة سلسة باستخدام عدة منحنيات بيزيه
ctx.beginPath();
ctx.moveTo(500, 300);
ctx.bezierCurveTo(550, 250, 600, 250, 650, 300);
ctx.bezierCurveTo(700, 350, 750, 350, 800, 300);
ctx.strokeStyle = '#9b59b6';
ctx.lineWidth = 3;
ctx.stroke();

// رسم شكل قلب باستخدام منحنيات بيزيه
ctx.beginPath();
ctx.moveTo(600, 180);
ctx.bezierCurveTo(600, 160, 570, 130, 540, 130);
ctx.bezierCurveTo(490, 130, 490, 180, 490, 180);
ctx.bezierCurveTo(490, 210, 540, 250, 600, 280);
ctx.bezierCurveTo(660, 250, 710, 210, 710, 180);
ctx.bezierCurveTo(710, 180, 710, 130, 660, 130);
ctx.bezierCurveTo(630, 130, 600, 160, 600, 180);
ctx.fillStyle = '#e74c3c';
ctx.fill();

الألوان: fillStyle و strokeStyle

خاصية fillStyle تعيّن اللون أو النمط المستخدم لملء الأشكال، بينما strokeStyle تعيّن اللون أو النمط لحدود الأشكال. كلتا الخاصيتين تقبلان أي قيمة لون CSS صالحة: أسماء الألوان وأكواد hex وRGB وRGBA وHSL وHSLA. يمكنك أيضا تعيين التدرجات والأنماط، والتي سنغطيها لاحقا.

مثال: العمل مع الألوان

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// أسماء الألوان
ctx.fillStyle = 'coral';
ctx.fillRect(20, 20, 100, 60);

// ألوان Hex
ctx.fillStyle = '#6c5ce7';
ctx.fillRect(140, 20, 100, 60);

// ألوان RGB
ctx.fillStyle = 'rgb(46, 204, 113)';
ctx.fillRect(260, 20, 100, 60);

// ألوان RGBA (مع الشفافية)
ctx.fillStyle = 'rgba(231, 76, 60, 0.7)';
ctx.fillRect(380, 20, 100, 60);

// ألوان HSL
ctx.fillStyle = 'hsl(210, 80%, 55%)';
ctx.fillRect(500, 20, 100, 60);

// HSLA مع الشفافية
ctx.fillStyle = 'hsla(48, 95%, 55%, 0.8)';
ctx.fillRect(620, 20, 100, 60);

// الشفافية العامة تؤثر على كل الرسم اللاحق
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#e74c3c';
ctx.fillRect(50, 120, 200, 100);
ctx.fillStyle = '#3498db';
ctx.fillRect(150, 160, 200, 100); // منطقة التداخل تمتزج

// إعادة تعيين الشفافية العامة
ctx.globalAlpha = 1.0;

التدرجات: الخطية والشعاعية

تسمح لك التدرجات بإنشاء انتقالات لونية سلسة. التدرج الخطي ينتقل بالألوان على طول خط مستقيم ويُنشأ باستخدام createLinearGradient(x0, y0, x1, y1). التدرج الشعاعي ينتقل بالألوان بين دائرتين ويُنشأ باستخدام createRadialGradient(x0, y0, r0, x1, y1, r1). بعد إنشاء كائن التدرج، تضيف نقاط توقف اللون لتحديد مكان ظهور كل لون على طول التدرج.

مثال: التدرجات الخطية والشعاعية

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// تدرج خطي (أفقي)
const linearHorizontal = ctx.createLinearGradient(50, 0, 350, 0);
linearHorizontal.addColorStop(0, '#e74c3c');
linearHorizontal.addColorStop(0.5, '#f39c12');
linearHorizontal.addColorStop(1, '#2ecc71');
ctx.fillStyle = linearHorizontal;
ctx.fillRect(50, 30, 300, 80);

// تدرج خطي (عمودي)
const linearVertical = ctx.createLinearGradient(0, 140, 0, 280);
linearVertical.addColorStop(0, '#3498db');
linearVertical.addColorStop(1, '#2c3e50');
ctx.fillStyle = linearVertical;
ctx.fillRect(50, 140, 300, 140);

// تدرج خطي (قطري)
const linearDiagonal = ctx.createLinearGradient(400, 30, 700, 280);
linearDiagonal.addColorStop(0, '#9b59b6');
linearDiagonal.addColorStop(0.5, '#e74c3c');
linearDiagonal.addColorStop(1, '#f1c40f');
ctx.fillStyle = linearDiagonal;
ctx.fillRect(400, 30, 300, 250);

// تدرج شعاعي
const radialGradient = ctx.createRadialGradient(200, 430, 10, 200, 430, 120);
radialGradient.addColorStop(0, '#f1c40f');
radialGradient.addColorStop(0.5, '#e67e22');
radialGradient.addColorStop(1, '#e74c3c');
ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(200, 430, 120, 0, Math.PI * 2);
ctx.fill();

// تدرج شعاعي لتأثير كرة ثلاثية الأبعاد
const sphereGradient = ctx.createRadialGradient(530, 400, 5, 560, 430, 100);
sphereGradient.addColorStop(0, '#ffffff');
sphereGradient.addColorStop(0.3, '#3498db');
sphereGradient.addColorStop(1, '#1a5276');
ctx.fillStyle = sphereGradient;
ctx.beginPath();
ctx.arc(560, 430, 100, 0, Math.PI * 2);
ctx.fill();

الأنماط

طريقة createPattern() تسمح لك باستخدام صورة أو Canvas آخر أو فيديو كنمط متكرر لملء الأشكال. تأخذ الطريقة مصدر الصورة ونوع التكرار: 'repeat' يبلّط في كلا الاتجاهين، 'repeat-x' يبلّط أفقيا، 'repeat-y' يبلّط عموديا، و'no-repeat' يعرض الصورة مرة واحدة فقط.

مثال: إنشاء واستخدام الأنماط

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// إنشاء نمط من canvas صغير (رقعة شطرنج)
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 40;
patternCanvas.height = 40;
const patternCtx = patternCanvas.getContext('2d');

// رسم بلاطة رقعة شطرنج
patternCtx.fillStyle = '#ecf0f1';
patternCtx.fillRect(0, 0, 40, 40);
patternCtx.fillStyle = '#bdc3c7';
patternCtx.fillRect(0, 0, 20, 20);
patternCtx.fillRect(20, 20, 20, 20);

// استخدام canvas الصغير كنمط متكرر
const checkerPattern = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillStyle = checkerPattern;
ctx.fillRect(50, 50, 300, 200);

// إنشاء نمط من صورة
const img = new Image();
img.onload = function() {
    const imagePattern = ctx.createPattern(img, 'repeat');
    ctx.fillStyle = imagePattern;
    ctx.fillRect(400, 50, 300, 200);
};
img.src = 'texture.png';

// إنشاء نمط مخطط
const stripeCanvas = document.createElement('canvas');
stripeCanvas.width = 20;
stripeCanvas.height = 20;
const stripeCtx = stripeCanvas.getContext('2d');
stripeCtx.fillStyle = '#3498db';
stripeCtx.fillRect(0, 0, 20, 20);
stripeCtx.fillStyle = '#2980b9';
stripeCtx.fillRect(0, 0, 10, 20);

const stripePattern = ctx.createPattern(stripeCanvas, 'repeat');
ctx.fillStyle = stripePattern;
ctx.beginPath();
ctx.arc(200, 400, 100, 0, Math.PI * 2);
ctx.fill();

رسم النصوص

توفر واجهة Canvas طريقتين لعرض النصوص: fillText(text, x, y) ترسم نصا مملوءا وstrokeText(text, x, y) ترسم حدود النص. تتحكم في المظهر من خلال خاصية font (باستخدام صيغة خط CSS)، وtextAlign للمحاذاة الأفقية، وtextBaseline للمحاذاة العمودية. معلمة رابعة اختيارية تحد من العرض الأقصى، وتقوم بتحجيم النص ليتناسب إذا لزم الأمر.

مثال: رسم وتنسيق النصوص

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// نص مملوء أساسي
ctx.font = '36px Arial';
ctx.fillStyle = '#2c3e50';
ctx.fillText('مرحبا Canvas!', 50, 60);

// نص بحدود (حدود فقط)
ctx.font = 'bold 48px Georgia';
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 2;
ctx.strokeText('نص بحدود', 50, 130);

// جمع الملء والحدود لتأثير عريض
ctx.font = 'bold 42px Verdana';
ctx.fillStyle = '#3498db';
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 1;
ctx.fillText('تأثير عريض', 50, 200);
ctx.strokeText('تأثير عريض', 50, 200);

// عرض محاذاة النص
ctx.font = '20px Arial';
ctx.fillStyle = '#333';
var centerX = canvas.width / 2;

// رسم خط مركزي عمودي للمرجع
ctx.beginPath();
ctx.moveTo(centerX, 240);
ctx.lineTo(centerX, 400);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke();

ctx.fillStyle = '#333';
ctx.textAlign = 'left';
ctx.fillText('محاذاة يسار', centerX, 270);

ctx.textAlign = 'center';
ctx.fillText('محاذاة وسط', centerX, 310);

ctx.textAlign = 'right';
ctx.fillText('محاذاة يمين', centerX, 350);

// إعادة تعيين المحاذاة
ctx.textAlign = 'left';

// قياس عرض النص
ctx.font = '24px monospace';
var text = 'قياس عرض النص';
var metrics = ctx.measureText(text);
ctx.fillText(text, 50, 450);
ctx.fillText('العرض: ' + metrics.width.toFixed(1) + 'px', 50, 480);

رسم الصور

طريقة drawImage() ترسم الصور أو إطارات الفيديو أو عناصر Canvas أخرى على Canvas الخاص بك. لها ثلاثة أشكال: الأبسط يأخذ صورة وموضعا drawImage(image, x, y)، والشكل المُحجّم يضيف العرض والارتفاع drawImage(image, x, y, width, height)، وشكل القص يسمح لك بقص جزء من الصورة المصدر drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) حيث معلمات s تحدد مستطيل المصدر ومعلمات d تحدد مستطيل الوجهة.

مثال: رسم الصور على Canvas

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const img = new Image();
img.onload = function() {
    // رسم بالحجم الأصلي عند الموضع (50, 50)
    ctx.drawImage(img, 50, 50);

    // رسم محجم لأبعاد محددة
    ctx.drawImage(img, 300, 50, 200, 150);

    // رسم جزء مقصوص (قص ورقة الرموز المتحركة)
    // المصدر: بدء من (10, 10)، أخذ منطقة 100x100
    // الوجهة: وضع عند (550, 50)، تحجيم إلى 150x150
    ctx.drawImage(img, 10, 10, 100, 100, 550, 50, 150, 150);

    // رسم مع إطار حدودي
    ctx.strokeStyle = '#2c3e50';
    ctx.lineWidth = 4;
    ctx.strokeRect(298, 48, 204, 154);
};
img.src = 'landscape.jpg';

// الرسم من canvas آخر
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
const offCtx = offscreenCanvas.getContext('2d');
offCtx.fillStyle = '#9b59b6';
offCtx.beginPath();
offCtx.arc(50, 50, 45, 0, Math.PI * 2);
offCtx.fill();

// استخدام canvas خارج الشاشة كمصدر صورة
ctx.drawImage(offscreenCanvas, 50, 350);
ctx.drawImage(offscreenCanvas, 170, 350);
ctx.drawImage(offscreenCanvas, 290, 350);
تحذير: ارسم الصور دائما داخل معالج حدث onload. إذا حاولت رسم صورة قبل انتهاء تحميلها، لن يظهر شيء على Canvas. لعدة صور، استخدم عدادا للتحميل أو Promise.all لضمان أن جميع الصور جاهزة قبل الرسم.

التحويلات: translate و rotate و scale

تسمح تحويلات Canvas بتحريك نظام الإحداثيات بأكمله وتدويره وتحجيمه بدلا من الأشكال الفردية. هذا يسهل رسم مشاهد معقدة مع عناصر مدوّرة أو محجّمة. طرق التحويل الرئيسية الثلاث هي: translate(x, y) تنقل نقطة الأصل، rotate(angle) تدوّر Canvas حول الأصل الحالي (بالراديان)، وscale(x, y) تُحجّم نظام الإحداثيات. التحويلات تراكمية -- كل واحدة تبني على الحالة السابقة.

مثال: استخدام التحويلات

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// الترجمة: نقل الأصل
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, 80, 40); // مرسوم عند الأصل الأصلي

ctx.translate(150, 80);
ctx.fillStyle = '#e74c3c';
ctx.fillRect(0, 0, 80, 40); // مرسوم عند الأصل المنقول (150, 80)

// إعادة تعيين التحويلات
ctx.setTransform(1, 0, 0, 1, 0, 0);

// التدوير: تدوير حول نقطة
// لتدوير حول مركز الشكل:
// 1. انتقل إلى مركز الشكل
// 2. قم بالتدوير
// 3. ارسم الشكل متمركزا عند الأصل
ctx.translate(400, 100);
ctx.rotate(Math.PI / 6); // 30 درجة

ctx.fillStyle = '#2ecc71';
ctx.fillRect(-50, -25, 100, 50); // متمركز عند الأصل

ctx.setTransform(1, 0, 0, 1, 0, 0);

// التحجيم: تكبير أو تصغير الأشياء
ctx.translate(200, 300);
ctx.scale(2, 2); // مضاعفة الحجم
ctx.fillStyle = '#9b59b6';
ctx.fillRect(0, 0, 50, 30); // يظهر بحجم 100x60

ctx.setTransform(1, 0, 0, 1, 0, 0);

// الجمع بين التحويلات لرسم نمط
for (let i = 0; i < 12; i++) {
    ctx.translate(600, 350);
    ctx.rotate((i * Math.PI * 2) / 12);
    ctx.fillStyle = 'hsl(' + (i * 30) + ', 70%, 55%)';
    ctx.fillRect(50, -10, 60, 20);
    ctx.setTransform(1, 0, 0, 1, 0, 0);
}

حفظ واستعادة الحالة

طرق save() وrestore() تدير مكدس حالة رسم Canvas. عند استدعاء save()، يتم دفع الحالة الحالية (بما في ذلك جميع التحويلات ومناطق القص وfillStyle وstrokeStyle وlineWidth وfont وglobalAlpha والمزيد) إلى مكدس. عند استدعاء restore()، يتم سحب آخر حالة محفوظة من المكدس وتطبيقها. هذا ضروري عند استخدام التحويلات، لأنه يسمح لك بتطبيق تغييرات مؤقتة ثم العودة إلى الحالة السابقة بنظافة.

مثال: save() و restore() عمليا

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// تعيين الحالة الأساسية
ctx.fillStyle = '#333';
ctx.font = '20px Arial';
ctx.fillText('الحالة الأساسية', 20, 30);

// حفظ الحالة وإجراء تغييرات
ctx.save();
ctx.fillStyle = '#e74c3c';
ctx.font = 'bold 28px Georgia';
ctx.translate(50, 80);
ctx.fillText('حالة معدلة', 0, 0);

// حفظ مرة أخرى (متداخل) وإجراء المزيد من التغييرات
ctx.save();
ctx.fillStyle = '#3498db';
ctx.rotate(0.1);
ctx.translate(0, 50);
ctx.fillText('حالة معدلة متداخلة', 0, 0);

// استعادة إلى أول حالة محفوظة
ctx.restore();
ctx.translate(0, 100);
ctx.fillText('العودة إلى أول حفظ (أحمر Georgia)', 0, 0);

// استعادة إلى الحالة الأساسية الأصلية
ctx.restore();
ctx.fillText('العودة إلى الحالة الأساسية (داكن Arial)', 20, 300);

// مثال عملي: رسم عدة أشكال مدوّرة
function drawRotatedRect(ctx, x, y, width, height, angle, color) {
    ctx.save();
    ctx.translate(x + width / 2, y + height / 2);
    ctx.rotate(angle);
    ctx.fillStyle = color;
    ctx.fillRect(-width / 2, -height / 2, width, height);
    ctx.restore(); // الحالة تُستعاد بالكامل بعد كل استدعاء
}

drawRotatedRect(ctx, 400, 100, 80, 40, Math.PI / 4, '#2ecc71');
drawRotatedRect(ctx, 520, 100, 80, 40, -Math.PI / 6, '#f39c12');
drawRotatedRect(ctx, 640, 100, 80, 40, Math.PI / 3, '#9b59b6');
نصيحة احترافية: أقرن دائما استدعاءات save() وrestore(). النمط الشائع هو استدعاء save() في بداية دالة الرسم وrestore() في نهايتها. هذا يضمن أن كل دالة تنظف تغييرات حالتها ولا تؤثر على كود الرسم الآخر.

الرسوم المتحركة باستخدام requestAnimationFrame

لإنشاء رسوم متحركة سلسة على Canvas، استخدم requestAnimationFrame() بدلا من setInterval() أو setTimeout(). يستدعي المتصفح دالة الرسم المتحرك قبل إعادة الرسم التالية للشاشة مباشرة، عادة 60 مرة في الثانية. هذا يضمن رسما متحركا سلسا متزامنا مع معدل تحديث الشاشة ويتوقف تلقائيا عندما لا يكون تبويب المتصفح مرئيا، مما يوفر المعالج والبطارية.

مثال: تحريك كرة مرتدة

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// خصائص الكرة
const ball = {
    x: 100,
    y: 100,
    radius: 25,
    dx: 4,   // السرعة الأفقية
    dy: 3,   // السرعة العمودية
    color: '#e74c3c'
};

function drawBall() {
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fillStyle = ball.color;
    ctx.fill();
    ctx.strokeStyle = '#c0392b';
    ctx.lineWidth = 2;
    ctx.stroke();
}

function update() {
    // تحريك الكرة
    ball.x += ball.dx;
    ball.y += ball.dy;

    // الارتداد عن الجدران
    if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
        ball.dx = -ball.dx;
    }
    if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
        ball.dy = -ball.dy;
    }
}

function animate() {
    // مسح Canvas بالكامل
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // رسم الخلفية
    ctx.fillStyle = '#ecf0f1';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // تحديث الموضع والرسم
    update();
    drawBall();

    // طلب الإطار التالي
    requestAnimationFrame(animate);
}

// بدء الرسم المتحرك
animate();

مثال: تحريك عدة كائنات مع دلتا الوقت

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// إنشاء عدة جسيمات
const particles = [];
for (let i = 0; i < 50; i++) {
    particles.push({
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        radius: Math.random() * 8 + 2,
        dx: (Math.random() - 0.5) * 200, // بكسل في الثانية
        dy: (Math.random() - 0.5) * 200,
        hue: Math.random() * 360
    });
}

let lastTime = 0;

function animate(currentTime) {
    // حساب دلتا الوقت بالثواني
    const deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    // تخطي الإطار الأول (دلتا الوقت ستكون كبيرة)
    if (deltaTime > 0.1) {
        requestAnimationFrame(animate);
        return;
    }

    // مسح canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // تحديث ورسم كل جسيم
    particles.forEach(function(particle) {
        // تحديث الموضع بناء على السرعة ودلتا الوقت
        particle.x += particle.dx * deltaTime;
        particle.y += particle.dy * deltaTime;

        // الالتفاف حول الحواف
        if (particle.x < 0) particle.x = canvas.width;
        if (particle.x > canvas.width) particle.x = 0;
        if (particle.y < 0) particle.y = canvas.height;
        if (particle.y > canvas.height) particle.y = 0;

        // تغيير اللون ببطء
        particle.hue = (particle.hue + 30 * deltaTime) % 360;

        // رسم الجسيم
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
        ctx.fillStyle = 'hsl(' + particle.hue + ', 80%, 60%)';
        ctx.fill();
    });

    requestAnimationFrame(animate);
}

// بدء الرسم المتحرك
requestAnimationFrame(animate);

بناء تطبيق رسم بسيط

دعنا نجمع كل ما تعلمته لبناء تطبيق رسم فعال. التطبيق سيسمح للمستخدم برسم خطوط حرة بالماوس واختيار الألوان وأحجام الفرشاة ومسح Canvas. هذا يوضح التعامل مع الأحداث ورسم المسارات وإدارة حالة Canvas تعمل معا.

مثال: تطبيق رسم كامل

// بنية HTML المطلوبة:
// <canvas id="drawCanvas" width="800" height="500"></canvas>
// <div id="controls">
//   <input type="color" id="colorPicker" value="#333333">
//   <input type="range" id="brushSize" min="1" max="50" value="5">
//   <span id="sizeLabel">5px</span>
//   <button id="clearBtn">مسح</button>
//   <button id="undoBtn">تراجع</button>
// </div>

const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');

// حالة الرسم
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let currentColor = '#333333';
let currentLineWidth = 5;

// سجل التراجع
const history = [];
const maxHistory = 20;

// حفظ الحالة الحالية في السجل
function saveState() {
    if (history.length >= maxHistory) {
        history.shift(); // إزالة أقدم حالة
    }
    history.push(canvas.toDataURL());
}

// إعداد canvas الأولي
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
saveState();

// الحصول على موضع الماوس نسبة إلى canvas
function getPosition(event) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top
    };
}

// بدء الرسم
canvas.addEventListener('mousedown', function(event) {
    isDrawing = true;
    var pos = getPosition(event);
    lastX = pos.x;
    lastY = pos.y;
});

// الرسم أثناء تحرك الماوس
canvas.addEventListener('mousemove', function(event) {
    if (!isDrawing) return;

    var pos = getPosition(event);

    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(pos.x, pos.y);
    ctx.strokeStyle = currentColor;
    ctx.lineWidth = currentLineWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.stroke();

    lastX = pos.x;
    lastY = pos.y;
});

// إيقاف الرسم
canvas.addEventListener('mouseup', function() {
    if (isDrawing) {
        isDrawing = false;
        saveState();
    }
});

canvas.addEventListener('mouseleave', function() {
    if (isDrawing) {
        isDrawing = false;
        saveState();
    }
});

// منتقي الألوان
document.getElementById('colorPicker').addEventListener('input', function(event) {
    currentColor = event.target.value;
});

// منزلق حجم الفرشاة
document.getElementById('brushSize').addEventListener('input', function(event) {
    currentLineWidth = parseInt(event.target.value, 10);
    document.getElementById('sizeLabel').textContent = currentLineWidth + 'px';
});

// زر المسح
document.getElementById('clearBtn').addEventListener('click', function() {
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    saveState();
});

// زر التراجع
document.getElementById('undoBtn').addEventListener('click', function() {
    if (history.length <= 1) return; // لا شيء للتراجع
    history.pop(); // إزالة الحالة الحالية
    var previousState = history[history.length - 1];

    var img = new Image();
    img.onload = function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0);
    };
    img.src = previousState;
});
ملاحظة: يستخدم تطبيق الرسم canvas.toDataURL() لحفظ لقطات Canvas لميزة التراجع. هذا يحول Canvas بالكامل إلى سلسلة صورة PNG مشفرة بـ base64. لعناصر Canvas كبيرة، فكر في استخدام getImageData() وputImageData() بدلا من ذلك، لأنهما يعملان مع بيانات البكسل الخام دون عبء ترميز PNG.

نصائح أداء Canvas

يصبح أداء رسم Canvas حرجا عند عرض مشاهد معقدة أو تشغيل رسوم متحركة بمعدل 60 إطارا في الثانية. إليك تقنيات تحسين رئيسية للحفاظ على سلاسة عمل Canvas:

  • تقليل تغييرات الحالة -- جمّع عمليات الرسم حسب النمط (اللون، عرض الخط). تغيير fillStyle أو strokeStyle بين كل استدعاء رسم مكلف.
  • استخدام canvas خارج الشاشة -- ارسم العناصر الثابتة المعقدة مسبقا على canvas خارج الشاشة، ثم ارسمها على Canvas الرئيسي باستدعاء drawImage() واحد.
  • مسح ما تغير فقط -- بدلا من مسح Canvas بالكامل في كل إطار، امسح فقط المناطق التي تحتاج تحديثا.
  • تجنب الإحداثيات العشرية -- استخدم إحداثيات صحيحة للعرض الحاد المحاذي للبكسل. الإحداثيات العشرية تفرض حسابات مكافحة التسنن.
  • تجميع عمليات المسار -- اجمع عدة أشكال في مسار واحد عندما تشترك في نفس النمط، ثم استدعِ fill() أو stroke() مرة واحدة.
  • استخدام requestAnimationFrame -- لا تستخدم أبدا setInterval للرسوم المتحركة. إنه لا يتزامن مع معدل تحديث الشاشة ويمكن أن يسبب تمزقا.

مثال: Canvas خارج الشاشة للأداء

// إنشاء canvas خارج الشاشة للخلفية الثابتة
const bgCanvas = document.createElement('canvas');
bgCanvas.width = 800;
bgCanvas.height = 600;
const bgCtx = bgCanvas.getContext('2d');

// رسم خلفية ثابتة معقدة مرة واحدة
function drawBackground() {
    bgCtx.fillStyle = '#1a1a2e';
    bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);

    // رسم 200 نجمة (مكلف لكن يتم مرة واحدة فقط)
    for (let i = 0; i < 200; i++) {
        bgCtx.beginPath();
        bgCtx.arc(
            Math.random() * bgCanvas.width,
            Math.random() * bgCanvas.height,
            Math.random() * 2 + 0.5,
            0, Math.PI * 2
        );
        bgCtx.fillStyle = 'rgba(255, 255, 255, ' + (Math.random() * 0.8 + 0.2) + ')';
        bgCtx.fill();
    }
}

drawBackground(); // عرض الخلفية مرة واحدة

// في حلقة الرسم المتحرك، فقط ارسم الخلفية المعروضة مسبقا
const mainCanvas = document.getElementById('myCanvas');
const mainCtx = mainCanvas.getContext('2d');

function animate() {
    // رسم الخلفية المعروضة مسبقا (سريع جدا: استدعاء drawImage واحد)
    mainCtx.drawImage(bgCanvas, 0, 0);

    // رسم العناصر المتحركة فوقها
    drawAnimatedElements(mainCtx);

    requestAnimationFrame(animate);
}

animate();

تمرين عملي

ابنِ مشروع Canvas تفاعليا يجمع بين الرسم والرسوم المتحركة وتفاعل المستخدم. أنشئ Canvas بحجم 800x600 مع الميزات التالية. أولا، ارسم خلفية حقل نجمي مع 100 دائرة صغيرة بأحجام وشفافيات متنوعة. ثانيا، أنشئ سفينة فضاء متحركة (مثلث) يتحكم فيها المستخدم بمفاتيح الأسهم -- استخدم translate() وrotate() للتعامل مع الحركة والاتجاه. ثالثا، عند ضغط المستخدم على مفتاح المسافة، ارسم قذيفة دائرية صغيرة تطير في الاتجاه الذي تواجهه السفينة الفضائية. رابعا، أضف خمسة أهداف كويكبية دائرية في مواضع عشوائية تغير لونها عند إصابتها بقذيفة. استخدم save() وrestore() لجميع التحويلات، ونفذ الرسم المتحرك باستخدام requestAnimationFrame()، واستخدم canvas خارج الشاشة لخلفية حقل النجوم الثابت، واعرض عداد نقاط ومقياس FPS مرسومين باستخدام fillText(). أضف ملء تدرجي للسفينة الفضائية واستخدم arc() لجميع الكائنات الدائرية.