Membuat animasi luaskan & ciutkan yang berperforma tinggi

Stephen McGruer
Stephen McGruer

TL;DR

Gunakan transformasi skala saat menganimasikan klip. Anda dapat mencegah turunan diregangkan dan miring selama animasi dengan menskalakannya secara terbalik.

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

Misalnya, menu yang diluaskan:

Beberapa opsi untuk mem-build ini memiliki performa yang lebih baik daripada opsi lainnya.

Buruk: Menganimasikan lebar dan tinggi pada elemen penampung

Anda bisa membayangkan menggunakan sedikit CSS untuk menganimasikan lebar dan tinggi pada 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 memerlukan animasi width dan height. Properti ini memerlukan penghitungan tata letak, dan menggambar hasilnya di setiap frame animasi, yang dapat sangat mahal, dan biasanya akan menyebabkan Anda kehilangan 60 fps. Jika Anda baru mengetahuinya, baca panduan Performa Rendering kami, tempat Anda bisa mendapatkan informasi selengkapnya tentang cara kerja proses rendering.

Buruk: Menggunakan properti clip atau clip-path CSS

Alternatif untuk menganimasikan width dan height mungkin adalah menggunakan properti clip (yang kini tidak digunakan lagi) untuk menganimasikan efek luaskan dan ciutkan. Atau, jika mau, Anda dapat menggunakan clip-path. Namun, penggunaan clip-path kurang didukung daripada clip. Namun, clip tidak digunakan lagi. Kanan. Namun, jangan putus asa, ini bukan 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, kelemahan pendekatan ini adalah masih memicu paint. Selain itu, properti clip, jika Anda menggunakan rute tersebut, memerlukan bahwa elemen yang mengoperasikannya diposisikan secara mutlak atau tetap, yang dapat memerlukan sedikit wrangling tambahan.

Baik: animasi skala

Karena efek ini melibatkan sesuatu yang menjadi lebih besar dan lebih kecil, Anda dapat menggunakan transformasi skala. Ini adalah kabar baik karena mengubah transformasi adalah sesuatu yang tidak memerlukan tata letak atau cat, dan yang dapat diserahkan browser ke GPU, yang berarti bahwa efeknya dipercepat dan kemungkinan besar mencapai 60 fps.

Kelemahan pendekatan ini, seperti sebagian besar hal dalam performa rendering, adalah memerlukan sedikit penyiapan. Namun, proses 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 diperlukan saat diciutkan, dan saat diluaskan. Mungkin untuk beberapa situasi, Anda tidak dapat mendapatkan kedua informasi ini sekaligus, dan Anda perlu — misalnya — mengalihkan beberapa class agar dapat membaca berbagai status komponen. Namun, jika Anda perlu melakukannya, berhati-hatilah: getBoundingClientRect() (atau offsetWidth dan offsetHeight) memaksa browser untuk menjalankan gaya dan meneruskan tata letak 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 wajar bahwa menu akan dimulai dalam skala alaminya (1, 1). Skala alami ini mewakili statusnya yang diperluas, yang berarti Anda harus menganimasikan dari versi yang diskalakan ke bawah (yang dihitung di atas) kembali ke skala alami tersebut.

Tapi tunggu! Tentunya ini akan menyesuaikan ukuran konten menu juga, bukan? Ya, seperti yang dapat Anda lihat di bawah.

Jadi, apa yang bisa Anda lakukan? Anda dapat menerapkan transformasi counter- ke konten, jadi misalnya jika penampung diskalakan ke 1/5 ukuran normalnya, Anda dapat menskalakan konten ke atas 5x untuk mencegah konten terhimpit. Ada dua hal yang perlu diperhatikan:

  1. Transformasi penghitung juga merupakan operasi penskalaan. Hal ini baik karena juga dapat dipercepat, seperti animasi pada penampung. Anda mungkin perlu memastikan bahwa elemen yang dianimasikan mendapatkan lapisan kompositornya sendiri (memungkinkan GPU 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 hal-hal menjadi sedikit lebih rumit, karena dengan asumsi bahwa animasi berada dalam CSS dan menggunakan fungsi easing, easing itu sendiri perlu diimbangi saat menganimasikan transformasi penghitung. Namun, menghitung kebalikan kurva untuk — misalnya — cubic-bezier(0, 0, 0.3, 1) tidak terlalu jelas.

Oleh karena itu, mungkin tergoda untuk membuat animasi efek menggunakan JavaScript. Lagi pula, Anda nanti bisa menggunakan persamaan easing untuk menghitung skala dan nilai skala berlawanan per frame. Kelemahan animasi berbasis JavaScript adalah apa yang terjadi saat thread utama (tempat JavaScript Anda berjalan) sibuk dengan beberapa tugas lain. Jawaban singkatnya adalah animasi Anda dapat tersendat atau berhenti sama sekali, yang tidak bagus untuk UX.

Langkah 2: Build Animasi CSS dengan cepat

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

Untuk membuat animasi keyframe, kita akan melakukan langkah dari 0 hingga 100 dan menghitung nilai skala yang akan diperlukan untuk elemen dan kontennya. Kemudian, ini dapat diringkas menjadi string, yang dapat dimasukkan ke halaman sebagai elemen gaya. Memasukkan gaya akan menyebabkan penerusan ReCalculate Styles di halaman, yang merupakan pekerjaan tambahan yang harus dilakukan browser, tetapi hanya akan melakukannya sekali saat komponen melakukan 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}
    }`;
}

Penasaran mungkin akan bertanya-tanya tentang fungsi ease() di dalam for-loop. Anda dapat menggunakan hal seperti ini untuk memetakan nilai dari 0 hingga 1 ke nilai yang disederhanakan.

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

Anda juga dapat menggunakan penelusuran Google untuk memetakan tampilannya. Praktis! Jika Anda memerlukan persamaan easing lainnya, lihat Tween.js oleh Soledad Penadés, yang berisi banyak persamaan easing.

Langkah 3: Aktifkan Animasi CSS

Dengan animasi ini dibuat dan di-bake ke halaman dalam JavaScript, langkah terakhir adalah mengubah 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;
}

Tindakan ini akan menyebabkan animasi yang dibuat di langkah sebelumnya berjalan. Karena animasi yang di-bake sudah di-ease, fungsi pengaturan waktu harus ditetapkan ke linear. Jika tidak, Anda akan melakukan ease di antara setiap keyframe yang akan terlihat sangat aneh.

Untuk menciutkan elemen kembali, ada dua opsi: perbarui animasi CSS agar berjalan mundur, bukan maju. Hal ini akan berfungsi dengan baik, tetapi "rasa" animasi akan terbalik, jadi jika Anda menggunakan kurva ease-out, kebalikannya akan terasa lebih mudah dimasukkan, yang akan membuatnya terasa lambat. Solusi yang lebih sesuai adalah membuat pasangan kedua animasi untuk menyempitkan elemen. Animasi ini dapat dibuat dengan cara yang sama persis 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: tampilan melingkar

Anda juga dapat menggunakan teknik ini untuk membuat animasi perluas dan ciutkan melingkar.

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

Peringatan tentang varian khusus ini: Chrome memiliki teks yang buram di layar DPI rendah selama animasi karena error pembulatan karena skala dan skala penghitung teks. Jika Anda tertarik dengan detailnya, ada laporan bug yang dapat Anda beri bintang dan ikuti.

Kode untuk efek perluasan melingkar dapat ditemukan di repo GitHub.

Kesimpulan

Jadi, itulah cara untuk melakukan animasi klip berperforma tinggi menggunakan transformasi skala. Dalam keadaan sempurna, akan sangat bagus jika animasi klip dipercepat (ada bug Chromium untuk itu yang dibuat oleh Jake Archibald), tetapi sampai kita mencapainya, Anda harus berhati-hati saat menganimasikan clip atau clip-path, dan tentu saja hindari menganimasikan width atau height.

Sebaiknya gunakan Web Animations untuk efek seperti ini, karena memiliki API JavaScript, tetapi dapat berjalan di thread compositor jika Anda hanya menganimasikan transform dan opacity. Sayangnya, dukungan untuk Animasi Web tidak terlalu baik, meskipun Anda dapat menggunakan progressive enhancement untuk menggunakannya jika tersedia.

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

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

Jika Anda ingin melihat kode untuk efek ini, lihat repositori GitHub Contoh Elemen UI dan, seperti biasa, beri tahu kami hasil Anda di komentar di bawah.