בניין מבצע הרחבה & כיווץ אנימציות

Stephen McGruer
Stephen McGruer

אמ;לק

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

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

לדוגמה, תפריט מורחב:

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

לא מומלץ: אנימציה של רוחב וגובה ברכיב מאגר

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

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

הבעיה המיידית בגישה הזו היא שהיא דורשת אנימציה של width ו-height. בנכסים האלה צריך לחשב את הפריסה ולצייר את התוצאות בכל פריים של האנימציה, פעולה שיכולה להיות יקרה מאוד ובדרך כלל תגרום לכם להפסיד את האפשרות להציג 60fps. אם זה חדש לכם, כדאי לקרוא את המדריכים שלנו בנושא ביצועי עיבוד, שבהם מוסבר איך תהליך העיבוד עובד.

לא מומלץ: שימוש במאפייני ה-CSS clip או clip-path

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

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

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

טוב: אנימציה של סקאלות

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

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

שלב 1: חישוב המצבים של ההתחלה והסיום

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

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

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

אבל רגע! בטח זה ישנה גם את הגודל של התוכן בתפריט, נכון? כן, כפי שאפשר לראות בהמשך.

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

  1. גם הטרנספורמציה ההפוכה היא פעולת שינוי קנה מידה. זה טוב כי אפשר גם להאיץ אותו, בדיוק כמו את האנימציה בקונטיינר. יכול להיות שתצטרכו לוודא שהרכיבים שמקבלים אנימציה מקבלים שכבת עיבוד גרפי משלהם (כדי לאפשר ל-GPU לעזור). לשם כך, אפשר להוסיף את הערך will-change: transform לרכיב, או את הערך backface-visiblity: hidden אם אתם צריכים לתמוך בדפדפנים ישנים יותר.

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

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

שלב 2: פיתוח אנימציות CSS בזמן אמת

הפתרון, שעשוי להיראות מוזר בהתחלה, הוא ליצור אנימציה עם פריימים מרכזיים באמצעות פונקציית האיזון שלנו באופן דינמי ולהחדיר אותה לדף לשימוש בתפריט. (תודה רבה למהנדס Chrome, רוברטו פלאק, שציין את זה!) היתרון העיקרי של השיטה הזו הוא שאפשר להריץ אנימציה עם פריימים מפתח שמשנה טרנספורמציות ב-Compositing, כלומר היא לא מושפעת מהמשימות בשרשור הראשי.

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

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

אנשים סקרנים עשויים לתהות מהי הפונקציה ease() בתוך לולאת ה-for. אפשר להשתמש בקוד דומה כדי למפות ערכים מ-0 עד 1 לערך מקביל עם עיכוב.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

אפשר גם להשתמש בחיפוש Google כדי להציג את המיקום על המפה. שימושי! אם אתם צריכים משוואות אחרות של עקומת העברה, כדאי לבדוק את Tween.js של Soledad Penadés, שמכיל המון משוואות כאלה.

שלב 3: מפעילים את האנימציות של CSS

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

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

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

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

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

גרסה מתקדמת יותר: חשיפות עגולות

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

העקרונות דומים במידה רבה לאלה של הגרסה הקודמת, שבה משנים את הגודל של רכיב ומקטינים את הגודל של הצאצאים המיידיים שלו. במקרה הזה, הערך של border-radius ברכיב שמתרחב הוא 50%, כך שהוא עגול, והוא עטוף ברכיב אחר עם הערך overflow: hidden. המשמעות היא שהעיגול לא יתרחב מעבר לגבולות הרכיב.

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

הקוד של אפקט ההרחבה העגול זמין במאגר GitHub.

מסקנות

אז זהו, הנה דרך ליצור אנימציות של קליפים עם ביצועים טובים באמצעות טרנספורמציות של שינוי קנה מידה. בעולם מושלם, היינו רוצים לראות הנפשות של קליפים מאיצה (יש באג ב-Chromium בנושא הזה שנוצר על ידי ג'ייק ארצ'יבולד), אבל עד שנגיע לשם, כדאי להיזהר כשמשתמשים בהנפשה של clip או clip-path, ובהחלט להימנע מהנפשה של width או height.

כדאי גם להשתמש ב-Web Animations לאפקטים כאלה, כי יש להם API ל-JavaScript, אבל הם יכולים לפעול בשרשור המאגר אם אתם יוצרים אנימציה רק ל-transform ול-opacity. לצערנו, התמיכה באנימציות אינטרנט לא טובה, אבל אפשר להשתמש בשיפור הדרגתי כדי להשתמש בהן אם הן זמינות.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

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

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