Worklet Animasi Houdini

Meningkatkan kualitas animasi aplikasi web Anda

TL;DR: Worklet Animasi memungkinkan Anda menulis animasi imperatif yang berjalan pada kecepatan frame native perangkat untuk kelancaran bebas jank tanpa jank dari perangkat, membuat animasi lebih tahan terhadap jank pada thread utama dan dapat ditautkan men-scroll, bukan waktu. Worklet Animasi ada di Chrome Canary (di belakang "Fitur Platform Web eksperimental" flag) 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 pengembangan dari apa yang sudah kita miliki, dan dengan alasan yang tepat! Mari kita mulai dari awal. Jika Anda ingin menganimasikan elemen DOM apa pun di web hari ini, Anda memiliki 2 1⁄2 pilihan: Transisi CSS untuk transisi A ke B sederhana, Animasi CSS untuk animasi berbasis waktu yang lebih bersifat siklus dan kompleks, serta Web Animations API (WAAPI) untuk animasi kompleks yang hampir sembarang bebas. Matriks dukungan WAAPI terlihat cukup suram, tetapi saat ini sedang naik. Sebelum itu, ada polyfill.

Kesamaan dari semua metode ini adalah metode tersebut stateless dan didorong oleh waktu. Tetapi beberapa efek yang coba dicoba oleh pengembang tidak berbasis waktu maupun stateless. Misalnya scroller paralaks yang terkenal adalah, karena nama menyiratkan, berbasis scroll. Mengimplementasikan scroller paralaks yang berperforma tinggi di web saat ini sangat sulit.

Dan bagaimana dengan stateless? Pikirkan tentang bilah alamat Chrome di Android, untuk contoh. Jika di-scroll ke bawah, layar akan ter-scroll hingga keluar. Tapi kedua Anda men-scroll ke atas, akan muncul kembali, meskipun Anda sudah setengah jalan ke bagian bawah halaman tersebut. Animasi tidak hanya bergantung pada posisi scroll, tetapi juga pada arah scroll Anda sebelumnya. Atribut ini bersifat stateful.

Masalah lainnya adalah penataan gaya scrollbar. Mereka terkenal tidak bergaya — atau sama setidaknya tidak cukup mudah untuk ditata. Bagaimana jika saya menginginkan kucingnyan sebagai scrollbar saya? Teknik apa pun yang Anda pilih, membuat scrollbar kustom bukanlah berperforma tinggi, atau mudah.

Intinya adalah semua hal ini canggung dan sulit untuk menerapkannya secara efisien. Kebanyakan dari mereka mengandalkan peristiwa dan/atau requestAnimationFrame, yang dapat mempertahankan kecepatan Anda pada 60 fps, bahkan saat layar mampu berjalan pada 90 fps, 120 fps atau lebih tinggi dan menggunakan sebagian kecil anggaran {i>frame<i} thread utama yang berharga.

Worklet Animasi memperluas kemampuan tumpukan animasi web untuk membuat efek semacam ini lebih mudah. Sebelum kita mempelajari lebih lanjut, pastikan kita telah mengikuti perkembangan dasar-dasar animasi.

Pengantar tentang animasi dan linimasa

WAAPI dan Animation Worklet memanfaatkan linimasa secara ekstensif untuk memungkinkan Anda mengatur animasi dan efek sesuai keinginan Anda. Bagian ini adalah penyegaran singkat atau pengantar garis waktu dan bagaimana mereka bekerja dengan animasi.

Setiap dokumen memiliki document.timeline. Dimulai dari 0 jika dokumen dibuat dan menghitung jumlah milidetik sejak dokumen mulai dibuat. Semua animasi dokumen relatif terhadap garis waktu 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 mengalami 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 adalah, linimasa mengontrol posisi kita di animasi kita!

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

WAAPI sangat canggih dan masih banyak fitur lain dalam API ini seperti easing, offset memulai, pembobotan keyframe, dan perilaku pengisian yang akan meledakkan cakupan artikel ini. Jika Anda ingin mengetahui lebih lanjut, sebaiknya baca artikel tentang Animasi CSS di Trik CSS ini.

Menulis Worklet Animasi

Setelah kita memiliki konsep {i>timeline<i}, kita dapat mulai melihat Worklet Animasi dan caranya memungkinkan Anda mengotak-atik linimasa! Animasi Worklet API tidak hanya didasarkan pada WAAPI, melainkan — dalam artian web yang dapat diperluas — merupakan primitif tingkat rendah yang menjelaskan cara kerja WAAPI. Dalam hal sintaks, mereka 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 terletak pada parameter pertama, yang merupakan nama worklet yang menjalankan animasi ini.

Deteksi fitur

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

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

Memuat worklet

Worklet adalah konsep baru yang diperkenalkan oleh satuan tugas Houdini untuk membuat banyak API baru lebih mudah di-build dan diskalakan. Kita akan membahas detail {i>worklet<i} sedikit lagi nanti, tetapi untuk kesederhanaan, Anda dapat menganggapnya murah dan untuk saat ini, thread ringan (seperti pekerja).

Kita perlu memastikan bahwa kita telah memuat {i>worklet<i} dengan nama "{i>passthrough<i}", 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 nama yang kita gunakan dalam konstruktor WorkletAnimation() di atas. Setelah pendaftaran selesai, promise yang ditampilkan oleh addModule() akan di-resolve dan kita dapat mulai membuat animasi menggunakan {i>worklet<i} itu.

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

Waktu

Parameter currentTime dari metode animate() kita adalah currentTime dari linimasa yang kita teruskan ke konstruktor WorkletAnimation(). Di contoh, kita baru saja meneruskan waktu tersebut ke efek. Tapi karena ini adalah kode JavaScript, dan kita dapat mengubah 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 untuk rentang [0; 2000], yaitu rentang waktu di mana efek kita didefinisikan. Baru saja animasinya terlihat sangat berbeda, tanpa perlu mengubah keyframe atau opsi animasi. Kode worklet dapat berupa kompleks secara bebas, dan memungkinkan Anda untuk menentukan secara terprogram efek mana diputar dalam urutan apa dan sejauh mana.

Opsi daripada Opsi

Anda mungkin ingin menggunakan kembali worklet dan mengubah nomornya. Oleh karena itu, 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 tersebut digerakkan dengan kode yang sama, tetapi dengan opsi yang berbeda.

Tampilkan negara bagian lokal Anda.

Seperti yang saya jelaskan sebelumnya, salah satu masalah utama mengerjakan animasi adalah animasi stateful. Worklet animasi diizinkan untuk menahan status. Namun, satu fitur inti dari worklet adalah bahwa mereka dapat dimigrasikan ke atau bahkan dihancurkan untuk menghemat sumber daya, yang juga akan menghancurkan status. Untuk mencegah hilangnya status, worklet animasi menawarkan hook yang dipanggil sebelum worklet dihancurkan yang dapat Anda gunakan untuk menampilkan status . Objek itu akan diteruskan ke konstruktor saat worklet dibuat ulang. Pada pembuatan awal, parameter tersebut adalah 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 mendapatkan skor 50/50 ke arah mana persegi akan berputar. Jika browser dihancurkan {i>worklet<i} dan memigrasikannya ke thread yang berbeda, akan ada Math.random() panggilan saat pembuatan, yang dapat menyebabkan perubahan tiba-tiba arah. Untuk memastikan hal itu tidak terjadi, kita mengembalikan animasi arah yang dipilih secara acak sebagai state dan menggunakannya dalam konstruktor, jika disediakan.

Terhubung ke kontinum ruang-waktu: ScrollLinimasa

Seperti yang telah ditunjukkan bagian sebelumnya, AnimationWorklet memungkinkan kita untuk secara terprogram menentukan bagaimana kemajuan garis waktu akan memengaruhi efek animasi. Namun sejauh ini, linimasa kami tetap document.timeline, yaitu melacak waktu.

ScrollTimeline membuka kemungkinan baru dan memungkinkan Anda menjalankan animasi dengan menggulir alih-alih waktu. Kita akan menggunakan kembali "passthrough" worklet untuk ini 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();

Kita membuat ScrollTimeline baru, bukan meneruskan document.timeline. Anda mungkin sudah menebaknya, ScrollTimeline tidak menggunakan waktu tapi Posisi scroll scrollSource untuk menyetel currentTime di worklet. Berada di-scroll ke paling atas (atau ke kiri) berarti currentTime = 0, sedangkan di-scroll ke paling bawah (atau kanan) akan menyetel currentTime ke timeRange. Jika Anda men-scroll kotak di demo, 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 di Anda harus selalu siap untuk NaN sebagai currentTime Anda. Sering masuk akal untuk ditetapkan secara {i>default<i} ke nilai 0.

Menautkan animasi dengan posisi scroll adalah sesuatu yang sudah lama dicari, tapi tidak pernah benar-benar dicapai pada tingkat {i>fidelity <i}ini (terlepas dari teknik solusi dengan CSS3D). Worklet Animasi memungkinkan efek ini diimplementasikan dengan mudah sekaligus berperforma tinggi. Contoh: efek scroll paralaks seperti ini demo menunjukkan bahwa hal tersebut sekarang hanya membutuhkan beberapa baris untuk menentukan animasi berbasis scroll.

Di balik layar

Worklet

Worklet adalah konteks JavaScript dengan cakupan yang terisolasi dan API yang sangat kecil ditampilkan. Platform API yang kecil memungkinkan pengoptimalan yang lebih agresif dari {i>browser<i} web, terutama pada perangkat kelas bawah. Selain itu, worklet tidak terikat pada loop peristiwa tertentu, tetapi bisa dipindahkan antar-thread sesuai kebutuhan. Ini adalah sangat penting untuk AnimationWorklet.

{i>Compositor NSync<i}

Anda mungkin tahu bahwa properti CSS tertentu cepat dianimasikan, sementara yang lain tidak. Beberapa properti hanya memerlukan beberapa pekerjaan di GPU untuk dianimasikan, sementara yang lain memaksa {i>browser<i} untuk mengatur ulang seluruh dokumen.

Di Chrome (seperti di banyak {i>browser<i} lain) kita memiliki proses yang disebut {i>compositor<i}, tugasnya — dan saya sangat menyederhanakannya — untuk mengatur {i>layer<i} dan tekstur dan kemudian memanfaatkan GPU untuk memperbarui layar secara teratur, idealnya secepat pembaruan layar (biasanya 60 Hz). Bergantung pada Properti CSS sedang dianimasikan, browser mungkin hanya perlu memiliki compositor melakukan tugasnya, sementara properti lain perlu menjalankan tata letak, yang merupakan operasi yang hanya dapat dilakukan oleh thread utama. Tergantung pada properti yang Anda berencana dianimasikan, {i>worklet<i} animasi Anda akan terikat ke elemen atau berjalan di utas terpisah yang disinkronkan dengan compositor.

Menampar di pergelangan tangan

Biasanya hanya ada satu proses {i>compositor<i} yang berpotensi dibagikan pada beberapa tab, karena GPU adalah sumber daya yang sangat bersaing. Jika compositor mendapat entah bagaimana diblokir, seluruh browser berhenti berfungsi dan menjadi tidak responsif terhadap input pengguna. Hal ini harus dihindari dengan segala cara. Jadi apa yang terjadi jika {i>worklet<i} tidak dapat mengirimkan data yang dibutuhkan compositor tepat waktu sebelum {i>frame<i} dirender?

Jika ini terjadi, worklet diizinkan — sesuai spesifikasi — untuk "tergelincir". Tertinggal compositor, dan compositor diizinkan untuk menggunakan kembali data dari {i>frame<i} terakhir untuk menjaga agar kecepatan frame tetap tinggi. Secara visual, ini akan terlihat seperti jank, tetapi perbedaannya adalah bahwa browser masih responsif terhadap input pengguna.

Kesimpulan

Ada banyak faset pada AnimationWorklet dan manfaatnya bagi web. Manfaat yang jelas adalah lebih banyak kontrol atas animasi dan cara baru untuk mendorong animasi untuk membawa tingkat ketelitian visual yang baru ke web. Namun, API juga memungkinkan Anda membuat aplikasi lebih tahan terhadap jank sambil akses terhadap semua kebaikan yang baru pada saat yang sama.

Worklet Animasi ada dalam Canary dan kami menargetkan Uji Coba Origin dengan Chrome 71. Kami sangat menantikan pengalaman web baru dan pengalaman hebat Anda tentang apa yang dapat kami tingkatkan. Tersedia juga polyfill yang memberi Anda API yang sama, tetapi tidak menyediakan isolasi performa.

Perlu diingat bahwa Transisi CSS dan Animasi CSS masih valid pilihan dan bisa jauh lebih sederhana untuk animasi dasar. Tetapi jika Anda ingin keren, AnimationWorklet siap membantu Anda!