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

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

الملخّص

ألا تهتم بالتفاصيل الصغيرة؟ هل تريد فقط إلقاء نظرة على العرض التوضيحي حول قطة ناين والحصول على المكتبة؟ ويمكنك العثور على الرمز التجريبي للعرض التوضيحي في مستودع GitHub.

LAM؛ WRA (معلومات طويلة ورياضية؛ ستُقرأ على أي حال)

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

ملخّص

لنبدأ بتلخيص طريقة عمل التمرير المتساوي.

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

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

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

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

الخطوة 1: وضعها في الاتجاه العكسي

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

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

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

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

جزء من رمز CSS يشوّه علامة div باستخدام سمة منظور CSS.

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

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

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

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

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

مصفوفة المنظور مضروبة في مصفوفة تمرير في عنصر تحويل المصفوفة
 يساوي أربعة في 4 مصفوفة هوية بسالب واحد على p في الصف الرابع
 العمود الثالث مضروبًا في أربعة في أربعة مصفوفة هوية وسالب 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].

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

لقد أدرجت خطوة وسيطة لإظهار تأثير مصفوفة تحويل العنصر. إذا لم تكن معتادًا على حساب المصفوفة، فلا بأس. في لحظة Eureka هي أنه في السطر الأخير ننتهي بإضافة إزاحة التمرير 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. ولكن ضع في اعتبارك أنه بسبب الحيل التي نجريها أثناء التنسيق، نحتاج إلى ترجمة عنصر -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؟

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

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

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

زعنفة

من خلال جمع كل هذه العناصر معًا، يمكننا الآن إنشاء شريط تمرير مخصص ومثالي في الإطارات، مثل الذي في العرض التوضيحي لقطة ناينة.

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

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