يمكن لرسوم JavaScript التعامل مع حالات لا يمكن أن تتعامل معها CSS، مثل التحرك على مسار معقد مختلف عن منحنيات بيزيه Bezier curves باستخدام دالة توقيت، أو رسوميات متحركة على لوحة رسم.
استخدام الدالة setInterval
يمكن إنجاز الرسوم المتحركة في صورة سلسلة من الإطارات، والتي تكون عادةً تغيرات صغيرةً في خصائص HTML/CSS، فعلى سبيل المثال: يؤدي تغيير قيمة الخاصية style.left
من 0px
إلى 100px
إلى تحريك العنصر، وإذا زدنا هذه القيمة ضمن الدالة setInterval
، فسيؤدي تغير مقداره 2px
مع تأخير ضئيل، لتكرار العملية بمقدار 50 مرةً في الثانية، مما يجعل الحركة سلسةً وناعمةً، ويُتَّبع هذا الأسلوب في السينما، فعرض 24 إطارًا في الثانية يجعل الصورة سلسةً.
إليك الشيفرة المجردة pseudo-code للفكرة:
let timer = setInterval(function() { if (animation complete) clearInterval(timer); else increase style.left by 2px }, 20); // تغير بمقدار 2 بكسل بتأخير 20 ميلي ثانية يعطي 50 إطار في الثانية
وهذا مثال أكثر تعقيدًا:
let start = Date.now(); // تذكر وقت البدء let timer = setInterval(function() { // كم مضى من الوقت منذ البداية let timePassed = Date.now() - start; if (timePassed >= 2000) { clearInterval(timer); // أنهي الحركة بعد ثانيتين return; } // ارسم إطار الحركة في اللحظة الحالية draw(timePassed); }, 20); // عندما يتغير الوقت بين 0 و2000 ميلي ثانية // تتغير قيمة الخاصية من 0 بكسل إلى 400 بكسل function draw(timePassed) { train.style.left = timePassed / 5 + 'px'; }
إليك المثال النموذجي التالي:
- شيفرة الملف index.html:
<!DOCTYPE HTML> <html> <head> <style> #train { position: relative; cursor: pointer; } </style> </head> <body> <img id="train" src="https://js.cx/clipart/train.gif"> <script> train.onclick = function() { let start = Date.now(); let timer = setInterval(function() { let timePassed = Date.now() - start; train.style.left = timePassed / 5 + 'px'; if (timePassed > 2000) clearInterval(timer); }, 20); } </script> </body> </html>
وستكون النتيجة:
استخدام الدالة requestAnimationFrame
لنفترض تنفيذ عدة حركات انتقالية معًا. إذا شغّلنا هذه الرسوم منفصلةً فسيعيد المتصفح -وعلى الرغم من أنّ لكل حركة دالة (setInterval(..., 20
خاصةً- رسم الصور بمعدل أكبر بكثير من 20 ميلي ثانية، ويحدث هذا لوجود أوقات بداية مختلفة لكل حركة، وبالتالي سيختلف تكرار التغيّر الذي ضبطناه عند 20 ميلي ثانية بالنسبة لكل حركة، وهكذا سنكوِّن حالات تشغيل مستقلةً لكل حركة انتقالية تتكرر كل 20 ميلي ثانية، وبكلمات أخرى انظر الشيفرة التالية:
setInterval(function() { animate1(); animate2(); animate3(); }, 20)
والتي ستكون أخفّ من ناحية التنفيذ على المتصفح من الشيفرة:
setInterval(animate1, 20); // حركة انتقالية مستقلة setInterval(animate2, 20); // في أماكن مختلفة من السكربت setInterval(animate3, 20);
ينبغي تجميع عمليات الرسم المستقلة لتسهيل الأمر على المتصفح، ولتخفيف الحِمل على وحدة المعالجة إلى جانب إظهار حركة أكثر نعومةً. تذكر دائمًا أنه يجب ألا نشغل عملية الرسم كل 20 ميلي ثانية، لأنها قد تزيد حمولة وحدة المعالجة، أو لوجود أسباب لتقليل عملية إعادة الرسم، مثل الحالة التي تكون ضمن نافذة مخفية للمتصفح. ولتمييز ذلك في JavaScript سنستخدم ميزةً تُدعى توقيت الحركة Animation timing، والتي تزوّدنا بالدالة requestAnimationFrame
التي تتعامل مع هذه الأمور وأكثر، وإليك صيغة استخدامها:
let requestId = requestAnimationFrame(callback)
تجدول الدالة requestAnimationFrame
دالة الاستدعاء callback
للعمل في أقرب وقت يريد فيه المتصفح تنفيذ الحركة، فإذا نفَّذنا أي تغييرات على العناصر ضمن الدالة callback
، فستُجمّع مع غيرها من دوال الاستدعاء التي تجدولها الدالة requestAnimationFrame
ومع رسوميات CSS، وهكذا سيعيد المتصفح الحسابات الهندسية، ثم يعيد الرسم مرةً واحدةً بدلًا من مرات متعددة.
يمكن استخدام القيمة requestId
التي تعيدها الدالة requestAnimationFrame
في إلغاء الاستدعاء:
// ألغ تنفيذ الاستدعاءات المجدولة cancelAnimationFrame(requestId);
لدالة الاستدعاء callback
وسيط واحد، وهو الوقت الذي انقضى منذ بداية تحميل الصفحة مقدرًا بالميكروثانية، والذي يمكن الحصول عليه باستدعاء التابع performance.now.
يُنفَّذ الاستدعاء callback
مبكرًا إلا في حالة التحميل الزائد للمعالج، أو عندما تقارب بطارية الحاسوب المحمول على النفاد أو لأسباب مشابهة، وتظهر الشيفرة التالية الوقت المستغرق خلال مرات التنفيذ العشرة الأولى للدالة requestAnimationFrame
، وهو عادةً بين 10-20 ميلي ثانية:
<script> let prev = performance.now(); let times = 0; requestAnimationFrame(function measure(time) { document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " "); prev = time; if (times++ < 10) requestAnimationFrame(measure); }) </script>
الرسومات المتحركة المهيكلة Structured animation
سننشئ الآن دالةً أكثر عموميةً مبنيةً على الدالة requestAnimationFrame
:
function animate({timing, draw, duration}) { let start = performance.now(); requestAnimationFrame(function animate(time) { // يتحرك الزمنين 0 و1 let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; // حساب الحالة الراهنة للرسوم المتحركة let progress = timing(timeFraction) draw(progress); // تنفيذ الرسم if (timeFraction < 1) { requestAnimationFrame(animate); } }); }
تقبل الدالة animate
ثلاثة معاملات تصف الحركة، وهي:
-
duration
: الزمن الكلي لتنفيذ الحركة مقدرًا بالميلي ثانية. -
(timing(timeFraction
: دالة توقيت تشابه الخاصيةtransition-timing-function
في CSS، وتعطي نسبة الوقت الذي انقضى (0 عند البداية و1 عند النهاية)، وتعيد ما يدل على اكتمال الحركة، مثل الإحداثيy
عند رسم منحني بيزيه، ولنتذكر أن الدالة الخطية تعني أن الحركة ستتقدم بانتظام وبالسرعة ذاتها:
function linear(timeFraction) { return timeFraction; }
وسيبدو الرسم البياني للدالة الخطية كالتالي:
وهي مشابهة تمامًا للخاصية transition-timing-function
، وسنرى لاحقًا بعض أشكال الاستخدام الأخرى.
-
(draw(progress
: وهي الدالة التي تأخذ معاملًا هو مقدار اكتمال الحركة وترسمه، وتشير القيمةprogress=0
إلى حالة بداية الحركة، بينما تشير القيمةprogress=1
إلى حالة النهاية، فهي الدالة التي ترسم الحركة فعليًا، إذا يمكنها نقل عنصر مثلًا:
function draw(progress) { train.style.left = progress + 'px'; }
أو تنفيذ أي شيء آخر، وبالتالي يمكننا تحريك أي شيء بالطريقة التي نريد، لنحرّك العنصر width
من 0 حتى 100% باستخدام هذه الدالة:
- شيفرة الملف animate.js:
unction animate({duration, draw, timing}) { let start = performance.now(); requestAnimationFrame(function animate(time) { let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; let progress = timing(timeFraction) draw(progress); if (timeFraction < 1) { requestAnimationFrame(animate); } }); }
- شيفرة الملف index.html:
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <style> progress { width: 5%; } </style> <script src="animate.js"></script> </head> <body> <progress id="elem"></progress> <script> elem.onclick = function() { animate({ duration: 1000, timing: function(timeFraction) { return timeFraction; }, draw: function(progress) { elem.style.width = progress * 100 + '%'; } }); }; </script> </body> </html>
ستكون النتيجة كالتالي:
وإليك الشيفرة المستخدمة:
animate({ duration: 1000, timing(timeFraction) { return timeFraction; }, draw(progress) { elem.style.width = progress * 100 + '%'; } });
يمكننا بهذا الأسلوب تنفيذ أي دوال توقيت ورسم على خلاف CSS، ولا ترتبط دوال التوقيت بمنحني بيزيه فقط، كما يمكن للدالة draw
أن تتخطى الخصائص إلى إنشاء عناصر جديدة لرسوميات نحتاجها، مثل الألعاب النارية.
دوال التوقيت
اطلعنا في الفقرات السابقة على أبسط الدوال وهي الدالة الخطية، لنرى الآن بعض الدوال الأخرى، حيث سنجرب بعض الحركات الانتقالية باستخدام دوال توقيت مختلفة.
دالة القوة من الدرجة n
يمكن استعمال progress
بدلالة القوة من الدرجة n
لتسريع الحركة، مثل الدالة التربيعية (أي من الدرجة 2):
function quad(timeFraction) { return Math.pow(timeFraction, 2) }
إليك الرسم البياني:
لترى النتيجة انقر على الشكل التالي:
كما يمكنك استعمال الدالة التكعيبية (من الدرجة 3)، وسترى أن سرعة الحركة ستزداد بزيادة درجة القوة، إليك نموذجًا تكون فيه progress
من الدرجة 5:
الدالة المثلثية القطعية arc
صيغة الدالة:
function circ(timeFraction) { return 1 - Math.sin(Math.acos(timeFraction)); }
الرسم البياني:
المثال النموذج:
دالة إطلاق السهم back
عند اطلاق السهم bow shooting فسنسحب وتر القوس ثم نحرره، وخلافًا للدالتين السابقتين، ستعتمد الدالة على معامل إضافي x
هو ثابت المرونة elasticity coefficient، والذي يُعرِّف المسافة التي نسحب بها وتر القوس:
function back(x, timeFraction) { return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x) }
الخط البياني للدالة عندما x = 1.5
:
المثال النموذجي عند نفس القيمة للمعامل x
:
دالة الارتداد bounce
عندما نرمي كرةً فستسقط للأسفل وترتد عدة مرات ثم تتوقف. تسلك الدالة bounce
هذا السلوك تمامًا لكن بترتيب معكوس، حيث يبدأ الارتداد مباشرةً، وتستخدم هذه الدالة بعض الثوابت الخاصة:
function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } }
إليك نموذجًا يستخدم دالة الإرتداد:
دالة الحركة المرنة elastic
إليك دالةً أخرى "مرنةً" تقبل معاملًا إضافيًا x
يضبط المجال الابتدائي للحركة:
function elastic(x, timeFraction) { return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction) }
الخط البياني للدالة عندما x=1.5
:
إليك نموذجًا عن استخدام الدالة:
الدوال بترتيب معكوس
تعرفنا حتى اللحظة على دوال التوقيت التي تُعرف بتحويلات الدخول السهل easeIn، لكننا قد نحتاج أحيانًا إلى عرض الحركة بترتيب معكوس. تُنفَّذ هذه الحركات بالتحويل الذي يُعرف باسم الخروج السهل easeOut.
التحويل easeOut
تُوضع الدالة timing
في هذا التحويل ضمن المُغلِّف timingEaseOut
:
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)
بعبارة أخرى لدينا دالة التحويل makeEaseOut
التي تقبل دالة توقيت نظاميةً وتعيد المُغلّف الذي يحيط بها:
// accepts a timing function, returns the transformed variant function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } }
يمكن على سبيل المثال اختيار الدالة bounce
التي شرحناها سابقًا وتطبيقها:
let bounceEaseOut = makeEaseOut(bounce);
وهكذا لن تكون الحركة الارتدادية في بداية الحركة بل في نهايتها، وستبدو الحركة أفضل:
- شيفرة الملف style.css:
#brick { width: 40px; height: 20px; background: #EE6B47; position: relative; cursor: pointer; } #path { outline: 1px solid #E8C48E; width: 540px; height: 20px; }
- شيفرة الملف index.html:
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://js.cx/libs/animate.js"></script> </head> <body> <div id="path"> <div id="brick"></div> </div> <script> function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } let bounceEaseOut = makeEaseOut(bounce); brick.onclick = function() { animate({ duration: 3000, timing: bounceEaseOut, draw: function(progress) { brick.style.left = progress * 500 + 'px'; } }); }; </script> </body> </html>
وستكون النتيجة:
سنرى هنا كيف سيغيّر التحويل سلوك الدالة، إذ ستعرَض التأثيرات الموجودة في بداية الحركة -مثل الارتداد- في النهاية، ففي الشكل التالي ستجد الارتداد الاعتيادي باللون الأحمر، والارتداد وفق تحويل الخروج السهل باللون الأزرق.
- الارتداد الاعتيادي: يرتد الكائن عند القاع، ثم يقفز بحدّة إلى القمة في النهاية.
-
باستخدام
easeOut
الخروج السهل: يقفز أولًا إلى القمة ثم يرتد هناك.
التحويل easeInOut
يمكن أن نُظهر التأثير المطلوب في بداية ونهاية الحركة معًا، ويُدعى هذا التحويل بالدخول والخروج السهل easeInOut. سنحسب حالة الحركة باعتماد تابع توقيت ما كالتالي:
if (timeFraction <= 0.5) { // نصف الحركة الأول return timing(2 * timeFraction) / 2; } else { // النصف الثاني للحركة return (2 - timing(2 * (1 - timeFraction))) / 2; }
- شيفرة المُغلِّف:
function makeEaseInOut(timing) { return function(timeFraction) { if (timeFraction < .5) return timing(2 * timeFraction) / 2; else return (2 - timing(2 * (1 - timeFraction))) / 2; } } bounceEaseInOut = makeEaseInOut(bounce);
إليك مثالًا نموذجيًا:
- شيفرة الملف style.css:
#brick { width: 40px; height: 20px; background: #EE6B47; position: relative; cursor: pointer; } #path { outline: 1px solid #E8C48E; width: 540px; height: 20px; }
- شيفرة الملف index.html:
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://js.cx/libs/animate.js"></script> </head> <body> <div id="path"> <div id="brick"></div> </div> <script> function makeEaseInOut(timing) { return function(timeFraction) { if (timeFraction < .5) return timing(2 * timeFraction) / 2; else return (2 - timing(2 * (1 - timeFraction))) / 2; } } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } let bounceEaseInOut = makeEaseInOut(bounce); brick.onclick = function() { animate({ duration: 3000, timing: bounceEaseInOut, draw: function(progress) { brick.style.left = progress * 500 + 'px'; } }); }; </script> </body> </html>
وستكون النتيجة:
يدمج التحويل كائنين رسوميين معًا، وهما التحويل easeIn
(الاعتيادي) في النصف الأول للحركة، والتحويل easeOut
(المعكوس) للنصف الثاني. سنرى التأثير بوضوح عند الموازنة بين التحويلات الثلاث easeIn
وeaseOut
وeaseInOut
عند تطبيقها على دالة التوقيت circ
:
-
easeIn
: باللون الأحمر. -
easeOut
: باللون الأخضر. -
easeInOut
: باللون الأزرق.
كما نرى طبقنا على النصف الأول من الحركة easeIn
، وعلى النصف الآخر easeOut
، لذا ستبدأ الحركة وتنتهي بنفس التأثير.
دالة draw أكثر تميزًا
يمكننا القيام بأكثر من مجرد تحريك العنصر، وكل ما علينا فعله هو كتابة شيفرة مناسبة للدالة draw
، إليك طريقةً لإظهار ارتداد أثناء كتابة نص مثلًا:
- شيفرة الملف style.css:
textarea { display: block; border: 1px solid #BBB; color: #444; font-size: 110%; } button { margin-top: 10px; }
- شيفرة الملف index.html:
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://js.cx/libs/animate.js"></script> </head> <body> <textarea id="textExample" rows="5" cols="60">He took his vorpal sword in hand: Long time the manxome foe he sought— So rested he by the Tumtum tree, And stood awhile in thought. </textarea> <button onclick="animateText(textExample)">Run the animated typing!</button> <script> function animateText(textArea) { let text = textArea.value; let to = text.length, from = 0; animate({ duration: 5000, timing: bounce, draw: function(progress) { let result = (to - from) * progress + from; textArea.value = text.substr(0, Math.ceil(result)) } }); } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } </script> </body> </html>
وستكون النتيجة كالتالي:
خلاصة
يمكن أن تساعدك JavaScript في تنفيذ الرسوميات التي لا تستطيع CSS التعامل معها، أو تلك التي تتطلب تحكمًا دقيقًا.
تّنفّذ حركات JavaScript باستخدام التابع المدمج requestAnimationFrame
الذي يسمح بإعداد دالة استدعاء تُشغَّل عندما يحضّر المتصفح نفسه لعملية إعادة الرسم Repaint. لن تُنفَّذ عملية إعادة الرسم إطلاقًا عندما تكون الصفحة في الخلفية، وبالتالي لن تعمل دالة الاستدعاء وسيُعلّق تنفيذ الحركة، ولن يكون هناك استهلاك للموارد.
إليك الدالة المساعدة animate
التي تحضّر معظم الرسوميات المتحركة التي تحتاجها:
function animate({timing, draw, duration}) { let start = performance.now(); requestAnimationFrame(function animate(time) { // timeFraction goes from 0 to 1 let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; // calculate the current animation state let progress = timing(timeFraction); draw(progress); // draw it if (timeFraction < 1) { requestAnimationFrame(animate); } }); }
الخيارات هي:
-
duration
: الزمن الكلي للحركة مقدرًا بالميلي ثانية. -
timing
: الدالة التي تحسب مقدار تقدم الحركة، حيث تقبل الدالة قيمًا زمنيةً هي نسبة بين 0 و1، وتعيد مقدار تقدم العملية. -
draw
: الدالة التي ترسم الحركة.
وبالطبع يمكن تحسين هذه الدوال وإضافة العديد من الأمور الأخرى، لكن لا يتكرر استخدام رسوميات JavaScript كثيرًا، فهي تستخدَم لإظهار شيء مهم لا لأمور تقليدية، لذا يمكنك إضافة الميزة التي تريدها عند الحاجة.
يمكن أن تستخدم JavaScript أي دوال توقيت، وقد غطينا الكثير منها في هذا الفصل وطبقنا عليها تحويلات عدةً، إذًا لسنا مقيدين بمنحنيات بيزيه كما هي الحال في CSS.
يمكننا استخدام draw
لتحريك أي شيء وليس خصائص CSS فقط.
مهام لإنجازها
1. تحريك كرة مرتدة
أنشئ كرةً ترتد كما في المثال التالي:
الحل
لجعل الكرة ترتد، سنسنتعمل الخاصية top
والخاصية position:absolute
مع الكرة والخاصية position:relative
مع الملعب، ويكن إحداثيات أرضية الملعب هي field.clientHeight
. تشير الخاصية top
إلى بداية أعلى الملعب لذا يجب أن تتغير من 0 إلى field.clientHeight - ball.clientHeight
وهو أدنى موضع يمكن أن تنخفض إليه حافة الكرة العلوية.
يمكن تطبيق تأثير الارتداد باستعمال دالة التوقيت bounce
في الوضع easeOut
.
إليك الشيفرة النهاية الناتجة:
let to = field.clientHeight - ball.clientHeight; animate({ duration: 2000, timing: makeEaseOut(bounce), draw(progress) { ball.style.top = to * progress + 'px' } });
2. حرك الكرة المرتدة إلى اليمين
اجعل الكرة ترتد إلى اليمين كالتالي:
اكتب الشيفرة بحيث تكون مسافة الانتقال إلى اليمين هي 100px
.
الحل
احتجنا في التمرين السابق إلى تحريك خاصية واحدة فقط، بينما سنحتاج في هذا التمرين إلى تحريك خاصية إضافية هي elem.style.left
.
نريد تغيير الاحداثيات الأفقية للكرة بزيادتها تدريجيًا نحو اليمين أثناء السقوط، لذا سنضيف حركة إضافة anumate
يمكن أن نستعمل معها دالة التوقيت linear
ولكن تبدو makeEaseOut(quad)
أفضل بكثير.
إليك الشيفرة النهاية الناتجة:
let height = field.clientHeight - ball.clientHeight; let width = 100; // animate top (bouncing) animate({ duration: 2000, timing: makeEaseOut(bounce), draw: function(progress) { ball.style.top = height * progress + 'px' } }); // animate left (moving to the right) animate({ duration: 2000, timing: makeEaseOut(quad), draw: function(progress) { ball.style.left = width * progress + "px" } });
ترجمة -وبتصرف- للفصل JavaScript Animation من سلسلة The Modern JavaScript Tutorial
اقرأ أيضًا
- المقال السابق: إنشاء رسوم متحركة باستخدام CSS
- النسخة العربية الكاملة لكتاب: التحريك عبر CSS
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.