Membuat animasi luaskan & ciutkan yang berperforma tinggi

Stephen McGruer
Stephen McGruer

TL;DR (Ringkasan)

Gunakan transformasi skala saat menganimasikan klip. Anda dapat mencegah turunan melebar dan miring selama animasi dengan melakukan penskalaan konter.

Sebelumnya, kami telah memposting pembaruan tentang cara membuat efek paralaks dan scroller tanpa batas yang berperforma tinggi. Dalam postingan ini, kita akan melihat apa saja yang diperlukan jika Anda menginginkan animasi klip yang berperforma tinggi. Jika Anda ingin melihat demo, lihat Contoh repositori GitHub Elemen UI.

Misalnya, menu yang dapat diluaskan:

Beberapa opsi untuk membuat API ini memiliki performa yang lebih baik daripada yang lain.

Buruk: Menganimasikan lebar dan tinggi pada elemen penampung

Anda dapat membayangkan menggunakan sedikit CSS untuk menganimasikan lebar dan tinggi di elemen container.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

Masalah langsung dengan pendekatan ini adalah pendekatan ini memerlukan animasi width dan height. Properti ini mengharuskan penghitungan tata letak dan menggambar hasilnya di setiap frame animasi, yang bisa sangat mahal, dan biasanya akan menyebabkan Anda kehilangan 60 fps. Jika ini adalah kabar Anda, baca panduan Performa Rendering kami yang berisi informasi selengkapnya tentang cara kerja proses rendering.

Buruk: Menggunakan properti klip CSS atau jalur klip

Alternatif untuk menganimasikan width dan height mungkin adalah dengan menggunakan properti clip (kini tidak digunakan lagi) untuk menganimasikan efek luaskan dan ciutkan. Atau, jika mau, Anda dapat menggunakan clip-path. Namun, penggunaan clip-path kurang didukung dengan baik daripada clip. Namun, clip tidak digunakan lagi. Oke. Tapi jangan putus asa, ini bukanlah solusi yang Anda inginkan.

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

Meskipun lebih baik daripada menganimasikan width dan height elemen menu, kekurangan pendekatan ini adalah masih memicu paint. Selain itu, jika Anda menempuh rute tersebut, properti clip mengharuskan elemen yang dioperasikannya diposisikan secara mutlak atau tetap, sehingga dapat memerlukan sedikit wrangling tambahan.

Bagus: menganimasikan skala

Karena efek ini melibatkan sesuatu yang semakin besar dan kecil, Anda dapat menggunakan transformasi skala. Ini adalah kabar baik karena mengubah transformasi adalah sesuatu yang tidak memerlukan layout atau paint, dan dapat diserahkan oleh browser ke GPU, yang berarti efeknya dipercepat dan jauh lebih mungkin mencapai 60 fps.

Kelemahan dari pendekatan ini, seperti kebanyakan hal dalam performa rendering, adalah bahwa pendekatan ini memerlukan sedikit penyiapan. Namun, hal ini benar-benar sepadan.

Langkah 1: Hitung status awal dan akhir

Dengan pendekatan yang menggunakan animasi skala, langkah pertama adalah membaca elemen yang memberi tahu Anda ukuran menu yang harus diciutkan, maupun saat diluaskan. Mungkin untuk beberapa situasi, Anda tidak bisa mendapatkan kedua bit informasi ini sekaligus, dan Anda harus — misalnya — mengganti beberapa class agar dapat membaca berbagai status komponen. Namun, jika Anda perlu melakukannya, berhati-hatilah: getBoundingClientRect() (atau offsetWidth dan offsetHeight) memaksa browser menjalankan gaya dan tata letak diteruskan jika gaya telah berubah sejak terakhir kali dijalankan.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

Dalam kasus seperti menu, kita dapat membuat asumsi yang masuk akal bahwa menu tersebut akan mulai berada dalam skala alaminya (1, 1). Skala alami ini mewakili status yang diperluas, yang berarti Anda harus melakukan animasi dari versi yang diperkecil (yang dihitung di atas) kembali ke skala alami tersebut.

Tapi tunggu! Tentu ini akan meningkatkan skala isi menu, bukan? Nah, seperti yang bisa Anda lihat di bawah ini, ya.

Jadi, apa yang dapat Anda lakukan? Anda dapat menerapkan transformasi counter-ke konten, jadi misalnya jika container diperkecil hingga 1/5 dari ukuran normalnya, Anda dapat menskalakan konten naik sebesar 5x agar konten tidak terpotong. Ada dua hal yang perlu diperhatikan tentang hal ini:

  1. Transformasi penghitung juga merupakan operasi skala. Ini bagus karena juga dapat diakselerasi, seperti animasi di container. Anda mungkin perlu memastikan bahwa elemen yang dianimasikan mendapatkan lapisan compositor-nya sendiri (memungkinkan GPU untuk membantu), dan untuk itu Anda dapat menambahkan will-change: transform ke elemen atau, jika Anda perlu mendukung browser lama, backface-visiblity: hidden.

  2. Transformasi penghitung harus dihitung per frame. Di sinilah segalanya bisa menjadi sedikit lebih rumit, karena dengan asumsi bahwa animasi berada di CSS dan menggunakan fungsi easing, easing itu sendiri perlu dilawan saat menganimasikan transformasi penghitung. Namun, menghitung kurva terbalik untuk — misalnya — cubic-bezier(0, 0, 0.3, 1) tidaklah terlalu jelas.

Kemudian, Anda mungkin tergoda untuk mempertimbangkan animasi efek menggunakan JavaScript. Lagi pula, Anda kemudian dapat menggunakan persamaan easing untuk menghitung nilai skala dan skala penghitung per frame. Kelemahan dari setiap animasi berbasis JavaScript adalah yang terjadi saat thread utama (tempat JavaScript Anda berjalan) sibuk dengan beberapa tugas lain. Jawaban singkatnya adalah animasi Anda bisa tersendat atau berhenti sama sekali, yang tidak bagus untuk UX.

Langkah 2: Buat Animasi CSS dengan cepat

Solusinya, yang mungkin tampak aneh pada awalnya, adalah dengan membuat animasi keyframe dengan fungsi easing kita sendiri secara dinamis dan memasukkannya ke dalam halaman untuk digunakan oleh menu. (Terima kasih banyak kepada insinyur Chrome Robert Flack karena telah menunjukkan ini!) Manfaat utama dari hal ini adalah animasi dengan keyframe yang mengubah transformasi dapat dijalankan di compositor, yang berarti bahwa animasi tidak terpengaruh oleh tugas di thread utama.

Untuk membuat animasi keyframe, kita akan melangkah dari 0 hingga 100 dan menghitung nilai skala yang diperlukan untuk elemen dan kontennya. Hal ini kemudian dapat diringkas menjadi string, yang dapat dimasukkan ke dalam halaman sebagai elemen gaya. Memasukkan gaya akan menyebabkan Recalculated Styles diteruskan di halaman, yang merupakan pekerjaan tambahan yang harus dilakukan browser, tetapi hanya akan melakukannya sekali saat komponen booting.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

Pengguna yang penasaran mungkin akan bertanya-tanya tentang fungsi ease() di dalam for-loop. Anda dapat menggunakan contoh seperti ini untuk memetakan nilai dari 0 ke 1 ke ekuivalen yang dikurangkan.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Anda juga dapat menggunakan Google Penelusuran untuk memetakan tampilannya. Berguna! Jika Anda membutuhkan persamaan easing lainnya, lihat Tween.js dari Soledad Penadés, yang berisi banyak persamaan.

Langkah 3: Aktifkan Animasi CSS

Setelah animasi ini dibuat dan diterapkan ke halaman di JavaScript, langkah terakhir adalah mengalihkan class yang mengaktifkan animasi.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

Hal ini menyebabkan animasi yang dibuat di langkah sebelumnya berjalan. Karena animasi yang direkam sudah dipermudah, fungsi pengaturan waktu harus disetel ke linear. Jika tidak, Anda akan memudahkan di antara setiap keyframe yang akan terlihat sangat aneh.

Jika harus menciutkan elemen kembali, ada dua opsi: perbarui animasi CSS agar berjalan secara terbalik, bukan maju. Opsi ini akan berfungsi dengan baik, tetapi "nuansa" animasi akan terbalik, jadi jika Anda menggunakan kurva easy-out, bagian sebaliknya akan terasa melambat in, yang akan membuatnya terasa lambat. Solusi yang lebih tepat adalah membuat pasangan kedua animasi untuk menciutkan elemen. Ini dapat dibuat dengan cara yang persis sama seperti animasi keyframe perluasan, tetapi dengan nilai awal dan akhir yang ditukar.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

Versi yang lebih canggih: menampilkan lingkaran

Anda juga dapat menggunakan teknik ini untuk membuat animasi perluasan dan penciutan melingkar.

Prinsipnya sebagian besar sama dengan versi sebelumnya, tempat Anda menskalakan elemen, dan menskalakan turunan langsungnya. Dalam hal ini, elemen yang ditingkatkan skalanya memiliki border-radius 50%, membuatnya berbentuk lingkaran, dan digabungkan oleh elemen lain yang memiliki overflow: hidden, yang berarti bahwa Anda tidak melihat lingkaran meluas di luar batas elemen.

Sebagai peringatan untuk varian khusus ini: Chrome memiliki teks buram di layar DPI rendah selama animasi karena error pembulatan akibat skala dan skala penghitung teks. Jika Anda ingin mengetahui detailnya, ada bug yang dilaporkan yang dapat Anda bintangi dan ikuti.

Kode untuk efek perluasan melingkar dapat ditemukan di repo GitHub.

Kesimpulan

Demikianlah, cara membuat animasi klip berperforma tinggi menggunakan transformasi skala. Di dunia yang sempurna, akan sangat baik untuk melihat animasi klip yang dipercepat (ada bug Chromium untuk itu yang dibuat oleh Jake Archibald), tetapi sampai kita sampai di sana, Anda harus berhati-hati saat menganimasikan clip atau clip-path, dan tentu saja menghindari animasi width atau height.

Sebaiknya gunakan Animasi Web untuk efek seperti ini, karena efek tersebut memiliki JavaScript API, tetapi dapat berjalan di thread compositor jika Anda hanya menganimasikan transform dan opacity. Sayangnya, dukungan untuk Animasi Web bukanlah hal yang baik, meskipun Anda dapat menggunakan {i>progressive enhancement<i} untuk menggunakannya jika tersedia.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

Hingga perubahan itu terjadi, meskipun Anda dapat menggunakan library berbasis JavaScript untuk melakukan animasi, Anda mungkin akan mendapatkan performa yang lebih andal dengan membuat animasi CSS dan menggunakannya. Demikian pula, jika aplikasi Anda sudah mengandalkan JavaScript untuk animasinya, sebaiknya Anda setidaknya konsisten dengan codebase yang ada.

Jika Anda ingin mempelajari kode untuk efek ini, lihat repo GitHub Contoh Elemen UI dan, seperti biasa, beri tahu kami cara Anda menyampaikannya melalui komentar di bawah.