Di sebuah situs klien satu halaman yang seluruh isinya digerakkan scroll, ada satu layer backdrop di belakang hero. Panel besar, tugasnya sederhana: naik dari bawah layar dan berhenti pas menutupi viewport begitu bagian itu masuk. Posisi parkirnya saya tulis di CSS, transform: translateY(102%), supaya sebelum JavaScript sempat jalan panel itu sudah berada di luar layar dan tidak ada kedipan di muat pertama. Angkanya 102 dan bukan 100 karena saya mau ada margin kecil buat pembulatan subpiksel, biar tidak ada sisa satu dua piksel yang mengintip di bawah lipatan. Sisanya saya serahkan ke GSAP: satu fromTo di yPercent, dari 100 ke 0.
Di layar ceritanya lain. Panelnya memang bergerak dan animasinya mulus, tapi mulainya jauh lebih rendah dari yang saya bayangkan, seperti berangkat dari dua kali lipat jarak yang saya minta. Lebih parah lagi di ujung: waktu tween-nya selesai, panel itu tidak pernah benar-benar duduk di posisi nol. Dia berhenti satu tinggi elemen di bawah tempat seharusnya, dan diam di situ. Dari kursi penonton itu kelihatan persis seperti layer yang macet.
Yang bikin bingung, GSAP tidak mengeluh sedikit pun. Tidak ada warning, tidak ada error, tween-nya berjalan sampai selesai dengan normal.
Menelusuri jejaknya
Tebakan pertama saya soal timing: mungkin trigger-nya nyala di titik yang salah, atau ada tween lain yang ikut menyentuh elemen yang sama. Saya cek satu per satu dan tidak ketemu apa-apa. Lagipula ada satu fakta yang seharusnya langsung menyingkirkan hipotesis timing: posisi akhirnya salah. Kalau cuma soal kapan animasinya jalan, titik istirahat di akhir tetap harus benar. Yang salah di sini justru posisi diamnya, jadi masalahnya ada di nilai, bukan di waktu.
Jadi saya berhenti menebak dan mulai mengukur. Cara tercepat melihat isi kepala GSAP soal sebuah elemen adalah menanyakannya langsung:
console.log(gsap.getProperty(backdrop, "yPercent"));
console.log(gsap.getProperty(backdrop, "y"));Yang pertama masuk akal, angkanya sesuai dengan tween yang sedang jalan. Yang kedua bikin saya berhenti sejenak: y balik dengan angka piksel yang besarnya kira-kira setinggi elemen itu sendiri. Padahal saya tidak pernah menulis y di mana pun. Tidak di fromTo, tidak di gsap.set, tidak di tween lain. Ada yang mengisinya tanpa saya minta, dan cuma ada satu nilai posisi yang saya tulis sendiri untuk elemen ini: translateY(102%) di stylesheet.
Akar masalahnya
GSAP tidak menyimpan transform sebagai satu string. Dia memecahnya jadi komponen-komponen terpisah yang bisa di-tween sendiri-sendiri: x dan y dalam piksel, xPercent dan yPercent dalam persen, lalu merangkainya kembali jadi satu nilai transform di setiap frame. Kuncinya ada di kata "merangkai kembali". Pergeseran vertikal akhir sebuah elemen bukan y saja atau yPercent saja, melainkan jumlah keduanya.
Waktu GSAP pertama kali menyentuh elemen, dia membaca transform yang sudah ada di sana dan memecahnya ke slot-slot tadi. Dan di sinilah jebakannya: translateY(102%) yang saya tulis di CSS tidak mendarat di yPercent. Dia dibaca sebagai nilai y, dalam piksel, sebagai komponen yang benar-benar terpisah dari yPercent yang sedang saya animasikan.
Begitu itu terbaca, dua gejala yang tadinya kelihatan berdiri sendiri langsung nyambung jadi satu. Di awal tween, y menyumbang kira-kira satu tinggi elemen dan yPercent: 100 menyumbang satu tinggi elemen lagi, jadi panelnya berangkat dari sekitar dua kali jarak yang saya niatkan. Di akhir tween, yPercent memang sudah nol, tapi y tidak pernah ikut nol karena tidak ada yang pernah menyentuhnya. Sisa piksel dari CSS itu tetap terpasang, dan panelnya berhenti satu tinggi elemen di bawah target. Bukan dua bug, satu bug dengan dua wajah.
Dan gagalnya senyap karena dari sudut pandang GSAP tidak ada yang salah. Ini bukan konflik yang perlu diperingatkan. Dua properti yang berbeda, dua-duanya berisi nilai yang sah, dan menjumlahkannya justru perilaku yang benar. Yang keliru cuma asumsi saya, bahwa transform yang saya tulis di CSS adalah titik nol yang netral untuk tween persen.
Perbaikannya
Perbaikannya kecil dan tidak butuh mengubah CSS sama sekali. Kunci y ke nol secara eksplisit, dan lakukan itu di kedua state fromTo:
// rusak: y hasil parse dari CSS ikut terbawa ke total transform
gsap.fromTo(
backdrop,
{ yPercent: 100 },
{ yPercent: 0 }
);
// benar: y dikunci nol di state awal dan state akhir
gsap.fromTo(
backdrop,
{ yPercent: 100, y: 0 },
{ yPercent: 0, y: 0 }
);Dengan y: 0 di state awal, nilai piksel yang tadinya nyangkut langsung dibuang begitu tween mulai, jadi panelnya berangkat dari satu tinggi elemen saja seperti yang saya maksud. Dengan y: 0 juga di state akhir, tidak ada satu pun state yang ditulis GSAP untuk elemen ini yang membiarkan y diisi dari luar. Ini penting kalau tween-nya bisa dibangun ulang, dibalik arah, atau nilai awalnya dihitung ulang saat layout berubah, karena di momen-momen itu GSAP membaca lagi kondisi elemen dan nilai piksel dari CSS punya kesempatan untuk masuk kembali. Menulisnya dua kali tidak memakan biaya apa-apa dan menutup pintu itu untuk selamanya.
Ada jalan lain yang juga menyelesaikan masalah: jangan parkir elemennya lewat transform persen di CSS, biarkan GSAP yang memegang transform itu sejak awal lewat gsap.set. Tapi itu berarti ada jendela waktu antara halaman ter-render dan skrip animasi jalan, dan di jendela itu panelnya berada di posisi nol, terlihat. Justru itu yang mau saya hindari waktu menulis translateY(102%) di CSS. Jadi saya memilih mempertahankan parkir di CSS dan mengunci y di JavaScript.
Pelajaran
- GSAP memisahkan
y(piksel) danyPercent(persen), dan pergeseran akhirnya adalah jumlah keduanya. Men-tween yang satu tidak menghapus yang lain. - Transform yang kamu tulis di CSS bukan titik awal yang netral. GSAP membacanya lebih dulu dan memecahnya ke komponennya sendiri, dan
translateYdalam persen tidak otomatis mendarat diyPercent. - Kalau animasinya mulus tapi elemennya berhenti di posisi yang salah, curigai nilainya, bukan timing-nya. Titik istirahat yang meleset hampir selalu soal nilai awal, bukan soal kapan tween-nya nyala.
gsap.getProperty()adalah cara paling cepat melihat apa yang sebenarnya GSAP simpan untuk sebuah elemen. Properti berisi angka yang tidak pernah kamu set adalah petunjuk besar.- Kalau elemennya sudah punya transform bawaan dari CSS, tulis
y: 0(danx: 0kalau geraknya horizontal) di kedua statefromTo. Murah, dan menutup satu kelas bug yang gagal tanpa suara.