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

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

אמ;לק

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

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

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

Recap

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

סגנון נקודות ה&#39;אגודל&#39; נקודה גובה מעל הגלילה הגובה שווה לגובה הגלילה
  מעל הגלילה נקודה גובה הגלילה אם ורק אם סגנון הנקודה האגודלית נקודה גובה
  שווה לגובה של גובה הגלילה כפול גובה הגלילה מעל הנקודות של הגלילה
  גובה.
<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 להבין כמה אנחנו צריכים לתרגם את האגודל שלנו לאורך ציר ה-z. אבל כדאי להמשיך חשוב לזכור שבגלל הקואורדינטה שלנו, אנחנו צריכים לתרגם -2px נוספים לאורך z. שימו לב שמבצעים גם טרנספורמציות של יסוד מימין לשמאל, כלומר כל התרגומים לפני המטריצה המיוחדת שלנו הפוך, כל התרגומים אחרי המטריצה המיוחדת שלנו יפעלו! קדימה codify את זה!

<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>

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

מה לגבי iOS?

אה, חבר ותיק, iOS Safari. כמו בגלילת פרלקס, אנחנו נתקלים כאן. מכיוון שאנחנו גוללים על רכיב מסוים, אנחנו צריכים לציין -webkit-overflow-scrolling: touch, אבל זה גורם להחלקה בתלת-ממד ולתצוגה אפקט הגלילה מפסיק לפעול. פתרנו את הבעיה הזו בפס ההזזה של פרלקס על ידי זיהוי iOS Safari והסתמכות על position: sticky כפתרון עקיף, נעשה כאן בדיוק את אותו הדבר. כדאי לעיין מאמר בנושא פרלקס כדי לרענן את הזיכרון.

מה לגבי סרגל הגלילה של הדפדפן?

במערכות מסוימות, נצטרך לטפל בסרגל גלילה מקורי וקבוע. בעבר, לא ניתן להסתיר את סרגל הגלילה (אלא אם פסאודו-בורר לא סטנדרטי). לכן כדי להסתיר אותו, עלינו להשתמש בהאקרים (ללא מתמטיקה). אנחנו כוללים רכיב גלילה בקונטיינר עם overflow-x: hidden והפוך רכיב גלילה רחב יותר מהמאגר. סרגל הגלילה המקורי של הדפדפן מחוץ לתצוגה.

Fin

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

אם לא ניתן לראות את חתול ניאן, סימן באג שמצאנו והגשנו במהלך יצירת ההדגמה (לחצו על האגודל כדי לגרום לחתול ניאן להופיע). Chrome ממש טוב במניעת עבודה מיותרת כמו ציור או אנימציה של דברים שלא מופיעים במסך. החדשות הרעות הן Matrix Shenanigans גורמות ל-Chrome לחשוב שה-GIF של חתול ניאן נמצא מחוץ למסך. אני מקווה שהבעיה תיפתר בקרוב.

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