SJ blog
frontend
B

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

GSAP ScrollTriggerでスクロール連動演出を組む:絶滅危惧野菜サイトから学ぶ実装パターン

スクロールに応じて3Dシーンが切り替わる没入型UIの駆動力はGSAP ScrollTrigger。タイムラインベースのアニメーション設計とスクロール同期の仕組みを、実在のサイトをケースに解説します。

一言結論

スクロール連動演出の設計で一番重要なのは「何を動かすか」ではなく「何を止めるか」で、ScrollTriggerのpin機能とスクラブ(scrub)の組み合わせが没入感の正体である。

絶滅危惧野菜プロジェクトのサイトをスクロールすると、野菜が切り替わり、地図が展開され、テキストがフェードインします。この「スクロールに応じて世界が変化する」体験を支えているのが GSAP + ScrollTrigger です。

JSバンドルから gsap, GSAP, ScrollTrigger, scrollTrigger のシグネチャが検出されており、GreenSock Animation Platformのスクロール拡張が使われていることが確定しています。

GSAPとは何か

GSAP(GreenSock Animation Platform)は、Webアニメーションのデファクトスタンダードライブラリです。CSS Transitionや Web Animations API と異なり、タイムライン(timeline)ベースのアニメーション設計ができるのが最大の特徴です。

// CSSのtransition
.box { transition: transform 0.3s ease; }
.box:hover { transform: translateX(100px); }

// GSAP
gsap.to(".box", {
  x: 100,
  duration: 0.3,
  ease: "power2.out"
});

ここまでだとCSS Transitionとほぼ同じですが、GSAPの真価は複数のアニメーションを時系列で制御するタイムラインにあります。

const tl = gsap.timeline();

tl.to(".title", { opacity: 1, y: 0, duration: 0.5 })
  .to(".subtitle", { opacity: 1, y: 0, duration: 0.3 }, "-=0.2") // 0.2秒オーバーラップ
  .to(".image", { scale: 1, duration: 0.8 }, "<");                // 同時に開始

これが CSS で書けるか?——不可能ではないが animation-delay の管理が地獄になります。

ScrollTriggerの核心:スクロール位置とタイムラインの同期

ScrollTriggerは「スクロール位置をタイムラインの再生位置に変換する」プラグインです。

gsap.registerPlugin(ScrollTrigger);

gsap.to(".vegetable", {
  rotationY: 360,
  scrollTrigger: {
    trigger: ".vegetable-section",
    start: "top center",     // セクション上端が画面中央に来たら開始
    end: "bottom center",    // セクション下端が画面中央に来たら終了
    scrub: true,             // スクロール位置にアニメーションを完全同期
  }
});

scrub: true がキーです。通常のアニメーションは「トリガー → 一定時間で完了」ですが、scrubを有効にするとスクロールバーがアニメーションの再生ヘッドになります。

  • スクロールを止めるとアニメーションも止まる
  • スクロールを戻すとアニメーションも巻き戻る

この挙動が「スクロールで世界が変化する」没入感の正体です。

pinで「止める」演出

このサイトで特に効果的なのは、スクロールしてもセクションが画面に固定されたまま中のコンテンツだけが変化するパターンです。ScrollTriggerの pin 機能がこれを実現します。

ScrollTrigger.create({
  trigger: ".vegetable-showcase",
  start: "top top",
  end: "+=3000",              // スクロール3000px分、固定し続ける
  pin: true,                  // このセクションを画面に固定
  onUpdate: (self) => {
    // self.progress が 0→1 に変化。これで中のコンテンツを切り替える
    const index = Math.floor(self.progress * vegetables.length);
    showVegetable(index);
  }
});

ユーザーの体験としては「スクロールしているのに画面が動かず、中の野菜だけが切り替わる」ように見えます。end: "+=3000" でスクロール量を指定しているため、ゆっくりスクロールすれば細かく野菜を見られるし、素早くスクロールすれば飛ばせます。ユーザーに制御権を渡しつつ没入させる技です。

タイムラインの組み合わせ

実際のサイトでは、pinの中でさらに複数のアニメーションをタイムラインで制御しているはずです。

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: ".about-section",
    start: "top top",
    end: "+=2000",
    pin: true,
    scrub: 1,       // 数値を入れると慣性がつく
  }
});

tl.fromTo(".about-title",
    { opacity: 0, y: 50 },
    { opacity: 1, y: 0 }
  )
  .fromTo(".about-text",
    { opacity: 0, y: 30 },
    { opacity: 1, y: 0 },
    "+=0.1"
  )
  .to(".about-bg", {
    scale: 1.1,
    duration: 1
  }, 0);

scrub: 1 は「スクロール位置への追従に1秒の遅延をかける」設定です。true だと即座に追従しますが、数値を入れるとイージングがかかり、スクロールを止めた後もフワッとアニメーションが追いつく動きになります。この「もたつき」が有機的な印象を作ります。

Three.jsとの連動

このサイトの真骨頂は、ScrollTriggerがThree.jsのレンダリングを制御しているところです。

// スクロール量でカメラ位置を変える
ScrollTrigger.create({
  trigger: ".scene-container",
  start: "top top",
  end: "bottom bottom",
  scrub: true,
  onUpdate: (self) => {
    // カメラをスクロールに応じて移動
    camera.position.z = 5 - self.progress * 3;
    // ライトの強度を変える
    spotLight.intensity = 0.5 + self.progress * 1.5;
  }
});

このように、GSAPはDOM要素だけでなく任意のJavaScriptオブジェクトのプロパティをアニメーションできます。Three.jsのcamera・light・mesh のプロパティを直接GSAPで制御するのが、3D + スクロール演出の標準パターンです。

没入型サイトの設計原則

ScrollTriggerを使った没入型サイトの設計で重要なのは「何を動かすか」ではなく**「何を止めるか」**です。

  • 止める: pin: true でセクションを固定する。スクロールの「進行感」を消す
  • 止めない: 普通のセクションはそのまま流す。止め続けるとユーザーが迷子になる
  • 境界を明示する: pin区間の入口・出口で背景色やテキストが変わることで「ここから体験が始まる」と伝える

止める箇所が多すぎるとユーザーは「スクロールが壊れている」と感じ、少なすぎると普通のサイトと変わりません。このサイトは「野菜の切り替え」と「地図の展開」の2箇所でpinを使い、残りは通常スクロールにしていると推測できます。メリハリがある。

パフォーマンス注意点

  • will-change の乱用回避: GSAP は内部で transform を使うが、will-change: transform を全要素に付けるとGPUメモリが枯渇する
  • scrub の数値選択: scrub: true(即時追従)はガタつきやすい。scrub: 0.5scrub: 1 が体験上の最適値
  • モバイルのタッチスクロール: iOS Safari はスクロールイベントの発火タイミングが独特で、pinの切り替え時にカクつくことがある。ScrollTrigger.normalizeScroll(true) で軽減可能
  • レンダリングの制御: ビューポート外ではThree.jsのrenderer.render() を止める。GPUリソースとバッテリーの節約

落とし穴

  • position: fixed との干渉: pinは内部で position: fixed を使う。既存のfixed要素(ヘッダーなど)があるとz-indexの戦争が起きる
  • レスポンシブ対応: end: "+=3000" のような固定値はPC向け。モバイルでは end: "+=1500" 程度に減らさないとスクロールが長すぎる
  • SEO: pin中のコンテンツはDOMに存在するが、Googlebot はスクロールを模倣しないため、pin内の動的テキストは静的HTMLとしても存在させる必要がある

参考: