star back image
people4
電飾 電飾
moon
astronaut

【GSAP】scrollTriggerで下スクロールで画面切り替えアニメーション

BLOG GSAPWEBログ
読了約:37分

従来のスクロールとは違って、プレゼンテーションやポートフォリオサイトのような印象を与えたい。セクション単位で画面切り替えをしているようなアニメーションを入れたい。そんな要件ありますか。

何を言ってるんですか。

なんかむかし見たことある気がしますぞ。

スクロールすると次のセクションがアニメーションしながらブラウザ上部でゆっくりと止まります。マウスホイール、キーボード矢印キー、タッチスワイプなどのスクロール操作で発火させます。

スクロールすると上部で止まるアニメーション。

止まる?そんなことできるんですか。

検索した感じでは「GSAP」(ジーサップ)を使う方が多いようです。

「GSAP」は派手なアニメーションを仕込みたい時に便利に使えるライブラリ。
有料のプラグインありの少し私には馴染みがなかったのですが、どうやらupdateで無料になったそうです。

以下はデモのリンクです。

DEMO:
https://gsap.com/demos/

4/30のアップデートをもって、有料プラグインも「100%無料」になったとのこと。商用利用も無料です。
https://gsap.com/blog/3-13/

商用利用もOKとは太っ腹ですね。

【共有】スクロールアニメーションのソースコード

サンプルを作成してみましたので共有したいです。

要件は以下です。

  • GSAPのScrollTriggerを使ったスクロールアニメーションを使う
  • スマホも同じようにする
  • おまけでswiperを入れる
【GSAP】ScrollTriggerサンプル
https://astrowave.jp/amnesia_record/scrolltrigger_gasp.php

健忘録リスト

以下にコードを貼り付けます。

html

<!doctype html>
<html lang="ja">
<head>

<title>【GSAP】ScrollTriggerアニメーション</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">

<!-- Swiper CSS -->
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css">
<!-- GSAP CDN -->
<script src="https://unpkg.com/gsap@3.12.2/dist/gsap.min.js"></script>
<script src="https://unpkg.com/gsap@3.12.2/dist/ScrollToPlugin.min.js"></script>
<!-- Swiper JS -->
<script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Helvetica Neue', Arial, sans-serif;
  background: #000;
  height: 100vh;
}

.section {
  width: 100%;
  height: 100vh;
  position: relative;
  overflow: hidden;
  margin: 0;
  padding: 0;
}

.section-content {
  position: absolute;
  top: 20px;
  left: 20px;
  text-align: left;
  color: white;
  z-index: 10;
}

.section-title {
  font-size: 3rem;
  font-weight: bold;
  margin-bottom: 1rem;
}

.section-subtitle {
  font-size: 1.2rem;
  opacity: 0.8;
}

#footer {
  width: 100%;
  background: linear-gradient(45deg, #1abc9c, #16a085);
  color: white;
  padding: 100px 0 50px;
  text-align: center;
  display: none;
}

.footer-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

.footer-title {
  font-size: 2.5rem;
  font-weight: bold;
  margin-bottom: 2rem;
}

.footer-subtitle {
  font-size: 1.2rem;
  opacity: 0.8;
  margin-bottom: 3rem;
}

.footer-links {
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  padding-top: 50px;
  margin-top: 50px;
}

.footer-links ul {
  list-style: none;
  display: flex;
  justify-content: center;
  gap: 30px;
  flex-wrap: wrap;
  margin-bottom: 30px;
}

.footer-links a {
  color: white;
  text-decoration: none;
  font-size: 14px;
  opacity: 0.8;
  transition: opacity 0.3s ease;
}

.footer-links a:hover {
  opacity: 1;
}

.footer-copyright {
  font-size: 12px;
  opacity: 0.6;
  margin-top: 20px;
}

.section-title {
  font-size: 2rem;
}

.section-subtitle {
  font-size: 1rem;
}

.footer-title {
  font-size: 1.8rem;
}

.footer-subtitle {
  font-size: 1rem;
}

.swiper-slide {
  font-size: 1.5rem;
}

.swiper {
  width: 100%;
  height: 100%;
}

.main-swiper,
.colla-swiper {
  height: 100vh;
  margin-top: 0;
}

.swiper-pagination {
  position: absolute;
  bottom: 20px;
  left: auto !important;
  right: 20px;
  text-align: right;
}

.swiper-pagination-bullet {
  background: rgba(255, 255, 255, 0.5);
  opacity: 1;
}

.swiper-pagination-bullet-active {
  background: #fff;
}

.swiper-slide picture,
.swiper-slide picture img {
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.swiper-slide picture img {
  object-fit: cover;
}

.section picture img {
  transition: transform 8s ease;
  transform: scale(1);
  transform-origin: center center;
}

.swiper-slide-active picture img,
body picture img {
  transform: scale(1.05);
}

.swiper-pagination{
  left: auto !important;
  right: 20px;
  text-align: right;
}

.slide-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 10;
  color: #fff;
  cursor: pointer;
  font-size: 2.27vw;
  font-weight: normal;
  text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
  text-align: center;
}

.slide-text h2 {
  margin: -8px 0 20px -2px;
  font-size: 2.27vw;
}

.slide-text h3 {
  font-size: 2.27vw;
}

.slide-text .sliderLink {
  font-size: 14px;
  position: relative;
  display: inline-block;
}

.slide-text .sliderLink::after {
  content: '';
  width: 100%;
  height: 1px;
  background: #fff;
  position: absolute;
  bottom: 0;
  display: block;
  transition-duration: 0.5s;
}

.slide-text:hover .sliderLink::after {
  width: 0;
  transition-duration: 0.5s;
}
</style>
</head>

<body>
  <!-- セクション1: メインスライダー -->
  <section class="section section1">
  <div class="section-content">
  <h2 class="section-title">SECTION 1</h2>
  <p class="section-subtitle">4枚のスライド</p>
  </div>

  <div class="swiper main-swiper">
  <ul class="swiper-wrapper">
    <li class="swiper-slide">
    <picture>
    <source media="(min-width: 1000px)" srcset="/amnesia_record/img/image_fx_1.jpg" alt="YouTube Test 1">
    <img src="/amnesia_record/img/image_fx_1.jpg" alt="YouTube Test 1">
    </picture>
    <div class="slide-text">
    <h2>SECTION 1</h2>
    <h3>Slide 1</h3>
    <div class="sliderLink">VIEW MORE</div>
    </div>
    </li>
    <li class="swiper-slide">
    <picture>
    <source media="(min-width: 1000px)" srcset="/amnesia_record/img/image_fx_2.jpg" alt="YouTube Test 2">
    <img src="/amnesia_record/img/image_fx_2.jpg" alt="YouTube Test 2">
    </picture>
    <div class="slide-text">
    <h2>SECTION 1</h2>
    <h3>Slide 2</h3>
    <div class="sliderLink">VIEW MORE</div>
    </div>
    </li>
    <li class="swiper-slide">
    <picture>
    <source media="(min-width: 1000px)" srcset="/amnesia_record/img/image_fx_3.jpg" alt="YouTube Test 3">
    <img src="/amnesia_record/img/image_fx_3.jpg" alt="YouTube Test 3">
    </picture>
    <div class="slide-text">
    <h2>SECTION 1</h2>
    <h3>Slide 3</h3>
    <div class="sliderLink">VIEW MORE</div>
    </div>
    </li>
    <li class="swiper-slide">
    <picture>
    <source media="(min-width: 1000px)" srcset="/amnesia_record/img/image_fx_4.jpg" alt="YouTube Test 4">
    <img src="/amnesia_record/img/image_fx_4.jpg" alt="YouTube Test 4">
    </picture>
    <div class="slide-text">
    <h2>SECTION 1</h2>
    <h3>Slide 4</h3>
    <div class="sliderLink">VIEW MORE</div>
    </div>
    </li>
  </ul>
  <div class="swiper-pagination"></div>
  </div>
  </section>

  <!-- セクション2: コラボスライダー -->
  <section class="section section2">
  <div class="section-content">
  <h2 class="section-title">SECTION 2</h2>
  <p class="section-subtitle">2枚のスライド</p>
  </div>

  <div class="swiper colla-swiper">
  <ul class="swiper-wrapper">
    <li class="swiper-slide">
    <picture>
    <source media="(min-width: 1000px)" srcset="/amnesia_record/img/image_fx_5.jpg" alt="Sort 1">
    <img src="/amnesia_record/img/image_fx_5.jpg" alt="Sort 1">
    </picture>
    <div class="slide-text">
    <h2>SECTION 2</h2>
    <h3>Slide 1</h3>
    <div class="sliderLink">VIEW MORE</div>
    </div>
    </li>
    <li class="swiper-slide">
    <picture>
    <source media="(min-width: 1000px)" srcset="/amnesia_record/img/image_fx_6.jpg" alt="Sort 2">
    <img src="/amnesia_record/img/image_fx_6.jpg" alt="Sort 2">
    </picture>
    <div class="slide-text">
    <h2>SECTION 2</h2>
    <h3>Slide 2</h3>
    <div class="sliderLink">VIEW MORE</div>
    </div>
    </li>
  </ul>
  <div class="swiper-pagination"></div>
  </div>
  </section>

  <!-- フッター(通常の高さ) -->
  <div id="footer">
  <div class="footer-content">
  <h2 class="footer-title">FOOTER</h2>
  <p class="footer-subtitle">通常の高さのフッター</p>
  
  <div class="footer-links">
    <ul>
    <li><a href="#">COMPANY</a></li>
    <li><a href="#">RECRUIT</a></li>
    <li><a href="#">PRIVACY POLICY</a></li>
    <li><a href="#">ABOUT THIS SITE</a></li>
    <li><a href="#">CAUTION</a></li>
    </ul>
    <div class="footer-copyright">
    © 2025 SAMPLE COMPANY. ALL RIGHTS RESERVED.
    </div>
  </div>
  </div>
  </div>

<script>
  // スタイルの実装
  document.addEventListener('DOMContentLoaded', function() {
    //console.log('=== 初期化開始 ===');

    // GSAPプラグインの初期化
    gsap.registerPlugin(ScrollToPlugin);
    //console.log('GSAP ScrollToPlugin初期化完了');

    // Swiperスライダーの初期化(方式で個別初期化)
    let mainSwiper = new Swiper(".main-swiper", {
      effect: "fade",
      loop: true,
      speed: 600,
      simulateTouch: true,
      allowTouchMove: true,
      fadeEffect: { crossFade: true },
      autoplay: {
        delay: 5000,
        disableOnInteraction: false
      },
      pagination: {
        el: ".main-swiper .swiper-pagination",
        clickable: true,
        type: "bullets"
      },
      on: {
        init: function() {
          console.log('Main Swiper初期化完了 - スライド数:', this.slides.length);
        }
      }
    });

    let collaSwiper = new Swiper(".colla-swiper", {
      effect: "fade",
      loop: true,
      speed: 600,
      simulateTouch: true,
      allowTouchMove: true,
      fadeEffect: { crossFade: true },
      autoplay: {
        delay: 5000,
        disableOnInteraction: false
      },
      pagination: {
        el: ".colla-swiper .swiper-pagination",
        clickable: true,
        type: "bullets"
      },
      on: {
        init: function() {
          console.log('Colla Swiper初期化完了 - スライド数:', this.slides.length);
        }
      }
    });

    // スタイルのスクロール制御
    const sections = document.querySelectorAll('.section');
    let currentIndex = 0;
    let isScrolling = false;
    let isFooterVisible = false;
    let swiperCooldown = false;

    // 初期位置を最上部に
    window.scrollTo(0, 0);

    function scrollToSection(index) {
      isScrolling = true;
      currentIndex = index;

      gsap.to(window, {
        scrollTo: { y: sections[index].offsetTop, autoKill: false },
        duration: 1.2, // より滑らかに
        ease: "power2.out", // イージングを調整
        onComplete: function() {
          isScrolling = false;
        }
      });
    }

    function showFooter() {
      isFooterVisible = true;
      isScrolling = true; // スクロール中フラグを設定
      const footer = document.getElementById('footer');
      footer.style.display = 'block';
      // セクションと同じスクロールアニメーション
      gsap.to(window, {
        scrollTo: { y: footer.offsetTop, autoKill: false },
        duration: 1.2,
        ease: "power2.out",
        onComplete: function() {
          isScrolling = false;
        }
      });
    }

    function hideFooter() {
      isFooterVisible = false;
      const footer = document.getElementById('footer');
      // セクションと同じスクロールアニメーション
      gsap.to(window, {
        scrollTo: { y: sections[sections.length - 1].offsetTop, autoKill: false },
        duration: 1.2,
        ease: "power2.out",
        onComplete: function() {
          footer.style.display = 'none';
          isScrolling = false;
        }
      });
    }

    function handleNavigation(deltaY) {
      if (isScrolling) return;

      if (isFooterVisible) {
        if (deltaY < 0) {
          hideFooter();
          currentIndex = sections.length - 1;
          scrollToSection(currentIndex);
        }
        return;
      }

      if (deltaY > 0) {
        if (currentIndex < sections.length - 1) {
          currentIndex++;
          scrollToSection(currentIndex);
        } else {
          showFooter();
        }
      } else {
        if (currentIndex > 0) {
          currentIndex--;
          scrollToSection(currentIndex);
        }
      }
    }

    // マウスホイールでのスクロール制御(スムーズ化)
    let lastWheel = 0;
    window.addEventListener('wheel', function(e) {
      let now = Date.now();
      if (now - lastWheel < 120) return; // 間隔を調整
      lastWheel = now;
      if (swiperCooldown) return;
      handleNavigation(e.deltaY);
    });

    // キーボードナビゲーション
    document.addEventListener('keydown', function(e) {
      if (isScrolling || swiperCooldown) return;
      if (isFooterVisible && e.key === "ArrowUp") {
        hideFooter();
        currentIndex = sections.length - 1;
        scrollToSection(currentIndex);
        return;
      }
      if (e.key === "ArrowDown") {
        if (currentIndex < sections.length - 1) {
          currentIndex++;
          scrollToSection(currentIndex);
        } else {
          showFooter();
        }
      } else if (e.key === "ArrowUp") {
        if (currentIndex > 0) {
          currentIndex--;
          scrollToSection(currentIndex);
        }
      }
    });

    // タッチスクロール
    let touchStartY = 0;
    let touchCurrentY = 0;

    window.addEventListener('touchstart', function(e) {
      touchStartY = e.touches[0].clientY;
    });

    window.addEventListener('touchmove', function(e) {
      touchCurrentY = e.touches[0].clientY;
    });

    window.addEventListener('touchend', function(e) {
      if (swiperCooldown) return;
      let deltaY = touchStartY - touchCurrentY;
      if (Math.abs(deltaY) > 30) {
        handleNavigation(deltaY);
      }
    });

    //console.log('初期化完了 - セクション方式で個別初期化');
  });
</script>
</body>
</html>

以上になります。
詳しくはAIに放り込んで聞いてください。

操作体験として若干、動作が強制的に感じて、あまり好きじゃないです。という個人的感想です。

参考サイト置き場

BRISK
GSAPを使って複雑なオープニングアニメーションを作ってみよう
https://b-risk.jp/blog/2022/05/gsap/

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

どこかで聞いたことがあるような。