اذهب إلى المحتوى

الرسم عبر عنصر canvas في HTML5


عبد اللطيف ايمش

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

html5-canvas.png

الدعم الأساسي للعنصر canvas

IE Firefox Safari Chrome Opera iPhone Android
7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+

* يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم العنصر <canvas> داخليًا.

كيف يبدو عنصر canvas؟ ليس له شكل، حقًا! ليس في عنصر <canvas> أي محتوى وليس له إطارٌ حتى. يُضاف عنصر canvas كالآتي:

<canvas width="300" height="225"></canvas>

لنضف إطارًا منقطًا لكي نستطيع أن نرى ما الذي نتعامل معه:

001.png

الشكل 1: عنصر canvas مع إطار

يمكن أن يكون لديك أكثر من عنصر <canvas> في نفس الصفحة، وسيظهر كل عنصر على حدة في شجرة DOM، ويحافظ كل عنصر canvas على خاصياته؛ فإن أعطيت كل عنصر canvas خاصية id، فيمكنك الوصول إليه كما تفعل مع أي عنصر آخر.

لنوسِّع الشيفرة السابقة لكي تتضمن خاصية id:

<canvas id="a" width="300" height="225"></canvas>

أصبح بإمكاننا بسهولة العثور على عنصر <canvas> السابق في شجرة DOM.

var a_canvas = document.getElementById("a");

الأشكال البسيطة

IE Firefox Safari Chrome Opera iPhone Android
7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+

* يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم رسم الأشكال البسيطة في العنصر <canvas> داخليًا.

يبدأ كل عنصر canvas فارغًاـ ثم علينا الرسم فيه. لنبدأ ببعض الأشكال.

يمكن استخدام الحدث (action) ‏onclick لاستدعاء الدالة الآتية عندما يضغط المستخدم على زرٍ ما:

function draw_b() {
  var b_canvas = document.getElementById("b");
  var b_context = b_canvas.getContext("2d");
  
  b_context.fillRect(50, 25, 150, 100);
}

لا يوجد شيءٌ مميزٌ في أول سطر من الدالة، إذ أنَّ مهمته هي العثور على عنصر <canvas> الموجود في شجرة DOM.

function draw_b() {
  var b_canvas = document.getElementById("b");
  var b_context = b_canvas.getContext("2d");

  b_context.fillRect(50, 25, 150, 100);
}

لدى كل عنصر canvas ما نسميه "إطار الرسم" (drawing context)، الذي يحدث فيه كل الأمور المسلية. فبعد أن تعثر على عنصر <canvas> في شجرة DOM (باستخدام document.getElementById()‎ أو أيّة طريقة أخرى) ستستطيع أن تستدعي الدالة getContext()‎، يجب عليك تمرير السلسلة النصية "2d" دومًا إلى الدالة getContext()‎.

  • س: هل يمكن رسم رسوميات 3D في canvas؟
  • ج: نعم، عبر تقنية WebGL التي يمكنها رسم الأشكال ثلاثية الأبعاد في المتصفحات دون إضافات. تدعم أغلبية المتصفحات الحديثة هذه التقنية (Firefox الإصدار الرابع وما بعده، و Chrome الإصدار التاسع وما بعده، و Opera 12 وما بعده، و Safari 5.1 وما بعده، و IE 11)؛ يتم العمل على تطوير هذه التقنية في مجموعة عمل WebGL.

إذًا، أصبح لديك عنصر <canvas> ولديك إطار الرسم (drawing context) الخاص به. إطار الرسم هو المكان الذي ستُعرَّف فيه جميع دوال وخاصيات الرسم. هنالك مجموعةٌ كاملةٌ من الخاصيات والدوال المُكرَّسة لرسم المستطيلات:

يمكن أن تكون الخاصية fillStyle لونًا من ألوان CSS، أو نقشًا (pattern)، أو تدرجًا لونيًا (gradient) (سنذكر مزيدًا من المعلومات عن التدرجات اللونية بعد قليل). القيمة الافتراضية لهذه الخاصية هي اللون الأسود، لكنك تستطيع أن تضبطها لما تشاء. سيتذكر كل إطار رسم (drawing context) خاصياته لطالما بقيت الصفحة مفتوحةً إلا إن فعلت شيئًا لإعادة ضبطه.

  1. الدالة (fillRect(x, y, width, height ترسم مستطيلًا مملوءًا باللون أو النقش الموجود في fillStyle.
  2. الخاصية strokeStyle شبيهة بخاصية fillStyle، فيمكن أن تكون لون CSS أو نقشًا أو تدرجًا لونيًا.
  3. الدالة (strokeRect(x, y, width, height ترسم مستطيلًا دون ملئه، إذ ترسم حوافه وإطاره الخارجي فحسب، وتَستعمل الخاصية strokeStyle لذلك.
  4. الدالة (clearRect(x, y, width, height تسمح كل البكسلات الموجودة في المستطيل المُحدَّد.

س: هل يمكنني أن أعيد ضبط لوحة الرسم في عنصر canvas؟

ج: نعم، فسيؤدي تحديد عرض أو ارتفاع عنصر <canvas> إلى إعادة ضبط (reset) كل الخاصيات في إطار الرسم إلى قيمها الافتراضية. لاحظ أنَّه ليس من الضروري تغيير العرض، إذ يمكنك أن تضبطه إلى قيمته الحالية كما يلي:

var b_canvas = document.getElementById("b");
b_canvas.width = b_canvas.width;

بالعودة إلى الشيفرة في المثال السابق:

var b_canvas = document.getElementById("b");
var b_context = b_canvas.getContext("2d");

b_context.fillRect(50, 25, 150, 100);

سترسم الدالة fillRect()‎ عند استدعائها مستطيلًا وتملؤه بنمط الملء الحالي الذي هو اللون الأسود (إلا إذا غيرتَه). يُعرَّف المستطيل عبر زاويته العليا اليسرى (50، 25)، وعرضه (150)، وارتفاعه (100). لكي نفهم ذلك فهمًا جيدًا، فلنلقِ نظرةً إلى نظام الإحداثيات في canvas.

الإحداثيات في عنصر canvas

يمكننا تخيل لوحة الرسم في عنصر canvas على أنها شبكة ثنائية البعد، ويكون مبدأ الإحداثيات فيها (‎0, 0) في الزاوية العليا اليسرى من لوحة الرسم.

تزداد القيم على المحور X عند الانتقال نحو الحافة اليمنى من لوحة الرسم، وتزداد القيم على المحور Y بالانتقال نحو الحافة السفلية من لوحة الرسم.

002.png

الشكل 2: توضيح لنظام الإحداثيات في عنصر canvas

رُسِمَ الشكل السابق عبر عنصر <canvas> الذي يحتوي على:

  • مجموعة من الخطوط الأفقية
  • مجموعة من الخطوط الشاقولية
  • خطين أفقيين سوداوين
  • خطين صغيرين سوداوين مائلين يشكلان سهمًا
  • خطين شاقوليين سوداوين
  • خطين صغيرين سوداوين مائلين يشكلان السهم الآخر
  • الحرف "x"
  • الحرف "y"
  • النص "(‎(0, 0" قرب الزاوية العليا اليسرى
  • النص "(‎500, 375)" قرب الزاوية السفلى اليمنى
  • نقطة في الزاوية العليا اليسرى، وأخرى في الزاوية السفلى اليمنى

علينا أولًا تعريف العنصر <canvas> نفسه، حيث يُحدِّد العنصر <canvas> خاصية العرض width والارتفاع height، والمُعرِّف id كي نستطيع العثور عليه بسهولة لاحقًا.

<canvas id="c" width="500" height="375"></canvas>

ثم علينا كتابة سكربت لكي نجد عنصر <canvas> في شجرة DOM ونحصل على إطار الرسم الخاص به.

var c_canvas = document.getElementById("c");
var context = c_canvas.getContext("2d");

نستطيع الآن البدء في رسم الخطوط.

المسارات

IE Firefox Safari Chrome Opera iPhone Android
7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+

* يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم المسارات في العنصر <canvas> داخليًا.

تخيل أنك تريد أن ترسم رسمةً بالحبر، من المؤكد أنَّك لن تبدأ الرسم بأقلام الحبر مباشرةً، لأنك قد تُخطئ؛ فعوضًا عن ذلك سترسم المستقيمات والمنحنيات بقلم الرصاص، ثم إن أعجبتك فيمكنك أن "تُحبِّرَها".

هنالك ما نسميه "المسارات" (paths) في عناصر canvas، وتعريف المسار يُشبِه تمامًا الرسم بقلم الرصاص؛ يمكنك رسم ما تشاء لكنه لن يكون جزءًا من اللوحة النهائية إلا إن أمسكت أقلام التحبير ومررتها فوق المسار الذي رسمتَه.

استعمل الدالتين الآتيتين لرسم المستقيمات "بقلم الرصاص":

  • (moveTo(x, y تحريك قلم الرصاص إلى نقطة البداية المُحدَّدة.
  • (lineTo(x, y رسم خط إلى نقطة النهاية المُحدَّدة.

سيزداد حجم المسار كلما استدعيت الدالتين moveTo()‎ و lineTo()‎. ترسم الدالتان السابقتان "بقلم الرصاص" (يمكنك أن تسمي هذه العملية ما تشاء)، أي أنَّك لن ترَ شيئًا على لوحة الرسم إلى أن تستدعي إحدى دوال "التحبير".

لنبدأ برسم الشبكة:

//رسم المستقيمات الشاقولية

for (var x = 0.5; x < 500; x += 10) {
  context.moveTo(x, 0);
  context.lineTo(x, 375);
}

//رسم المستقيمات الأفقية

for (var y = 0.5; y < 375; y += 10) {
  context.moveTo(0, y);
  context.lineTo(500, y);
}

تلك الدوال "رصاصية"، أي لن يظهر شيءٌ على لوحة الرسم بعد؛ إذ سنحتاج إلى دالة "تحبير" لإظهار تلك الخطوط.

context.strokeStyle = "#eee";
context.stroke();

الدالة storke()‎ هي إحدى دوال "التحبير"، وهي تُحبِّر المسار المعقد الذي عرَّفتَه بدوال moveTo()‎ و lineTo()‎ السابقة. خاصية strokeStyle تتحكم بلون تلك الخطوط. هذه هي النتيجة:

003.png

الشكل 3: شبكة مرسومة داخل عنصر canvas

  • س: لماذا بدأت x و y من 0.5 وليس من 0؟
  • ج: تخيل أنَّ كل بكسل هو مربع كبير، وأنَّ الإحداثيات ذات الرقم الكامل (0، 1، 2…) هي حواف تلك المربعات؛ فإذا أردت أن ترسم خطًا عرضُه واحدة الأطوال (one-unit-wide) بين الإحداثيات ذات الرقم الكامل، فسوف يمتد إلى أن يصل إلى طرفَي المربع (انظر الشكل الآتي للإيضاح)، وسيكون الخط الناتج مرسومًا بعرض بكسلين. أما لرسم خط عرضه بكسل واحد، فعليك أن تُزيح الإحداثيات بمقدار 0.5 عموديًا على منحى (أو اتجاه) الخط.

على سبيل المثال، إذا كنت تحاول رسم خط من (‎1, 0) إلى (‎1, 3)، فسيرسم المتصفح خطًا يغطي 0.5 بكسل على جانبَيّ x=1، ولكن الشاشة غير قادرة على عرض نصف بكسل، لذلك سيمتد الخط لكي يُغطي بكسلين:

004.png

أما لو أردت أن ترسم خطًا من (1.5, 0) إلى (1.5, 3)، فسيرسم المتصفح خطأ يغطي 0.5 بكسل على طرفَي x=1.5، الذي يؤدي إلى رسم خط بعرض 1 بكسل:

005.png

لنرسم الآن السهم الأفقي. جميع الخطوط والمنحنيات الموجودة على نفس المسار ستُرسَم بنفس اللون (أو التدرج اللوني، وسنأتي لذكر ذلك لاحقًا). لكننا نريد أن نرسم السهم بلون مختلف (الأسود)، لذلك نحتاج إلى البدء بمسار (path) جديد.

//مسار جديد

context.beginPath();

context.moveTo(0, 40);
context.lineTo(240, 40);

context.moveTo(260, 40);
context.lineTo(500, 40);

context.moveTo(495, 35);
context.lineTo(500, 40);

context.lineTo(495, 45);

وبنفس الطريقة نرسم السهم الشاقولي؛ ولمّا كان السهم الشاقولي بنفس لون السهم الأفقي، فلا حاجة إلى إنشاء مسار جديد، إذ سيُشكِّل السهمان مسارًا واحدًا.

//لا حاجة إلى إنشاء مسار جديد

context.moveTo(60, 0);
context.lineTo(60, 153);

context.moveTo(60, 173);
context.lineTo(60, 375);

context.moveTo(65, 370);
context.lineTo(60, 375);

context.lineTo(55, 370);

لقد قلتُ أنَّ هذين السهمين يجب أن يُرسما باللون الأسود، لكن لون strokeStyle ما يزال "الفضي الفاتح" (لا تتم عملية إعادة تعيين قيم fillStyle و strokeStyle عندما تبدأ مسارًا جديدًا). لا بأس، لأننا رسمنا إلى الآن "بالرصاص" ولم "نُحبِّر" بعد، علينا الآن أن نضبط قيمة strokeStyle إلى اللون الأسود؛ وإلا سيُرسم هاذين السهمين بالفضي الفاتح وسيصعب علينا رؤيتهما. سيُغيّر السطران الآتيان اللون إلى الأسود ويرسمان الخطوط في لوحة الرسم:

context.strokeStyle = "#000";
context.stroke();

هذه هي النتيجة:

006.png

الشكل 4: المحوران مرسومان دون تسمية على لوحة الرسم

النص

IE Firefox Safari Chrome Opera iPhone Android
7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+

* يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم طباعة النصوص في العنصر <canvas> داخليًا.

** يتطلب Firefox 3.0 مكتبة للتوافقية.

بالإضافة إلى القدرة على رسم خطوط في لوحة الرسم، يمكنك أيضًا "رسم" الجمل النصية. وعلى عكس النصوص في الصفحة المحيطة بلوحة الرسم، لا يوجد هنالك أيّة خاصيات CSS تتعلق بالتخطيط (layout) مثل floats و margins و padding و word wrapping. تستطيع ضبط بعض خاصيات الخط، ثم اختيار نقطة على لوحة الرسم و"رسم" نصك هناك.

تتوفر خاصيات الخطوط الآتية في لوحة الرسم:

  • الخاصية font التي يمكن أن تُضبَط إلى أي شيءٍ تستطيع ضبطه في خاصية font في CSS. وهذا يتضمن نمط الخط، ونوع الخط، وسماكة الخط، وحجم الخط، وارتفاع السطر، واسم "عائلة" الخط.
  • الخاصية textAlign تتحكم بمحاذاة النص، وهي شبيهة (لكن ليس مماثلة تمامًا) بخاصية text-align في CSS. القيم المحتملة هي start و end و left و right و center.
  • الخاصية textBaseline تتحكم في مكان "رسم" النص نسبةً إلى نقطة البداية. القيم المحتملة هي top أو hanging أو middle أو alphabetic أو ideographic أو bottom.
  • الخاصية textBaseline معقدة ودقيقة، لأن طريقة كتابة النصوص معقدة (لا أقصد هنا الإنكليزية، لكنك تستطيع رسم أي محرف يونيكود في عنصر canvas، وكتابة نصوص يونيكود معقدة)،

تشرح مواصفات HTML5 مختلف خطوط الأساس (baselines):

أعلى مربع em هو تقريبًا على مستوى أعلى المحارف في الخط (font)، أما "hanging baseline" فهو مكان ارتكاز بعض المحارف مثل आ، أما خطmiddle فهو يقع في منتصف المسافة بين أعلى مربع em وأسفل مربع em. أما خط alphabetic فهو مكان ارتكاز الأحرف مثل Á و ÿ و f و Ω، وخط ideographic هو مكان ارتكاز المحارف مثل 私 و 達، وأسفل مربع em هو تقريبًا أسفل المحارف الموجودة في خطٍ (font) ما. قد يكون أعلى وأسفل الصندوق المحيط (bounding box) بعيدًا عن خطوط الأساس السابقة نتيجةً لامتداد بعض المحارف خارج مربع em.

007.png

الشكل 5: خطوط الأساس

تستطيع استخدام top أو middle أو bottom لخاصية textBaseline للأبجديات البسيطة مثل الإنكليزية دون أن تكترث للبقية.

لنرسم بعض النصوص! النص المرسوم داخل عنصر canvas يرث حجم الخط ونمطه من عنصر <canvas> نفسه، لكنك تستطيع إعادة تعريف تلك القيم بضبط خاصية font في إطار الرسم.

//تغيير نمط الخط

context.font = "bold 12px sans-serif";

context.fillText("x", 248, 43);
context.fillText("y", 58, 165);

ترسم الخاصية fillText()‎ النص.

context.font = "bold 12px sans-serif";

//رسم النص

context.fillText("x", 248, 43);
context.fillText("y", 58, 165);
  • س: هل أستطيع استخدام مقاسات الخطوط النسبية لرسم النصوص في عنصر canvas؟
  • ج: نعم. مثل أي عنصر HTML آخر في صفحتك، يحسب العنصر <canvas> مقاس الخط بناءً على قواعد CSS في صفحتك. فإذا ضبطت خاصية context.font إلى مقاس خط نسبي مثل 1.5em أو ‎150%، فسيضرب متصفحك هذا الرقم بحجم الخط المحسوب لعنصر <canvas> نفسه.

أريد أن يكون أعلى النص الموجود في الزاوية العليا اليسرى على بعد y=5 من الحافة العلوية، لكنني كسول ولا أريد قياس ارتفاع النص وحساب بعد خط الأساس (baseline)، وإنما سأضبط textBaseline إلى top وسأمرر إحداثيات الزاوية العليا اليسرى من مربع النص.

context.textBaseline = "top";
context.fillText("( 0 , 0 )", 8, 5);

أما للنص في الزاوية السفلى اليمنى، فسأرسم الزاوية السفلى اليمنى للنص في الإحداثيات (492,370)، أي بضعة بكسلات من الزاوية السفلى اليمنى من لوحة الرسم؛ لكنني لا أريد قياس عرض أو ارتفاع النص، لهذا أضبط الخاصية textAlign إلى right والخاصية textBaseline إلى botton، ثم استدعي الدالة fillText()‎ مُمرِّرًا إليها إحداثيات الزاوية السفلى اليمنى من مربع النص.

context.textAlign = "right";
context.textBaseline = "bottom";
context.fillText("( 500 , 375 )", 492, 370);

وهذه هي النتيجة:

008.png

الشكل 6: تسمية المحاور

لقد نسينا النقاط الموجودة على الزوايا! سنتعلم كيف نرسم الدوائر لاحقًا، أما الآن، فسأغش قليلًا وأرسمها كمستطيلات.

context.fillRect(0, 0, 3, 3);
context.fillRect(497, 372, 3, 3);

هذا كل ما في الأمر! هذه هي النتيجة النهائية:

009.png

الشكل 7: الرسم النهائي للإحداثيات عبر عنصر canvas

التدرجات اللونية

  IE Firefox Safari Chrome Opera iPhone Android
التدرجات اللونية الخطية 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+
التدرجات اللونية الشعاعية 9.0+ 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+

* يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم التدرجات اللونية في العنصر <canvas> داخليًا.

لقد تعلمنا سابقًا في هذا الفصل كيف نرسم مستطيلًا مملوءًا بلونٍ ما، ثم تعلمنا كيف نرسم مستطيلًا ذا إطارٍ ملوَّن بلونٍ معين. لكن الأمر ليس محدودًا للألوان فقط، إذ يمكنك فعل ما تشاء بالتدرجات اللونية، لننظر إلى أحد الأمثلة.

0010.png

الشكل 8: تدرج لوني من اليسار إلى اليمين

سيبدو وسم canvas كغيره من الوسوم:

<canvas id="d" width="300" height="225"></canvas>

علينا أولًا أن نعثر على عنصر <canvas> ثم نحصل على إطار الرسم:

var d_canvas = document.getElementById("d");
var context = d_canvas.getContext("2d");

يمكننا أن نبدأ بتعريف التدرج اللوني بعد حصولنا على إطار الرسم. التدرج اللوني هو انتقالٌ ناعمٌ بين لونين أو أكثر. يمكن تعريف نوعين من التدرجات اللونية في إطار الرسم:

  • الدالة (createLinearGradient(x0, y0, x1, y1 ترسم تدرجًا لونيًا عبر قطعة مستقيمة من النقطة (x0, y0) إلى (x1, y1).
  • الدالة (createRadialGradient(x0, y0, r0, x1, y1, r1 ترسم تدرجًا لونيًا عبر مخروط (cone) بين دائرتين. وتُمثِّل أول ثلاثة معاملات (parameters) دائرة البداية ذات المركز (x0, y0) ونصف القطر r0. أما آخر ثلاثة معاملات فهي تمثل دائرة النهاية ذات المركز (x1, y1) ونصف القطر r1.

لنُنشِئ تدرجًا لونيًا خطيًّا (linear)؛ يمكن أن تكون التدرجات اللونية بأي قياس، لكننا سنجعل هذا التدرج بعرض 300 بكسل، مثل لوحة الرسم.

var my_gradient = context.createLinearGradient(0, 0, 300, 0);

لما كانت قيم y (الوسيط الثاني والرابع) تساوي 0، فسيكون اتجاه التدرج اللوني من اليسار إلى اليمين.

يمكنك تعريف ألوان التدرج بعد أن تحصل على كائن التدرج اللوني. يمكن أن يكون للتدرج محطتَي توقف لوني أو أكثر. التوقف اللوني (color stop) يمكن أن يكون في أي مكان في التدرج. ولإضافة توقف لوني، عليك تحديد مكانه ضمن التدرج اللوني، يمكن أن تكون تلك القيم بين 0 و 1.

لنُعرِّف تدرجًا لونيًا من الأسود إلى الأبيض.

my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");

لا يُرسَم أيّ شيءٍ في لوحة الرسم عند تعريف التدرج اللوني، إذ سيُخزَّن ذاك الكائن في مكانٍ ما في الذاكرة. أما لرسم التدرج اللوني، فعليك أن تضبط خاصية fillStyle إلى ذاك التدرج ثم ترسم شكلًا ما مثل مستطيل أو مستقيم.

context.fillStyle = my_gradient;
context.fillRect(0, 0, 300, 225);

وهذه هي النتيجة:

011.png

الشكل 9: تدرج لوني من اليسار إلى اليمين

لنفترض أنَّك تريد تدرجًا لونيًا من الأعلى إلى الأسفل؛ فسيكون عليك إنشاء كائن للتدرج اللوني تكون فيه قيمة x ثابتة (الوسيطين الأول والثالث)، وتجعل قيم y (الوسطين الثاني والرابع) تتراوح بين 0 إلى ارتفاع لوحة الرسم.

var my_gradient = context.createLinearGradient(0, 0, 0, 225);

my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");

context.fillStyle = my_gradient;
context.fillRect(0, 0, 300, 225);

وهذه هي النتيجة:

012.png

الشكل 10: تدرج لوني من الأعلى إلى الأسفل

وتستطيع أيضًا أن تجعل التدرجات اللونية قطرية.

//قيم x و y متغيرة

var my_gradient = context.createLinearGradient(0, 0, 300, 225);

my_gradient.addColorStop(0, "black");
my_gradient.addColorStop(1, "white");

context.fillStyle = my_gradient;
context.fillRect(0, 0, 300, 225);

وهذه هي النتيجة:

013.png

الشكل 11: تدرج لوني قطري

الصور

IE Firefox Safari Chrome Opera iPhone Android
7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+

* يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم تضمين الصور في العنصر <canvas> داخليًا.

يُظهِر الشكل الآتي قطةً معروضةٌ عبر عنصر <img>:

cat.png

الشكل 12: قطة معروضة عبر عنصر img

أما هذه فهي نفس القطة لكن مرسومة في عنصر canvas:

cat2.png

الشكل 13: قطة معروضة ضمن عنصر canvas

هنالك دالة اسمها drawImage()‎ في إطار الرسم تُستعمل لتضمين صورة في عنصر canvas. تأخذ هذه الدالة ثلاثة، أو خمسة، أو تسعة وسائط:

  • (drawImage(image, dx, dy تأخذ صورةً وترسمها في لوحة الرسم، والإحداثيات المعطية (dx, dy) هي إحداثيات الزاوية العليا اليسرى من الصورة. فلو مررنا الإحداثيات (‎(0, 0 فستُرسَم الصورة في الزاوية العليا اليسرى من لوحة الرسم.
  • (drawImage(image, dx, dy, dw, dh تأخذ صورةً وتغير عرضها إلى dw وارتفاعها إلى dh، ثم ترسمها على لوحة الرسم في الإحداثيات (dx, dy).
  • (drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh تأخذ صورةً وتقتطعها لتصبح مساويةً إلى المستطيل (sx, sy, sw, sh) ثم تغير أبعادها إلى (dw, dh) ثم ترسمها على لوحة الرسم في الإحداثيات (dx, dy).

توضِّح مواصفات HTML5 معاملات (parameters) الدالة drawImage()‎:

  • المستطيل المصدر هو المستطيل (ضمن حدود الصورة المصدرية) الذي تكون رؤوس أضلاعه هي (sx, sy)، و (sx+sw, sy)، و (sx+sw, sy+sh)، و (sx, sy+sh).
  • أما المستطيل الوجهة، فهو المستطيل (ضمن حدود لوحة الرسم) الذي تكون رؤوس أضلاعه هي النقاط (dx, dy)، و (dx+dw, dy)، و (dx+dw, dy+dh)، و (dx, dy+dh).

014.png

الشكل 14: تمثيل رسومي لمعاملات الدالة drawImage

لرسم صورة داخل لوحة الرسم، فستحتاج إلى صورة! يمكن أن تكون الصورة عنصر <img> موجود مسبقًا، أو بإمكانك إنشاء كائن Image()‎ باستخدام JavaScript. وفي كلا الحالتين عليك أن تتأكد أنَّ الصورة محمَّلة تحميلًا كاملًا قبل أن ترسمها في لوحة الرسم.

إذا كنت تستخدم عنصر <img> موجود مسبقًا، فيمكنك رسم الصورة في لوحة الرسم أثناء الحدث window.onload.

<img id="cat" src="images/cat.png" alt="sleeping cat" width="177" height="113">
<canvas id="e" width="177" height="113"></canvas>

<script>
  window.onload = function() {
    var canvas = document.getElementById("e");
    var context = canvas.getContext("2d");
    var cat = document.getElementById("cat");
    context.drawImage(cat, 0, 0);
  };
</script>

أما لو كنتَ تُنشِئ كائن الصورة عبر JavaScript، فيمكنك رسم الصورة داخل لوحة الرسم أثناء الحدث Image.onload.

<canvas id="e" width="177" height="113"></canvas>

<script>
  var canvas = document.getElementById("e");
  var context = canvas.getContext("2d");
  var cat = new Image();

  cat.src = "images/cat.png";
  cat.onload = function() {
    context.drawImage(cat, 0, 0);
  };
</script>

المعاملان الاختياريان الثالث والرابع في دالة drawImage()‎ يتحكمان في تغيير أبعاد الصورة.

هذه هي نفس الصورة بعد تقليل أبعادها إلى النصف مرسومةً بشكل تكراري في إحداثياتٍ مختلفة ضمن نفس لوحة الرسم.

015.png

الشكل 15: الكثير من القطط!

هذا هو السكربت الذي يؤدي إلى رسم الشكل السابق:

cat.onload = function() {
  
  for (var x = 0, y = 0;
    x < 500 && y < 375;
    x += 50, y += 37) {

    //تغيير أبعاد الصورة
    context.drawImage(cat, x, y, 88, 56);
  }
};

ربما تتساءل سؤالًا منطقيًا: لماذا تريد رسم صورة في لوحة الرسم أصلًا؟ بماذا ستستفيد من التعقيد الناتج عن رسم صورة داخل لوحة الرسم عوضًا عن استخدام عنصر <img> وبعض خاصيات CSS؟ حتى التأثير السابق (تكرار صورة القطة) يمكن أن يتم باستخدام 10 عناصر <img> متداخلة.

الجواب المبسط هو: لنفس سبب حاجتك إلى رسم النصوص في لوحة الرسم. لاحظ كيف تُضمَّن مخطط إحداثيات canvas نصًا ومستقيماتٍ وأشكالًا. فوضع النص ضمن لوحة الرسم هو جزءٌ من عملٍ أكبر. والمخططات الأكثر تعقيدًا ستستفيد كثيرًا من الدالة drawImage()‎ لتضمين الأيقونات والرسوميات وغيرها.

ماذا عن متصفح IE؟

لم تكن الإصدارات الأقدم من الإصدار التاسع من متصفح Internet Explorer تدعم الواجهة البرمجية (API) لعنصر canvas (يدعم IE9 واجهة canvas البرمجية بالكامل). لكن الإصدارات القديمة من Internet Explorer تدعم تقنية مملوكة من مايكروسوفت اسمها VML، التي تفعل العديد من الأشياء التي يفعلها العنصر <canvas>، وبهذا وُلِدَت المكتبة excanvas.js.

Explorercanvas ‏(excanvas.js) هي مكتبة JavaScript مفتوحة المصدر مُرخَّصة برخصة Apache التي تجعل من الممكن استعمال واجهة canvas البرمجية في متصفح Internet Explorer. عليك تضمين وسم <script> الآتي في أعلى صفحتك لكي تستخدمها.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Dive Into HTML5</title>
    <!--[if lt IE 9]>
    <script src="excanvas.js"></script>
    <![endif]-->
  </head>
  <body>
    ...
  </body>
</html>

الأسطر ‎<!--[if lt IE 9]>‎ و ‎<![endif]-->‎ تسمى التعليقات الشرطية. حيث يعاملها Internet Explorer كأنها عبارة if الشرطية: "إن كان الإصدار الحالي من متصفح Internet Explorer أقل من الإصدار 9، فنفِّذ الشيفرة الآتية."، وستعامل بقية المتصفحات تلك الشيفرة على أنها تعليقات HTML.

فالخلاصة هي أنَّ متصفح Internet Explorer (الإصدار الثامن والسابع) سيُنزِّل السكربت ثم سيُنفِّذه، لكن ستتجاهله بقية المتصفحات تمامًا، وهذا يجعل صفحتك تُحمَّل بشكلٍ أسرع في المتصفحات التي تدعم واجهة canvas البرمجية داخليًا (أي أنها لن تُنزِّل السكربت، ولن تنفذه، ولن تفعل أيّ شيءٍ معه).

ليس عليك فعل أي شيءٍ إضافي بعد تضمين excanvas.js في ترويسة صفحتك. ضمِّن عناصر <canvas> في شيفرتك أو أنشِأها ديناميكيًا باستخدام JavaScript، واتبع التعليمات الواردة في هذا الفصل للحصول على لوحة رسم عنصر <canvas>، ثم تستطيع البدء برسم الأشكال والنصوص والنقوش.

ليس تمامًا… هنالك بعض المحدوديات:

  • يمكن أن تكون التدرجات اللونية خطيةً فقط. التدرجات اللونية الشعاعية غير مدعومة.
  • يجب تكرار النقوش (patterns) في كلا الاتجاهين.
  • ميزة Clipping regions غير مدعومة.
  • تغيير الأبعاد غير المنتظم لا يستطيع تغيير أبعاد حدود الأشكال (strokes) بشكلٍ صحيح.

إنها بطيئة؛ ولا أظن أنَّ ذلك صادمٌ لأي شخص، فمن المعلوم أنَّ مُفسِّر JavaScript في متصفح Internet Explorer أبطأ من غيره من المتصفحات؛ فما بالك برسم أشكال معقدة عبر مكتبة JavaScript ستحول الدوال والأوامر إلى تقنيةٍ مختلفة تمامًا؟ لن تلاحظ بطئًا في الأداء في الأمثلة البسيطة مثل رسم بعض الخطوط وإجراء بعض العمليات على صورة، لكنك سترى ذلك بوضوح بعد أن تحاول إنشاء تأثيرات حركية باستخدام تقنية canvas.

هنالك تحذيرٌ آخر حول استخدام excanvas.js، وهذه مشكلة واجهتني أثناء إنشاء أمثلة هذا الفصل. يُهيّئ ExplorerCanvas واجهته البرمجية لعنصر canvas في مكان تضمينك لسكربت excanvas.js في صفحة HTML. لكن ذلك لا يعني أنَّ Internet Explorer جاهزٌ لاستخدامها مباشرةً، وفي بعض الحالات ستواجه حالة خاصة تكون فيها واجهة canvas جاهزة "تقريبًا" (لكن ليس تمامًا) للاستعمال. الأعراض الظاهرة لهذه المشكلة هي أنَّ Internet Explorer سيشتكي من أنَّ "object doesn’t support this property or method" في كل مرة تحاول فيها فعل أي شيء مع عنصر <canvas> مثل الحصول على لوحة الرسم الخاصة به.

أسهل حل هذه المشكلة هي تأجيل كل العمليات التي ستجريها على canvas إلى أن يحدث الحدث onload، ولكن هذا قد يستغرق بعضًا من الوقت، خصيصًا إذا كانت في صفحتك الكثير من الصور أو مقاطع الفيديو، التي ستؤخر الحدث onload، لكن ذلك سيعطي المكتبة ExplorerCanvas وقتًا لفعل اللازم.

مثال متكامل

"الضامة" هي لعبةٌ قديمةٌ جدًا، وهنالك عدِّة نسخ منها؛ أنشأتُ في هذا المثال نسخة السوليتر (أي التي يلعبها شخصٌ واحد فقط) من الضامة ذات تسعِ قطعٍ في رقعةٍ مقاسها 9×9. تتواجد القطع في بداية اللعبة في مربع 3×3 في الركن الأسفل الأيسر من الرقعة. هدف اللعبة هو نقل جميع القطع كي تُشكِّل مربع 3×3 في الركن الأعلى الأيمن من الرقعة بأقل عدد من الحركات.

هنالك نوعان من الحركات المسموحة في الضامة:

خذ قطعةً وحركها إلى أي مربع فارغ مجاور. المربع "الفارغ" يعني أنَّه لا يحتوي على قطعةٍ فيه حاليًا. أما "مجاور" فهي تعني المربع الذي يقع مباشرةً في شمال أو جنوب أو شرق أو غرب أو الشمال الغربي أو الشمال الشرقي أو الجنوب الغربي أو الجنوب الشرقي من موقع القطعة الحالي (لا يمكن الالتفاف في الرقعة من طرفٍ آخر لآخر، فلو كانت قطعتك في العمود الأيسر من الرقعة، فلا يمكنك أن تحركها نحو الغرب أو الشمال الغربي أو الجنوب الغربي. وإذا كانت قطعتك في السطر الأدنى من الرقعة، فلا تستطيع تحريكها نحو الجنوب أو الجنوب الشرقي أو الجنوب الغربي).

يمكنك أخذ قطعة وجعلها "تقفر" فوق قطعة مجاورة، ويمكنك تكرار ذلك. هذا يعني أنَّه إذا قفزت فوق قطعة مجاورة ثم قفزت فوق قطعة أخرى مجاورة لموقعك الجديد، فكل هذا يُحتَسب على أنَّه حركة واحدة. وفي الحقيقة، يتم احتساب أي عدد من القفزات على أنها خطوة واحدة (لأن الهدف من اللعبة هو تقليل العدد الكلي من الحركات؛ ويتطلب اللعب بشكلٍ جيد في الضامة بناءً واستخدامًا لسلاسل طويلة من القطع لكي تستطيع القطع الأخرى القفز قوفها في سلاسل طويل).

هذه لقطة شاشة للعبة، يمكنك زيارة الصفحة الآتية لتجربة اللعبة تجربةً حيةً أو لإلقاء نظرة إلى مصدر الصفحة.

016.png

الشكل 16: لقطة شاشة للعبة الضامة

كيف تعمل؟ أنا ممتن لسؤالك. لن أريك كل الشيفرات هنا (يمكنك رؤيتها في halma.js)، وسأتخطى معظم الشيفرات التي تستعمل في تحديد آلية اللعب، لكنني سأركِّز على بعض الأجزاء التي مهتمها هي الرسم في عنصر canvas والاستجابة إلى نقرات الفأرة.

سنُهيّئ اللعبة أثناء تحميل الصفحة بضبط أبعاد عنصر <canvas> ثم تخزين مرجع (reference) متعلق بلوحة الرسم.

gCanvasElement.width = kPixelWidth;
gCanvasElement.height = kPixelHeight;
gDrawingContext = gCanvasElement.getContext("2d");

ثم سنفعل شيئًا لم نفعله من قبل قط: سنضيف متتبع أحداث إلى العنصر <canvas> لكي يتتبع حدث النقر على العنصر.

gCanvasElement.addEventListener("click", halmaOnClick, false);

ستُستدَعى الدالة halmaOnClick()‎ عندما ينقر المستخدم في أي مكان ضمن العنصر canvas، والوسيط الذي يُمرَّر إليها هو كائن MouseEvent الذي يحتوي على معلومات حول مكان نقر المستخدم.

function halmaOnClick(e) {

  var cell = getCursorPosition(e);

  for (var i = 0; i < gNumPieces; i++) {
    if ((gPieces.row == cell.row) &&
    (gPieces.column == cell.column)) {
      clickOnPiece(i);
      return;
    }
  }
  clickOnEmptyCell(cell);
}

الخطوة الآتية هي أخذ الكائن MouseEvent وحساب في أي مربع في رقعة الضامة قد تم النقر. تحتل رقعة الضامة كامل عنصر canvas، ولهذا تكون كل نقرة هي نقرة في مكانٍ ما على الرقعة، وكل ما علينا هو معرفة المكان. هذا أمرٌ معقدٌ بعض الشيء، لأن الأحداث المولدة من الفأرة تختلف بشكلٍ أو بآخر بين المتصفحات.

function getCursorPosition(e) {
  var x;
  var y;

  if (e.pageX != undefined && e.pageY != undefined) {
    x = e.pageX;
    y = e.pageY;
  }
  else {
    x = e.clientX + document.body.scrollLeft +
    document.documentElement.scrollLeft;

    y = e.clientY + document.body.scrollTop +
    document.documentElement.scrollTop;
  }
}

أصبح لدينا في هذه المرحلة إحداثيات x و y نسبةً إلى المستند (أي صفحة HTML كلها)، أي أنها ليس مفيدةً بعد؛ حيث نريد أن تكون الإحداثيات منسوبةً إلى لوحة الرسم.

x -= gCanvasElement.offsetLeft;
y -= gCanvasElement.offsetTop;

أصبح لدينا إحداثيات x و y منسوبةً إلى لوحة الرسم، هذا يعني أنَّه لو كانت قيمة x مساويةً للصفر و y مساوية للصفر، فهذا يعني أنَّ المستخدم قد نقر على البكسل في أعلى وأيسر لوحة الرسم.

من هنا سنستطيع معرفة أي مربع قد نقر المستخدم عليه، ثم نتصرف بناءً على ذلك.

var cell = new Cell(Math.floor(y/kPieceHeight),

Math.floor(x/kPieceWidth));
return cell;

أوه! أمرُ الأحداث المرتبطة بالفأرة معقدٌ. لكنك يمكنك استخدام نفس التسلسل المنطقي (أو بالأحرى، يمكنك استخدام نفس الشيفرة) في جميع البرمجيات التي تعتمد على عنصر canvas. تذكر: نقرة بالفأرة ← إحداثيات منسوبة إلى المستند كله ← إحداثيات منسوبة إلى لوحة الرسم ← شيفرة خاصة بالتطبيق.

حسنًا، لنلقِ نظرةً على آلية الرسم الرئيسية. لمّا كانت الرسوميات بسيطةً جدًا، فقررت مسح محتويات لوحة الرسم وإعادة رسم الرقعة في كل مرة يحصل فيها تغييرٌ في اللعبة ولكن هذا ليس ضروريًا. سيحتفظ إطار الرسم في عنصر canvas بمحتواه الذي رسمته عليه حتى لو تخطاه المستخدم بتمريره في الصفحة، أو إذا غيّر إلى لسانٍ (tab) آخر ثم عاد إليه لاحقًا. إذا كنتَ تُطوِّر تطبيقًا مبنيًا على عنصر canvas فيه رسوميات معقدة (مثل ألعاب الورق) فيمكنك تحسين الأداء بتتبع المناطق التي يجب مسحها من لوحة الرسم وإعادة الرسم داخل تلك المناطق فقط. لكن هذا خارجٌ عن نطاق هذا الكتاب.

gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight);

يجب أن تكون الشيفرات الآتية مألوفةً لديك، لأنها شبيهة بتلك التي استعملناها لرسم مخطط إحداثيات canvas سابقًا.

gDrawingContext.beginPath();

// الخطوط الشاقولية
for (var x = 0; x <= kPixelWidth; x += kPieceWidth) {
    gDrawingContext.moveTo(0.5 + x, 0);
    gDrawingContext.lineTo(0.5 + x, kPixelHeight);
}

// الخطوط الأفقية
for (var y = 0; y <= kPixelHeight; y += kPieceHeight) {
    gDrawingContext.moveTo(0, 0.5 + y);
    gDrawingContext.lineTo(kPixelWidth, 0.5 + y);
}

// لنرسم الرسمة
gDrawingContext.strokeStyle = "#ccc";
gDrawingContext.stroke();

المتعة الحقيقة تبدأ عندما نهم برسم القطع. القطعة هي دائرة، لكننا لم نرسم الدوائر من قبل، وعلاوةً على ذلك، لو اختار المستخدم قطعةً لكي يحركها، فسنحتاج إلى رسم تلك القطعة كدائرة مملوءة. يُمثِّل المعامل p هنا قطعة، التي لها خاصيات row (سطر) و column (عمود) التي تُحدِّد المكان الحالي للقطعة. سنستخدم آلية لتحويل (column, row) إلى إحداثيات منسوبة إلى لوحة الرسم (x, y)، ثم سنرسم دائرة، ثم -إن كانت القطعة مُحدَّدةً من المستخدم- سنملأ الدائرة بلونٍ معين.

function drawPiece(p, selected) {
  var column = p.column;
  var row = p.row;
  var x = (column * kPieceWidth) + (kPieceWidth/2);
  var y = (row * kPieceHeight) + (kPieceHeight/2);
  var radius = (kPieceWidth/2)  (kPieceWidth/10);

أصبح لدينا الآن إحداثيات (x, y) منسوبة إلى لوحة الرسم لمركز الدائرة التي نريد رسمها. للأسف لا توجد دالة باسم circle()‎ في واجهة canvas البرمجية، لكن هنالك الدالة arc()‎؛ لكن الدائرة ما هي إلا قوسٌ تتصل بدايته مع نهايته. هل تتذكر أساسيات الهندسة؟ تأخذ الدالة arc()‎ نقطة المركز (x, y)، ونصف قطر، وزاوية البداية والنهاية (بالردايان)، ورايةً (flag) لتحدد اتجاه الدوران (false لاتجاه دوران عقارب الساعة، و true لعكس جهة دوران عقارب الساعة). يمكنك استخدام وحدة Math المبنية في لغة JavaScript لحساب الزوايا بالردايان.

gDrawingContext.beginPath();
gDrawingContext.arc(x, y, radius, 0, Math.PI * 2, false);
gDrawingContext.closePath();

انتظر برهة! لم يُرسَم شيء. الدالة arc()‎ تشبه عمل moveTo()‎ و lineTo()‎. حيث ترسم "بقلم الرصاص". ولرسم الدائرة فعليًا، علينا ضبط خاصية strokeStyle ثم استدعاء الدالة stroke()‎ "لتحبيرها".

gDrawingContext.strokeStyle = "#000";
gDrawingContext.stroke();

ماذا لو كانت القطعة مُحدَّدةً؟ يمكننا إعادة استخدام نفس المسار الذي أنشأناه لرسم حدود القطعة لملئها بلونٍ معيّن.

if (selected) {
  gDrawingContext.fillStyle = "#000";
  gDrawingContext.fill();
}

هذا كل ما في الأمر. تهتم بقية السكربت بالبنية المنطقية للعبة، مثل التفريق بين الحركات الصحيحة وغير الصحيحة، وإحصاء عدد الحركات، ومعرفة إذا انتهت اللعبة.

مرحى! لقد أنشأنا لعبةً عبر عنصر canvas برسم 9 دوائر وبعض المستقيمات ودالة وحيدة مرتبطة بالحدث onclick.

لمزيد من المعلومات حول عنصر canvas يمكنك الإطلاع على سلسلة التعامل مع العنصر canvas باستخدام جافاسكربت.

ترجمة -وبتصرّف- لفصل "Canvas" من كتاب Dive Into HTML5 لمؤلفه Mark Pilgrim.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...