تخطَّ إلى المحتوى

HTML5+JS

تُنشأ التأثيرات باستخدام JavaScript الخالصة (vanilla) على عنصر canvas في HTML5. في هذا القسم، سنتناول الأساسيات: رسم الأشكال، والرسم بالتكرار، وإضافة الحركة.

سأستخدم في هذا الدرس قالب lightscript عامًا:

<head>
<meta description="Template"/>
<meta publisher="WhirlwindFX" />
</head>
<body style="margin: 0; padding: 0;">
<canvas id="exCanvas" width="320" height="200"></canvas>
</body>
<script>
// Get the canvas element from the DOM
var c = document.getElementById("exCanvas");
var ctx = c.getContext("2d");
var width = 320;
var height = 200;
var hue = 0;
function update() {
// Code goes here
window.requestAnimationFrame(update);
}
window.requestAnimationFrame(update);
</script>

سواء كنت تستخدم خطًا واحدًا أو عدة خطوط، تبقى العملية كما هي.

  1. ctx.beginPath() تبدأ شكلك
  2. ctx.moveTo(x, y) تحدد النقطة الأولى
  3. ctx.lineTo(x, y) تحدد النقطة التالية وخطًا يصلها بالنقطة الأولى. استخدم عدة استدعاءات لـ lineTo لرسم أشكال أكبر.
  4. عيّن ctx.strokeStyle و/أو ctx.fillStyle (إذا كان الشكل مغلقًا)
  5. استخدم ctx.stroke() لرسم الخطوط، وctx.fill() لملء أي أشكال مغلقة. من المهم ملاحظة أن كل شيء حتى هذه النقطة افتراضي - لا يُرسم أي شيء حتى تُنفَّذ هذه الأوامر.

إليك ثلاثة أمثلة تستخدم الخطوط:

function update() {
// One Line
ctx.beginPath();
ctx.moveTo(30, 30);
ctx.lineTo(30, 170);
ctx.strokeStyle = "blue";
ctx.stroke();
// L
ctx.beginPath();
ctx.moveTo(80, 30);
ctx.lineTo(80, 100);
ctx.lineTo(120, 100);
ctx.strokeStyle = "blue";
ctx.stroke();
// Filled Triangle
ctx.beginPath();
ctx.moveTo(130, 30);
ctx.lineTo(130, 100);
ctx.lineTo(170, 100);
ctx.lineTo(130, 30);
ctx.fillStyle = "red";
ctx.strokeStyle = "blue";
ctx.fill();
ctx.stroke();
window.requestAnimationFrame(update);
}

من المهم ملاحظة ترتيب الرسم هنا: يُرسم المثلث المملوء أخيرًا، لذا فإنه سيغطي الشكلين الآخرين لو كانا في الموضع نفسه. بالإضافة إلى ذلك، يُرسم الحد الخارجي (stroke) للمثلث بعد الملء. ولو عكسنا هذه الخطوات، لغطّى الملء الأحمر النصف الداخلي من الحد الخارجي.

المستطيلات بسيطة ومفيدة، خاصةً بالنسبة للدقة المتوسطة للوحة مفاتيح RGB. العملية مشابهة لرسم الخطوط - ما عليك سوى استبدال الأمرين moveTo وlineTo بالأمر rect(x, y, width, height).

function update() {
// Basic Square
ctx.beginPath();
ctx.rect(30, 30, 75, 75);
ctx.strokeStyle = "blue";
ctx.stroke();
// Filled Rectangle
ctx.beginPath();
ctx.rect(125, 30, 125, 100);
ctx.fillStyle = "red";
ctx.strokeStyle = "blue";
ctx.fill();
ctx.stroke();
window.requestAnimationFrame(update);
}

تُرسم الأقواس بتدوير نقطة خارجية حول نقطة مركزية، ويمكن استخدامها لإنشاء دوائر كاملة. أمر القوس هو ctx.arc(x, y, radius, start angle, end angle)، حيث تُعطى قياسات الزاويتين بوحدة الراديان. وعلى عكس المستطيلات، يُرسم القوس بحيث تكون نقطة أصل الشكل (x, y) في مركز القوس.

function update() {
// Half-circle
ctx.beginPath();
ctx.arc(70, 100, 30, 0, Math.PI);
ctx.strokeStyle = "blue";
ctx.stroke();
// 3/4 circle
ctx.beginPath();
ctx.arc(140, 100, 30, 0, Math.PI * (3/2));
ctx.strokeStyle = "blue";
ctx.stroke();
// Full circle
ctx.beginPath();
ctx.arc(210, 100, 30, 0, Math.PI * 2);
ctx.strokeStyle = "blue";
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();
window.requestAnimationFrame(update);
}

كما ترى، تُرسم الأقواس في اتجاه عقارب الساعة افتراضيًا. ويمكن عكس ذلك باستخدام زاوية نهاية أصغر من زاوية البداية، أو بإضافة “true” كوسيط أخير إلى الطريقة ctx.arc().

قد يستغرق رسم ثلاثة أشكال يدويًا عشرين سطرًا من الشيفرة بسهولة، لذا إذا احتجت إلى رسم مئة شكل، فنحن بحاجة إلى نهج أكثر كفاءة. الحلقات في JavaScript طريقة بسيطة لتكرار المهام، ويمكن تطبيقها على الرسم أيضًا.

إليك مثالًا يستخدم حلقة for لرسم صف من المربعات المتناوبة (شطرنجية):

function update() {
for(let i = 0; i < 8; i++){
// 1. Create rectangle path and draw the stroke for each.
ctx.beginPath();
ctx.rect(i * 20, 20, 20, 20);
ctx.strokeStyle = "black";
ctx.stroke();
// 2. If 'i' is even, the fillStyle is black. Otherwise it's white.
if(i % 2 == 0){
ctx.fillStyle = "black";
} else {
ctx.fillStyle = "white";
}
// 3. Fill the shape after setting the fillStyle.
ctx.fill();
}
window.requestAnimationFrame(update);
}

إليك مثالًا باستخدام حلقة while لرسم شبكة شطرنجية. سأغيّر قليلًا درجة إضاءة المربعات السوداء ودرجة لون (hue) المربعات البيضاء في كل تكرار باستخدام القوالب النصية (template literals).

function update() {
var i = 0;
while(i < 16){
// 1. X and Y calculations with 'row' and 'column'
// This operation rounds the result down to the nearest integer. For i = (0-3) the row will be 0, for i = (4-7) the row will be 1, and so on
var iRow = Math.floor(i / 4);
// This operation finds the remainder after division, limiting column to the (0-3) range.
var iCol = i % 4;
// Multiply by the square width to find this square's x-origin
var ix = iCol * 20;
// Multiply by the square height to find this square's y-origin
var iy = iRow * 20;
// 2. Path and stroke
ctx.beginPath();
ctx.rect(ix, iy, 20, 20);
ctx.strokeStyle = "black";
ctx.stroke();
// 3. If the row is even, every other square is filled black. If odd, switch the black squares. Each iteration adds a small amount of the lightness component to the black, and hue to the white.
if(iRow % 2 == 0){
if(i % 2 == 0){
// 'Black'
ctx.fillStyle = `hsl(1, 0%, ${5 * i}%)`
} else {
// 'White'
ctx.fillStyle = `hsl(${10 * i}, 100%, 50%)`;
}
} else {
if(i % 2 == 0){
// 'White'
ctx.fillStyle = `hsl(${10 * i}, 100%, 50%)`;
} else {
// 'Black'
ctx.fillStyle = `hsl(1, 0%, ${5 * i}%)`
}
}
ctx.fill();
i++;
}
window.requestAnimationFrame(update);
}

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

المعيار الذهبي للتذبذب (الحركة المنتظمة ذهابًا وإيابًا) هو أخذ sin() أو cosine() لمؤقت زمني. ستُرجع كلتا العمليتين قيمة بين -1 و1 لأي قيمة تُمرَّر إليها، حتى لو كانت تتزايد فقط. والفرق الوحيد بينهما أنهما غير متزامنتين قليلًا - فعندما تكون قيمة cos تساوي 0 تكون قيمة sin إما -1 أو 1، والعكس صحيح. يمكننا الاستفادة من ذلك في هذا الرسم المتحرك -

function update() {
// Find current time in milliseconds, then divide to slow down the animation speed
let time = Date.now() / 100;
// Draw a background so the old shapes don't stick around
ctx.fillStyle = "white";
ctx.fillRect(0, 0, 320,200);
ctx.beginPath();
//USE ONE EXAMPLE AT A TIME
// 1st example - side to side, y is constant
ctx.arc(15 * Math.cos(time) + 160, 100, 30, 0, Math.PI * 2);
// 2nd example - up and down, x is constant
ctx.arc(160, 15 * Math.sin(time) + 100, 30, 0, Math.PI * 2);
// 3rd example - circle, x and y are variable
ctx.arc(15 * Math.cos(time) + 160, 15 * Math.sin(time) + 100, 30, 0, Math.PI * 2);
ctx.fillStyle = "black";
ctx.fill();
window.requestAnimationFrame(update);
}

لم يتبقَّ لدينا سوى بضع سمات لتغييرها بمرور الوقت - نصف قطر الدائرة، والملء، والحد الخارجي. سأترك زوايا القوس كما هي في الوقت الحالي لإبقاء الشكل بسيطًا، لأن البقية ستكون لافتة للنظر بما فيه الكفاية.

function update() {
let time = Date.now() / 100;
ctx.fillStyle = "white";
ctx.fillRect(0, 0, 320,200);
ctx.beginPath();
ctx.arc(15 * Math.cos(time) + 160, 15 * Math.sin(time) + 100, 30 + 15 * Math.sin(time), 0, Math.PI * 2);
ctx.fillStyle = `hsl(${180 + 180 * Math.cos(time)}, 100%, 50%)`;
ctx.fill();
ctx.strokeStyle = `hsl(${180 + 180 * Math.sin(time)}, 100%, 50%)`;
ctx.stroke();
window.requestAnimationFrame(update);
}

يمكن تغيير كل سمة من سمات كل شكل أنشأناه حتى الآن بمرور الوقت، وكل رسم متحرك أصنعه هو مزيج من المهارات المذكورة أعلاه. لمزيد من التفاصيل حول صياغة رسوم متحركة فعّالة وقابلة لإعادة الاستخدام، راجع صفحة الاستدعاءات الراجعة!