Worklet Animasi Houdini

Tingkatkan animasi aplikasi web Anda

TL;DR: Worklet Animasi memungkinkan Anda menulis animasi imperatif yang berjalan pada kecepatan frame native perangkat untuk kelancaran bebas jank yang lebih besar, membuat animasi Anda lebih tahan terhadap jank pada thread utama, dan dapat ditautkan untuk men-scroll, bukan waktu. Worklet Animasi tersedia di Chrome Canary (di belakang tanda "Fitur Platform Web Eksperimental") dan kami merencanakan Uji Coba Origin untuk Chrome 71. Anda dapat mulai menggunakannya sebagai {i>progressive enhancement<i} sekarang.

Animation API Lain?

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

Kesamaan dari semua metode ini adalah bahwa metode ini bersifat stateless dan berbasis waktu. Namun, beberapa efek yang dicoba oleh developer tidak berbasis waktu atau stateless. Misalnya, scroller paralaks yang terkenal, sesuai dengan namanya, berbasis scroll. Menerapkan scroller paralaks berperforma tinggi di web saat ini sangat sulit.

Dan bagaimana dengan status stateless? Misalnya, pikirkan tentang kolom URL Chrome di Android. Jika Anda scroll ke bawah, scroll hingga keluar dari tampilan. Namun, begitu Anda men-scroll ke atas, halaman akan kembali, meskipun Anda hanya setengah jalan ke bawah halaman tersebut. Animasi tidak hanya bergantung pada posisi scroll, tetapi juga pada arah scroll Anda sebelumnya. Bersifat stateful.

Masalah lainnya adalah menata gaya scrollbar. Ikon itu terkenal tidak bergaya — atau setidaknya tidak cukup ditata. Bagaimana jika saya menginginkan kucing nyan sebagai scrollbar? Teknik apa pun yang Anda pilih, membuat scrollbar kustom tidak berperforma baik atau mudah.

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

Worklet Animasi memperluas kemampuan stack animasi web untuk mempermudah efek ini. Sebelum kita mempelajari lebih dalam, pastikan kita sudah mengetahui dasar-dasar animasi.

Penjelasan tentang animasi dan linimasa

WAAPI dan Animation Worklet memanfaatkan linimasa secara ekstensif untuk memungkinkan Anda mengorkestrasi animasi dan efek sesuai keinginan. Bagian ini adalah pengingat singkat atau pengantar linimasa dan cara kerjanya dengan animasi.

Setiap dokumen memiliki document.timeline. Dimulai dari 0 saat dokumen dibuat dan menghitung milidetik sejak dokumen mulai ada. Semua animasi dokumen bekerja secara relatif terhadap linimasa ini.

Untuk lebih memperjelas, 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 menggunakan currentTime linimasa sebagai waktu mulainya. Animasi memiliki penundaan 3.000 md, yang berarti 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 adalah, linimasa mengontrol posisi kita dalam animasi!

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

WAAPI sangat canggih dan terdapat banyak fitur lainnya dalam API ini seperti easing, offset awal, pembobotan keyframe, dan perilaku pengisian yang akan melampaui cakupan artikel ini. Jika Anda ingin tahu lebih banyak, sebaiknya baca artikel tentang Animasi CSS tentang Trik CSS ini.

Menulis Worklet Animasi

Setelah memahami konsep linimasa, kita dapat mulai melihat Worklet Animasi dan bagaimana hal itu memungkinkan Anda mengotak-atik linimasa. Animation Worklet API tidak hanya didasarkan pada WAAPI, tetapi — dalam artian web yang dapat diperluas — merupakan primitif tingkat rendah yang menjelaskan cara kerja WAAPI. Dalam hal sintaks, 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 menggerakkan animasi ini.

Deteksi fitur

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

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

Memuat worklet

Worklet adalah konsep baru yang diperkenalkan oleh gugus tugas Houdini untuk membuat banyak API baru lebih mudah di-build dan diskalakan. Kita akan membahas detail worklet selanjutnya nanti, tetapi untuk memudahkan, Anda dapat menganggapnya sebagai thread murah dan ringan (seperti pekerja) untuk saat ini.

Kita perlu memastikan bahwa kita 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 sedang terjadi di sini? Kita mendaftarkan class sebagai animator menggunakan panggilan registerAnimator() AnimationWorklet, memberinya nama "passthrough". Nama ini sama dengan yang kami gunakan di konstruktor WorkletAnimation() di atas. Setelah pendaftaran selesai, promise yang ditampilkan oleh addModule() akan diselesaikan dan kita dapat mulai membuat animasi menggunakan worklet tersebut.

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

Waktu

Parameter currentTime dari metode animate() adalah currentTime dari linimasa yang kita teruskan ke konstruktor WorkletAnimation(). Dalam contoh sebelumnya, kita baru saja menerapkan waktu tersebut. Tapi karena ini adalah kode JavaScript, kita bisa distorsi 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 digunakan untuk menentukan efek. Sekarang animasinya terlihat sangat berbeda, tanpa perlu mengubah keyframe atau opsi animasi. Kode worklet dapat bersifat arbitrer, dan memungkinkan Anda secara terprogram menentukan efek yang dimainkan dalam urutan tertentu dan sejauh mana.

Opsi daripada Opsi

Anda mungkin ingin menggunakan kembali worklet dan mengubah angkanya. 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.

Berikan negara bagian lokal Anda!

Seperti yang saya petunjukkan sebelumnya, salah satu masalah utama worklet animasi yang ingin dipecahkan adalah animasi stateful. Worklet animasi diizinkan untuk mempertahankan status. Namun, salah satu fitur inti worklet adalah worklet 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. Saat 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 Anda memuat ulang demo ini, Anda memiliki peluang 50/50 ke arah mana persegi akan berputar. Jika browser menghapus worklet dan memigrasikannya ke thread lain, akan ada panggilan Math.random() lain pada pembuatan, yang dapat menyebabkan perubahan arah secara tiba-tiba. Untuk memastikan hal itu tidak terjadi, kami menampilkan animasi arah yang dipilih secara acak sebagai state dan menggunakannya dalam konstruktor, jika disediakan.

Terhubung ke rangkaian waktu ruang-waktu: ScrollTimeline

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

ScrollTimeline membuka berbagai kemungkinan baru dan memungkinkan Anda menggerakkan animasi dengan men-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();

Daripada meneruskan document.timeline, kita akan membuat ScrollTimeline baru. Anda mungkin sudah menebaknya, ScrollTimeline tidak menggunakan waktu, tetapi posisi scroll scrollSource untuk menyetel currentTime di worklet. Di-scroll hingga ke atas (atau kiri) berarti currentTime = 0, sedangkan di-scroll ke 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 dapat di-scroll, currentTime linimasa akan menjadi NaN. Jadi, terutama dengan mempertimbangkan desain responsif, Anda harus selalu siap untuk NaN sebagai currentTime. Sebaiknya tetapkan nilai 0 sebagai default.

Menautkan animasi dengan posisi scroll adalah sesuatu yang sudah lama dicari, tetapi tidak pernah benar-benar dicapai pada tingkat fidelitas ini (selain solusi praktis dengan CSS3D). Worklet Animasi memungkinkan efek ini diterapkan dengan 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 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 pada loop peristiwa tertentu, tetapi dapat dipindahkan antar-thread sesuai kebutuhan. Hal ini sangat penting untuk AnimationWorklet.

Kompositor NSync

Anda mungkin tahu bahwa properti CSS tertentu dapat dianimasikan dengan cepat, sedangkan yang lain tidak. Beberapa properti hanya memerlukan upaya di GPU untuk dianimasikan, sementara yang lain memaksa browser untuk menata ulang tata letak seluruh dokumen.

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

Tepukan di pergelangan tangan

Biasanya hanya ada satu proses compositor yang kemungkinan dibagikan ke beberapa tab, karena GPU adalah resource yang sangat sulit dibagikan. Jika compositor diblokir, seluruh browser akan berhenti dan menjadi tidak responsif terhadap input pengguna. Hal ini harus dihindari dengan segala cara. Jadi, apa yang terjadi jika worklet tidak dapat mengirimkan data yang dibutuhkan compositor tepat waktu agar frame dirender?

Jika ini terjadi, worklet diizinkan — per spesifikasi — untuk "slip". Compositor berada di belakang compositor, dan compositor diizinkan untuk menggunakan kembali data frame terakhir agar kecepatan frame tetap tinggi. Secara visual, tampilannya akan terlihat seperti jank, tetapi perbedaan besarnya adalah browser masih responsif terhadap input pengguna.

Kesimpulan

Ada banyak faset AnimationWorklet dan manfaatnya bagi web. Manfaat yang nyata adalah lebih banyak kontrol atas animasi dan cara baru untuk mendorong animasi guna memberikan tingkat fidelitas visual yang baru ke web. Namun, desain API juga memungkinkan Anda membuat aplikasi lebih tahan terhadap jank sambil mendapatkan akses ke semua manfaat baru secara bersamaan.

Worklet Animasi tersedia dalam Canary dan kami ingin melakukan Uji Coba Origin dengan Chrome 71. Kami menantikan pengalaman web baru yang luar biasa dari Anda dan mengetahui apa yang dapat kami tingkatkan. Ada juga polyfill yang memberi Anda API yang sama, tetapi tidak menyediakan isolasi performa.

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