שיפור האנימציות באפליקציית האינטרנט
קיצור דרך: Animation Worklet מאפשר לכתוב אנימציות גורפות שפועלות בקצב הפריימים המקורי של המכשיר, כדי ליהנות מתנועה חלקה במיוחד ללא תנודות (jank)™. בנוסף, האנימציות עמידות יותר בפני תנודות בשרשור הראשי, וניתן לקשר אותן לגלילה במקום לזמן. Animation Worklet נמצא ב-Chrome Canary (מאחורי הדגל 'תכונות ניסיוניות של פלטפורמת אינטרנט'), ואנחנו מתכננים גרסת מקור לניסיון ל-Chrome 71. אפשר להתחיל להשתמש בה כשיפור הדרגתי היום.
עוד Animation API?
לא, זהו תוסף למה שכבר יש לנו, ויש לכך סיבה טובה. נתחיל מההתחלה. אם אתם רוצים להוסיף אנימציה לאלמנט DOM כלשהו באינטרנט, יש לכם היום 2 אפשרויות וחצי: CSS Transitions למעברים פשוטים מ-A ל-B, CSS Animations לאנימציות מורכבות יותר מבוססות-זמן שעשויות להיות מחזוריות, ו-Web Animations API (WAAPI) לאנימציות מורכבות כמעט באופן שרירותי. מטריית התמיכה של WAAPI נראית די עגומה, אבל היא בדרך לשיפור. עד אז, יש polyfill.
המשותף לכל השיטות האלה הוא שהן ללא מצב (stateless) ומבוססות-זמן. עם זאת, חלק מהאפקטים שהמפתחים מנסים לא מבוססים על זמן או על מצב. לדוגמה, גלילה בפרלקס (Parallax) היא, כפי שרואים מהשם, גלילה מבוססת. הטמעת גלילה בפרלקס עם ביצועים טובים באינטרנט היא קשה להפתיע.
מה קורה במקרה של חוסר אזרחות? לדוגמה, סרגל הכתובות של Chrome ב-Android. אם גוללים למטה, היא נעלמת מהתצוגה. אבל ברגע שגוללים למעלה, הוא חוזר, גם אם אתם באמצע הדף. ההנפשה תלויה לא רק במיקום הגלילה, אלא גם בכיוון הגלילה הקודם. הוא עם שמירת מצב.
בעיה נוספת היא עיצוב של פסורי גלילה. קשה מאוד לעצב אותן – או לפחות לא מספיק. מה קורה אם רוצים להשתמש בחתול ניאן כסרגל גלילה? לא משנה באיזו שיטה תבחרו, פיתוח פס גלילה בהתאמה אישית לא קל ולא יעיל.
הנקודה היא שכל הדברים האלה לא נוחים, וקשה מאוד, אם בכלל, להטמיע אותם בצורה יעילה. רובן מסתמכות על אירועים ו/או על requestAnimationFrame
, שעשויים להמשיך אתכם בקצב של 60fps, גם אם המסך שלכם מסוגל לפעול בקצב של 90fps, 120fps או יותר, ולהשתמש בחלק קטן מתקציב המסגרות היקר של הליבה הראשית.
Animation 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
של ציר הזמן בתור שעת ההתחלה שלה. לאנימציה שלנו יש עיכוב של 3000ms, כלומר היא תתחיל (או תהפוך ל'פעילה') כשציר הזמן יגיע ל-`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
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000. הנקודה היא ש-timeline קובע איפה אנחנו נמצאים באנימציה.
אחרי שהאנימציה תגיע לתמונה המרכזית האחרונה, היא תזנק חזרה לתמונה המרכזית הראשונה ותתחיל את המחזור הבא של האנימציה. התהליך הזה חוזר על עצמו 3 פעמים בסך הכול, כי הגדרנו את iterations: 3
. אם רוצים שהאנימציה לעולם לא תפסיק, צריך לכתוב iterations: Number.POSITIVE_INFINITY
. זוהי התוצאה של הקוד שלמעלה.
WAAPI הוא כלי חזק מאוד, ויש לו עוד הרבה תכונות ב-API, כמו עקומת האצה, הזזות של תחילת האנימציה, שקלול של נקודות מפתח והתנהגות מילוי, שאנחנו לא יכולים להרחיב עליהן במאמר הזה. למידע נוסף, מומלץ לקרוא את המאמר הזה על אנימציות CSS ב-CSS Tricks.
כתיבת עבודה של אנימציה
עכשיו, אחרי שהבנו את המושג של צירי זמן, נתחיל להסתכל על Animation Worklet ואיך הוא מאפשר לכם להתעסק בצירי זמן. Animation Worklet API לא מבוסס רק על 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!
}
טעינת וורקלט
Worklets הם מושג חדש שהושק על ידי כוח המשימה של Houdini כדי להקל על פיתוח ועל התאמה לעומס של הרבה ממשקי API חדשים. בהמשך נפרט יותר על worklets, אבל בינתיים אפשר להתייחס אליהם כאל חוטים זולים וקלים (כמו עובדים).
לפני שמצהירים על האנימציה, צריך לוודא שהטענו וורקלט בשם 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 הזה.
השיטה animate()
של המופע שלנו תופעל לכל פריים שהדפדפן רוצה ליצור, ותעביר את currentTime
של ציר הזמן של האנימציה ואת האפקט שעומד בטיפול. יש לנו רק אפקט אחד, KeyframeEffect
, ואנחנו משתמשים ב-currentTime
כדי להגדיר את localTime
של האפקט, ולכן האנימטור הזה נקרא 'מעבר'. עם הקוד הזה ל-worklet, ה-WAAPI וה-AnimationWorklet שלמעלה פועלים בדיוק באותו אופן, כפי שאפשר לראות בדמו.
שעה
הפרמטר currentTime
של השיטה animate()
הוא currentTime
של ציר הזמן שהעברנו למבנה 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], שהוא טווח הזמן שבו ההשפעה מוגדרת. עכשיו האנימציה נראית שונה מאוד, בלי לשנות את נקודות ה-keyframe או את האפשרויות של האנימציה. קוד ה-worklet יכול להיות מורכב ככל הצורך, ומאפשר להגדיר באופן פרוגרמטי אילו אפקטים יופעלו, בסדר ובמידה הרצויים.
אפשרויות מעל אפשרויות
יכול להיות שתרצו לעשות שימוש חוזר ב-worklet ולשנות את המספרים שלו. לכן, ה-constructor של 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();
בדוגמה הזו, שני האנימציות מופעלות על ידי אותו קוד, אבל עם אפשרויות שונות.
מה המדינה שלך?
כפי שציינתי קודם, אחת מהבעיות העיקריות ש-Animation Worklet נועד לפתור היא אנימציות עם מצב (stateful). אסור להשתמש ב-worklets של אנימציה כדי לשמור מצב. עם זאת, אחת מהתכונות המרכזיות של רכיבי worklet היא שאפשר להעביר אותם לשרשור אחר או אפילו להשמיד אותם כדי לחסוך במשאבים, וכתוצאה מכך גם את המצב שלהם. כדי למנוע אובדן מצב, ב-worklet של אנימציה יש הוק שנקרא לפני שה-worklet נהרס, שבו אפשר להשתמש כדי להחזיר אובייקט מצב. האובייקט הזה יועבר למבנה כשיוצרים מחדש את ה-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 הראשון שלנו מסוג 'מעבר':
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 = 0
. כשגוללים עד למטה (או ימינה), הערך של currentTime
מוגדר כ-timeRange
. בהדגמה הזו, אפשר לגלול את התיבה כדי לשלוט במיקום שלה.
אם יוצרים ScrollTimeline
עם רכיב שלא גוללים, הערך של currentTime
בציר הזמן יהיה NaN
. לכן, במיוחד כשמדובר בעיצוב רספונסיבי, תמיד כדאי להיערך מראש ל-NaN
בתור currentTime
. לרוב מומלץ להגדיר ערך ברירת מחדל של 0.
קישור אנימציות למיקום הגלילה הוא משהו שאנשים חיפשו במשך זמן רב, אבל אף פעם לא הצליחו להשיג את רמת האמינות הזו (מלבד פתרונות זמניים לא מוצלחים באמצעות CSS3D). בעזרת Animation Worklet אפשר להטמיע את האפקטים האלה בצורה פשוטה, תוך שמירה על ביצועים גבוהים. לדוגמה: אפקט גלילה של פרלקס כמו בדמו הזה מראה שאפשר להגדיר אנימציה שמבוססת על גלילה בכמה שורות בלבד.
מאחורי הקלעים
מודולים מסוג worklet
וורקלטס הם הקשרים של JavaScript עם היקף מבודד ופלטפורמת API קטנה מאוד. שטח ה-API הקטן מאפשר לבצע אופטימיזציה אגרסיבית יותר בדפדפן, במיוחד במכשירים פשוטים. בנוסף, וורקלטס לא מקושרים ללולאת אירועים ספציפית, אלא אפשר להעביר אותם בין חוטים לפי הצורך. הדבר חשוב במיוחד ל-AnimationWorklet.
Compositor NSync
יכול להיות שאתם יודעים שמאפייני CSS מסוימים מאפשרים ליצור אנימציה במהירות, בעוד שאחרים לא. בחלק מהנכסים צריך רק לבצע קצת עבודה על המעבד הגרפי (GPU) כדי להוסיף אנימציה, בעוד שבחלק מהנכסים צריך לאלץ את הדפדפן לעצב מחדש את כל המסמך.
ב-Chrome (כמו בדפדפנים רבים אחרים) יש תהליך שנקרא 'מרכז הרכבה', ותפקידו – ואני מסביר את זה בפשטות רבה – הוא לסדר שכבות ומרקמים, ולאחר מכן להשתמש ב-GPU כדי לעדכן את המסך באופן סדיר ככל האפשר, ובאופן אידיאלי במהירות הגבוהה ביותר שבה המסך יכול להתעדכן (בדרך כלל 60Hz). בהתאם למאפייני ה-CSS שאתם רוצים להוסיף להם אנימציה, יכול להיות שהדפדפן פשוט יצטרך לבקש מה-compositor לבצע את העבודה, בעוד שמאפיינים אחרים יצטרכו להריץ פריסה, פעולה שרק השרשור הראשי יכול לבצע. בהתאם למאפיינים שאתם מתכננים להוסיף להם אנימציה, ה-worklet של האנימציה יהיה קשור לשרשור הראשי או יפעל בשרשור נפרד בסנכרון עם ה-compositor.
נזיפה קלה
בדרך כלל יש רק תהליך אחד של עיבוד תמונה, שעשוי להיות משותף לכמה כרטיסיות, כי ה-GPU הוא משאב במאבק תחרותי. אם ה-Compositing חסום מסיבה כלשהי, כל הדפדפן נעצר ולא מגיב לקלט של המשתמש. חשוב להימנע מכך בכל מחיר. מה קורה אם ה-worklet לא יכול לספק את הנתונים הנדרשים למעבד התמונה בזמן כדי שאפשר יהיה ליצור את התמונה?
במקרה כזה, ה-worklet רשאי "להחליק" – בהתאם למפרט. הוא מאחר אחרי המאגר, והמאגר רשאי לעשות שימוש חוזר בנתונים של הפריים האחרון כדי לשמור על קצב הפריימים. מבחינה ויזואלית, זה נראה כמו תנודות, אבל ההבדל הגדול הוא שהדפדפן עדיין מגיב לקלט של המשתמש.
סיכום
ל-AnimationWorklet יש הרבה היבטים ויתרונות באינטרנט. היתרונות הברורים הם שליטה רבה יותר באנימציות ודרכים חדשות להפעלת אנימציות, שמאפשרות לכם להגיע לרמת נאמנות חזותית חדשה באינטרנט. אבל העיצוב של ממשקי ה-API מאפשר גם לשפר את עמידות האפליקציה בפני תנודות חדות, תוך קבלת גישה לכל התכונות החדשות בו-זמנית.
Animation Worklet נמצא בגרסת Canary, ואנחנו שואפים להתחיל את תקופת הניסיון בגרסת המקור עם Chrome 71. אנחנו מחכים בקוצר רוח לשמוע על החוויה החדשה והמצוינת שלכם באינטרנט, ועל הדרכים שבהן נוכל לשפר אותה. יש גם polyfill שמספק את אותו API, אבל לא מספק בידוד ביצועים.
חשוב לזכור שאפשר להשתמש עדיין באפשרויות של CSS Transitions ו-CSS Animations, ושהן יכולות להיות הרבה יותר פשוטות ליצירת אנימציות בסיסיות. אבל אם אתם רוצים להוסיף קצת פלפל, AnimationWorklet יעזור לכם.