CSS Deep-Dive - trim3d() للحصول على شريط تمرير مخصّص ومثالي للإطارات

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

TL;DR

هل لا تهمّك التفاصيل الصغيرة؟ هل تريد فقط الاطّلاع على العرض الترويجي لـ Nyan cat والحصول على المكتبة؟ يمكنك العثور على رمز العرض الترويجي في مستودع GitHub.

LAM;WRA (Long and mathematical; will read anyways)

منذ فترة، أنشأنا شريط تمرير مرئيًا (هل قرأت هذه المقالة؟ إنّه جيد جدًا ويستحق وقتك. من خلال دفع العناصر للخلف باستخدام عمليات التحوّل المجسمة في CSS، تم نقل العناصر بسرعة أقل من سرعة الانتقال الفعلية.

ملخّص

لنبدأ بملخص عن آلية عمل شريط التمرير المتغير الإيحاء.

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

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

الخطوة 0: ما الذي نريد فعله؟

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

الخطوة 1: الرجوع إلى الخطوة السابقة

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

للحصول على أي نوع من الإسقاط المنظوري بالمعنى الرياضي، من المرجّح أن تنتهي باستخدام الإحداثيات المتجانسة. لن أشرح بالتفصيل ماهية هذه الإحداثيات وسبب استخدامها، ولكن يمكنك اعتبارها إحداثيات ثلاثية الأبعاد مع إحداثية رابعة إضافية تُسمى w. يجب أن يكون هذا الإحداثي 1 ما لم تكن تريد حدوث تشويه في المنظور. ليس علينا القلق بشأن تفاصيل w لأنّنا لن نستخدم أي قيمة أخرى غير 1. وبالتالي، أصبحت جميع النقاط من الآن فصاعدًا متجهات رباعية الأبعاد [x, y, z, w=1]، وبالتالي يجب أن تكون المصفوفات بمقاس 4×4 أيضًا.

يمكنك ملاحظة أنّ CSS تستخدم إحداثيات متجانسة في باطنها عند تحديد مصفوفات 4×4 في خاصية التحويل باستخدام الدالة matrix3d(). يأخذ matrix3d 16 وسيطة (لأنّ المصفوفة هي 4×4)، مع تحديد عمود تلو الآخر. وبالتالي، يمكننا استخدام هذه الدالة لتحديد عمليات الدوران والترجمة يدويًا وما إلى ذلك، ولكن ما تسمح لنا به أيضًا هو التلاعب بإحداثي w.

قبل أن نتمكّن من استخدام matrix3d()، نحتاج إلى سياق ثلاثي الأبعاد، لأنّه بدون سياق ثلاثي الأبعاد لن يكون هناك أي تشويه للمنظور ولن تكون هناك حاجة إلى إحداثيات متجانسة. لإنشاء سياق ثلاثي الأبعاد، نحتاج إلى حاوية تحتوي على perspective وبعض العناصر التي يمكننا تحويلها في المساحة الثلاثية الأبعاد التي تم إنشاؤها حديثًا. على سبيل المثال:

جزء من رمز CSS يشوه div باستخدام سمة
    perspective في CSS

يعالج محرّك CSS العناصر داخل حاوية المنظور على النحو التالي:

  • حوِّل كل زاوية (نقطة) من العنصر إلى إحداثيات متجانسة [x,y,z,w]، بالنسبة إلى حاوية المنظور.
  • طبِّق جميع عمليات التحويل للعنصر كمصفوفات من اليمين إلى اليسار.
  • إذا كان عنصر المنظور قابلاً للتقديم أو الإيقاف، طبِّق مصفوفة للتقديم أو الإيقاف.
  • طبِّق مصفوفة المنظور.

مصفوفة التمرير هي عملية ترجمة على طول المحور ص. إذا نتقلنا للأسفل بمقدار 400 بكسل، يجب نقل جميع العناصر بمقدار 400 بكسل. مصفوفة المنظور هي مصفوفة "تجذب" النقاط إلى أقرب نقطة تلاشي كلما كانت أبعد في الفضاء الثلاثي الأبعاد. ويؤدي ذلك إلى تحقيق كلا التأثيرَين، وهما جعل الأشياء تبدو أصغر عندما تكون في الخلف، وجعلها "تتحرك ببطء أكبر" عند ترجمتها. وبالتالي، إذا تم دفع عنصر للخلف، سيؤدي التحويل بمقدار 400 بكسل إلى تحرّك العنصر بمقدار 300 بكسل فقط على الشاشة.

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

يقع المربّع داخل حاوية منظورية ذات قيمة p لسمة perspective ، ولنفترض أنّ الحاوية قابلة للتقديم أو الإيقاف وأنّه تمّ التقديم بقيمة n بكسل.

مصفوفة المنظور مضروبة في مصفوفة التمرير مضروبة في مصفوفة تحويل العناصر
  تساوي مصفوفة الهوية 4 × 4 مع ناقص واحد على p في الصف الرابع
  العمود الثالث مضروبًا في مصفوفة الهوية 4 × 4 مع ناقص n في
  الصف الثاني العمود الرابع مضروبًا في مصفوفة تحويل العناصر.

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

بالنسبة إلى شريط التمرير، نريد العكس، أي أن يتحرك العنصر للأسفل عند الانتقال للأسفل. في ما يلي حيلة يمكننا استخدامها: قلب الإحداثي w لزوايا المربّع. إذا كان الإحداثي w هو -1، سيتم تطبيق جميع الترجمات في الاتجاه المعاكس. كيف يمكننا تنفيذ ذلك؟ يهتم محرّك CSS بتحويل زوايا المربّع إلى إحداثيات متماثلة، ويضبط w على 1. حان وقت تألق matrix3d().

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

لن تؤدي هذه المصفوفة إلى أي شيء سوى إبطال w. لذلك، عندما يحوّل محرّك CSS كلّ زاوية إلى متجه من النوع [x,y,z,1]، ستحوّله المصفوفة إلى [x,y,z,-1].

مصفوفة الهوية 4 × 4 التي تحتوي على 1 ناقص p في الصف الرابع
  العمود الثالث مضروبة في مصفوفة الهوية 4 × 4 التي تحتوي على n ناقص في
  الصف الثاني العمود الرابع مضروبة في مصفوفة الهوية 4 × 4 التي تحتوي على 1 ناقص في
  الصف الرابع العمود الرابع مضروبة في المتّجه الرباعي الأبعاد x وy وz و1 يساوي مصفوفة الهوية
  4 × 4 التي تحتوي على 1 ناقص p في الصف الرابع العمود الثالث،
  n ناقص في الصف الثاني العمود الرابع و1 ناقص في
  الصف الرابع العمود الرابع يساوي المتّجه الرباعي الأبعاد x وy زائد n وz ناقص z على
  p ناقص 1.

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

ومع ذلك، إذا وضعنا هذه المصفوفة في المثال، لن يتم عرض العنصر. ويعود السبب في ذلك إلى أنّ مواصفات CSS تتطلّب أن يمنع أي نقطة عندها w < 0 عرض العنصر. وبما أنّ الإحداثي z لدينا هو 0 حاليًا، وp هو 1، سيكون w -1.

لحسن الحظ، يمكننا اختيار قيمة z. للتأكّد من أنّنا سنحصل على w=1، نحتاج إلى ضبط z = -2.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

ها قد عاد المربّع.

الخطوة 2: تحريك العنصر

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

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

والآن، حرِّك المربّع. يتحرك المربّع الأحمر للأسفل.

الخطوة 3: تحديد حجم العنصر

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

يتألف شريط التمرير عادةً من "مؤشر" و"مسار"، مع العلم أنّ المسار ليس مرئيًا دائمًا. يتناسب ارتفاع إصبع الإبهام مباشرةً مع مقدار المحتوى المرئي.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight هو ارتفاع العنصر القابل للتقديم أو الإيقاف، في حين أنّ scroller.scrollHeight هو الارتفاع الإجمالي للمحتوى القابل للتقديم أو الإيقاف. scrollerHeight/scroller.scrollHeight هو جزء المحتوى الذي يظهر. يجب أن تكون نسبة المساحة الرأسية التي تغطيها الصورة المصغّرة مساوية لنسبة المحتوى المرئي:

ارتفاع نقطة نمط مؤشر التمرير مقسومًا على ارتفاع شريط التمرير يساوي ارتفاع شريط التمرير مقسومًا على ارتفاع نقطة التمرير في شريط التمرير إذا كان ارتفاع نقطة نمط مؤشر التمرير مقسومًا على ارتفاع شريط التمرير يساوي ارتفاع شريط التمرير مضروبًا في ارتفاع شريط التمرير مقسومًا على ارتفاع نقطة التمرير في شريط التمرير.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

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

إنّ المعلومات المهمة هي أنّنا نريد أن تتم محاذاة الحافة السفلية للإصبع مع الحافة السفلية للعنصر القابل للتنقّل عند الانتقال إلى أسفل الشاشة. بعبارة أخرى، إذا تحركنا scroller.scrollHeight - scroller.height بكسل، نريد أن يتم نقل إصبع الإبهام بمقدار scroller.height - thumb.height. لكل بكسل من شريط التمرير، نريد أن يتحرك إبهمنا جزءًا من بكسل:

يساوي المُعامل ارتفاع نقطة التمرير مطروحًا منه ارتفاع نقطة الإبهام على ارتفاع نقطة التمرير
  مطروحًا منه ارتفاع نقطة التمرير.

وهذا هو عامل التوسيع. نحتاج الآن إلى تحويل عامل التكبير إلى ترجمة على طول محور z، وهو ما سبق أن فعلناه في مقالة التمرير المتغير وفقًا لالقسم ذي الصلة في المواصفات: يساوي عامل القياس p/(p − z). يمكننا حلّ هذه المعادلة من أجل z لتحديد المقدار الذي نحتاجه لنقل إصبع الإبهام على طول محور z. يُرجى مراعاة أنّه بسبب التلاعب في إحداثيات w، علينا ترجمة -2px إضافية على طول z. يُرجى العلم أيضًا أنّ عمليات تحويل العنصر تُطبَّق من اليمين إلى اليسار، ما يعني أنّه لن يتم قلب كل عمليات النقل قبل المصفوفة الخاصة، ولكن سيتم قلب كل عمليات النقل بعد المصفوفة الخاصة. لنضع قواعد مكتوبة لهذا الإجراء.

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

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

ماذا عن أجهزة iOS؟

مرحبًا، صديقي القديم Safari على iOS. كما هو الحال مع التمرير بزاوية متناوبة، نواجه مشكلة هنا. بما أنّنا ننتقل للأعلى أو للأسفل في عنصر، يجب تحديد -webkit-overflow-scrolling: touch، ولكنّ ذلك يؤدي إلى تسطيح العناصر الثلاثية الأبعاد ويتوقف تأثير التمرير بالكامل. لقد حللنا هذه المشكلة في شريط التمرير المتغير المظهر من خلال رصد Safari على نظام التشغيل iOS والاعتماد على position: sticky كحل بديل، وسنفعل الشيء نفسه بالضبط هنا. يمكنك الاطّلاع على مقالة التماثل لتجديد ذاكرتك.

ماذا عن شريط التمرير في المتصفّح؟

في بعض الأنظمة، سنتعامل مع شريط تمرير أصلي دائم. في السابق، كان لا يمكن إخفاء شريط التمرير (إلا باستخدام عنصر اختيار زائف غير عادي). لذا، لإخفائه، علينا اللجوء إلى بعض أساليب الاختراق (غير الحسابية). نلفّ عنصر التمرير في حاوية باستخدام overflow-x: hidden ونجعل عنصر التمرير أوسع من الحاوية. أصبح شريط التمرير الأصلي للمتصفّح غير مرئي الآن.

فين

بعد جمع كل هذه العناصر معًا، يمكننا الآن إنشاء شريط التمرير المخصّص الذي يناسب الإطار تمامًا، مثل الشريط المعروض في العرض الترويجي لتطبيق Nyan cat.

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

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