פרלקסציה ביצועית

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

איור של פרלקס.

אמ;לק

  • אל תשתמשו באירועי גלילה או ב-background-position כדי ליצור אנימציות פרלקסה.
  • כדי ליצור אפקט פרלקסה מדויק יותר, אפשר להשתמש בטרנספורמציות CSS 3D.
  • ב-Safari לנייד, צריך להשתמש ב-position: sticky כדי לוודא שאפקט הפרלקסה מועבר.

אם אתם רוצים להשתמש בפתרון המובנה, אתם יכולים לעבור אל מאגר הדוגמאות של רכיבי ממשק המשתמש ב-GitHub ולהוריד את קובץ ה-JS של Parallax helper. אפשר לראות הדגמה חיה של גלילת פרלקסה במאגר GitHub.

בעיות שקשורות לפרלקסה

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

לא מומלץ: שימוש באירועי גלילה

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

המידע החשוב הזה מסביר למה צריך להימנע מפתרון מבוסס-JavaScript שמזיז אלמנטים על סמך אירועי גלילה: JavaScript לא מבטיח שהתנועה של אפקט הפרלקסה תהיה מסונכרנת עם מיקום הגלילה בדף. בגרסאות ישנות יותר של Mobile Safari, אירועי גלילה הועברו בפועל בסוף הגלילה, ולכן לא הייתה אפשרות ליצור אפקט גלילה מבוסס-JavaScript. בגרסאות חדשות יותר יש אירועי גלילה במהלך האנימציה, אבל בדומה ל-Chrome, על בסיס 'הכי טוב שאפשר'. אם ה-main thread עסוק בעבודה אחרת, אירועי הגלילה לא יועברו באופן מיידי, כלומר אפקט הפרלקסה לא יפעל.

גרוע: מתבצע עדכון של background-position

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

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

CSS בתלת-ממד

גם סקוט קאלום וגם קית' קלארק עשו עבודה משמעותית בתחום השימוש ב-CSS 3D כדי ליצור תנועת פרלקסה, והטכניקה שבה הם משתמשים היא למעשה:

  • מגדירים רכיב מכיל לגלילה עם overflow-y: scroll (וכנראה עם overflow-x: hidden).
  • לאותו אלמנט מוסיפים ערך perspective וערך perspective-origin שמוגדר כ-top left או כ-0 0.
  • לצאצאים של האלמנט הזה מוחל תרגום ב-Z, והם מוגדלים בחזרה כדי ליצור תנועת פרלקסה בלי להשפיע על הגודל שלהם במסך.

ה-CSS לגישה הזו נראה כך:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

בהנחה שיש קטע HTML כזה:

<div class="container">
    <div class="parallax-child"></div>
</div>

התאמת קנה המידה לפרספקטיבה

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

בדוגמה של הקוד שלמעלה, הפרספקטיבה היא 1px, והמרחק של parallax-child בציר Z הוא ‎-2px. כלומר, צריך להגדיל את האלמנט פי 3, וזה הערך שמופיע בקוד: scale(3).

לכל תוכן שלא הוחל עליו ערך translateZ, אפשר להזין במקומו את הערך אפס. המשמעות היא שהקנה מידה הוא (perspective - 0) / perspective, והתוצאה היא ערך של 1, כלומר לא בוצעה התאמה כלפי מעלה או כלפי מטה. מאוד שימושי.

איך הגישה הזו פועלת

חשוב להבין למה זה עובד, כי נשתמש בידע הזה בהמשך. גלילה היא למעשה טרנספורמציה, ולכן אפשר להאיץ אותה. היא כוללת בעיקר הזזה של שכבות באמצעות ה-GPU. בגלילה רגילה, שבה אין מושג של פרספקטיבה, הגלילה מתבצעת ביחס של 1:1 כשמשווים בין רכיב הגלילה לבין רכיבי הצאצא שלו. אם גוללים רכיב למטה ב-300px, רכיבי הצאצא שלו עוברים טרנספורמציה למעלה באותו סכום: 300px.

עם זאת, החלת ערך פרספקטיבה על רכיב הגלילה משבשת את התהליך הזה, כי היא משנה את המטריצות שמהוות בסיס לשינוי הגלילה. עכשיו גלילה של 300px עשויה להזיז את רכיבי הצאצא ב-150px בלבד, בהתאם לערכים של perspective ו-translateZ שבחרתם. אם ערך translateZ של רכיב הוא 0, הוא יגולל ביחס של 1:1 (כמו שהיה בעבר), אבל רכיב צאצא שנדחף במישור Z הרחק מנקודת המבט יגולל בקצב שונה. התוצאה הסופית: תנועת פרלקסה. חשוב מאוד לדעת שהטיפול בזה מתבצע באופן אוטומטי כחלק ממנגנון הגלילה הפנימי של הדפדפן, כך שאין צורך להאזין לאירועי scroll או לשנות את background-position.

הבעיה: Safari לנייד

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

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

ב-HTML שלמעלה, התג .parallax-container הוא חדש, והוא יגרום לערך perspective להיות שטוח, כך שנאבד את אפקט הפרלקסה. ברוב המקרים, הפתרון די פשוט: מוסיפים transform-style: preserve-3d לרכיב, וכך גורמים להפצת אפקטים תלת-ממדיים (כמו ערך הפרספקטיבה שלנו) שהוחלו ברמה גבוהה יותר בעץ.

.parallax-container {
  transform-style: preserve-3d;
}

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

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

position: sticky בא לעזרה!

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

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

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

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

השינוי הזה משחזר את אפקט הפרלקסה ב-Mobile Safari, וזו בשורה מצוינת לכולם.

הערות לגבי מיקום קבוע

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

  • עם position: sticky, ככל שהרכיב קרוב יותר ל-z=0, כך הוא זז פחות.
  • בלי position: sticky, ככל שהרכיב קרוב יותר ל-z=0, כך הוא זז יותר.

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

צילום מסך עם פרספקטיבה של פרלקס

הדגמה של רוברט פלאק שמראה איך position: sticky משפיע על גלילת פרלקסה.

באגים שונים ופתרונות עקיפים

אבל כמו בכל דבר, יש עדיין בעיות שצריך לפתור:

  • התמיכה בקיבוע לא עקבית. התמיכה עדיין בהטמעה ב-Chrome, אין תמיכה ב-Edge, וב-Firefox יש באגים בעיבוד כשמשלבים בין sticky לבין טרנספורמציות של פרספקטיבה. במקרים כאלה, כדאי להוסיף קוד קטן כדי להוסיף רק את position: sticky (הגרסה עם הקידומת -webkit-) כשצריך, כלומר רק ב-Mobile Safari.
  • האפקט לא פועל ב-Edge בלי הגדרה. דפדפן Edge מנסה לטפל בגלילה ברמת מערכת ההפעלה, וזה בדרך כלל טוב, אבל במקרה הזה זה מונע ממנו לזהות את שינויי הפרספקטיבה במהלך הגלילה. כדי לפתור את הבעיה, אפשר להוסיף רכיב עם מיקום קבוע, כי נראה שזה גורם ל-Edge לעבור ל שיטת גלילה שלא מבוססת על מערכת ההפעלה, ומבטיח שהשינויים בפרספקטיבה יובאו בחשבון.
  • "התוכן בדף גדל מאוד!" הרבה דפדפנים לוקחים בחשבון את קנה המידה כשהם מחליטים מה הגודל של תוכן הדף, אבל לצערי Chrome ו-Safari לא לוקחים בחשבון את הפרספקטיבה. לכן, אם למשל מוחל על רכיב קנה מידה של 3x, יכול להיות שתראו פסי גלילה וכדומה, גם אם הרכיב הוא 1x אחרי שהוחל עליו perspective. אפשר לפתור את הבעיה הזו על ידי שינוי הגודל של הרכיבים מהפינה השמאלית התחתונה (עם transform-origin: bottom right). הפתרון הזה עובד כי הוא גורם לרכיבים גדולים מדי לגדול לתוך "האזור השלילי" (בדרך כלל הפינה השמאלית העליונה) של האזור שניתן לגלילה. באזורים שניתנים לגלילה אי אפשר לראות או לגלול לתוכן באזור השלילי.

סיכום

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

מוזמנים לנסות ולספר לנו איך היה.