تحسين الصور المتحركة في تطبيقك على الويب
ملخّص: تتيح لك Animation Worklet كتابة صور متحركة إلزامية تعمل بمعدّل عرض اللقطات الأصلي للجهاز، ما يوفّر سلاسة إضافية™ وخالية من التشويش، ويجعل الصور المتحركة أكثر مرونة في مواجهة التشويش في سلسلة التعليمات الرئيسية، ويمكن ربطها بالتمرير بدلاً من الوقت. تتوفّر Animation Worklet في Chrome Canary (خلف علامة "ميزات تجريبية للنظام الأساسي للويب") ونخطّط لإجراء تجربة أصل في الإصدار 71 من Chrome. ويمكنك البدء في استخدامه كتحسين تدريجي اليوم.
واجهة برمجة تطبيقات أخرى للرسوم المتحركة؟
في الواقع، لا، بل هو امتداد لما لدينا حاليًا، ولدينا سبب وجيه لذلك. لنبدأ من البداية. إذا أردت تحريك أي عنصر DOM على الويب اليوم، لديك خياران ونصف: انتقالات CSS للانتقالات البسيطة من أ إلى ب، ورسوم CSS المتحركة للرسوم المتحركة الأكثر تعقيدًا والتي قد تكون دورية والمستندة إلى الوقت، وWeb Animations API (WAAPI) للرسوم المتحركة المعقدة بشكل شبه عشوائي. مصفوفة التوافق الخاصة بواجهة برمجة التطبيقات WAAPI تبدو قاتمة إلى حد ما، ولكنها في طريقها إلى التحسّن. حتى ذلك الحين، يتوفّر رمز polyfill.
القاسم المشترك بين كل هذه الطرق هو أنّها لا تحتفظ بأي حالة وتعتمد على الوقت. لكن بعض التأثيرات التي يحاول المطوّرون تطبيقها ليست مرتبطة بالوقت ولا عديمة الحالة. على سبيل المثال، إنّ أداة التمرير المنظرية الشهيرة هي، كما يوحي الاسم، مستندة إلى التمرير. إنّ تنفيذ أداة تمرير متوازية عالية الأداء على الويب اليوم أمر صعب بشكل مفاجئ.
وماذا عن عدم الاحتفاظ بالحالة؟ لنأخذ شريط العناوين في Chrome على Android كمثال. إذا انتقلت للأسفل، سيتم إخفاء الإشعار. ولكن بمجرد التمرير للأعلى، ستظهر من جديد، حتى إذا كنت في منتصف الصفحة. لا تعتمد الحركة على موضع التمرير فقط، بل أيضًا على اتجاه التمرير السابق. إنّها ذات حالة.
هناك مشكلة أخرى وهي تصميم أشرطة التمرير. وهي معروفة بأنّه لا يمكن تطبيق أنماط عليها، أو على الأقل لا يمكن تطبيق أنماط كافية عليها. ماذا لو أردت قطة نيان كشريط تمرير؟ مهما كانت التقنية التي تختارها، فإنّ إنشاء شريط تمرير مخصّص ليس سهلاً ولا فعّالاً.
النقطة المهمة هي أنّ كل هذه الأمور محرجة ويصعب تنفيذها بكفاءة، بل قد يكون ذلك مستحيلاً. تعتمد معظم هذه الطرق على الأحداث و/أو
requestAnimationFrame
، ما قد يجعلك تستخدم 60 لقطة في الثانية، حتى عندما تكون شاشتك
قادرة على عرض 90 أو 120 لقطة في الثانية أو أكثر، وتستخدم جزءًا من
ميزانية إطارات سلسلة التعليمات الرئيسية الثمينة.
توسّع Animation Worklet إمكانات حزمة الرسوم المتحركة على الويب لتسهيل إنشاء هذا النوع من التأثيرات. قبل أن نتعمّق في الموضوع، لنراجع معًا أساسيات الرسوم المتحركة.
مقدمة حول الصور المتحركة والمخططات الزمنية
تستخدِم واجهة برمجة التطبيقات WAAPI وAnimation Worklet المخططات الزمنية على نطاق واسع للسماح لك بتنظيم الحركات والمؤثرات بالطريقة التي تريدها. هذا القسم هو عبارة عن تذكير سريع أو مقدّمة حول المخططات الزمنية وكيفية عملها مع الرسوم المتحركة.
يحتوي كل مستند على document.timeline
. يبدأ هذا العداد من 0 عند إنشاء المستند، ويحتسب عدد الملّي ثانية منذ بدء توفّر المستند. تعمل جميع الرسوم المتحركة في المستند بالنسبة إلى هذا المخطط الزمني.
لجعل الأمور أكثر وضوحًا، لنلقِ نظرة على مقتطف WAAPI التالي
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
عندما نستدعي animation.play()
، تستخدم الحركة currentTime
في المخطط الزمني كوقت بدء. تتضمّن الصورة المتحركة تأخيرًا بمقدار 3000 ملي ثانية، ما يعني أنّ الصورة المتحركة ستبدأ (أو تصبح "نشطة") عندما يصل المخطط الزمني إلى `startTime
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000. النقطة المهمة هي أنّ المخطط الزمني يتحكّم في موضعنا في الرسوم المتحركة.
بعد أن تصل الحركة إلى آخر إطار رئيسي، ستعود إلى الإطار الرئيسي الأول وتبدأ التكرار التالي للحركة. تتكرر هذه العملية 3 مرات إجمالاً لأننا ضبطنا iterations: 3
. إذا أردنا ألا تتوقف الحركة أبدًا، سنكتب iterations: Number.POSITIVE_INFINITY
. في ما يلي
نتيجة الرمز
أعلاه.
تتضمّن WAAPI العديد من الميزات الإضافية، مثل التباطؤ والتسارع، وإزاحات البدء، وأوزان الإطارات الرئيسية، وسلوك التعبئة، ما يجعلها تتجاوز نطاق هذه المقالة. إذا أردت معرفة المزيد، أنصحك بقراءة هذه المقالة حول CSS Animations على CSS Tricks.
كتابة Animation Worklet
بعد أن فهمنا مفهوم المخططات الزمنية، يمكننا البدء في استكشاف Animation Worklet وكيفية استخدامها لتعديل المخططات الزمنية. لا تستند واجهة برمجة التطبيقات Animation Worklet إلى WAAPI فحسب، بل هي أيضًا، من ناحية الويب القابل للتوسيع، عنصر أساسي منخفض المستوى يوضّح طريقة عمل WAAPI. من ناحية بناء الجملة، هما متشابهان إلى حد كبير:
Animation Worklet | WAAPI |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
يكمن الاختلاف في المَعلمة الأولى، وهي اسم البرنامج الصغير الذي يشغّل هذه الحركة.
رصد الميزات
متصفّح Chrome هو أول متصفّح يوفّر هذه الميزة، لذا عليك التأكّد من أنّ الرمز البرمجي لا يتوقّع توفُّر AnimationWorklet
فقط. لذلك، قبل تحميل
البرنامج الصغير، علينا رصد ما إذا كان متصفّح المستخدم يتوافق مع
AnimationWorklet
من خلال إجراء فحص بسيط:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
جارٍ تحميل تطبيق مصغّر
Worklet هو مفهوم جديد قدّمه فريق عمل Houdini لتسهيل إنشاء العديد من واجهات برمجة التطبيقات الجديدة وتوسيع نطاقها. سنتناول تفاصيل Worklets لاحقًا، ولكن يمكنك حاليًا اعتبارها سلاسل تنفيذ رخيصة وخفيفة الوزن (مثل العاملين) لتبسيط الأمر.
يجب التأكّد من أنّه تم تحميل worklet باسم "passthrough"، قبل تعريف الحركة:
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
ما الذي يحدث هنا؟ نحن بصدد تسجيل فئة كعنصر رسوم متحركة باستخدام استدعاء registerAnimator()
في AnimationWorklet، مع منحها الاسم "passthrough".
وهو الاسم نفسه الذي استخدمناه في الدالة الإنشائية WorkletAnimation()
أعلاه. بعد اكتمال عملية التسجيل، سيتم حلّ الوعد الذي تم إرجاعه بواسطة addModule()
، ويمكننا البدء في إنشاء رسوم متحركة باستخدام هذا التطبيق الصغير.
سيتم استدعاء طريقة animate()
في مثيلنا لكل إطار يريد المتصفّح عرضه، مع تمرير currentTime
للجدول الزمني للرسوم المتحركة، بالإضافة إلى التأثير الذي تتم معالجته حاليًا. لدينا تأثير واحد فقط، وهو KeyframeEffect
، ونستخدم currentTime
لضبط localTime
الخاص بالتأثير، وهذا هو السبب في تسمية أداة تحريك الصور هذه باسم "تمرير". باستخدام هذا الرمز الخاص بـ Worklet، سيتصرف كل من WAAPI وAnimationWorklet أعلاه بالطريقة نفسها تمامًا، كما يمكنك أن ترى في العرض التوضيحي.
الوقت
المَعلمة currentTime
في طريقتنا animate()
هي currentTime
المخطط الزمني الذي مرّرناه إلى الدالة الإنشائية WorkletAnimation()
. في المثال السابق، مرّرنا هذا الوقت إلى التأثير. ولكن بما أنّ هذا الرمز البرمجي مكتوب بلغة JavaScript، وبما أنّه يمكننا تشويه الوقت 💫
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
نأخذ Math.sin()
currentTime
، ونعيد تعيين هذه القيمة إلى النطاق [0; 2000]، وهو النطاق الزمني الذي تم تحديد التأثير له. الآن، تبدو الصورة المتحركة مختلفة تمامًا، بدون أن نغيّر الإطارات الرئيسية أو خيارات الصورة المتحركة. يمكن أن يكون رمز Worklet معقّدًا بشكل كبير، ويتيح لك تحديد التأثيرات التي يتم تشغيلها والترتيب الذي يتم تشغيلها به ومدى تأثيرها.
خيارات فوق خيارات
قد تحتاج إلى إعادة استخدام بطاقة صغيرة وتغيير أرقامها. لهذا السبب، يتيح لك الدالة الإنشائية WorkletAnimation تمرير عنصر خيارات إلى Worklet:
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
في هذا المثال، يتم تشغيل كلتا الصورتَين المتحرّكتَين باستخدام الرمز البرمجي نفسه، ولكن مع خيارات مختلفة.
أريد معرفة حالتك المحلية!
كما أشرتُ سابقًا، إحدى المشاكل الرئيسية التي يهدف Animation Worklet إلى حلّها هي
الرسوم المتحركة المستندة إلى الحالة. يُسمح لبرامج Animation Worklet المصغّرة بالاحتفاظ بالحالة. ومع ذلك، إحدى الميزات الأساسية في Worklet هي إمكانية نقلها إلى سلسلة محادثات مختلفة أو حتى إيقافها لتوفير الموارد، ما يؤدي أيضًا إلى إيقاف حالتها. لمنع فقدان الحالة، يوفّر تطبيق Animation Worklet خطافًا يتم استدعاؤه قبل إيقاف تطبيق Worklet، ويمكنك استخدامه لعرض عنصر الحالة. سيتم تمرير هذا العنصر إلى الدالة الإنشائية عند إعادة إنشاء worklet. عند الإنشاء الأوّلي، ستكون قيمة هذا المَعلمة undefined
.
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
في كل مرة تعيد فيها تحميل هذا العرض التوضيحي، ستكون لديك فرصة بنسبة %50/50 لتحديد اتجاه دوران المربع. إذا كان المتصفّح سيوقف عمل وحدة worklet وينقلها إلى سلسلة محادثات مختلفة، سيتم إجراء مكالمة Math.random()
أخرى عند الإنشاء، ما قد يؤدي إلى تغيير مفاجئ في الاتجاه. لضمان عدم حدوث ذلك، نعرض اتجاهات الصور المتحركة المحدّدة عشوائيًا على أنّها حالة ونستخدمها في الدالة الإنشائية، إذا تم توفيرها.
الاستفادة من الزمكان: ScrollTimeline
كما أوضح القسم السابق، تتيح لنا AnimationWorklet تحديد كيفية تأثير تقدّم المخطط الزمني في تأثيرات الصورة المتحركة بشكل آلي. ولكن حتى الآن، كان المخطط الزمني لدينا دائمًا document.timeline
، وهو
يتتبّع الوقت.
تتيح لك ScrollTimeline
إمكانات جديدة وتسمح لك بتشغيل الرسوم المتحركة من خلال التمرير بدلاً من الوقت. سنعيد استخدام أول
Worklet "تمرير" أنشأناه في هذا
العرض التوضيحي:
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
بدلاً من تمرير document.timeline
، سننشئ ScrollTimeline
جديدًا.
كما قد تتوقّع، لا تستخدم ScrollTimeline
الوقت، بل تستخدم موضع التمرير الخاص بـ scrollSource
لضبط currentTime
في Worklet. يعني الانتقال إلى أعلى الصفحة (أو إلى اليسار) ضبط currentTime = 0
، بينما يعني الانتقال إلى أسفل الصفحة (أو إلى اليمين) ضبط currentTime
على timeRange
. إذا مرّرت المربّع في هذا
العرض التوضيحي، يمكنك
التحكّم في موضع المربّع الأحمر.
إذا أنشأت ScrollTimeline
باستخدام عنصر لا يمكن التمرير فيه، ستكون قيمة currentTime
في المخطط الزمني NaN
. لذلك، يجب أن تكون دائمًا مستعدًا لاستخدام NaN
كـ currentTime
، خاصةً مع مراعاة التصميم المتجاوب. وغالبًا ما يكون من المنطقي ضبط القيمة التلقائية على 0.
لطالما كان الربط بين الرسوم المتحركة وموضع التمرير من الميزات المطلوبة، ولكن لم يتم تحقيقها بهذا المستوى من الدقة (باستثناء الحلول البديلة التي تعتمد على CSS3D). تتيح Animation Worklet تنفيذ هذه التأثيرات بطريقة مباشرة مع الحفاظ على مستوى عالٍ من الأداء. على سبيل المثال: توضّح هذه التجربة التي تعرض تأثير التمرير المنظر أنّ تحديد رسم متحرك مستند إلى التمرير يتطلّب الآن بضعة أسطر فقط.
الخيارات المتقدّمة
Worklets
Worklets هي سياقات JavaScript ذات نطاق معزول وواجهة برمجة تطبيقات صغيرة جدًا. تتيح مساحة واجهة برمجة التطبيقات الصغيرة إجراء تحسينات أكثر فعالية من المتصفّح، خاصةً على الأجهزة ذات المواصفات المنخفضة. بالإضافة إلى ذلك، لا ترتبط Worklet بحلقة أحداث معيّنة، ولكن يمكن نقلها بين سلاسل التعليمات حسب الضرورة. ويُعدّ ذلك مهمًا بشكل خاص بالنسبة إلى AnimationWorklet.
Compositor NSync
من المحتمل أنّك تعرف أنّ بعض خصائص CSS يمكن تحريكها بسرعة، في حين أنّ البعض الآخر لا يمكن تحريكه بسرعة. بعض السمات تحتاج فقط إلى بعض العمل على وحدة معالجة الرسومات (GPU) لتحريكها، بينما تجبر سمات أخرى المتصفح على إعادة تخطيط المستند بأكمله.
في Chrome (كما هو الحال في العديد من المتصفحات الأخرى)، لدينا عملية تُعرف باسم "المُركِّب"، وهي مسؤولة عن ترتيب الطبقات والملمس، ثم استخدام وحدة معالجة الرسومات لتعديل الشاشة بأسرع ما يمكن، ويُفضّل أن يكون ذلك بأسرع ما يمكن للشاشة أن تتعدّل (عادةً 60 هرتز). استنادًا إلى خصائص CSS التي يتم تحريكها، قد يحتاج المتصفّح إلى أن يقوم برنامج التجميع بعمله، بينما تحتاج خصائص أخرى إلى تشغيل التنسيق، وهي عملية لا يمكن إلا للخيط الرئيسي تنفيذها. استنادًا إلى الخصائص التي تخطّط لتحريكها، سيتم ربط Animation Worklet بالخيط الرئيسي أو سيتم تشغيلها في خيط منفصل بالتزامن مع برنامج التجميع.
Slap on the wrist
عادةً ما تكون هناك عملية واحدة فقط لتجميع الصور تتم مشاركتها على الأرجح بين علامات تبويب متعددة، لأنّ وحدة معالجة الرسومات هي مورد مطلوب بشدة. إذا تم حظر برنامج التجميع بطريقة ما، سيتوقف المتصفّح بأكمله ولن يستجيب لإدخالات المستخدم. ويجب تجنُّب ذلك بأي ثمن. ماذا يحدث إذا لم يتمكّن Worklet من تقديم البيانات التي يحتاجها برنامج التركيب في الوقت المناسب لعرض الإطار؟
في حال حدوث ذلك، يُسمح لبرنامج العمل الصغير "بالتأخّر" وفقًا للمواصفات. ويكون هذا الإطار متأخرًا عن إطار التركيب، ويُسمح لإطار التركيب بإعادة استخدام بيانات الإطار الأخير للحفاظ على معدّل عرض اللقطات. من الناحية المرئية، سيبدو ذلك وكأنّه تشوّش، ولكن الفرق الكبير هو أنّ المتصفّح سيظل يستجيب لإدخال المستخدم.
الخاتمة
تتضمّن AnimationWorklet العديد من الجوانب والمزايا التي تقدّمها للويب. وتتمثّل المزايا الواضحة في توفير المزيد من التحكّم في الرسوم المتحركة وطرق جديدة لتشغيلها، ما يتيح مستوى جديدًا من الدقة المرئية على الويب. لكن يتيح لك تصميم واجهات برمجة التطبيقات أيضًا جعل تطبيقك أكثر مرونة في التعامل مع مشاكل التشويش، مع إمكانية الوصول إلى جميع الميزات الجديدة في الوقت نفسه.
تتوفّر Animation Worklet في الإصدار Canary، ونسعى إلى إطلاقها في الإصدار التجريبي من المصدر مع الإصدار 71 من Chrome. نتطلّع إلى تجارب الويب الجديدة الرائعة التي ستنشئها وإلى معرفة ما يمكننا تحسينه. يتوفّر أيضًا polyfill الذي يمنحك واجهة برمجة التطبيقات نفسها، ولكنّه لا يوفّر عزل الأداء.
ضَع في اعتبارك أنّ CSS Transitions وCSS Animations لا تزالان خيارَين صالحَين، ويمكن أن تكونا أبسط بكثير بالنسبة إلى الحركات الأساسية. ولكن إذا كنت بحاجة إلى استخدام تأثيرات أكثر تعقيدًا، يمكنك الاستعانة بواجهة برمجة التطبيقات AnimationWorklet.