Worklet Animasi Houdini

Meningkatkan kualitas animasi webapp

TL;DR: Animation Worklet memungkinkan Anda menulis animasi imperatif yang berjalan dengan kecepatan frame native perangkat untuk kelancaran ekstra tanpa jank yang halus™, membuat animasi Anda lebih tahan terhadap jank thread utama dan dapat ditautkan untuk men-scroll, bukan waktu. Worklet Animasi ada di Chrome Canary (di belakang tanda "Fitur Web Platform Eksperimental") dan kami merencanakan Uji Coba Origin untuk Chrome 71. Anda dapat mulai menggunakannya sebagai progressive enhancement sekarang.

Animation API lainnya?

Sebenarnya tidak, ini adalah ekstensi dari yang sudah kita miliki, dan dengan alasan yang baik. Mari kita mulai dari awal. Jika ingin menganimasikan elemen DOM di web apa pun saat ini, Anda memiliki 2 1⁄2 pilihan: Transisi CSS untuk transisi A ke B sederhana, Animasi CSS untuk animasi berbasis waktu yang lebih kompleks dan kompleks, serta Web Animations API (WAAPI) untuk animasi kompleks yang hampir bebas. Matriks dukungan WAAPI terlihat cukup suram, tetapi sedang dalam proses. Sebelum itu, ada polyfill.

Kesamaan semua metode ini adalah stateless dan berbasis waktu. Namun, beberapa efek yang dicoba developer tidak berbasis waktu atau stateless. Misalnya, scroll paralaks yang terkenal, seperti yang namanya, didorong scroll. Ternyata, menerapkan scroll parallax yang berperforma baik di web saat ini sangat sulit.

Bagaimana dengan status stateless? Misalnya, bayangkan kolom URL Chrome di Android. Jika Anda men-scroll ke bawah, iklan akan keluar dari tampilan. Namun, begitu Anda men-scroll ke atas, halaman akan kembali, meskipun Anda berada di tengah-tengah halaman tersebut. Animasi tidak hanya bergantung pada posisi scroll, tetapi juga pada arah scroll Anda sebelumnya. Ini bersifat stateful.

Masalah lainnya adalah menata gaya scrollbar. Mode tersebut terkenal tidak bergaya — atau setidaknya tidak cukup bergaya. Bagaimana jika saya ingin kucingnyan sebagai scrollbar saya? Apa pun teknik yang Anda pilih, membuat scrollbar kustom tidak berperforma baik, atau mudah.

Intinya, semua hal ini canggung dan sulit hingga tidak mungkin diimplementasikan secara efisien. Sebagian besar bergantung pada peristiwa dan/atau requestAnimationFrame, yang mungkin membuat Anda tetap berada pada 60 fps, meskipun layar mampu berjalan pada 90 fps, 120 fps, atau lebih tinggi dan menggunakan sebagian dari anggaran frame thread utama yang berharga.

Worklet Animasi memperluas kemampuan stack animasi web untuk memudahkan efek semacam ini. Sebelum kita mulai, mari kita pastikan kita telah mengetahui dasar-dasar animasi terbaru.

Pengantar tentang animasi dan linimasa

WAAPI dan Worklet Animasi menggunakan linimasa secara ekstensif untuk memungkinkan Anda mengatur animasi dan efek sesuai keinginan. Bagian ini adalah pengingat atau pengantar singkat tentang linimasa dan cara kerjanya dengan animasi.

Setiap dokumen memiliki document.timeline. Nilai ini dimulai dari 0 saat dokumen dibuat dan menghitung milidetik sejak dokumen mulai ada. Semua animasi dokumen berfungsi relatif terhadap linimasa ini.

Agar lebih jelas, mari kita lihat cuplikan WAAPI ini

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();

Saat kita memanggil animation.play(), animasi akan menggunakan currentTime linimasa sebagai waktu mulainya. Animasi kita memiliki penundaan 3000 md, yang berarti bahwa animasi akan dimulai (atau menjadi "aktif") saat linimasa mencapai `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`. Intinya, linimasa mengontrol posisi kita dalam animasi.

Setelah mencapai keyframe terakhir, animasi akan kembali ke keyframe pertama dan memulai iterasi animasi berikutnya. Proses ini diulang sebanyak 3 kali sejak kita menetapkan iterations: 3. Jika kita ingin animasi tidak pernah berhenti, kita akan menulis iterations: Number.POSITIVE_INFINITY. Berikut adalah hasil kode di atas.

WAAPI sangat canggih dan ada banyak fitur lainnya dalam API ini seperti easing, offset awal, pembobotan keyframe, dan perilaku pengisian yang akan menghilangkan cakupan artikel ini. Jika Anda ingin mengetahui lebih lanjut, sebaiknya baca artikel tentang Animasi CSS di CSS Tricks.

Menulis Worklet Animasi

Setelah memahami konsep linimasa, kita dapat mulai melihat Worklet Animasi dan cara kerjanya untuk mengubah linimasa. Animation Worklet API tidak hanya didasarkan pada WAAPI, tetapi — dalam arti web yang dapat diperluas — merupakan primitif tingkat rendah yang menjelaskan cara kerja WAAPI. Dari segi sintaksis, keduanya sangat mirip:

Worklet Animasi 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();
        

Perbedaannya ada pada parameter pertama, yaitu nama worklet yang mendorong animasi ini.

Deteksi fitur

Chrome adalah browser pertama yang mengirimkan fitur ini, jadi Anda harus memastikan kode Anda tidak hanya mengharapkan AnimationWorklet ada di sana. Jadi, sebelum memuat worklet, kita harus mendeteksi apakah browser pengguna memiliki dukungan untuk AnimationWorklet dengan pemeriksaan sederhana:

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

Memuat worklet

Worklet adalah konsep baru yang diperkenalkan oleh tim kerja Houdini untuk membuat banyak API baru lebih mudah dibuat dan diskalakan. Kita akan membahas detail worklet secara lebih lanjut nanti, tetapi untuk memudahkan, Anda dapat menganggapnya sebagai thread yang murah dan ringan (seperti pekerja) untuk saat ini.

Kita harus memastikan telah memuat worklet dengan nama "passthrough", sebelum mendeklarasikan animasi:

// 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;
    }
  }
);

Apa yang terjadi di sini? Kita mendaftarkan class sebagai animator menggunakan panggilan registerAnimator() AnimationWorklet, dengan memberinya nama "passthrough". Ini adalah nama yang sama dengan yang kita gunakan di konstruktor WorkletAnimation() di atas. Setelah pendaftaran selesai, promise yang ditampilkan oleh addModule() akan di-resolve dan kita dapat mulai membuat animasi menggunakan worklet tersebut.

Metode animate() dari instance kita akan dipanggil untuk setiap frame yang ingin dirender browser, dengan meneruskan currentTime linimasa animasi serta efek yang sedang diproses. Kita hanya memiliki satu efek, KeyframeEffect dan menggunakan currentTime untuk menetapkan localTime efek, sehingga animator ini disebut "passthrough". Dengan kode ini untuk worklet, WAAPI dan AnimationWorklet di atas berperilaku sama persis, seperti yang dapat Anda lihat di demo.

Waktu

Parameter currentTime dari metode animate() adalah currentTime dari linimasa yang kami teruskan ke konstruktor WorkletAnimation(). Dalam contoh sebelumnya, kita hanya meneruskan waktu tersebut ke efek. Namun, karena ini adalah kode JavaScript, dan kita dapat menyimpang waktu 💫

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)
      );
    }
  }
);

Kita mengambil Math.sin() dari currentTime, dan memetakan ulang nilai tersebut ke rentang [0; 2000], yang merupakan rentang waktu yang ditentukan untuk efek kita. Sekarang, animasi terlihat sangat berbeda, tanpa mengubah keyframe atau opsi animasi. Kode worklet dapat sangat kompleks, dan memungkinkan Anda menentukan secara terprogram efek mana yang diputar dalam urutan dan sejauh mana.

Opsi daripada Opsi

Anda mungkin ingin menggunakan kembali worklet dan mengubah jumlahnya. Karena alasan ini, konstruktor WorkletAnimation memungkinkan Anda meneruskan objek opsi ke 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();

Dalam contoh ini, kedua animasi dijalankan dengan kode yang sama, tetapi dengan opsi yang berbeda.

Tampilkan negara bagian lokal Anda.

Seperti yang saya jelaskan sebelumnya, salah satu masalah utama worklet animasi yang ingin diselesaikan adalah animasi stateful. Worklet animasi diizinkan untuk menyimpan status. Namun, salah satu fitur inti worklet adalah migrasi dapat dimigrasikan ke thread lain atau bahkan dihancurkan untuk menghemat resource, yang juga akan menghancurkan statusnya. Untuk mencegah hilangnya status, worklet animasi menawarkan hook yang dipanggil sebelum worklet dihancurkan yang dapat Anda gunakan untuk menampilkan objek status. Objek tersebut akan diteruskan ke konstruktor saat worklet dibuat ulang. Pada pembuatan awal, parameter tersebut akan menjadi 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,
      };
    }
  }
);

Setiap kali memuat ulang demo ini, Anda memiliki peluang 50/50 ke arah mana kotak akan berputar. Jika browser menghapus worklet dan memigrasikannya ke thread lain, akan ada panggilan Math.random() lain saat pembuatan, yang dapat menyebabkan perubahan arah secara tiba-tiba. Untuk memastikan hal itu tidak terjadi, kita menampilkan arah animasi yang dipilih secara acak sebagai status dan menggunakannya dalam konstruktor, jika disediakan.

Terhubung ke rangkaian ruang-waktu: ScrollTimeline

Seperti yang telah dijelaskan di bagian sebelumnya, AnimationWorklet memungkinkan kita untuk menentukan secara terprogram pengaruh perubahan linimasa terhadap efek animasi. Namun, sejauh ini, linimasa kita selalu berupa document.timeline, yang melacak waktu.

ScrollTimeline membuka kemungkinan baru dan memungkinkan Anda mendorong animasi dengan scroll, bukan waktu. Kita akan menggunakan kembali worklet "passthrough" pertama untuk demo ini:

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();

Alih-alih meneruskan document.timeline, kita membuat ScrollTimeline baru. Anda mungkin sudah menebaknya, ScrollTimeline tidak menggunakan waktu, tetapi posisi scroll scrollSource untuk menyetel currentTime di worklet. Di-scroll sepenuhnya ke atas (atau kiri) berarti currentTime = 0, sementara di-scroll ke paling bawah (atau kanan) akan menetapkan currentTime ke timeRange. Jika men-scroll kotak dalam demo ini, Anda dapat mengontrol posisi kotak merah.

Jika Anda membuat ScrollTimeline dengan elemen yang tidak di-scroll, currentTime linimasa akan menjadi NaN. Jadi, terutama dengan desain responsif dalam pikiran, Anda harus selalu siap untuk NaN sebagai currentTime. Sering kali masuk akal untuk ditetapkan secara default ke nilai 0.

Menautkan animasi dengan posisi scroll adalah sesuatu yang telah lama dicari, tetapi tidak pernah benar-benar dicapai pada tingkat fidelitas ini (selain solusi hacky dengan CSS3D). Worklet Animasi memungkinkan efek ini diterapkan dengan cara yang mudah sekaligus berperforma tinggi. Misalnya: efek scroll paralaks seperti demo ini menunjukkan bahwa kini hanya diperlukan beberapa baris untuk menentukan animasi berbasis scroll.

Di balik layar

Worklet

Worklet adalah konteks JavaScript dengan cakupan yang terisolasi dan platform API yang sangat kecil. Platform API yang kecil memungkinkan pengoptimalan yang lebih agresif dari browser, terutama di perangkat kelas bawah. Selain itu, worklet tidak terikat dengan loop peristiwa tertentu, tetapi dapat dipindahkan antar-thread sesuai kebutuhan. Hal ini sangat penting untuk AnimationWorklet.

Compositor NSync

Anda mungkin tahu bahwa properti CSS tertentu cepat dianimasikan, sementara yang lainnya tidak. Beberapa properti hanya memerlukan beberapa pekerjaan di GPU agar dapat dianimasikan, sementara properti lainnya memaksa browser menata ulang seluruh dokumen.

Di Chrome (seperti di banyak browser lain), kami memiliki proses yang disebut compositor, yang tugasnya — dan saya sangat menyederhanakannya — untuk mengatur lapisan dan tekstur lalu menggunakan GPU untuk memperbarui layar sesering mungkin, idealnya secepat pembaruan layar (biasanya 60 Hz). Bergantung pada properti CSS yang sedang dianimasikan, browser mungkin hanya perlu memiliki compositor untuk melakukan tugasnya, sementara properti lain perlu menjalankan tata letak, yang merupakan operasi yang hanya dapat dilakukan oleh thread utama. Bergantung pada properti yang ingin Anda animasikan, worklet animasi akan terikat ke thread utama atau berjalan di thread terpisah yang disinkronkan dengan komposer.

Pukulan di pergelangan tangan

Biasanya hanya ada satu proses kompositor yang berpotensi dibagikan di beberapa tab, karena GPU adalah resource yang sangat diperebutkan. Jika kompositor terblokir, seluruh browser akan berhenti dan tidak responsif terhadap input pengguna. Hal ini harus dihindari dengan segala cara. Jadi, apa yang terjadi jika worklet Anda tidak dapat mengirimkan data yang diperlukan kompositor tepat waktu agar frame dirender?

Jika ini terjadi, worklet diizinkan — sesuai spesifikasi — untuk "tergelincir". Hal ini tertinggal dari compositor, dan compositor diizinkan untuk menggunakan kembali data frame terakhir untuk menjaga kecepatan frame tetap tinggi. Secara visual, hal ini akan terlihat seperti jank, tetapi perbedaan terbesarnya adalah browser masih responsif terhadap input pengguna.

Kesimpulan

Ada banyak aspek pada AnimationWorklet dan manfaat yang diberikannya ke web. Manfaat yang jelas adalah lebih banyak kontrol atas animasi dan cara baru untuk mendorong animasi guna menghadirkan tingkat fidelitas visual baru ke web. Namun, desain API juga memungkinkan Anda membuat aplikasi lebih tahan terhadap jank sekaligus mendapatkan akses ke semua manfaat baru secara bersamaan.

Worklet Animasi ada di Canary dan kami menargetkan Uji Coba Origin dengan Chrome 71. Kami tidak sabar untuk mengetahui pengalaman web baru Anda yang luar biasa dan mendengar apa yang dapat kami tingkatkan. Ada juga polyfill yang memberi Anda API yang sama, tetapi tidak memberikan isolasi performa.

Perlu diingat bahwa Transisi CSS dan Animasi CSS masih merupakan opsi yang valid dan dapat jauh lebih sederhana untuk animasi dasar. Namun, jika Anda ingin tampil mewah, AnimationWorklet siap membantu.