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

Robert Flack
Robert Flack

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

איור עם אפקט פרלקס.

אמ;לק

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

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

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

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

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

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

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

שגיאה: מתבצע עדכון של 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>

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

דחיפת רכיב הצאצא לאחור תגרום לו להיות קטן יותר באופן יחסי לערך הפרספקטיבה. אפשר לחשב את מידת ההגדלה באמצעות המשוואה הבאה: (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;
}

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

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

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

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

סיכום

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

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