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

פול לואיס
סטיבן מק'גרואר
סטיבן מק'גרואר

אמ;לק

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

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

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

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

אז מה אפשר לעשות בעניין הזה? ובכן, אפשר להחיל על התוכן counter-. לדוגמה, אם הקונטיינר הוקטן ל-1/5 מגודלו הרגיל, אפשר להגדיל את התוכן counter- כדי למנוע כיווץ של התוכן. יש שני דברים שכדאי לשים לב אליהם:

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

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

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

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

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

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

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-loop). אפשר להשתמש בערך כזה כדי למפות ערכים מ-0 ל-1 לערך המקביל שלהם.

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

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

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

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

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

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

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

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

מסקנות

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

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

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

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

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