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

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

الملخّص

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

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

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

ملخّص

لنبدأ بملخّص طريقة عمل شريط تمرير اختلاف المنظر.

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

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

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

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

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

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

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

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

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

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

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

ومع ذلك، إذا وضعنا هذه المصفوفة في المثال فقط، لن يتم عرض العنصر. وذلك لأن مواصفات CSS تتطلب أن يتم حظر عرض العنصر إذا كان أي رأس به w < 0. وبما أن إحداثي z تساوي حاليًا 0 وص 1 تساوي 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 هو الجزء المرئي من المحتوى. يجب أن تكون نسبة المساحة الرأسية التي يغطيها الإبهام مساوية لنسبة المحتوى المرئي:

ارتفاع نقطة نمط نقطة الإبهام فوق scrollerHeight يساوي ارتفاع شريط التمرير فوق نقطة التمرير وارتفاع نقطة التمرير إذا كان ارتفاع نقطة نمط نقطة الإبهام يساوي ارتفاع أشرطة التمرير مضروبًا ارتفاع شريط التمرير فوق ارتفاع نقطة التمرير.
<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 ونجعل عنصر التمرير أعرض من الحاوية. شريط التمرير الأصلي في المتصفح لا يظهر الآن.

زعانف

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

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

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