CSS Deep-Dive – matrix3d() לסרגל גלילה מותאם אישית עם פריים מושלם

סרגלי גלילה מותאמים אישית הם נדירים ביותר, והסיבה העיקרית לכך היא שסרגלי גלילה הם אחד מהרכיבים הנותרים באינטרנט שכמעט לא ניתנים לסגנון (אני רואה אותך, בוחר התאריכים). אפשר להשתמש ב-JavaScript כדי ליצור קוד משלכם, אבל זה יקר, רמת הדיוק נמוכה ויכול להיות שיהיו עיכובים. במאמר הזה נשתמש במטריצות CSS לא רגילות כדי ליצור פס גלילה מותאם אישית שלא מחייב JavaScript בזמן הגלילה, אלא רק קוד הגדרה.

אמ;לק

לא אכפת לך מהדברים הקטנים? רוצה פשוט לצפות בהדגמה של חתול ניאן ולקבל את הספרייה? תוכלו למצוא את קוד ההדגמה במאגר GitHub שלנו.

LAM;WRA (ארוך ומתמטי, יקרא בכל זאת)

לפני זמן מה, יצרנו גלילה מסוג פרלקס (האם קראת את המאמר הזה? זה ממש טוב, היה שווה את הזמן!). דחיפת רכיבים לאחור באמצעות טרנספורמציות תלת-ממדיות של CSS גורמת לכך שהרכיבים זזים לאט יותר ממהירות הגלילה שלנו בפועל.

Recap

נתחיל בסיכום של אופן הפעולה של פס הגלילה של הפרלקס.

כפי שאפשר לראות באנימציה, השגנו את אפקט הפרלקס על ידי דחיפת רכיבים 'אחורה' במרחב בתלת-ממד, לאורך ציר ה-Z. למעשה, גלילה במסמך היא תרגום לאורך ציר ה-Y. כך שאם נגלול למטה ב-100 פיקסלים, למשל, כל רכיב יתורגם למעלה ב-100 פיקסלים. זה נכון לגבי כל הרכיבים, גם אלה שנמצאים "יותר לאחור". אבל בגלל שהם רחוקים יותר מהמצלמה, התנועה הגלויה שלהם במסך תהיה פחות מ-100 פיקסלים, מה שיניב את אפקט הפרלקס הרצוי.

כמובן, אם מזיזים רכיב בחזרה בחלל, גם הוא ייראה קטן יותר. אנחנו מתקנים את זה על ידי הגדלת הרכיב בחזרה. הבנו את החישוב המדויק כשיצרנו את מפתח הגלילה של פרלקס, אז לא אגדיר את כל הפרטים.

שלב 0: מה המטרה שלנו?

פסי גלילה. זה מה שאנחנו עומדים ליצור. אבל האם חשבתם אי פעם על מה שהם עושים? בהחלט לא. סרגלי גלילה מציינים כמה מהתוכן הזמין מופיע כרגע וכמה התקדמות התקדמתם כקוראים. אם גוללים למטה, גם סרגל הגלילה מציין שאתם מתקדמים לקראת הסוף. אם כל התוכן מתאים לאזור התצוגה, סרגל הגלילה מוסתר בדרך כלל. אם התוכן גדול פי 2 מהגובה של אזור התצוגה, סרגל הגלילה ימלא 1⁄2 מהגובה של אזור התצוגה. תוכן ששווה פי 3 מהגובה של אזור התצוגה משנה את גודל סרגל הגלילה ל-1⁄3 מאזור התצוגה וכו'. אפשר לראות את הדפוס. במקום לגלול, אפשר גם ללחוץ ולגרור את סרגל הגלילה כדי לנוע מהר יותר באתר. כמות ההתנהגות מפתיעה לרכיב לא בולט כמו זה. בואו נילחם בקרב אחד בכל פעם.

שלב 1: שימוש הפוך

בסדר, נוכל לגרום לרכיבים לנוע לאט יותר ממהירות הגלילה באמצעות טרנספורמציות תלת-ממדיות של CSS, כפי שמתואר במאמר הגלילה הפרלקס. אפשר גם להפוך את הכיוון? מסתבר שאנחנו יכולים, וזו הדרך שלנו לבנות סרגל גלילה מושלם ומותאם אישית. כדי להבין איך זה עובד, קודם נעבור על כמה יסודות של תלת-ממד ב-CSS.

במובן המתמטי של היטל פרספקטיבה מכל סוג שהוא, סביר להניח שבסופו של דבר תשתמשו בקואורדינטות הומוגניות. אני לא אפרט על מהן ולמה הן פועלות, אבל אפשר לחשוב עליהן כמו על קואורדינטות תלת-ממדיות עם קואורדינטה רביעית נוספת שנקראת w. הקואורדינטה הזו צריכה להיות 1, אלא אם רוצים שיהיה עיוות פרספקטיבה. אנחנו לא צריכים לדאוג לפרטים של w כי לא נשתמש בערך אחר מ-1. לכן כל הנקודות הן מעכשיו בווקטורים דו ממדיים [x, y, z, w=1] וכתוצאה מכך המטריצות צריכות להיות גם הן 4x4.

אירוע אחד שבו אפשר לראות ש-CSS משתמש בקואורדינטות הומוגניות בחלק התחתון הוא כשמגדירים מטריצות 4x4 משלכם בנכס טרנספורמציה באמצעות הפונקציה matrix3d(). הפונקציה matrix3d מקבלת 16 ארגומנטים (כי המטריצה היא 4x4), ומציינת עמודה אחת אחרי השנייה. לכן נוכל להשתמש בפונקציה הזו כדי לציין באופן ידני סיבובים, תרגומים וכו'. אבל היא גם מאפשרת לנו להתעסק עם קואורדינטת ה-W הזו!

לפני שנוכל להשתמש ב-matrix3d(), אנחנו צריכים הקשר תלת-ממדי – כי בלי הקשר תלת-ממדי לא יהיה עיוות של נקודת המבט או צורך בקואורדינטות הומוגניות. כדי ליצור הקשר תלת-ממדי, אנחנו צריכים קונטיינר עם perspective וכמה רכיבים בתוכו שנוכל לבצע טרנספורמציה במרחב התלת-ממדי החדש שנוצר. לדוגמה: example

קטע של קוד CSS שמעוות div באמצעות מאפיין הפרספקטיבה של
 ה-CSS.

הרכיבים בתוך קונטיינר של נקודת מבט מעובדים על ידי מנוע ה-CSS באופן הבא:

  • אפשר להפוך כל פינה (קודקוד) של רכיב לקואורדינטות הומוגניות [x,y,z,w], ביחס לקונטיינר של הפרספקטיבה.
  • מחילים את כל השינויים של היסוד כמטריצות ימין לשמאל.
  • אם ניתן לגלול את רכיב הפרספקטיבה, החילו מטריצה של גלילה.
  • החלה של מטריצת הפרספקטיבה.

מטריצת הגלילה היא תרגום לאורך ציר ה-y. אם גוללים למטה ב-400 פיקסלים, צריך להעביר את כל הרכיבים למעלה ב-400 פיקסלים. מטריצת הפרספקטיבה היא מטריצה ש"מושכת" נקודות קרוב יותר לנקודת ההיעלמות, רחוק יותר במרחב התלת-ממדי. כך ניתן להשיג את שתי ההשפעות של הקטנת האובייקט כאשר הם רחוקים יותר, והם גם גורמים להם "לנוע לאט יותר" כשמתורגמים. לכן, אם אלמנט נדחף לאחור, תרגום של 400 פיקסלים יגרום לרכיב להזיז רק 300 פיקסלים על המסך.

אם אתם רוצים לדעת את כל הפרטים, כדאי לקרוא את spec של מודל הטרנספורמציה של שירות ה-CSS, אבל לצורך המאמר הזה, פישטתי את האלגוריתם שלמעלה.

התיבה שלנו נמצאת בתוך קונטיינר של פרספקטיבה עם הערך p במאפיין perspective, ונניח שאפשר לגלול את הקונטיינר שגולל למטה ב-n פיקסלים.

מטריצת פרספקטיבה כפול מטריצת גלילה כפול מטריצת טרנספורמציה של אלמנט
  שווה למטריצת זהות 4 על ארבע עם מינוס 1 מעל 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].

מטריצת זהות 1 על ארבע עם מינוס אחד מעל p בשורה הרביעית
 בעמודה שלישית כפול מטריצת זהות ארבע על ארבע עם מטריצת זהות מינוס n בשורה השנייה, עמודה רביעית כפול ארבע על ארבע מטריצת זהות עם מינוס אחד בשורה הרביעית כפול וקטור ממדי x, y, z, 1 שווה ל-4 על מטריצת זהות

ציינתי שלב ביניים כדי להראות את ההשפעה של מטריצת הטרנספורמציה של הרכיבים שלנו. אם לא מרגישים בנוח עם מתמטיקה מטריצות, זה בסדר. ברגע האאורקה הוא שבשורה האחרונה אנחנו מוסיפים את היסט הגלילה 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. לכל פיקסל של גלילה, אנחנו רוצים שהאגודל שלנו יזיז שבר של פיקסל:

הגורם שווה לגובה של נקודת הגלילה פחות הגובה של הגלילה מעל הגלילה נקודה
  גובה הגלילה פחות הגובה של הגלילה.

זה הגורם שלנו להתאמה לעומס (scaling). עכשיו עלינו להמיר את הגורם לשינוי קנה מידה לתרגום לאורך ציר ה-z, שכבר עשינו במאמר הגלילה הפרלקס. לפי הקטע הרלוונטי במפרט: גורם ההתאמה שווה ל-p/(p – 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 יהפוך להרבה יותר קל אפקטים של גלילה כך שיהיו מושלמים לגלילה.