worklet של Houdini'

אפשר לשפר את האנימציות של אפליקציית האינטרנט

TL;DR: אנימציה Worklet מאפשר לכם לכתוב אנימציות ציווי שפועלות בקצב הפריימים המקורי של המכשיר, כדי ליהנות מחלקיות חמימות וללא הפרעותTM נוספות, וכך להפוך את האנימציות לעמידות יותר בפני ג'אנק ב-thread הראשי, וניתן לקשר אותן לגלילה במקום לזמן. worklet של האנימציה נמצא ב-Chrome Canary (מאחורי הדגל 'תכונות ניסיוניות של פלטפורמת האינטרנט') ואנחנו מתכננים גרסת מקור לניסיון ל-Chrome 71. תוכלו להתחיל להשתמש בו בתור שיפור הדרגתי היום.

ממשק API נוסף לאנימציה?

למעשה לא, זו הרחבה של מה שכבר יש לנו, ויש סיבה טובה לכך! נתחיל מההתחלה. היום ניתן ליצור אנימציה של כל רכיב DOM באינטרנט מ-2 עד 2 אפשרויות: מעברי CSS למעברים פשוטים מ-A ל-B, אנימציות CSS עבור אנימציות מבוססות-זמן שעשויות להיות מחזוריות ומורכבות יותר, ו-Web Animations API (WAAPI) לאנימציות מורכבות כמעט שרירותיות. מטריצת התמיכה של WAAPI נראית די קודרת, אבל היא בדרך למעלה. עד אז, יש polyfill.

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

ומה לגבי חוסר סטטוס? חשבו לדוגמה על סרגל הכתובות של Chrome ב-Android. אם גוללים למטה, גוללים אל מחוץ לתצוגה. אבל בשנייה שגוללים למעלה הוא חוזר, גם אם אתם נמצאים באמצע הדף. האנימציה תלויה לא רק במיקום הגלילה, אלא גם בכיוון הגלילה הקודם. הוא מציין הצהרות.

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

העניין הוא שכל הדברים האלה הם מוזרים וקשה עד בלתי אפשרי ליישם אותם ביעילות. רובם מסתמכים על אירועים ו/או requestAnimationFrame, שעשויים להשאיר אתכם בקצב של 60fps, גם אם המסך יכול לפעול במהירות של 90fps או 120fps או יותר, ומנצלים חלק מסוים מהתקציב של מסגרת ה-thread הראשית היקרה שלכם.

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

מבוא לאנימציות וצירי זמן

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

בכל מסמך יש document.timeline. הערך מתחיל ב-0 כשהמסמך נוצר וסופר את אלפיות השנייה שחלפו מאז שהמסמך התחיל בקיים. כל האנימציות במסמך פועלות ביחס לציר הזמן הזה.

כדי להמחיש קצת יותר, נסתכל על קטע הקוד הזה של WAAPI

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

כשאנחנו קוראים ל-animation.play(), האנימציה משתמשת ב-currentTime של ציר הזמן בתור שעת ההתחלה. באנימציה שלנו יש עיכוב של 3,000 אלפיות השנייה, כלומר האנימציה תתחיל (או תהפוך ל'פעילה') כשציר הזמן יגיע ל-'startTime'

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000'. הנקודה היא שפקדי ציר הזמן יופיעו באנימציה שלנו!

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

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

כתיבת worklet של אנימציה

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

worklet של אנימציה WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

ההבדל הוא בפרמטר הראשון, שהוא השם של ה-worklet שמניע את האנימציה הזו.

זיהוי תכונות

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

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

טעינת worklet

Worklets הם קונספט חדש שהושק על ידי כוח המשימה של Houdini כדי להקל על הבנייה וההתאמה של רבים מממשקי ה-API החדשים. נדבר מעט יותר על הפרטים של ה-worklets מאוחר יותר, אבל כדי לפשט את העניינים, אפשר להתייחס אליהם כאל שרשורים זולים וקלים (כמו עובדים) בשלב זה.

אנחנו צריכים לוודא שטענו את worklet בשם "passthrough", לפני הכרזה על האנימציה:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

מה קורה פה? אנחנו רושמים כיתה כאנימטורים באמצעות הקריאה registerAnimator() של AnimationWorklet, ושמה לה את השם "passthrough". זה אותו השם שבו השתמשנו במרכיב WorkletAnimation() שלמעלה. בסיום הרישום, ההבטחה שהוחזרה על ידי addModule() תפוג, ונוכל להתחיל ליצור אנימציות באמצעות ה-worklet הזה.

המערכת תפעיל את השיטה animate() במכונה שלנו עבור כל פריים שהדפדפן רוצה לעבד, ותעביר את ה-currentTime של ציר הזמן של האנימציה ואת האפקט שנמצא בתהליך עיבוד. יש לנו רק אפקט אחד, KeyframeEffect, ואנחנו משתמשים ב-currentTime כדי להגדיר את האפקט localTime, ומכאן למה האנימטור הזה נקרא Passthrough. בעזרת הקוד הזה ל-worklet, ה-WAAPI וה-AnimationWorklet שלמעלה מתנהגים בדיוק באותו אופן, כפי שניתן לראות בהדגמה.

שעה

הפרמטר currentTime של השיטה animate() הוא ה-currentTime של ציר הזמן שהעברנו אל ה-constructor WorkletAnimation(). בדוגמה הקודמת העברנו את הזמן לתוצאה. אבל מכיוון שזהו קוד JavaScript, ואנחנו יכולים לעוות את הזמן 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

אנחנו לוקחים את הערך Math.sin() של currentTime, וממפים מחדש את הערך הזה לטווח [0; 2000], שהוא טווח הזמן שעבורו מוגדר ההשפעה שלנו. עכשיו האנימציה נראית שונה מאוד, ללא שינוי של תמונות המפתח או של אפשרויות האנימציה. קוד ה-worklet יכול להיות מורכב באופן שרירותי, ומאפשר לקבוע באופן פרוגרמטי אילו אפקטים יופעלו, באיזה סדר ובאיזה היקף.

אפשרויות מעל 'אפשרויות'

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

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

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

בואו לראות את המדינה שלכם!

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

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

בכל פעם שתרעננו את ההדגמה, יש לכם סיכוי של 50/50 לכיוון הכיוון של הריבוע. אם הדפדפן נקרע את ה-worklet ומעביר אותו לשרשור אחר, תתבצע קריאה נוספת ל-Math.random() במהלך היצירה, מה שעלול לגרום לשינוי פתאומי בכיוון. כדי לוודא שזה לא יקרה, אנחנו מחזירים את הכיוון שנבחר באופן אקראי כ-state, ומשתמשים בו במבנה, אם צוין.

התחברות ברצף הזמן-חלל: ScrollTimeline

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

ScrollTimeline פותח אפשרויות חדשות ומאפשר לך להזיז אנימציות באמצעות גלילה במקום זמן. אנחנו עומדים להשתמש שוב בפקודת ה-"pass-through" הראשונה שלנו להדגמה הזו:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

במקום לעבור document.timeline, אנחנו יוצרים ScrollTimeline חדש. אולי ניחשת נכון, ב-ScrollTimeline לא נעשה שימוש בזמן, אבל המיקום של הגלילה scrollSource מוגדר על ידי currentTime ב-worklet. כשגוללים עד למעלה (או שמאלה) המשמעות היא currentTime = 0, בזמן שגוללים עד למטה (או ימינה) מגדירה את currentTime כ-timeRange. אם גוללים את התיבה בהדגמה, אפשר לקבוע את המיקום של התיבה האדומה.

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

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

אפשרויות מתקדמות

worklets

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

קומפוזיטור NSync

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

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

טפיחה על פרק כף היד

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

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

סיכום

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

worklet של האנימציה נמצא ב-Canary, ואנחנו שואפים לגרסת המקור לניסיון ב-Chrome 71 אנחנו מחכים בקוצר רוח לחוויות השימוש החדשות באינטרנט, ונשמח לשמוע מה נוכל לשפר. יש גם polyfill שמעניק את אותו API, אבל לא מאפשר לבודד ביצועים.

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