Menganimasikan buram

Efek blur adalah cara yang bagus untuk mengalihkan fokus pengguna. Membuat beberapa elemen visual tampak buram sekaligus menjaga fokus elemen lain secara alami akan mengarahkan fokus pengguna. Pengguna mengabaikan konten yang diburamkan dan berfokus pada konten yang dapat mereka baca. Salah satu contohnya adalah daftar ikon yang menampilkan detail tentang setiap item saat diarahkan kursor. Selama waktu tersebut, pilihan yang tersisa dapat diburamkan untuk mengalihkan pengguna ke informasi yang baru ditampilkan.

TL;DR

Menganimasikan pemburaman bukanlah opsi yang tepat karena prosesnya sangat lambat. Sebagai gantinya, hitungkan terlebih dahulu serangkaian versi yang semakin buram dan lakukan transisi silang di antara versi tersebut. Rekan saya, Yi Gu, menulis library untuk menangani semuanya untuk Anda. Lihat demo kami.

Namun, teknik ini dapat cukup mengganggu jika diterapkan tanpa periode transisi. Menganimasikan pemburaman — bertransisi dari tidak diburamkan menjadi diburamkan — tampaknya merupakan pilihan yang wajar, tetapi jika pernah mencoba melakukannya di web, Anda mungkin mendapati bahwa animasinya tidak lancar, seperti yang ditunjukkan demo ini jika Anda tidak memiliki mesin yang canggih. Bisakah kita melakukan yang lebih baik?

Permasalahan

Markup
diubah menjadi tekstur oleh CPU. Tekstur diupload ke GPU. GPU
menggambar tekstur ini ke framebuffer menggunakan shader. Pemburaman terjadi di shader.

Saat ini, kita tidak dapat membuat animasi pemburaman berfungsi secara efisien. Namun, kita dapat menemukan solusi yang terlihat cukup baik, tetapi secara teknis, bukan buram animasi. Untuk memulai, mari kita pahami terlebih dahulu alasan pemburaman animasi lambat. Untuk memburamkan elemen di web, ada dua teknik: Properti filter CSS dan filter SVG. Berkat peningkatan dukungan dan kemudahan penggunaan, filter CSS biasanya digunakan. Sayangnya, jika Anda diwajibkan untuk mendukung Internet Explorer, Anda tidak punya pilihan selain menggunakan filter SVG karena IE 10 dan 11 mendukungnya, tetapi tidak mendukung filter CSS. Kabar baiknya adalah solusi kami untuk menganimasikan buram berfungsi dengan kedua teknik tersebut. Jadi, mari kita coba temukan bottleneck dengan melihat DevTools.

Jika mengaktifkan "Paint Flashing" di DevTools, Anda tidak akan melihat flash sama sekali. Sepertinya tidak ada proses pengecatan ulang yang terjadi. Dan secara teknis hal ini benar karena "repaint" mengacu pada CPU yang harus mewarnai ulang tekstur elemen yang dipromosikan. Setiap kali elemen dipromosikan dan diburamkan, pemburaman diterapkan oleh GPU menggunakan shader.

Filter SVG dan filter CSS menggunakan filter konvolusi untuk menerapkan buram. Filter konvolusi cukup mahal karena untuk setiap piksel output, sejumlah piksel input harus dipertimbangkan. Makin besar gambar atau makin besar radius blur, makin mahal efeknya.

Dan di sinilah letak masalahnya, kita menjalankan operasi GPU yang agak mahal setiap frame, sehingga menghabiskan anggaran frame sebesar 16 md dan akhirnya jauh di bawah 60 fps.

Rabbit Hole

Jadi, apa yang dapat kita lakukan agar proses ini berjalan lancar? Kita dapat menggunakan trik sulap. Alih-alih mengoanimasi nilai blur sebenarnya (radius blur), kita melakukan pra-komputasi beberapa salinan yang diburamkan dengan nilai blur yang meningkat secara eksponensial, lalu melakukan transisi silang di antara keduanya menggunakan opacity.

Cross-fade adalah serangkaian transisi fade-in dan fade-out opasitas yang tumpang-tindih. Misalnya, jika memiliki empat tahap pemburaman, kita akan memudarkan tahap pertama sekaligus memudarkan tahap kedua secara bersamaan. Setelah tahap kedua mencapai opasitas 100% dan tahap pertama mencapai 0%, kita akan memudarkan tahap kedua sekaligus memudarkan tahap ketiga. Setelah selesai, kita akhirnya memudarkan tahap ketiga dan memudar versi keempat dan terakhir. Dalam skenario ini, setiap tahap akan memerlukan ¼ dari total durasi yang diinginkan. Secara visual, ini terlihat sangat mirip dengan blur animasi yang sebenarnya.

Dalam eksperimen kami, meningkatkan radius pemburaman secara eksponensial per tahap menghasilkan hasil visual terbaik. Contoh: Jika memiliki empat tahap pemburaman, kita akan menerapkan filter: blur(2^n) ke setiap tahap, yaitu tahap 0: 1 piksel, tahap 1: 2 piksel, tahap 2: 4 piksel dan tahap 3: 8 piksel. Jika kita memaksa setiap salinan yang diburamkan ini ke lapisannya sendiri (disebut "mempromosikan") menggunakan will-change: transform, mengubah opasitas pada elemen ini akan sangat cepat. Secara teori, hal ini akan memungkinkan kita melakukan pemuatan awal pada pekerjaan pemburaman yang mahal. Ternyata, logikanya salah. Jika menjalankan demo ini, Anda akan melihat bahwa kecepatan frame masih di bawah 60 fps, dan pemburaman sebenarnya lebih buruk dari sebelumnya.

DevTools
  menampilkan rekaman aktivitas saat GPU memiliki periode waktu sibuk yang lama.

Pemeriksaan singkat pada DevTools mengungkapkan bahwa GPU masih sangat sibuk dan memperluas setiap frame hingga ~90 md. Namun, mengapa? Kita tidak lagi mengubah nilai blur, hanya opasitas, jadi apa yang terjadi? Sekali lagi, masalahnya terletak pada sifat efek blur: Seperti yang dijelaskan sebelumnya, jika elemen dipromosikan dan diburamkan, efeknya akan diterapkan oleh GPU. Jadi, meskipun kita tidak lagi menganimasikan nilai blur, tekstur itu sendiri masih tidak diburamkan dan perlu diburamkan ulang setiap frame oleh GPU. Alasan kecepatan frame menjadi lebih buruk daripada sebelumnya berasal dari fakta bahwa dibandingkan dengan penerapan sederhana, GPU sebenarnya memiliki lebih banyak pekerjaan daripada sebelumnya, karena sebagian besar waktu dua tekstur terlihat yang perlu diburamkan secara independen.

Hasil yang kita dapatkan tidak bagus, tetapi membuat animasi menjadi sangat cepat. Kita kembali ke tidak mempromosikan elemen yang akan diburamkan, tetapi mempromosikan wrapper induk. Jika elemen diburamkan dan dipromosikan, efeknya akan diterapkan oleh GPU. Inilah yang membuat demo kita lambat. Jika elemen diburamkan tetapi tidak dipromosikan, pemburaman akan dirasterisasi ke tekstur induk terdekat. Dalam kasus ini, elemen wrapper induk yang dipromosikan. Gambar yang diburamkan kini menjadi tekstur elemen induk dan dapat digunakan kembali untuk semua frame mendatang. Hal ini hanya berfungsi karena kita tahu bahwa elemen yang diburamkan tidak dianimasikan dan menyimpan cache-nya sebenarnya bermanfaat. Berikut adalah demo yang menerapkan teknik ini. Saya ingin tahu pendapat Moto G4 tentang pendekatan ini? Peringatan spoiler: ia menganggapnya hebat:

DevTools
  menampilkan rekaman aktivitas saat GPU memiliki banyak waktu tidak ada aktivitas.

Sekarang kita memiliki banyak headroom di GPU dan 60 fps yang lancar. Kita berhasil!

Produksi

Dalam demo, kami menduplikasi struktur DOM beberapa kali agar salinan konten dapat diburamkan dengan kekuatan yang berbeda. Anda mungkin bertanya-tanya bagaimana cara kerja ini di lingkungan produksi karena mungkin memiliki beberapa efek samping yang tidak diinginkan dengan gaya CSS penulis atau bahkan JavaScript-nya. Anda benar. Masuk ke DOM Bayangan.

Meskipun sebagian besar orang menganggap DOM Bayangan sebagai cara untuk melampirkan elemen "internal" ke Elemen Khusus mereka, DOM Bayangan juga merupakan isolasi dan primitif performa. JavaScript dan CSS tidak dapat menembus batas Shadow DOM yang memungkinkan kita menduplikasi konten tanpa mengganggu gaya atau logika aplikasi developer. Kita sudah memiliki elemen <div> untuk setiap salinan yang akan dirasterisasi dan sekarang menggunakan <div> ini sebagai host bayangan. Kita membuat ShadowRoot menggunakan attachShadow({mode: 'closed'}) dan melampirkan salinan konten ke ShadowRoot, bukan <div> itu sendiri. Kita harus memastikan untuk juga menyalin semua stylesheet ke ShadowRoot untuk menjamin bahwa salinan kita diberi gaya dengan cara yang sama seperti aslinya.

Beberapa browser tidak mendukung Shadow DOM v1, dan untuk browser tersebut, kami kembali hanya menduplikasi konten dan berharap tidak ada yang rusak. Kita dapat menggunakan polyfill Shadow DOM dengan ShadyCSS, tetapi kita tidak menerapkannya di library.

Selesai. Setelah mempelajari pipeline rendering Chrome, kami menemukan cara menganimasikan pemburaman secara efisien di seluruh browser.

Kesimpulan

Efek semacam ini tidak boleh digunakan dengan sembarangan. Karena kita menyalin elemen DOM dan memaksanya ke lapisannya sendiri, kita dapat mendorong batas perangkat tingkat rendah. Menyalin semua stylesheet ke setiap ShadowRoot juga merupakan potensi risiko performa, jadi Anda harus memutuskan apakah lebih suka menyesuaikan logika dan gaya agar tidak terpengaruh oleh salinan di LightDOM atau menggunakan teknik ShadowDOM kami. Namun, terkadang teknik kita mungkin merupakan investasi yang berharga. Lihat kode di repositori GitHub kami serta demo dan hubungi saya di Twitter jika ada pertanyaan.