סרגלי גלילה מותאמים אישית הם נדירים ביותר, והסיבה העיקרית לכך היא שסרגלי גלילה הם אחד מהרכיבים הנותרים באינטרנט שכמעט לא ניתנים לסגנון (אני רואה אותך, בוחר התאריכים). אפשר להשתמש ב-JavaScript כדי ליצור גרסת JavaScript משלכם, אבל זה יקר, איכות התצוגה נמוכה ויכול להיות שיהיו עיכובים. במאמר הזה נשתמש במטריצות CSS לא רגילות כדי ליצור גלילה מותאמת אישית שלא דורשת JavaScript בזמן הגלילה, אלא רק קוד הגדרה.
אמ;לק
לא אכפת לך מהדברים הקטנים? רוצים רק לצפות בהדגמה של Nyan cat ולקבל את הספרייה? הקוד של הדמו זמין במאגר GitHub שלנו.
LAM;WRA (Long and mathematical; will read anyways)
לפני זמן מה פיתחנו גלילה בפרלקס (קראתם את המאמר הזה? זה ממש טוב, היה שווה את הזמן!). כשדוחפים רכיבים לאחור באמצעות טרנספורמציות 3D ב-CSS, הרכיבים זזים לאט יותר ממהירות הגלילה בפועל.
Recap
נתחיל בסיכום של אופן הפעולה של גלילה בפרלקס.
כפי שמוצג באנימציה, השגנו את אפקט התלת-ממד על ידי דחיפת הרכיבים 'לאחור' במרחב תלת-ממדי, לאורך ציר Z. למעשה, גלילה במסמך היא תרגום לאורך ציר ה-Y. כך שאם גוללים למטה, למשל ב-100 פיקסלים, כל רכיב יתורגם למעלה ב-100 פיקסלים. זה רלוונטי לכל הרכיבים, גם לאלה שנמצאים 'מאחור יותר'. אבל כי הם רחוקים יותר מהמצלמה, התנועה שלהם שנצפית במסך תהיה פחות מ-100 פיקסלים, וכך יתקבל אפקט הפרלקס הרצוי.
כמובן, העברת רכיב אחורה במרחב תגרום לו להיראות קטן יותר, ואנחנו מתקנים את זה על ידי שינוי הגודל של הרכיב חזרה. חישבנו את המתמטיקה המדויקת כשפיתחנו את הגלילה בפרלקס, כך שאני לא אצטרך לחזור על כל הפרטים.
שלב 0: מה אנחנו רוצים לעשות?
סרגלי גלילה. זה מה שאנחנו הולכים ליצור. אבל האם אי פעם חשבתם מה הם עושים? בהחלט לא. סרגל הגלילה הוא אינדיקטור של כמה מהתוכן הזמין גלוי כרגע ושל ההתקדמות שלכם כקוראים. אם גוללים למטה, גם סרגל הגלילה מציין שאתם מתקדמים לקראת הסוף. אם כל התוכן נכנס לאזור התצוגה, פס ההזזה בדרך כלל מוסתר. אם גובה התוכן הוא פי 2 מגובה אזור התצוגה, פס ההזזה ממלא מחצית מגובה אזור התצוגה. תוכן שגובהו פי 3 מגובה אזור התצוגה גורם לסרגל הגלילה להתכוונן ל-⅓ מאזור התצוגה וכו'. אתם מבינים את התמונה. במקום לגלול, אפשר גם ללחוץ ולגרור את פס ההזזה כדי לעבור באתר מהר יותר. זו כמות מפתיעה של התנהגויות לאלמנט לא בולט כזה. נלחם במאבק אחד בכל פעם.
שלב 1: העברת הרכב למצב נסיעה לאחור
בסדר, אפשר לגרום לרכיבים לנוע לאט יותר ממהירות הגלילה באמצעות טרנספורמציות 3D של CSS, כפי שמתואר במאמר על גלילה בפרלקס. אפשר גם להפוך את הכיוון? מסתבר שאנחנו יכולים, וזו הדרך שלנו לבנות סרגל גלילה מושלם ומותאם אישית. כדי להבין איך זה עובד, קודם נלמד כמה עקרונות בסיסיים של CSS 3D.
במובן המתמטי של היטל פרספקטיבה מכל סוג שהוא, סביר להניח שבסופו של דבר תשתמשו בקואורדינטות הומוגניות. אני לא אפרט על מהן ולמה הן פועלות, אבל אפשר לחשוב עליהן כמו על קואורדינטות תלת-ממדיות עם קואורדינטה רביעית נוספת שנקראת w. הקואורדינטה הזו צריכה להיות 1, אלא אם רוצים שיהיה עיוות פרספקטיבה. אין לנו מה לדאוג לגבי הפרטים של w כי לא נשתמש בערך אחר מלבד 1. לכן, מעכשיו כל הנקודות הן וקטורים 4-ממדיים [x, y, z, w=1], ולכן גם המטריצות צריכות להיות בגודל 4x4.
מקרה אחד שבו אפשר לראות ש-CSS משתמש בקואורדינטות הומוגניות מתחת לפני השטח הוא כשמגדירים מטריצות 4x4 משלכם במאפיין טרנספורמציה באמצעות הפונקציה matrix3d()
. הפונקציה matrix3d
מקבלת 16 ארגומנטים (כי המטריצה היא 4x4), ומציינת עמודה אחת אחרי השנייה. לכן נוכל להשתמש בפונקציה הזו כדי לציין באופן ידני סיבובים, תרגומים וכו'. אבל היא גם מאפשרת לנו להתעסק עם קואורדינטת ה-W הזו!
כדי שנוכל להשתמש ב-matrix3d()
, אנחנו צריכים הקשר תלת-ממדי – כי בלי הקשר תלת-ממדי לא תהיה עיוות פרספקטיבה ולא תהיה צורך בקואורדינטות הומוגניות. כדי ליצור הקשר תלת-ממדי, אנחנו צריכים קונטיינר עם perspective
ורכיבים מסוימים בתוכו שאפשר לבצע בהם טרנספורמציה במרחב התלת-ממדי החדש שנוצר. לדוגמה: example
מנוע ה-CSS מעבד את הרכיבים בתוך קונטיינר תצוגה בפרספקטיבה באופן הבא:
- הופכים כל פינה (קודקוד) של רכיב לקואורדינטות הומוגניות
[x,y,z,w]
, ביחס לקונטיינר התצוגה בפרספקטיבה. - מחילים את כל הטרנספורמציות של הרכיב כמטריצות מימין לשמאל.
- אם ניתן לגלול את רכיב הפרספקטיבה, החילו מטריצה של גלילה.
- מחילים את מטריצת הפרספקטיבה.
מטריצת הגלילה היא תרגום לאורך ציר ה-y. אם גוללים למטה ב-400 פיקסלים, צריך להזיז למעלה את כל הרכיבים ב-400 פיקסלים. מטריצת הפרספקטיבה היא מטריצת 'משיכה' של נקודות קרוב יותר לנקודת היעלמות ככל שהן נמצאות רחוק יותר במרחב תלת-ממדי. כך אפשר גם להציג דברים קטנים יותר כשהם רחוקים יותר, וגם לגרום להם "לנוע לאט יותר" כשהם מתרגמים. לכן, אם רכיב נדחף לאחור, תרגום של 400px יגרום לרכיב לזוז רק 300px במסך.
אם אתם רוצים לדעת את כל הפרטים, כדאי לקרוא את המפרט של מודל הטרנספורמציה של שירות ה-CSS, אבל לצורך המאמר הזה, פישטתי את האלגוריתם שלמעלה.
התיבה שלנו נמצאת בתוך מאגר תצוגה פרופרקטיבית עם הערך p למאפיין perspective
, ונניח שהמאגר ניתן לגלילה ושהגלילה למטה היא ב-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]
.
ציינתי שלב ביניים כדי להראות את ההשפעה של מטריצת הטרנספורמציה של הרכיבים שלנו. אם אתם לא בקיאים במתמטיקה של מטריצות, זה בסדר. הרגע הגדול הוא שבשורה האחרונה אנחנו מוסיפים את ההזזה 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: גורמים לו לזוז
עכשיו התיבה שלנו מוצגת ומתנהגת כמו שהיא הייתה מתנהגת ללא טרנספורמציות. בשלב הזה לא ניתן לגלול בקונטיינר התצוגה בפרספקטיבה, ולכן אי אפשר לראות אותו, אבל אנחנו יודעים שהרכיב שלנו ינוע בכיוון השני כשנגלול. אז בואו נגלל מהמאגר, נכון? אפשר פשוט להוסיף רכיב spacer שמתפוס נפח:
<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. אבל חשוב לזכור שבגלל השטויות שעשינו עם קואורדינטת 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?
אה, החבר הישן שלי, iOS Safari. כמו בגלילה בפרלקס, גם כאן נתקלנו בבעיה. מכיוון שאנחנו גוללים על רכיב, אנחנו צריכים לציין את הערך -webkit-overflow-scrolling: touch
, אבל זה גורם לשטחיות של 3D וכל אפקט הגלישה שלנו מפסיק לפעול. פתרת את הבעיה בגלילה בפרלקס על ידי זיהוי של iOS Safari והסתמכת על position: sticky
כפתרון זמני, ונעשה בדיוק את אותו הדבר כאן. כדאי לעיין במאמר בנושא תזוזת Parallax כדי לרענן את הזיכרון.
מה קורה עם פס ההזזה בדפדפן?
בחלק מהמערכות נצטרך להשתמש בסרגל גלילה קבוע ויליד.
בעבר לא ניתן היה להסתיר את פס ההזזה (למעט באמצעות פסאודו-סלקטור לא סטנדרטי).
לכן כדי להסתיר אותו, עלינו להשתמש בהאקרים (ללא מתמטיקה). אנחנו עוטפים את רכיב הגלילה בקונטיינר באמצעות overflow-x: hidden
, וגורמים לרכיב הגלילה להיות רחב יותר מהקונטיינר. סרגל הגלילה המקורי של הדפדפן לא גלוי עכשיו.
סנפיר
עכשיו, כשאנחנו משלבים את כל הרכיבים, אנחנו יכולים ליצור פס גלילה בהתאמה אישית שמתאים לפריים – כמו זה שמוצג בהדגמה של Nyan cat.
אם אתם לא רואים את החתול ניאן, סימן שאתם נתקלים בבאג שמצאנו והגשנו במהלך יצירת ההדגמה (לוחצים על האגודל כדי להציג את חתול ניאן). Chrome מצטיין במניעת עבודה מיותרת, כמו ציור או אנימציה של דברים שלא מוצגים במסך. החדשות הרעות הן שהטריקסות שלנו גורמות ל-Chrome לחשוב שה-GIF של חתול ניאן לא מופיע במסך. אני מקווה שהבעיה תיפתר בקרוב.
זהו. זה היה הרבה עבודה. תודה שקראת את כל המאמר. צריך לעבוד קשה כדי שהדבר יפעל, וכנראה שרק במקרים נדירים כדאי לעשות את המאמץ, אלא אם סרגל גלילה מותאם אישית הוא חלק חיוני מחוויית השימוש. אבל טוב לדעת שאפשר, לא? העובדה שאי אפשר ליצור סרגל גלילה מותאם אישית בקלות כזו מראה שיש עבודה שצריך לעשות בצד ה-CSS. אבל אל דאגה! בעתיד, AnimationWorklet של Houdini יעזור לכם ליצור אפקטים מקושרים לגלילה כמו זה, עם תצוגה מושלמת בכל פריים.