ScrollTrigger Advanced: GSAP + SVG, Video, dan Canvas

Setelah menguasai teknik-teknik dasar ScrollTrigger seperti fade-in, parallax, dan pinning, saatnya memasuki wilayah advanced yang benar-benar membedakan website profesional dari yang biasa-biasa saja.

Di level ini, kita tidak hanya menganimasikan elemen HTML biasa, tetapi juga memanipulasi:

  • SVG (Scalable Vector Graphics): Gambar vektor yang bisa di-animate per path atau atribut.
  • Video: Sinkronisasi playback video dengan posisi scroll.
  • Canvas: Rendering grafis real-time yang bisa dikendalikan scroll.

Kombinasi teknologi ini membuka kemungkinan tak terbatas untuk menciptakan pengalaman web yang benar-benar unik dan memorable.


Mengapa Teknik Advanced Ini Penting?

  1. Diferensiasi Brand: Sedikit website yang menggunakan kombinasi SVG + ScrollTrigger atau Video Scrubbing. Ini membuat brand Anda menonjol.
  2. Engagement Tinggi: Pengguna akan menghabiskan lebih banyak waktu di halaman karena pengalaman interaktifnya yang menarik.
  3. Storytelling Kuat: SVG animated dan video scrubbing sempurna untuk menceritakan narrative produk atau brand story yang kompleks.
  4. Performance Awareness: Menguasai Canvas dan optimization techniques menunjukkan pemahaman mendalam tentang performa web.

Teknik 1: SVG Path Animation dengan ScrollTrigger

SVG Path Animation

Konsep Dasar

SVG memiliki properti strokeDasharray dan strokeDashoffset yang memungkinkan kita "menggambar" garis secara progressive.

Rumus:

textstrokeDasharray: [panjang garis] strokeDashoffset: [offset garis] (0 = terlihat penuh, [panjang] = tersembunyi)

Dengan mengubah strokeDashoffset seiring scroll, kita membuat efek "drawing on scroll".

Kegunaan Real-World:

  • Menggambar roadmap produk saat user scroll
  • Animasi diagram alur (flowchart)
  • Animasi signature atau autograph
  • Timeline visual yang di-trigger scroll

Setup SVG yang Benar

<!-- SVG harus memiliki viewBox yang benar agar responsif -->
<svg viewBox="0 0 200 200" width="100%" height="auto">
  <!-- Path dengan stroke yang tebal agar terlihat jelas -->
  <path id="drawPath" 
        d="M 20 20 Q 100 20 180 100 T 20 180"
        stroke="white"
        stroke-width="4"
        fill="none"
        stroke-linecap="round"
        stroke-linejoin="round"/>
</svg>

Properti Penting:

  • fill="none": Jangan isi path, hanya stroke (garis tepi).
  • stroke-linecap="round": Ujung garis membulat (lebih halus).
  • stroke-linejoin="round": Sudut path membulat (lebih smooth).

Animasi dengan GSAP

svg-path-animation.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ScrollTrigger Advanced - SVG Path Animation (Tailwind)</title>
    <!-- Tailwind CDN -->
    <script src="<https://cdn.tailwindcss.com>"></script>
  </head>
  <body
    class="min-h-screen bg-[radial-gradient(circle_at_top,_#0f0c29,_#302b63,_#24243e)] text-white overflow-x-hidden font-sans"
  >
    <!-- INTRO -->
    <section
      class="min-h-screen flex flex-col items-center justify-center text-center px-5"
    >
      <h1
        class="text-[clamp(2.5rem,5vw,4rem)] mb-5 bg-gradient-to-r from-fuchsia-500 to-indigo-700 bg-clip-text text-transparent font-bold"
      >
        SVG Path Drawing
      </h1>
      <p class="text-lg opacity-80 mb-6 max-w-xl">
        Scroll ke bawah untuk melihat garis SVG “digambar” secara otomatis.
      </p>
      <div
        class="mt-5 text-2xl animate-bounce select-none"
      >
        ↓ Scroll Down ↓
      </div>
    </section>

    <!-- SECTION 1: SQUARE -->
    <section class="min-h-screen flex items-center justify-center px-5">
      <div
        class="w-full max-w-5xl flex flex-wrap gap-10 items-center justify-between"
      >
        <div class="flex-1 min-w-[260px] flex flex-col items-center">
          <svg
            viewBox="0 0 200 200"
            class="w-full max-w-xs h-auto drop-shadow-[0_0_10px_rgba(255,0,204,0.4)]"
          >
            <defs>
              <linearGradient
                id="grad1"
                x1="0%"
                y1="0%"
                x2="100%"
                y2="100%"
              >
                <stop offset="0%" style="stop-color:#ff00cc;stop-opacity:1" />
                <stop offset="100%" style="stop-color:#00ffcc;stop-opacity:1" />
              </linearGradient>
            </defs>

            <!-- Path kotak sederhana -->
            <path
              d="M 20 20 L 180 20 L 180 180 L 20 180 Z"
              stroke="url(#grad1)"
              stroke-width="6"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
              class="draw-me"
            />
          </svg>
          <div
            class="mt-5 text-2xl font-semibold bg-gradient-to-r from-fuchsia-500 to-teal-300 bg-clip-text text-transparent"
          >
            Square Shape
          </div>
        </div>

        <div class="flex-1 min-w-[260px] px-3">
          <h2 class="text-3xl mb-3 text-teal-300">
            Teknik Dasar: Garis Lurus
          </h2>
          <p class="text-[1.05rem] leading-relaxed text-slate-100/80">
            Ini adalah bentuk dasar animasi SVG path. Dengan mengubah properti
            de>stroke-dashoffset</code> dari panjang total path menjadi 0,
            kita menciptakan ilusi garis yang sedang digambar.
          </p>
        </div>
      </div>
    </section>

    <!-- SPACER -->
    <div
      class="h-[30vh] flex items-center justify-center opacity-40 italic text-sm"
    >
      Lanjut ke bentuk organik...
    </div>

    <!-- SECTION 2: WAVE -->
    <section class="min-h-screen flex items-center justify-center px-5">
      <div
        class="w-full max-w-5xl flex flex-wrap gap-10 items-center justify-between"
      >
        <div class="flex-1 min-w-[260px] px-3 text-right">
          <h2 class="text-3xl mb-3 text-teal-300">
            Teknik Organik: Kurva Bezier
          </h2>
          <p class="text-[1.05rem] leading-relaxed text-slate-100/80">
            Path yang lebih kompleks dengan lengkungan (curves) memberikan
            kesan yang lebih dinamis dan organik. Sangat cocok untuk
            storytelling visual atau ilustrasi flow.
          </p>
        </div>

        <div class="flex-1 min-w-[260px] flex flex-col items-center">
          <svg
            viewBox="0 0 200 200"
            class="w-full max-w-xs h-auto drop-shadow-[0_0_10px_rgba(255,0,204,0.4)]"
          >
            <defs>
              <linearGradient
                id="grad2"
                x1="0%"
                y1="0%"
                x2="100%"
                y2="0%"
              >
                <stop offset="0%" style="stop-color:#00ffcc;stop-opacity:1" />
                <stop offset="100%" style="stop-color:#ff00cc;stop-opacity:1" />
              </linearGradient>
            </defs>

            <!-- Path bergelombang -->
            <path
              d="M 10 100 Q 50 10 90 100 T 190 100"
              stroke="url(#grad2)"
              stroke-width="6"
              fill="none"
              stroke-linecap="round"
              class="draw-me"
            />
          </svg>
          <div
            class="mt-5 text-2xl font-semibold bg-gradient-to-r from-teal-300 to-fuchsia-500 bg-clip-text text-transparent"
          >
            Wavy Line
          </div>
        </div>
      </div>
    </section>

    <!-- SPACER -->
    <div
      class="h-[30vh] flex items-center justify-center opacity-40 italic text-sm"
    >
      Lanjut ke bentuk kompleks...
    </div>

    <!-- SECTION 3: STAR -->
    <section class="min-h-screen flex items-center justify-center px-5">
      <div
        class="w-full max-w-5xl flex flex-wrap gap-10 items-center justify-between"
      >
        <div class="flex-1 min-w-[260px] flex flex-col items-center">
          <svg
            viewBox="0 0 200 200"
            class="w-full max-w-xs h-auto drop-shadow-[0_0_10px_rgba(255,0,204,0.4)]"
          >
            <defs>
              <linearGradient
                id="grad3"
                x1="50%"
                y1="0%"
                x2="50%"
                y2="100%"
              >
                <stop offset="0%" style="stop-color:#ff00cc;stop-opacity:1" />
                <stop offset="50%" style="stop-color:#ffff00;stop-opacity:1" />
                <stop offset="100%" style="stop-color:#ff00cc;stop-opacity:1" />
              </linearGradient>
            </defs>

            <!-- Path bintang kompleks -->
            <path
              d="M 100 10 L 123 65 L 182 65 L 135 102 L 153 160 L 100 125 L 47 160 L 65 102 L 18 65 L 77 65 Z"
              stroke="url(#grad3)"
              stroke-width="4"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
              class="draw-me"
            />
          </svg>
          <div
            class="mt-5 text-2xl font-semibold bg-gradient-to-r from-fuchsia-500 via-yellow-300 to-fuchsia-500 bg-clip-text text-transparent"
          >
            Complex Star
          </div>
        </div>

        <div class="flex-1 min-w-[260px] px-3">
          <h2 class="text-3xl mb-3 text-teal-300">
            Teknik Lanjut: Poligon Tertutup
          </h2>
          <p class="text-[1.05rem] leading-relaxed text-slate-100/80">
            Path tertutup dengan banyak sudut (vertex). ScrollTrigger
            menghitung panjang total keliling bentuk ini dan menganimasikannya
            dengan presisi tinggi.
          </p>
        </div>
      </div>
    </section>

    <div
      class="h-[50vh] flex items-center justify-center opacity-40 italic text-sm"
    >
      Selesai! Scroll ke atas untuk mengulang.
    </div>

    <!-- GSAP + ScrollTrigger -->
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      function initSvgAnimations() {
        const paths = document.querySelectorAll(".draw-me");

        paths.forEach((path) => {
          const length = path.getTotalLength();

          // State awal: garis disembunyikan
          path.style.strokeDasharray = length;
          path.style.strokeDashoffset = length;

          const triggerElement = path.closest("section");

          gsap.to(path, {
            strokeDashoffset: 0,
            ease: "none",
            scrollTrigger: {
              trigger: triggerElement,
              start: "top center",
              end: "bottom center",
              scrub: 1,
              markers: false
            }
          });
        });
      }

      window.addEventListener("load", initSvgAnimations);
    </script>
  </body>
</html>


Penjelasan

  • path.getTotalLength(): Fungsi ini sangat penting. Ia menghitung berapa piksel panjang total dari garis SVG tersebut, tidak peduli seberapa rumit bentuknya (kurva, zig-zag, lingkaran).
  • strokeDasharray = length: Kita membuat pola garis putus-putus di mana panjang "garis" dan panjang "spasi"-nya sama persis dengan panjang total path.
  • strokeDashoffset = length: Kita menggeser pola garis tersebut sejauh panjang totalnya. Akibatnya, yang terlihat di layar hanyalah bagian "spasi" (kosong), sehingga garis tampak tersembunyi.
  • GSAP to(..., { strokeDashoffset: 0 }): Saat ScrollTrigger aktif, GSAP mengembalikan offset ke 0 secara bertahap. Ini membuat bagian "garis" masuk kembali ke tampilan, menciptakan ilusi seolah-olah garis tersebut sedang digambar dari awal sampai akhir.
  • scrub: 1: Ini membuat animasi terikat dengan scrollbar. Jika Anda berhenti scroll, animasi berhenti. Jika scroll mundur, animasi mundur (garis menghapus diri sendiri). Angka 1 memberikan sedikit efek smoothing (inersia) agar gerakan terasa lebih lembut.

Teknik 2: Video Scrubbing dengan ScrollTrigger

Video Scrubbing

Konsep Dasar

Biasanya video dimainkan secara otomatis. Tapi dengan ScrollTrigger, kita bisa:

  • Pause & Play video berdasarkan scroll position.
  • Scrub (menggeser) frame video mengikuti scroll, seperti timeline editor.
  • Timeline Visualization: Video menjadi "progress bar" visual yang menunjukkan narasi sesuai scroll.

Kegunaan Real-World:

  • Demo produk yang bisa dikontrol user via scroll.
  • Explainer video yang progresif (frame demi frame saat scroll).
  • Dokumentasi visual yang interactive.

Contoh Kode

file: video-scrubbing.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ScrollTrigger + Video Scrubbing (Tailwind)</title>
    <!-- Tailwind CDN -->
    <script src="<https://cdn.tailwindcss.com>"></script>
  </head>
  <body class="bg-slate-950 text-slate-50 overflow-x-hidden">
    <!-- Intro -->
    <section
      class="min-h-screen flex flex-col items-center justify-center text-center px-4 bg-gradient-to-b from-slate-900 to-slate-950"
    >
      <h1
        class="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight bg-gradient-to-r from-sky-400 via-fuchsia-500 to-orange-400 bg-clip-text text-transparent mb-4"
      >
        Scroll‑Driven Video
      </h1>
      <p class="max-w-xl text-slate-300 text-base md:text-lg">
        Geser halaman untuk mengontrol timeline video secara presisi. Cocok
        untuk demo produk sinematik atau storytelling interaktif.
      </p>
      <div
        class="mt-10 text-xs tracking-[0.25em] uppercase text-slate-400 flex flex-col items-center gap-1 animate-bounce"
      >
        <span class="text-xl">↓</span>
        <span>Scroll untuk mulai</span>
      </div>
    </section>

    <!-- Area scroll panjang -->
    <section class="h-[450vh] bg-slate-950">
      <!-- Video sticky di tengah layar -->
      <div
        class="sticky top-0 h-screen flex items-center justify-center overflow-hidden"
      >
      <div
      class="relative w-full max-w-5xl aspect-video rounded-3xl overflow-hidden shadow-[0_40px_120px_rgba(15,23,42,0.95)] border border-slate-700/50 bg-slate-900"
    >
      <!-- VIDEO -->
      <video
        id="scrubVideo"
        playsinline
        webkit-playsinline
        preload="auto"
        muted
        class="w-full h-full object-cover brightness-[0.85] contrast-[1.08] saturate-[1.1] scale-[1.02]"
      >
        <source
          src="<http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4>"
          type="video/mp4"
        />
        Browser Anda tidak mendukung video.
      </video>
    
      <!-- Gradient atas & bawah supaya teks kebaca -->
      <div
        class="pointer-events-none absolute inset-x-0 top-0 h-1/3 bg-gradient-to-b from-slate-900/90 to-transparent z-10"
      ></div>
      <div
        class="pointer-events-none absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-slate-950/95 to-transparent z-10"
      ></div>
    
      <!-- Badge + pill yang SELALU tampil -->
      <div
        class="pointer-events-none absolute inset-0 z-20 flex flex-col justify-end gap-3 px-6 md:px-10 pb-7 md:pb-9"
      >
        <div
          class="inline-flex items-center gap-2 text-[0.7rem] tracking-[0.26em] uppercase text-indigo-200"
        >
          <span
            class="w-1.5 h-1.5 rounded-full bg-emerald-400 shadow-[0_0_0_4px_rgba(52,211,153,0.4)]"
          ></span>
          ScrollSync Engine
        </div>
    
        <!-- bar / pill di bawah -->
        <div
          class="mt-3 inline-flex items-center px-3 py-1 rounded-full bg-slate-900/80 border border-slate-600/60 text-[0.75rem] text-slate-100"
        >
          GSAP ScrollTrigger • Timeline terikat ke posisi scroll
        </div>
      </div>
    
      <!-- STEP TEKS: hanya SATU yang aktif per fase scroll -->
    
      <!-- STEP 1 -->
      <div
        id="step-1"
        class="pointer-events-none absolute inset-0 z-30 flex flex-col justify-center px-6 md:px-10 opacity-0"
      >
        <div
          class="text-[0.7rem] tracking-[0.22em] uppercase text-indigo-200 mb-2"
        >
          01 • Discover
        </div>
        <h3
          class="text-2xl md:text-4xl lg:text-[2.8rem] font-semibold mb-2 leading-tight drop-shadow-[0_16px_40px_rgba(15,23,42,0.95)]"
        >
          Masuk perlahan ke scene kota.
        </h3>
        <p class="max-w-md text-sm md:text-base text-slate-100/80">
          Di awal scroll, pengguna diajak melihat overview scene untuk membangun
          konteks visual.
        </p>
      </div>
    
      <!-- STEP 2 (seperti di screenshot kamu) -->
      <div
        id="step-2"
        class="pointer-events-none absolute inset-0 z-30 flex flex-col justify-center px-6 md:px-10 opacity-0"
      >
        <div
          class="text-[0.7rem] tracking-[0.22em] uppercase text-indigo-200 mb-2"
        >
          02 • Focus
        </div>
        <h3
          class="text-2xl md:text-4xl lg:text-[2.8rem] font-semibold mb-2 leading-tight drop-shadow-[0_16px_40px_rgba(15,23,42,0.95)]"
        >
          Detail bergerak mengikuti gesture.
        </h3>
        <p class="max-w-lg text-sm md:text-base text-slate-100/80">
          Saat scroll berlanjut, pengguna bisa mengontrol ritme dan memilih bagian
          mana yang ingin diamati lebih lama. Kendalikan setiap frame
          <span class="text-sky-400 font-semibold">dengan scroll.</span>
        </p>
      </div>
    
      <!-- STEP 3 -->
      <div
        id="step-3"
        class="pointer-events-none absolute inset-0 z-30 flex flex-col justify-center px-6 md:px-10 opacity-0"
      >
        <div
          class="text-[0.7rem] tracking-[0.22em] uppercase text-indigo-200 mb-2"
        >
          03 • Highlight
        </div>
        <h3
          class="text-2xl md:text-4xl lg:text-[2.8rem] font-semibold mb-2 leading-tight drop-shadow-[0_16px_40px_rgba(15,23,42,0.95)]"
        >
          Ending sinematik yang terkendali.
        </h3>
        <p class="max-w-md text-sm md:text-base text-slate-100/80">
          Di bagian akhir, timeline berhenti di frame hero yang siap digunakan
          untuk call‑to‑action atau pesan utama produk.
        </p>
      </div>
    </div>
    
      </div>
    </section>

    <!-- Outro -->
    <section
      class="min-h-[60vh] flex items-center justify-center text-center px-4 bg-gradient-to-t from-slate-900 to-slate-950"
    >
      <div class="max-w-xl">
        <h2
          class="text-2xl md:text-3xl lg:text-4xl font-bold mb-3 text-slate-50"
        >
          Plug & play untuk landing page premium.
        </h2>
        <p class="text-slate-300 text-sm md:text-base">
          Ganti saja sumber videonya dengan demo produkmu, sesuaikan teks di tiap
          fase, dan teknik ini siap dipakai di project nyata dengan nuansa
          sinematik.
        </p>
      </div>
    </section>

    <!-- GSAP & ScrollTrigger -->
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      const video = document.getElementById("scrubVideo");

      // Pastikan video siap sehingga duration sudah terbaca
      video.addEventListener("loadedmetadata", () => {
        initScrollScrub();
      });

      function initScrollScrub() {
        video.pause();
        video.currentTime = 0;

        // 1) Scrub video: currentTime mengikuti scroll
        gsap.to(video, {
          currentTime: video.duration || 1,
          ease: "none",
          scrollTrigger: {
            trigger: ".video-section",
            start: "top top",
            end: "bottom bottom",
            scrub: 1
          }
        });

        // 2) Step-teks yang muncul di fase scroll berbeda
        const steps = [
          { id: "#step-1", start: "8%", end: "30%" },
          { id: "#step-2", start: "35%", end: "65%" },
          { id: "#step-3", start: "70%", end: "95%" }
        ];

        steps.forEach((step) => {
          gsap.fromTo(
            step.id,
            { opacity: 0, y: 30 },
            {
              opacity: 1,
              y: 0,
              ease: "power2.out",
              scrollTrigger: {
                trigger: ".video-section",
                start: `${step.start} top`,
                end: `${step.end} top`,
                scrub: true
              }
            }
          );

          gsap.to(step.id, {
            opacity: 0,
            y: -20,
            ease: "power2.out",
            scrollTrigger: {
              trigger: ".video-section",
              start: `${step.end} top`,
              end: `${parseInt(step.end) + 5}% top`,
              scrub: true
            }
          });
        });
      }

      // Jika browser mencoba autoplay, paksa pause
      video.addEventListener("play", () => {
        if (!ScrollTrigger.getAll().length) {
          video.pause();
        }
      });
    </script>
  </body>
</html>

Penjelasan:

  • currentTime adalah posisi waktu video (dalam detik).
  • GSAP membuat animasi dari currentTime = 0 ke currentTime = video.duration.
  • ScrollTrigger:
    • trigger: ".video-section" → rentang scroll dihitung sepanjang section tinggi 450vh tadi.
    • start: "top top" → saat bagian atas .video-section menyentuh atas viewport, scrubbing dimulai.
    • end: "bottom bottom" → saat bagian bawah .video-section mencapai bawah viewport, scrubbing selesai (video berada di frame terakhir).
    • scrub: 1 → pergerakan animasi mengikuti scroll dengan sedikit smoothing 1 detik. Scroll naik ↔ video mundur; scroll turun ↔ video maju.
  • ease: "none" → kecepatan perubahan currentTime linear, agar benar‑benar sinkron dengan pergerakan scroll.

Teknik 3: Canvas Animation dengan ScrollTrigger

Canvas Aniamtion

Konsep Dasar

Canvas adalah area pixel-by-pixel yang bisa di-render dengan JavaScript. Ini sempurna untuk:

  • Particle Systems: Ribuan partikel yang bergerak smooth.
  • Real-time Graphics: Visualisasi data, waveform, atau animasi komplex.
  • Interactive Visualizations: Grafik yang merespons scroll atau mouse.

Keunggulan Canvas:

  • Performa tinggi bahkan untuk ratusan elemen (lebih cepat dari SVG untuk jumlah besar).
  • Kontrol pixel-level penuh.
  • Bisa merender 3D dengan WebGL.

Kelemahan Canvas:

  • Tidak semudah SVG untuk markup/CSS.
  • Perlu JavaScript untuk setiap frame.
  • Text rendering tidak sebaik SVG.

Contoh Kode

file: canvas-scrolltrigger.html

<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ScrollTrigger + Canvas (Tailwind)</title>
    <script src="<https://cdn.tailwindcss.com>"></script>
  </head>
  <body class="min-h-screen bg-slate-950 text-slate-50 overflow-x-hidden font-sans">
    
    <!-- INTRO -->
    <section class="min-h-screen flex flex-col items-center justify-center text-center px-5 bg-gradient-to-b from-slate-900 to-slate-950">
      <h1 class="text-[clamp(2.4rem,5vw,3.4rem)] font-extrabold tracking-tight bg-gradient-to-r from-cyan-400 via-sky-500 to-fuchsia-500 bg-clip-text text-transparent mb-4">
        Canvas Energy Bar
      </h1>
      <p class="max-w-xl text-base md:text-lg text-slate-300">
        Scroll ke bawah untuk mengisi “energy bar” di dalam canvas. Nilai progress (0–100%) dikendalikan penuh oleh posisi scroll Anda.
      </p>
      <div class="mt-8 text-xs tracking-[0.25em] uppercase text-slate-400 flex flex-col items-center gap-1 animate-bounce">
        <span class="text-xl">↓</span>
        <span>Scroll untuk mulai</span>
      </div>
    </section>

    <!-- MAIN CANVAS SECTION -->
    <section class="canvas-section min-h-[250vh] flex items-center justify-center px-5 bg-slate-950 relative">
      <!-- Sticky Wrapper -->
      <div class="sticky top-0 w-full max-w-5xl flex flex-col md:flex-row items-center gap-10 min-h-screen">
        
        <!-- 1. Card Canvas -->
        <div class="flex-1 flex items-center justify-center w-full">
          <div class="relative w-full max-w-xl aspect-[16/7] bg-slate-900/80 border border-slate-700/70 rounded-3xl shadow-[0_30px_80px_rgba(15,23,42,0.9)] flex items-center justify-center">
            
            <canvas id="energyCanvas" class="w-[90%] h-[55%]"></canvas>

            <div class="absolute inset-x-0 bottom-4 flex items-center justify-between px-8 text-xs md:text-sm text-slate-300/80">
              <span>Scroll Progress → Energy</span>
              <span id="energyLabel" class="font-semibold text-cyan-300">0%</span>
            </div>
          </div>
        </div>

        <!-- 2. Teks Penjelasan -->
        <div class="flex-1 max-w-md space-y-4">
          <p class="text-sm font-mono text-cyan-300/80">Teknik 3 · Canvas + ScrollTrigger</p>
          <h2 class="text-2xl md:text-3xl font-semibold text-cyan-300">
            Menghubungkan progress scroll ke gambar di canvas.
          </h2>
          <p class="text-slate-200/85 text-sm md:text-base leading-relaxed">
            Alih-alih memanipulasi elemen DOM yang berat, kita menggunakan Canvas API untuk merender bar energi secara real-time.
          </p>
          <p class="text-slate-200/85 text-sm md:text-base leading-relaxed">
            ScrollTrigger mengirimkan nilai 0 sampai 1 berdasarkan posisi scroll. Kita menggunakan nilai ini untuk menggambar ulang (redraw) canvas setiap frame, menciptakan animasi yang super-smooth dan sinkron.
          </p>
        </div>

      </div>
    </section>

    <!-- OUTRO -->
    <section class="h-[40vh] flex items-center justify-center text-slate-500">
      Akhir demo canvas. Scroll naik–turun untuk melihat energinya berubah.
    </section>

    <!-- GSAP -->
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      const canvas = document.getElementById("energyCanvas");
      const ctx = canvas.getContext("2d");
      const label = document.getElementById("energyLabel");

      // Object proxy untuk menyimpan nilai progress animasi
      let progressProxy = { value: 0 };

      // 1. Fungsi Resize: Menjaga ketajaman canvas di semua layar
      function resizeCanvas() {
        const dpr = window.devicePixelRatio || 1;
        const rect = canvas.getBoundingClientRect();
        
        // Set dimensi fisik canvas (dikali DPR untuk retina display)
        canvas.width = rect.width * dpr;
        canvas.height = rect.height * dpr;
        
        // Normalisasi sistem koordinat agar coding drawing lebih mudah (0..width)
        ctx.scale(dpr, dpr);
        
        // Simpan dimensi logis untuk dipakai di fungsi draw
        canvas.logicalWidth = rect.width;
        canvas.logicalHeight = rect.height;

        // Redraw saat resize
        draw(progressProxy.value);
      }

      // 2. Fungsi Draw: Menggambar ulang canvas setiap frame
      function draw(progress) {
        const w = canvas.logicalWidth;
        const h = canvas.logicalHeight;
        
        // Bersihkan canvas sebelum menggambar frame baru
        ctx.clearRect(0, 0, w, h);

        const radius = h / 2; 

        // A. Gambar Track (Background Bar)
        ctx.fillStyle = "rgba(15, 23, 42, 0.6)"; 
        ctx.strokeStyle = "rgba(51, 65, 85, 0.8)"; 
        ctx.lineWidth = 2;
        
        ctx.beginPath();
        if (ctx.roundRect) {
            ctx.roundRect(0, 0, w, h, radius);
        } else {
            ctx.rect(0, 0, w, h); 
        }
        ctx.fill();
        ctx.stroke();

        // B. Gambar Fill (Isi Bar)
        const fillWidth = Math.max(0, w * progress);
        
        if (fillWidth > 0) {
          ctx.save();
          
          // Clip area agar fill tidak keluar dari rounded corner
          ctx.beginPath();
          if (ctx.roundRect) {
              ctx.roundRect(0, 0, w, h, radius);
          } else {
              ctx.rect(0, 0, w, h);
          }
          ctx.clip();

          // Gradient Warna
          const grad = ctx.createLinearGradient(0, 0, w, 0);
          grad.addColorStop(0, "#22c55e");   // Hijau
          grad.addColorStop(0.5, "#0ea5e9"); // Biru Langit
          grad.addColorStop(1, "#d946ef");    // Ungu

          ctx.fillStyle = grad;
          ctx.fillRect(0, 0, fillWidth, h);
          ctx.restore();
        }

        // C. Gambar Garis Pembagi (Ticks)
        ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
        ctx.lineWidth = 2;
        const steps = 5; 
        for (let i = 1; i < steps; i++) {
          const x = (w / steps) * i;
          ctx.beginPath();
          ctx.moveTo(x, h * 0.2);
          ctx.lineTo(x, h * 0.8);
          ctx.stroke();
        }

        // D. Update Label Teks
        if(label) label.textContent = Math.round(progress * 100) + "%";
      }

      // Inisialisasi
      resizeCanvas();
      window.addEventListener("resize", resizeCanvas);
      draw(0); // Gambar frame awal

      // 3. Animasi GSAP + ScrollTrigger
      gsap.to(progressProxy, {
        value: 1,
        ease: "none",
        scrollTrigger: {
          trigger: ".canvas-section",
          start: "top center",    // Mulai saat bagian atas section di tengah layar
          end: "bottom center",   // Selesai saat bagian bawah section di tengah layar
          scrub: 1,               // Smooth scrubbing 1 detik
        },
        onUpdate: () => {
          // Trik: Paksa nilai jadi 1 jika sudah > 99% agar tidak "nyangkut" di 99%
          if (progressProxy.value > 0.91) progressProxy.value = 1;
          draw(progressProxy.value);
        }
      });

    </script>
  </body>
</html>

Penjelasan

  • Canvas vs DOM: Kita tidak menggunakan <div> CSS width untuk animasi ini, melainkan elemen <c**anvas>**. Ini memberikan performa lebih tinggi (60fps stabil) karena browser hanya menggambar satu elemen bitmap, bukan me-layout ulang elemen HTML.
  • Proxy Object: GSAP menganimasikan properti value pada sebuah objek JavaScript biasa (progressProxy) dari 0 ke 1. Objek ini tidak terlihat di layar, tapi nilainya kita pakai.
  • onUpdate: Setiap kali scroll bergeser, GSAP mengupdate nilai proxy tadi. Fungsi onUpdate mengambil nilai baru tersebut, lalu memerintahkan Canvas untuk menggambar ulang (draw) bar energi dengan lebar baru.
  • Math Logic: Logika progressProxy.value > 0.91 ditambahkan untuk memastikan bar benar-benar penuh (100%) saat scroll selesai, mengatasi masalah rounding error di mana animasi kadang berhenti di 96%.

Teknik 4: Gabungan Advanced (SVG + Video + Data)

Combo Animation
<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Teknik 4: Immersive Timeline</title>
    <script src="<https://cdn.tailwindcss.com>"></script>
  </head>
  <body class="bg-slate-950 text-slate-50 font-sans overflow-x-hidden selection:bg-cyan-500/30">

    <!-- BACKGROUND VIDEO LAYER -->
    <div class="fixed inset-0 w-full h-full z-0">
        <video 
          id="bgVideo" 
          class="w-full h-full object-cover opacity-40 grayscale-[50%] brightness-75"
          playsinline webkit-playsinline muted preload="auto">
          <source src="<http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4>" type="video/mp4">
        </video>
        <div class="absolute inset-0 bg-gradient-to-r from-slate-950 via-slate-950/80 to-slate-950/60"></div>
        <div class="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-slate-950/40"></div>
    </div>

    <!-- CONTENT LAYER -->
    <div class="relative z-10">
      
      <!-- HERO -->
      <section class="h-screen flex flex-col justify-center px-8 md:px-20 max-w-4xl">
        <h1 class="text-6xl md:text-8xl font-bold tracking-tight leading-tight mb-6">
          PROJECT <br>
          <span class="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-teal-400">AERO</span>
        </h1>
        <p class="text-xl text-slate-400 max-w-xl border-l-4 border-cyan-500/30 pl-6">
          Sebuah eksplorasi visual menggabungkan data telemetri, video scrubbing, dan navigasi SVG dalam satu pengalaman timeline yang mulus.
        </p>
        <div class="mt-12 animate-pulse text-sm tracking-[0.3em] text-cyan-500 font-mono">
          SCROLL TO BEGIN_
        </div>
      </section>

      <!-- TIMELINE SECTION -->
      <section class="timeline-section h-[400vh] relative">
        
        <div class="sticky top-0 h-screen w-full flex items-center pl-8 md:pl-20 pr-8">
          
          <!-- SVG TIMELINE -->
          <div class="relative h-[60vh] w-[60px] flex-shrink-0 mr-10 md:mr-20 hidden md:block">
             <svg class="h-full w-full overflow-visible" viewBox="0 0 60 600" preserveAspectRatio="none">
               <path d="M 30 0 L 30 600" stroke="#334155" stroke-width="2" fill="none" />
               
               <path id="activeLine" d="M 30 0 L 30 600" stroke="#22d3ee" stroke-width="4" stroke-linecap="round" fill="none" />

               ircle cx="30" cy="0" r="6" class="fill-slate-950 stroke-slate-600 stroke-2" />
               ircle cx="30" cy="200" r="6" class="dot-marker fill-slate-950 stroke-slate-600 stroke-2" />
               ircle cx="30" cy="400" r="6" class="dot-marker fill-slate-950 stroke-slate-600 stroke-2" />
               ircle cx="30" cy="600" r="6" class="dot-marker fill-slate-950 stroke-slate-600 stroke-2" />
             </svg>
          </div>

          <!-- DYNAMIC CONTENT AREA -->
          <div class="flex-1 relative h-[60vh] flex items-center">
            
            <!-- STEP 1 -->
            <div class="step-content absolute top-1/2 -translate-y-1/2 opacity-0 translate-x-10 transition-all duration-700 w-full max-w-2xl">
              <div class="font-mono text-cyan-400 text-sm mb-2 tracking-widest">PHASE 01</div>
              <h2 class="text-5xl md:text-6xl font-bold text-white mb-6">System Initiation</h2>
              <p class="text-lg text-slate-300 leading-relaxed border-l-2 border-slate-700 pl-6">
                Sistem melakukan boot-up sequence. Video scrubbing disinkronisasi dengan posisi scroll pengguna untuk memberikan feedback visual instan.
              </p>
              <div class="mt-8 grid grid-cols-2 gap-4 font-mono text-xs opacity-70">
                 <div class="bg-slate-900/80 p-3 rounded border border-slate-700">
                    <div>CPU_LOAD</div>
                    <div class="text-xl text-green-400 mt-1">12%</div>
                 </div>
                 <div class="bg-slate-900/80 p-3 rounded border border-slate-700">
                    <div>MEMORY</div>
                    <div class="text-xl text-green-400 mt-1">OK</div>
                 </div>
              </div>
            </div>

            <!-- STEP 2 -->
            <div class="step-content absolute top-1/2 -translate-y-1/2 opacity-0 translate-x-10 transition-all duration-700 w-full max-w-2xl">
              <div class="font-mono text-purple-400 text-sm mb-2 tracking-widest">PHASE 02</div>
              <h2 class="text-5xl md:text-6xl font-bold text-white mb-6">Vertical Liftoff</h2>
              <p class="text-lg text-slate-300 leading-relaxed border-l-2 border-slate-700 pl-6">
                Mencapai ketinggian jelajah. Perhatikan bagaimana garis timeline SVG terisi seiring dengan pergerakan video drone yang mulai terbang.
              </p>
               <div class="mt-8 flex items-center gap-4">
                  <div class="h-2 flex-1 bg-slate-800 rounded-full overflow-hidden">
                     <div class="h-full bg-purple-500 w-[75%] animate-pulse"></div>
                  </div>
                  <span class="font-mono text-purple-400">THRUST 75%</span>
               </div>
            </div>

            <!-- STEP 3 -->
            <div class="step-content absolute top-1/2 -translate-y-1/2 opacity-0 translate-x-10 transition-all duration-700 w-full max-w-2xl">
              <div class="font-mono text-orange-400 text-sm mb-2 tracking-widest">PHASE 03</div>
              <h2 class="text-5xl md:text-6xl font-bold text-white mb-6">Stabilization</h2>
              <p class="text-lg text-slate-300 leading-relaxed border-l-2 border-slate-700 pl-6">
                Gyroscope aktif. Kamera stabil. Data telemetri menunjukkan kondisi optimal untuk pengambilan gambar resolusi tinggi.
              </p>
            </div>

            <!-- STEP 4 -->
            <div class="step-content absolute top-1/2 -translate-y-1/2 opacity-0 translate-x-10 transition-all duration-700 w-full max-w-2xl">
              <div class="font-mono text-emerald-400 text-sm mb-2 tracking-widest">PHASE 04</div>
              <h2 class="text-5xl md:text-6xl font-bold text-white mb-6">Mission Complete</h2>
              <p class="text-lg text-slate-300 leading-relaxed border-l-2 border-slate-700 pl-6">
                Timeline selesai. Seluruh data telah direkam. Siap untuk proses download log penerbangan.
              </p>
              <button class="mt-8 group flex items-center gap-3 px-8 py-4 bg-emerald-500/10 border border-emerald-500/50 hover:bg-emerald-500 hover:text-black transition-all rounded-none">
                <span>DOWNLOAD LOGS</span>
                <span class="group-hover:translate-x-1 transition-transform">→</span>
              </button>
            </div>

          </div>
        </div>
      </section>

      <!-- OUTRO -->
      <section class="h-[50vh] flex flex-col items-center justify-center relative z-20 bg-slate-950">
         <div class="h-[1px] w-20 bg-slate-700 mb-8"></div>
         <p class="text-slate-500">End of Transmission</p>
      </section>
    </div>

    <!-- SCRIPTS -->
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>

    <script>
      gsap.registerPlugin(ScrollTrigger);

      const video = document.getElementById('bgVideo');
      const activeLine = document.getElementById('activeLine');
      const steps = document.querySelectorAll('.step-content');
      const dots = document.querySelectorAll('.dot-marker');

      const length = activeLine.getTotalLength();
      activeLine.style.strokeDasharray = length;
      activeLine.style.strokeDashoffset = length;

      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: ".timeline-section",
          start: "top top",
          end: "bottom bottom",
          scrub: 1,
        }
      });

      video.pause();
      tl.to(video, {
        currentTime: video.duration || 10,
        ease: "none",
        duration: 10
      }, 0);

      tl.to(activeLine, {
        strokeDashoffset: 0,
        ease: "none",
        duration: 10
      }, 0);

      const stepDuration = 10 / steps.length;

      steps.forEach((step, i) => {
        const startTime = i * stepDuration;
        
        tl.fromTo(step, 
          { opacity: 0, x: 50, display: 'none' }, 
          { opacity: 1, x: 0, display: 'block', duration: 0.5, 
            onStart: () => {
              if(dots[i]) gsap.to(dots[i], { stroke: '#22d3ee', strokeWidth: 4, duration: 0.3 });
            },
            onReverseComplete: () => {
              if(dots[i]) gsap.to(dots[i], { stroke: '#475569', strokeWidth: 2, duration: 0.3 });
            }
          }, 
          startTime + 0.5
        );

        if (i < steps.length - 1) {
          tl.to(step, { 
            opacity: 0, 
            y: -30, 
            duration: 0.5,
            display: 'none' 
          }, startTime + stepDuration - 0.5);
        }
      });

      video.addEventListener('loadedmetadata', () => ScrollTrigger.refresh());
      
    </script>
  </body>
</html>

  1. Video sebagai "Canvas" Latar (Fixed Background)
    • Alih-alih video ditaruh di kotak kecil, video dijadikan background penuh layar yang diam (fixed).
    • Diberi filter gelap (overlay) agar teks di atasnya selalu terbaca jelas, menciptakan suasana sinematik.
  2. Satu Garis Waktu Utama (Master Timeline)
    • Kita menggunakan GSAP Timeline tunggal. Ini ibarat "benang merah" yang mengikat semua elemen.
    • Saat Anda scroll dari 0% ke 100% halaman, timeline ini bergerak dari detik 0 ke detik 10.
    • Sinkronisasi Total: Posisi video, panjang garis SVG, dan munculnya teks semua terikat pada satu timeline ini. Tidak ada animasi yang jalan sendiri-sendiri.
  3. Navigasi Visual (SVG Line)
    • Garis vertikal di kiri berfungsi sebagai penunjuk "kita ada di mana".
    • Efek garis yang "mengisi" diri sendiri (strokeDashoffset) memberikan kepuasan visual progress.
  4. Konten yang "Bernapas" (Layout Asimetris)
    • Layout dibuat lega (banyak ruang kosong) dengan teks di kanan dan garis di kiri.
    • Teks tidak muncul kaget, tapi masuk perlahan (slide in) memberikan kesan elegan dan premium.

Best Practices untuk ScrollTrigger Advanced

1. Performance Optimization

Problem: Terlalu banyak ScrollTrigger bisa membuat scroll jadi lag di mobile.

Solusi:

  • Gunakan trigger: document.body dan kalkulasi posisi manual daripada buat trigger per elemen.
  • Debounce atau throttle resize events.
  • Batasi jumlah Canvas elements aktif.
  • Gunakan will-change CSS untuk optimasi GPU.
css.animated-element {
  will-change: transform;
}

2. Accessibility

Problem: Animasi bisa mengganggu user dengan sensitivitas motion.

Solusi: Deteksi dan hormati prefers-reduced-motion.

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion) {
  *// Jalankan animasi normal*
} else {
  *// Versi static atau simplified*
}

3. Responsive Design

Canvas dan SVG perlu di-resize saat window berubah. Gunakan ScrollTrigger.refresh().

javascriptwindow.addEventListener('resize', () => {
  *// Resize canvas*
  canvas.width = canvas.offsetWidth;
  *// Refresh ScrollTrigger*
  ScrollTrigger.getAll().forEach(trigger => trigger.refresh());
});

4. Mobile Considerations

  • Video di mobile sering di-pause untuk hemat data. Berikan fallback (gambar static atau GIF).
  • Canvas rendering bisa lambat di mobile. Test di real device, bukan simulasi.
  • Kurangi kompleksitas animasi untuk device lebih lemah.

Tools & Resources

  • Optimasi SVG: Use SVG Optimize tool untuk mengurangi file size.
  • Kompresi Video: Use ffmpeg atau HandBrake untuk kompresi optimal.
  • Canvas Debugging: Chrome DevTools -> Sources -> Canvas inspector.
  • Performance Monitoring: Use Lighthouse atau WebPageTest.

ScrollTrigger Advanced membuka pintu untuk menciptakan pengalaman web yang truly interactive dan memorable. SVG animasi, video scrubbing, dan canvas graphics bukan hanya untuk portofolio designer, tetapi juga untuk aplikasi bisnis modern yang ingin stand out.

Kunci sukses:

  1. Mulai dengan 1 teknik: Master SVG dulu, baru kombinasikan dengan video atau canvas.
  2. Test performa: Jangan sacrifice user experience untuk efek visual.
  3. Responsive-first: Desain untuk mobile dulu, baru scale ke desktop.
  4. Dokumentasi & cleanup: Biarkan comment di code agar mudah di-maintain.

Happy animating! 🚀