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

Robert Flack
Robert Flack

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

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

אמ;לק

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

אם אתם מחפשים את פתרון ההוספה, עברו למאגר GitHub של UI Element Samples וקנו את Parallax helper JS! אפשר לצפות בהדגמה של גלילה בפרלקס במאגר GitHub.

בעיות שקשורות לתמונות 'תלת-ממד'

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

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

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

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

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

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

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

CSS בתלת-ממד

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

  • מגדירים רכיב מכיל שאפשר לגלול בו באמצעות 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>

שינוי קנה המידה לפי פרספקטיבה

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

בקוד שלמעלה, הפרספקטיבה היא 1px והמרחק ב-Z של parallax-child הוא -2px. כלומר, צריך להגדיל את האלמנט ב-3x, והערך הזה מופיע בקוד: 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;
}

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

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

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);
}

הפעולה הזו מחזירה את אפקט התלת-ממד ל-Safari בנייד, וזו חדשה נהדרת!

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

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

  • כשהרכיב position: sticky קרוב יותר ל-z=0, הוא נמוך יותר.
  • בלי position: sticky, ככל שהרכיב קרוב יותר ל-z=0, הוא נע יותר.

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

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

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

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

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

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

סיכום

שימוש מושכל ב-Parallaxing יכול להניב אפקט משעשע. כפי שאפשר לראות, אפשר להטמיע אותו בצורה יעילה, עם התאמה לגלילה ובכל הדפדפנים. כדי להשיג את האפקט הרצוי צריך קצת התמצאות מתמטית וכמות קטנה של צירופים (boilerplate), לכן יצרנו ספרייה קטנה של עזרים ודוגמה, שאפשר למצוא במאגר ה-UI Element Samples GitHub.

כדאי לנסות את התכונה הזו ולספר לנו איך היא עובדת.