star back image
people4
電飾 電飾
moon
astronaut

【GSAP】scrollTriggerでスクロール中に横スクロールしたい

BLOG GSAPWEBログ
読了約:30分

スクロール中に横スクロールしたいです。
そんな要件はありますでしょうか。

横ということはスライダーですか?

いえ、違います。
マウスなどのスクロールで下にスクロールしていってる時に、ある部分で横スクロールになってほしいそうです。

え、どうやるの。

JavaScriptライブラリの「GSAP」(ジーサップ)を使用します。

「GSAP」は派手なアニメーションを仕込みたい時に便利に使えるライブラリで、とても有名ということでした。
できること、その種類はとても沢山あります。以下はデモのリンクです。

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

有料のプラグインもあり、ちょっと馴染みがなかったのですが、どうやらupdateで無料になるそうです。

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

無料ならastrowaveでも使わせてもらいたいカモ。

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

今回は横スクロールのみで使用します。
サンプルを作成しましたので以下に共有したいです。

【共有】ソースコード

作成しましたサンプルは「GSAP」を利用します。

要件は以下です。

  • 下へスクロール中に横スクロールしたい
  • スマホの時は横スクロールを動作させない
gsap横スクロール(サンプルページ)
https://astrowave.jp/amnesia_record/gsap_scroll.php

position:stickyを使いますので、表示が変なときはoverflow:hiddenが悪さをしていないかご注意ください。

↓1行目から、cdnのリンクで2種ライブラリを読み込みさせてもらいました。

html

<!-- gsap -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>

<div class="main">
  <h1>「GSAP」(ジーサップ)の横スクロール</h1>
  <section class="contents">
    top contents
  </section>
  <section id="gsap_cont">
    <div class="scroll-inner">
      <div id="wrapper" class="l-hero-wrapper">
        <div class="scroll-set">
          <div class="movie_set l-hero-panel">
            <h2 class="h2_ttl sp_only">Ojisan Style</h2>
            <div class="txt_set">
              <h2 class="h2_ttl pc_only">Ojisan Style 1</h2>
              <p class="messe_ttl">「おじさんと遊ぼう」</p>
              <p class="messe_sub_ttl">OLD FASHION</p>
              <p class="messe_txt">サンプルムービーにてご紹介。サンプルムービーにてご紹介。サンプルムービーにてご紹介。サンプルムービーにてご紹介。</p>
            </div>
            <div class="poster">
              <img src="https://astrowave.jp/amnesia_record/img/portrait_movie_cover.webp" alt="ojisan style">
            </div>
          </div>
          <div class="img_set l-hero-panel">
            <div class="photo01">
              <img src="https://astrowave.jp/amnesia_record/img/portrait_movie_cover.webp" alt="ojisan style">
            </div>
          </div>
          <div class="movie_set borders l-hero-panel">
            <div class="txt_set">
              <h2 class="h2_ttl">Ojisan Style 2</h2>
              <p class="messe_ttl">「おじさんが悲しむ」</p>
              <p class="messe_sub_ttl">ピュア ハート</p>
              <p class="messe_txt">サンプルムービーにてご紹介。サンプルムービーにてご紹介。サンプルムービーにてご紹介。サンプルムービーにてご紹介。</p>
            </div>
            <div class="poster">
              <img src="https://astrowave.jp/amnesia_record/img/portrait_movie_cover.webp" alt="ojisan style">
            </div>
          </div>
          <div class="img_set l-hero-panel">
            <div class="photo02">
              <img src="https://astrowave.jp/amnesia_record/img/portrait_movie_cover.webp" alt="ojisan style">
            </div>
          </div>
          <div class="movie_set borders l-hero-panel">
            <div class="txt_set">
              <h2 class="h2_ttl">Ojisan Style 3</h2>
              <p class="messe_ttl">「おじさんが喜ぶ」</p>
              <p class="messe_sub_ttl">ハッピー ハート</p>
              <p class="messe_txt">サンプルムービーにてご紹介。サンプルムービーにてご紹介。サンプルムービーにてご紹介。サンプルムービーにてご紹介。</p>
            </div>
            <div class="poster">
              <img src="https://astrowave.jp/amnesia_record/img/portrait_movie_cover.webp" alt="ojisan style">
            </div>
          </div>
          <div class="img_set l-hero-panel">
            <div class="photo03">
              <img src="https://astrowave.jp/amnesia_record/img/portrait_movie_cover.webp" alt="ojisan style">
            </div>
          </div>
        </div>
      </div>
    </div>
  </section>
  <section class="contents">
    bottom contents
  </section>
</div>

css

<style>
/*------------------------------------
画面サイズ 768px 未満 (SP)
-------------------------------------*/
@media (width < 768px) {
  .contents {
    background: lightcoral;
    height: 200px;
  }
  #gsap_cont {
    background:rgb(255, 170, 0);
    overflow: unset;
    -ms-overflow-style: none;
    position: relative;
  }
  #gsap_cont .poster {
    width: 100%;
  }
  #gsap_cont .poster img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  #gsap_cont::-webkit-scrollbar{
    display: none;
  }
  #gsap_cont .scroll-set {
    display: block;
    flex-wrap: wrap;
  }
  #gsap_cont .l-hero-wrapper{
    width: 100%;
    height: auto;
    margin: 0;
    padding: 0;
    display: block;
    flex-wrap: wrap;
  }
  #gsap_cont .l-hero-panel {
		width: 100%;
		height: 100%;
  }
  #gsap_cont .h2_ttl{
    font-weight: 600;
    font-size: 15px;
    letter-spacing: -0.02em;
    line-height: 1.4;
    color:rgb(0, 0, 0);
    text-align: center;
    width: 100%;
    padding: 77px 0 36px;
  }
  #gsap_cont .movie_set {
    width: auto;
    display: flex;
    flex-wrap: wrap;
  }
  #gsap_cont .movie_set.borders {
    padding: 96px 0 40px;
  }
  #gsap_cont .movie_set.borders .h2_ttl {
    display: none;
  }
  #gsap_cont .txt_set {
    width: 100%;
    padding: 0 0;
    order: 2;
  }
  #gsap_cont .txt_set .messe_ttl {
    font-weight: 500;
    font-size: 23px;
    letter-spacing: 0.05em;
    line-height: 30px;
    text-align: center;
    color:rgb(0, 0, 0);
    margin: 32px 0 0;
  }
  #gsap_cont .txt_set .messe_sub_ttl {
    font-weight: 500;
    font-size: 11px;
    letter-spacing: 0.05em;
    line-height: 1.4;
    text-align: center;
    color:rgb(0, 0, 0);
    margin: 22px 0 0;
  }
  #gsap_cont .txt_set .messe_txt {
    font-weight: 500;
    font-size: 13px;
    letter-spacing: -0.01em;
    line-height: 26px;
    text-align: left;
    color:rgb(0, 0, 0);
    width: auto;
    margin: 30px 26px 0;
  }
  #gsap_cont .img_set {
    display: block;
    padding: 0;
    flex: 1;
  }
  #gsap_cont .img_set > div{
    width: auto;
  }
  #gsap_cont .img_set .photo01 {
    padding: 55px 52px 0;
  }
  #gsap_cont .img_set .photo02 {
    padding: 26px 52px 40px;
  }
  #gsap_cont .img_set .photo03 {
    padding: 16px 52px 76px;
  }
  #gsap_cont .img_set img {
    width: 100%;
    height: auto;
  }
}

/*------------------------------------
画面サイズ 768px 以上 (PC)
-------------------------------------*/
@media (786px <= width) {
  .contents {
    width: 100%;
    height: 100svh;
    background: lightcoral;
    position: relative;
  }
  #gsap_cont {
    background:rgb(255,  170, 0);
    position: relative;
    height: 250vh;
  }
  #gsap_cont::-webkit-scrollbar{
    display: none;
  }
  #gsap_cont#wrapper{
    overflow: hidden;
    position: relative;
  }
  #gsap_cont .scroll-inner{
    overflow: hidden;
    position: sticky;
    top: 0;
    left: 0;
    display: flex;
    justify-content: flex-start;
    align-items: flex-start;
  }
  #gsap_cont .poster img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  #gsap_cont .scroll-set {
    display: flex;
    flex-wrap: nowrap;
  }
  #gsap_cont .l-hero-wrapper{
    height: 100svh;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: nowrap;
    flex-shrink: 0;
  }
  #gsap_cont .l-hero-panel {
    width: 100%;
    height: auto;
  }
  #gsap_cont .h2_ttl{
    font-weight: 600;
    font-size: 17px;
    line-height: 1.4;
    color:rgb(0, 0, 0);
    text-align: center;
    padding: 80px 0 0;
  }
  #gsap_cont .movie_set {
    width: auto;
    display: flex;
  }
  #gsap_cont .movie_set.borders {
  }
  #gsap_cont .txt_set {
    width: auto;
    padding: 0 80px;
  }
  #gsap_cont .txt_set .messe_ttl {
    font-weight: 500;
    font-size: 26px;
    letter-spacing: 0.05em;
    line-height: 30px;
    text-align: center;
    color:rgb(0, 0, 0);
    margin: 30px 0 0;
  }
  #gsap_cont .txt_set .messe_sub_ttl {
    font-weight: 500;
    font-size: 12px;
    letter-spacing: 0.05em;
    line-height: 1.4;
    text-align: center;
    color:rgb(0, 0, 0);
    margin: 26px 0 0;
  }
  #gsap_cont .txt_set .messe_txt {
    font-weight: 500;
    font-size: 13px;
    letter-spacing: 0.05em;
    line-height: 25px;
    text-align: left;
    color:rgb(0, 0, 0);
    width: 360px;
    margin: 40px auto 0;
  }
  #gsap_cont .img_set {
    display: flex;
    padding: 150px 60px;
    flex: 1;
  }
  #gsap_cont .img_set > div{
    width: auto;
  }
  #gsap_cont .img_set .photo02 {
    padding: 0 0 0 60px;
  }
  #gsap_cont .img_set .photo03 {
    padding: 0 50px 0 0;
  }
  #gsap_cont .img_set img {
    width: auto;
    height: 100%;
  }
}
</style>

JavaScript

<script>
class HorizontalScroll {
  constructor() {
    // #gsap_cont が存在しなければ処理を終了
    if (!document.querySelector('#gsap_cont')) return;

    // GSAP と ScrollTrigger プラグインを登録
    gsap.registerPlugin(ScrollTrigger);

    // ページの全コンテンツが読み込まれた後に初期化
    window.addEventListener('load', () => {
      this.scrollInit();
    });

    // リサイズイベントを処理
    this.resizeHandler();
  }

  scrollInit() {
    // 対象となる要素を取得
    this.section = document.querySelector('#gsap_cont');
    this.wrap = document.querySelector('#gsap_cont #wrapper');
    this.inner = document.querySelector('#gsap_cont');

    // 画面サイズがスマホかどうかを判定(768px 以下の場合はスマホ)
    this.isMobile = window.matchMedia('screen and (max-width: 768px)').matches;

    // スマホの場合はスクロールアニメーションを適用しない
    if (!this.isMobile) {
      // 初期化時にサイズを更新してスクロールアニメーションを適用
      this.updateDimensions();
      this.applyScrollAnimation();
    }
  }

  resizeHandler() {
    let resizeTimeout;

    window.addEventListener('resize', () => {
      if (resizeTimeout) clearTimeout(resizeTimeout);

      resizeTimeout = setTimeout(() => {
        this.isMobile = window.matchMedia('screen and (max-width: 768px)').matches;

        if (!this.isMobile) {
          this.updateDimensions();
          ScrollTrigger.refresh();
          this.applyScrollAnimation();
        } else {
          ScrollTrigger.getAll().forEach(trigger => trigger.kill());
          gsap.set(this.wrap, { x: 0 });
        }
      }, 200); // 200ms程度の遅延を設定
    });
  }

  updateDimensions() {
    // 横幅を取得して必要な移動量を計算
    this.innerWidth = this.inner.offsetWidth;
    this.wrapWidth = this.wrap.offsetWidth;
    this.xMove = Math.min(0, this.innerWidth - this.wrapWidth); // 負の値にならないようにする
  }

  applyScrollAnimation() {
    // 既存のアニメーションを停止
    gsap.killTweensOf(this.wrap);

    // 新たにスクロールアニメーションを設定
    gsap.fromTo(
      this.wrap,
      { x: 0 }, // 初期状態
      {
        x: this.xMove, // 計算した移動量
        scrollTrigger: {
          trigger: this.section, // トリガーとなる要素
          start: 'top -300px', // アニメーション開始位置
          end: () => `+=${this.section.offsetHeight * 0.5}`, // アニメーション終了位置
          scrub: 1, // スクロールとアニメーションを同期
          markers: false, // デバッグ用のマーカーを表示しない
          invalidateOnRefresh: true, // リフレッシュ時に再計算
          onLeave: () => {
            document.querySelector('#gsap_cont')?.classList.add('is_moved');
          },
          onEnterBack: () => {
            document.querySelector('#gsap_cont')?.classList.remove('is_moved');
          }
        },
      }
    );
  }
}

// DOM が読み込まれたらクラスをインスタンス化
window.addEventListener('DOMContentLoaded', () => {
  new HorizontalScroll();
});
</script>

以上になります。

参考サイト置き場

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

星間旅路のメロディ

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

この操作盤は、ハイテク技術の塊のようです。