Menganimasikan buram

Pemburaman adalah cara yang bagus untuk mengalihkan fokus pengguna. Membuat beberapa elemen visual tampak buram sekaligus mempertahankan elemen lain dalam fokus secara alami akan mengarahkan fokus pengguna. Pengguna mengabaikan konten yang diburamkan dan berfokus pada konten yang dapat dibaca. Salah satu contohnya adalah daftar ikon yang menampilkan detail setiap item saat kursor diarahkan ke atasnya. Selama waktu tersebut, pilihan lainnya dapat diburamkan untuk mengalihkan pengguna ke informasi yang baru ditampilkan.

TL;DR (Ringkasan)

Menganimasikan blur bukanlah pilihan karena sangat lambat. Sebagai gantinya, hitung dahulu serangkaian versi yang makin diburamkan dan cross-fade di antara versi tersebut. Kolega saya, Yi Gu, menulis library untuk menangani semuanya untuk Anda. Lihat demo kami.

Namun, teknik ini bisa sangat mengagetkan jika diterapkan tanpa periode transisi. Menganimasikan blur — bertransisi dari tidak diburamkan menjadi buram — sepertinya merupakan pilihan yang wajar, tetapi jika pernah mencoba melakukannya di web, Anda mungkin menemukan bahwa animasinya tidak halus, seperti yang ditunjukkan demo ini jika Anda tidak memiliki mesin yang canggih. Bisakah kami melakukan yang lebih baik?

Permasalahan

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

Saat ini, kita tidak dapat membuat animasi pemburaman secara efisien. Namun, kita dapat menemukan solusi yang terlihat cukup bagus, namun secara teknis, bukan animasi blur. Untuk memulai, pertama-tama mari pahami mengapa animasi blur 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 diharuskan untuk mendukung Internet Explorer, Anda tidak memiliki pilihan selain menggunakan filter SVG karena IE 10 dan 11 mendukung filter tersebut, tetapi tidak dengan filter CSS. Kabar baiknya adalah solusi untuk menganimasikan buram akan berfungsi pada kedua teknik tersebut. Jadi, mari kita coba menemukan bottleneck dengan melihat DevTools.

Jika mengaktifkan "Paint Flashing" di DevTools, Anda tidak akan melihat flash sama sekali. Sepertinya penggambaran ulang tidak dilakukan. Dan itu secara teknis benar karena "penggambaran ulang" mengacu pada CPU yang harus mengecat 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 blur. Filter konvolusi cukup mahal karena untuk setiap piksel output, sejumlah piksel input harus dipertimbangkan. Semakin besar gambar atau semakin besar radius blur, semakin mahal efeknya.

Dan di situlah masalahnya, kami menjalankan operasi GPU yang agak mahal setiap frame, meniup anggaran frame kami sebesar 16 md dan karenanya berakhir di bawah 60 fps.

Menjelajahi lubang kelinci

Jadi, apa yang bisa kita lakukan untuk membuat ini berjalan lancar? Kita bisa menggunakan sulap! Daripada menganimasikan nilai blur yang sebenarnya (radius blur), kita pra-hitung beberapa salinan buram tempat nilai blur meningkat secara eksponensial, lalu cross-fade di antara salinan tersebut menggunakan opacity.

Cross-fade adalah serangkaian fade-in dan fade-out opasitas yang tumpang tindih. Misalnya, jika kita memiliki empat tahap pemburaman, kita memudarkan tahap pertama dan memudar pada tahap kedua secara bersamaan. Setelah tahap kedua mencapai opasitas 100% dan tahap pertama telah mencapai 0%, kita memperjelas tahap kedua dan memudar di tahap ketiga. Setelah selesai, kita akhirnya memperjelas tahap ketiga dan memudar pada versi keempat dan terakhir. Dalam skenario ini, setiap tahap akan memerlukan 1⁄4 dari total durasi yang diinginkan. Secara visual, ini terlihat sangat mirip dengan buram animasi yang nyata.

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

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

Dengan melihat DevTools secara sekilas, GPU masih sangat sibuk dan meregangkan setiap frame hingga ~90 md. Mengapa? Kita tidak mengubah nilai blur lagi, hanya opasitas, jadi apa yang terjadi? Masalahnya, sekali lagi, terletak pada sifat efek blur: Seperti yang dijelaskan sebelumnya, jika elemen dipromosikan dan diburamkan, efek tersebut akan diterapkan oleh GPU. Jadi, meskipun kita tidak menganimasikan nilai blur lagi, teksturnya tetap tidak diburamkan dan perlu diburamkan ulang setiap frame oleh GPU. Penyebab kecepatan frame menjadi lebih buruk daripada sebelumnya berasal dari fakta bahwa dibandingkan dengan implementasi baru, GPU sebenarnya memiliki lebih banyak pekerjaan daripada sebelumnya, karena sering kali dua tekstur terlihat dan perlu diburamkan secara terpisah.

Apa yang kami pikirkan tidak terlalu bagus, tapi itu membuat animasinya super cepat. Kita kembali ke tidak mempromosikan elemen yang akan diburamkan, tetapi mempromosikan wrapper induk. Jika elemen diburamkan dan dipromosikan, efek akan diterapkan oleh GPU. Inilah yang membuat demo kami lambat. Jika elemen diburamkan tetapi tidak dipromosikan, pemburaman dirasterisasi ke tekstur induk terdekat. Dalam kasus kita, itu adalah elemen wrapper induk yang dipromosikan. Gambar yang diburamkan sekarang menjadi tekstur elemen induk dan dapat digunakan kembali untuk semua frame di masa mendatang. Ini hanya berfungsi karena kita tahu bahwa elemen yang diburamkan tidak dianimasikan dan menyimpannya dalam cache sebenarnya bermanfaat. Berikut adalah demo yang menerapkan teknik ini. Kira-kira apa yang dipikirkan Moto G4 tentang pendekatan ini? Peringatan {i>spoiler<i}: menurutnya ini bagus:

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

Sekarang kita punya kapasitas besar di GPU dan 60 fps yang sangat halus. Kita berhasil!

Produksi

Dalam demo, kita menduplikasi struktur DOM beberapa kali agar salinan kontennya dapat diburamkan dengan kekuatan yang berbeda. Anda mungkin ingin tahu bagaimana cara kerjanya dalam lingkungan produksi, karena hal ini mungkin memiliki beberapa efek samping yang tidak diinginkan pada gaya CSS penulis atau bahkan JavaScript-nya. Kamu benar. Masuk Shadow DOM!

Meskipun kebanyakan orang menganggap Shadow DOM sebagai cara untuk melampirkan elemen "internal" ke Elemen Kustom, elemen ini juga merupakan primitif isolasi dan performa. JavaScript dan CSS tidak dapat menembus batas Shadow DOM yang memungkinkan kita menduplikasi konten tanpa mengganggu gaya developer atau logika aplikasi. 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. Jika demikian, kita kembali dengan hanya menduplikasi konten dan mengharapkan yang terbaik agar tidak ada yang rusak. Kita dapat menggunakan Shadow DOM polyfill dengan ShadyCSS, tetapi tidak diterapkan dalam library ini.

Nah, begitulah. Setelah menelusuri pipeline rendering Chrome, kami menemukan cara menganimasikan blur di seluruh browser secara efisien.

Kesimpulan

Efek seperti ini tidak boleh digunakan dengan enteng. Karena kita menyalin elemen DOM dan memaksanya ke lapisannya sendiri, kita dapat mendorong batas perangkat yang lebih rendah. Menyalin semua stylesheet ke setiap ShadowRoot juga berpotensi menimbulkan risiko performa, jadi Anda harus memutuskan apakah akan menyesuaikan logika dan gaya agar tidak terpengaruh oleh salinan di LightDOM atau menggunakan teknik ShadowDOM kami. Tapi terkadang teknik kita bisa menjadi investasi yang berharga. Lihat kode di repositori GitHub kami serta demo, dan hubungi saya di Twitter jika Anda memiliki pertanyaan.