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

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