إنشاء صور متحركة قابلة للتوسيع والتصغير

ستيفن ماكجروير
ستيفن ماكجروير

الملخّص

استخدام تحويلات المقياس عند المقاطع المتحركة. يمكنك منع الأطفال من التمدّد والانحراف أثناء الرسوم المتحركة عن طريق تحجيمهم.

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

خذ، على سبيل المثال، قائمة موسعة:

وتكون بعض خيارات إنشاء هذا النموذج أكثر أداءً من غيرها.

سيئ: تحريك العرض والارتفاع في عنصر حاوية

يمكنك أن تتخيل استخدام جزء من CSS لتحريك العرض والارتفاع في عنصر الحاوية.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

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

غير صحيح: استخدام خصائص مقطع CSS أو مسار المقطع

يمكن أن يكون استخدام السمة clip (المتوقّفة نهائيًا) بديلاً لتحريك width وheight لتحريك تأثير التوسيع والتصغير. ويمكنك استخدام clip-path بدلاً من ذلك. ومع ذلك، يكون استخدام clip-path أقل دعمًا من clip. مع ذلك، تم إيقاف واجهة برمجة التطبيقات clip نهائيًا. نحو اليمين لكن لا تقلق، فهذا ليس الحل الذي تريده على أي حال!

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

بالرغم من أنّه أفضل من إضافة تأثيرات حركية إلى width وheight لعنصر القائمة، فإنّ الجانب السلبي لهذا النهج هو أنّه لا يزال يؤدي إلى عرض محتوى الصفحة. وإذا اتّبعت هذه السمة أيضًا، تتطلب السمة clip أن يكون العنصر الذي تعمل عليه في موضعه الأساسي أو ثابتًا، ما قد يتطلّب بعض المشاكل الإضافية.

جيد: موازين متحركة

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

والجانب السلبي لهذا المنهج، مثل معظم العناصر المتعلقة بأداء العرض، هو أنه يتطلب القليل من الإعداد. لكن الأمر يستحق ذلك تمامًا!

الخطوة 1: حساب حالتي البداية والنهاية

من خلال نهج يستخدم الرسوم المتحركة للمقياس، تتمثل الخطوة الأولى في قراءة العناصر التي تخبرك بحجم القائمة التي يجب أن تكون عند تصغيرها وعند توسيعها. قد يكون الأمر أنه في بعض الحالات لا يمكنك الحصول على كل من هذه المعلومات دفعة واحدة، وأنك تحتاج - على سبيل المثال - إلى تبديل بعض الفئات لتتمكن من قراءة الحالات المختلفة للمكون. ومع ذلك، عليك توخي الحذر، لأنّ getBoundingClientRect() (أو offsetWidth وoffsetHeight) يفرضان على المتصفِّح تشغيل الأنماط وتمريرات التنسيقات في حال تم تغيير الأنماط منذ آخر مرة تم تشغيلها فيها.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

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

ولكن، مهلاً! من المؤكد أن هذا من شأنه أن يزيد من حجم محتويات القائمة أيضًا، أليس كذلك؟ حسنًا، كما ترى أدناه، نعم.

إذًا، ما الذي يمكنك فعله حيال ذلك؟ يمكنك تطبيق counter- على المحتوى. على سبيل المثال، إذا تم تصغير الحاوية إلى 1/5 حجمها العادي، يمكنك تغيير حجم المحتوى counter- بمقدار 5 أضعاف لتجنّب ضغط المحتوى. وهناك أمران يجب ملاحظتهما بشأن ذلك:

  1. والتحويل العكسي هو أيضًا عملية على نطاق واسع. وهذا أمر جيد لأنه يمكن تسريعه أيضًا، تمامًا مثل الرسوم المتحركة في الحاوية. قد تحتاج إلى التأكد من أن العناصر المتحركة تحصل على طبقة التركيب الخاصة بها (تمكين وحدة معالجة الرسومات من أجل المساعدة)، ولذلك يمكنك إضافة will-change: transform إلى العنصر، أو backface-visiblity: hidden إذا كنت تريد إتاحة استخدام المتصفحات القديمة.

  2. يجب حساب التحويل العكسي لكل إطار. هذا هو المكان الذي يمكن أن تزداد فيه الأمور تعقيدًا، فبافتراض أن الرسوم المتحركة تكون في CSS وتستخدم وظيفة التخفيف، يجب مصادفة التخفيف نفسه عند تحريك التحويل العكسي. ومع ذلك، فإنّ حساب المنحنى العكسي لـ cubic-bezier(0, 0, 0.3, 1) مثلاً ليس بالوضوح المطلوب.

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

الخطوة الثانية: إنشاء صور متحركة بتنسيق CSS بسرعة

يكمن الحل، الذي قد يبدو غريبًا في البداية، في إنشاء صورة متحركة ضمن إطار رئيسي باستخدام وظيفة التخفيف الخاصة بنا ديناميكيًا وإدخالها في الصفحة لاستخدامها من خلال القائمة. (شكرًا جزيلاً لمهندس Chrome "روبرت فلاك" على الإشارة إلى ذلك!) الفائدة الأساسية لهذا الأمر هي أنه يمكن تشغيل صورة متحركة ذات إطار رئيسي تعمل على تغيير التحويلات في المُنشئ، ما يعني أنها لا تتأثر بالمهام في سلسلة التعليمات الرئيسية.

لإنشاء رسم متحرك للإطار الرئيسي، ننتقل من 0 إلى 100 ونحسب قيم المقياس المطلوبة للعنصر ومحتواه. ويمكن تلخيصها في سلسلة يمكن إدخالها في الصفحة كعنصر نمط. سيؤدي إدخال الأنماط إلى تمرير "إعادة حساب الأنماط" على الصفحة، وهو عمل إضافي يتعين على المتصفح القيام به، لكنه لن يفعل ذلك إلا مرة واحدة عند تشغيل المكوِّن.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

قد يتساءل الفضوليون إلى ما لا نهاية عن الدالة ease() في for-loop. يمكنك استخدام شيء مثل هذا لتعيين القيم من 0 إلى 1 إلى مكافئ سهل.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

يمكنك أيضًا استخدام بحث Google لتحديد الشكل الذي ستظهر به. مفيد! إذا كنت بحاجة إلى معادلات أخرى للتخفيف، يمكنك مراجعة Tween.js by Soledad Penadés التي تتضمّن مجموعة كاملة من هذه المعادلات.

الخطوة 3: تفعيل "الصور المتحركة لصفحات الأنماط المتتالية" (CSS)

بعد إنشاء هذه الصور المتحركة ودمجها في الصفحة باستخدام JavaScript، تتمثل الخطوة الأخيرة في تبديل الفئات التي تمكّن الصور المتحركة.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

يؤدي هذا إلى تشغيل الرسوم المتحركة التي تم إنشاؤها في الخطوة السابقة. ونظرًا لأن الرسوم المتحركة المخبوزة قد تم تخفيفها مسبقًا، يجب ضبط وظيفة التوقيت على linear وإلا فإنك ستتحرك بين كل إطار رئيسي، ولكنها ستبدو غريبة جدًا!

عندما يتعلق الأمر بتصغير العنصر مرة أخرى، هناك خياران: تحديث صورة CSS المتحركة لتشغيلها بالعكس بدلاً من التقديم. سيعمل ذلك بشكل جيد، ولكن سيتم عكس "الإحساس" الذي تحدثه الصورة المتحركة، لذا إذا استخدمت منحنى إرخاء، سيتحرك الاتجاه العكسي في، مما سيجعله ركودًا. والحل الأنسب هو إنشاء زوج ثاني من الرسوم المتحركة لتصغير العنصر. يمكن إنشاؤها بالطريقة نفسها المستخدَمة في الرسوم المتحركة لتوسيع الإطار الرئيسي، ولكن مع تبديل قيمتَي البداية والنهاية.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

نسخة أكثر تقدمًا: الكشف الدائري

من الممكن أيضًا استخدام هذا الأسلوب لإنشاء رسوم متحركة دائرية للتوسيع والتصغير.

المبادئ هي نفسها إلى حد كبير مثل الإصدار السابق، حيث يمكنك قياس أحد العناصر وتعديل عناصره الثانوية المباشرة. في هذه الحالة، يحتوي العنصر الذي يتم رفع حجمه على border-radius بنسبة %50، ما يجعله دائريًا ومحاط بعنصر آخر يحتوي على overflow: hidden، ما يعني أنّ الدائرة لا تتسع خارج حدود العنصر.

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

يمكن العثور على رمز تأثير التوسيع الدائري في مستودع GitHub.

الاستنتاجات

إذًا إليك الأمر، طريقة لتنفيذ مقاطع متحركة ذات أداء جيد باستخدام تحويلات المقياس. في عالم مثالي، سيكون من الرائع أن نرى تسريع الرسوم المتحركة للمقاطع (هناك خطأ في Chromium لذلك من إنشاء "جيك أرشيبالد")، ولكن يجب أن تكون حذرًا عند الانتقال إلى clip أو clip-path، وعليك توخي الحذر عند إنشاء لقطات متحرّكة clip أو clip-path، وبالتأكيد تجنُّب إنشاء الصور المتحركة width أو height.

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

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

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

إذا أردت إلقاء نظرة على الرمز البرمجي لهذا التأثير، يمكنك إلقاء نظرة على نسخة واجهة المستخدم النموذجية على GitHub، وكالعادة، يُرجى إعلامنا برأيك في التعليقات أدناه.