worklet של Houdini'

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

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

עוד ממשק API לאנימציה?

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

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

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

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

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

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

מבוא לאנימציות ולוחות זמנים

WAAPI ו-אנימציה 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 theמשך זמןoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'sזמן נוכחיisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. הנקודה היא כדי לקבוע איפה אנחנו נמצאים באנימציה שלנו!

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

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

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

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

אנחנו צריכים לוודא שטענו 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". זה אותו השם שבו השתמשנו ב-constructor של WorkletAnimation() שלמעלה. אחרי ש ההרשמה הושלמה, ההבטחה שהוחזרה על ידי addModule() תטופל ו אנחנו יכולים להתחיל ליצור אנימציות באמצעות ה-worklet הזה.

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

שעה

הפרמטר currentTime של ה-method 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 ולשנות את המספרים שלו. לכן constructor של workletAnimationcom מאפשר להעביר אובייקט אפשרויות ל-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();

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

אני רוצה לקבל את המדינה המקומית שלך!

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

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

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

ScrollTimeline פותח אפשרויות חדשות ומאפשר לך ליצור אנימציות באמצעות גלילה במקום זמן. נשתמש שוב במודל 'מעבר בין מכשירים' worklet של זה demo:

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. אם גוללים את התיבה demo, אפשר קובעים את מיקום התיבה האדומה.

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

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

מה צריך לעשות

worklet

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

מחבר NSync

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

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

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

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

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

סיכום

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

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

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