D
P
0
← Semua artikel Read in English

WordPress & PHP di Produksi

"Teks Dulu Animasi Baru Teks Lagi": `gsap.set` yang Menunggu Preloader Selesai

· · 4 menit baca
"Teks Dulu Animasi Baru Teks Lagi": `gsap.set` yang Menunggu Preloader Selesai

Kadang laporan bug dari klien lebih tepat daripada istilah teknis yang saya punya untuk menggantikannya. Kali ini kalimatnya begini, dan saya kutip persis: "teks dulu animasi baru teks lagi".

Enam kata, dan di dalamnya sudah ada seluruh urutan kejadian yang perlu saya perbaiki. Ini bukan temuan saya sendiri di layar sendiri, ini teguran yang datang dari sisi klien tentang animasi intro hero yang duduk di belakang preloader.

Tiga tahap yang dilaporkan, semuanya di satu kedipan

Kalau kalimat tadi diurai, yang dilaporkan sebenarnya tiga tahap berturut-turut. Teks hero tampil penuh selama satu frame. Lalu teks itu disembunyikan. Lalu baru dia dianimasikan masuk seperti seharusnya.

Satu frame terdengar terlalu singkat untuk jadi keluhan. Di 60 fps, satu frame itu 1000 dibagi 60, sekitar 16 milidetik. Tapi kedipan sependek itu tetap kelihatan justru karena kontrasnya ekstrem: dari konten penuh, ke kosong, lalu isi lagi. Mata tidak perlu membaca teksnya untuk sadar ada sesuatu yang muncul dan langsung dicabut.

set() menunggu preloader, hero-nya tidak ikut menunggu

Bentuknya kira-kira begini, dengan nama dan nilai yang saya sederhanakan, dan ketiga tahap itu sudah bisa dibaca dari situ. Semua initial state gsap.set dipanggil di dalam callback selesai-preloader, satu paket dengan timeline animasinya.

document.addEventListener("DOMContentLoaded", () => {
  runPreloader({
    onComplete: () => {
      // initial state ikut menumpang di sini, dan ini masalahnya
      gsap.set(".hero__title span", { yPercent: 110, opacity: 0 });
      gsap.set(".hero__subtitle", { opacity: 0 });
 
      gsap
        .timeline()
        .to(".hero__title span", { yPercent: 0, opacity: 1, stagger: 0.06 })
        .to(".hero__subtitle", { opacity: 1 }, "-=0.3");
    },
  });
});

Masalahnya, hero itu sudah ter-render di belakang preloader sejak halaman dimuat. Preloader hanya menutupinya, tidak menundanya. Jadi selama preloader masih di depan, hero sudah ada di sana dalam kondisi apa adanya: teks penuh, tanpa yPercent, tanpa opacity: 0, karena tidak ada satu pun set() yang sudah dijalankan.

Begitu preloader terangkat, frame pertama yang sampai ke mata pengunjung adalah konten penuh yang belum tersentuh itu. Baru setelah frame tersebut set() jalan dan menyembunyikannya, lalu timeline menganimasikannya masuk. Urutannya persis seperti yang dilaporkan.

Yang bikin kasus ini enak dijelaskan setelah ketahuan: semua kodenya memang jalan, dan urutan lengkapnya sampai ke mata pengunjung. Yang salah cuma satu, ada bagian yang jalan satu frame terlalu telat.

Perbaikannya: pisahkan yang menyiapkan dari yang memainkan

Perbaikannya bukan menggeser timing atau menambah delay, tapi memecah intro jadi dua fungsi dengan waktu panggil yang berbeda.

Fungsi pertama isinya SEMUA initial state gsap.set, dan dipanggil langsung di DOMContentLoaded, selagi preloader masih menutup halaman. Fungsi kedua isinya hanya timeline .to(), dan dipanggil saat preloader selesai.

function prepIntro() {
  // semua initial state, tanpa kecuali
  gsap.set(".hero__title span", { yPercent: 110, opacity: 0 });
  gsap.set(".hero__subtitle", { opacity: 0 });
}
 
function initIntro() {
  // hanya timeline .to(), tidak ada set() di sini
  gsap
    .timeline()
    .to(".hero__title span", { yPercent: 0, opacity: 1, stagger: 0.06 })
    .to(".hero__subtitle", { opacity: 1 }, "-=0.3");
}
 
document.addEventListener("DOMContentLoaded", () => {
  prepIntro();
  runPreloader({ onComplete: initIntro });
});

Sekarang penyembunyian terjadi di balik preloader, di saat tidak ada mata yang bisa melihatnya. Frame pertama setelah preloader terangkat menampilkan hero yang sudah dalam state awal animasi, bukan konten penuh. Kedipannya hilang bukan karena dipercepat, tapi karena dipindahkan ke belakang tirai.

Satu jaring pengaman kalau timeline preloader mati

Konsekuensi dari pemecahan ini: hero sekarang disembunyikan lebih awal dan lebih pasti, jadi kalau initIntro() tidak pernah terpanggil, halaman berhenti dalam keadaan kosong. Karena itu ada failsafe timeout yang tetap memanggil intro kalau timeline preloader mati.

let introStarted = false;
 
function playIntroOnce() {
  if (introStarted) return;
  introStarted = true;
  initIntro();
}
 
// angka contoh, sesuaikan dengan durasi wajar preloader di situsmu
const FAILSAFE_MS = 4000;
 
document.addEventListener("DOMContentLoaded", () => {
  prepIntro();
  runPreloader({ onComplete: playIntroOnce });
  setTimeout(playIntroOnce, FAILSAFE_MS);
});

Penjaga introStarted ada supaya intro tidak jalan dua kali kalau preloader ternyata selesai normal sebelum timeout-nya lewat.

Yang saya bawa pulang

Stack-nya GSAP dan JavaScript di dalam tema WordPress.