שיפור האנימציות באפליקציית האינטרנט
אמ;לק: worklet של אנימציה מאפשר לכתוב אנימציות חיוניות שרצות בקצב הפריימים המקורי של המכשיר, בשביל אותה החלקה בלי חמאה בלבד (jank-free jank). Animation Worklet נמצא ב-Chrome Canary (מאחורי הדגל 'תכונות ניסיוניות של פלטפורמת אינטרנט'), ואנחנו מתכננים גרסת מקור לניסיון ל-Chrome 71. אפשר להתחיל להשתמש בו כשיפור הדרגתי עוד היום.
ממשק API נוסף ליצירת אנימציות?
למעשה לא, זו הרחבה של מה שכבר יש לנו, ויש סיבה טובה לעשות זאת! נתחיל מההתחלה. אם אתם רוצים להוסיף אנימציה לאלמנט DOM כלשהו באינטרנט, יש לכם היום 2 אפשרויות וחצי: CSS Transitions למעברים פשוטים מ-A ל-B, CSS Animations לאנימציות מבוססות-זמן מורכבות יותר, שעשויות להיות מחזוריות, ו-Web Animations API (WAAPI) לאנימציות מורכבות כמעט באופן שרירותי. מטריית התמיכה של WAAPI נראית די עגומה, אבל היא בדרך לשיפור. עד אז, יש polyfill.
המשותף לכל השיטות האלה הוא שהן ללא מצב (stateless) ומבוססות-זמן. אבל חלק מהאפקטים שמפתחים מנסים להשתמש בהם הם לא מבוססי-זמן או ללא שמירת מצב. לדוגמה, גלילה בפרלקס (Parallax) היא, כפי שרואים מהשם, גלילה מבוססת. הטמעת גלילה בפרלקס עם ביצועים טובים באינטרנט היא קשה להפתיע.
מה לגבי אנשים ללא אזרחות? לדוגמה, סרגל הכתובות של Chrome ב-Android. אם גוללים למטה, הוא נגלל מחוץ לתצוגה. אבל ברגע שגוללים למעלה, הוא חוזר, גם אם אתם באמצע הדף. ההנפשה תלויה לא רק במיקום הגלילה, אלא גם בכיוון הגלילה הקודם. היא מלאה.
בעיה נוספת היא עיצוב של פסורי גלילה. קשה מאוד לעצב אותם – או לפחות לא מספיק. מה קורה אם רוצים להשתמש בחתול ניאן כסרגל גלילה? לא משנה באיזו שיטה תבחרו, בניית פס גלילה בהתאמה אישית היא לא קלה ולא יעילה.
הנקודה היא שכל הדברים האלה לא נוחים, וקשה מאוד, אם בכלל, להטמיע אותם ביעילות. רובם מסתמכים על אירועים ו/או requestAnimationFrame
, שעשויים לשמור על קצב של 60FPS, גם אם המסך יכול לפעול ב-90fps או 120fps או יותר, והם מנצלים חלק קטן מהתקציב היקר של הפריים הראשי.
ה-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 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.
כתיבת עבודה של אנימציה
עכשיו, אחרי שהבנו את המושג של צירי זמן, נוכל להתחיל להבין איך אפשר להשתמש ב-Animation Worklet כדי לשנות את צירי הזמן. ה-אנימציה של 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!
}
טעינת 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.
זה אותו השם שבו השתמשנו ב-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], שהוא טווח הזמן שעבורו מוגדרת ההשפעה שלנו. עכשיו האנימציה נראית שונה מאוד, בלי לשנות את תמונות המפתח או את אפשרויות האנימציה. קוד ה-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();
בדוגמה הזו, שני האנימציות מופעלות באמצעות אותו קוד, אבל עם אפשרויות שונות.
אני רוצה לקבל את המדינה המקומית שלך!
כפי שציינתי קודם, אחת מהבעיות העיקריות ש-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.
מחבר NSync
יכול להיות שאתם יודעים שמאפייני CSS מסוימים מאפשרים ליצור אנימציה במהירות, בעוד שאחרים לא. בחלק מהנכסים צריך רק לבצע קצת עבודה על המעבד הגרפי (GPU) כדי להוסיף אנימציה, בעוד שבחלק מהנכסים צריך לאלץ את הדפדפן לעצב מחדש את כל המסמך.
ב-Chrome (כמו בדפדפנים רבים אחרים) יש תהליך שנקרא 'מרכז הרכבה', ותפקידו – ואני מסביר את זה בפשטות רבה – הוא לסדר שכבות ומרקמים, ולאחר מכן להשתמש ב-GPU כדי לעדכן את המסך באופן סדיר ככל האפשר, ובאופן אידיאלי במהירות הגבוהה ביותר שבה המסך יכול להתעדכן (בדרך כלל 60Hz). בהתאם למאפייני ה-CSS שאתם רוצים להוסיף להם אנימציה, יכול להיות שהדפדפן פשוט יצטרך לבקש מה-compositor לבצע את העבודה, בעוד שמאפיינים אחרים יצטרכו להריץ פריסה, פעולה שרק השרשור הראשי יכול לבצע. בהתאם למאפיינים שאתם מתכננים להוסיף להם אנימציה, ה-worklet של האנימציה יהיה קשור לשרשור הראשי או יפעל בשרשור נפרד בסנכרון עם ה-compositor.
נזיפה קלה
בדרך כלל יש רק תהליך אחד של עיבוד תמונה, שעשוי להיות משותף לכמה כרטיסיות, כי ה-GPU הוא משאב במאבק תחרותי. אם המאגר נחסם בדרך כלשהי, כל הדפדפן נעצר ולא מגיב לקלט של המשתמש. חשוב להימנע מכך בכל מחיר. מה קורה אם ה-worklet לא יכול לספק את הנתונים הנדרשים למעבד התמונה בזמן כדי שאפשר יהיה ליצור את התמונה?
במקרה כזה, ה-worklet מותר - לפי המפרט - "slip". הוא מאחר אחרי המאגר, והמאגר רשאי לעשות שימוש חוזר בנתונים של הפריים האחרון כדי לשמור על קצב הפריימים. מבחינה ויזואלית, זה נראה כמו תנודות, אבל ההבדל הגדול הוא שהדפדפן עדיין מגיב לקלט של המשתמש.
סיכום
ל-AnimationWorklet יש הרבה היבטים ויתרונות באינטרנט. היתרונות הברורים הם שליטה רבה יותר באנימציות ודרכים חדשות להפעלת אנימציות, שמאפשרות לכם להגיע לרמת נאמנות חזותית חדשה באינטרנט. אבל העיצוב של ממשקי ה-API מאפשר גם לשפר את עמידות האפליקציה בפני תנודות חדות, תוך קבלת גישה לכל התכונות החדשות בו-זמנית.
worklet של האנימציה נמצא ב-Canary, ואנחנו שואפים ליצור גרסת מקור לניסיון ב-Chrome 71. אנחנו מחכים בקוצר רוח לשמוע על החוויה החדשה והמצוינת שלכם באינטרנט, ועל הדרכים שבהן נוכל לשפר אותה. יש גם polyfill שמספק את אותו ממשק API, אבל לא מספק בידוד ביצועים.
חשוב לזכור שמעברי CSS ואנימציות של CSS עדיין תקפות, ויכולות להיות הרבה יותר פשוטות לאנימציות בסיסיות. אבל אם אתם רוצים להוסיף קצת פלפל, AnimationWorklet יעזור לכם.