本文までスキップする 本文までスキップする

【デモあり】GSAPで横スクロールアニメーションを実装する

こんにちは。デザイナーの山田です。

ようやく秋の訪れを感じるようになりました。
年々秋の間隔が短くなっている気がしますが、今年はいつまで秋が続くのでしょうか。
京都の冬は寒いので、しっかり防寒対策をしていきたいと思います。

さて先日、弊社のお客様であるプロバンクホーム様のサイト制作を担当させていただきました。

その中で、横スクロールアニメーションを実装する機会があったのですが、今回はその備忘録を兼ねて、実装方法をまとめていきたいと思います。

GSAPについて・導入方法

実装するにあたり、今回はGSAP(GreenSock Animation Platform)というJavaScriptアニメーションライブラリを使用します。GSAPは、軽量かつパフォーマンス・機能性に優れたライブラリで、簡単にアニメーションを実装できることが特徴です。
このサイトにも、GSAPを使用している箇所がいくつかあったりします。気になる方は是非探してみてください。

インストール

GSAPを使用するにあたり、事前準備を終わらせておきましょう。
ファイルをCDNで読み込むか、npmやyarnでインストールしてくるか、環境などに応じてお好きな方法を選んでください。

CDN

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/gsap.min.js"></script>

npm / yarn

npm install gsap
yarn add gsap
// モジュールをimport
import { gsap } from "gsap";

また、今回GSAPのプラグインであるScrollTriggerも使用していきますので、こちらの設定も併せて済ませておいてください。

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/ScrollTrigger.min.js"></script>
// モジュールをimport
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
// npm / yarnでインストールした場合、`gsap.registerPlugin()`で指定する必要があります
gsap.registerPlugin(ScrollTrigger);

アニメーションを実装する

準備が整ったところで、いよいよ実装していきましょう。
今回、解説にあたってデモページを作成してみました。
この記事では、こちらの内容をベースに説明を進めていきます。

また、解説では便宜上下記のように記述していきます。

#wrapper要素 … wrapper
.panel要素 … panel
それでは内容について詳しく触れていきます。

HTML / CSS

まずは、HTMLとCSSです。

<div class="l-hero">
  <div class="l-hero-wrapper" id="wrapper">
    <div class="l-hero-panel l-hero-panel-01 panel" id="panel01">
      <p class="l-hero-panel__contents">Panel 1/4</p>
    </div>
    <div class="l-hero-panel l-hero-panel-02 panel" id="panel02">
      <p class="l-hero-panel__contents">Panel 2/4</p>
    </div>
    <div class="l-hero-panel l-hero-panel-03 panel" id="panel03">
      <p class="l-hero-panel__contents">Panel 3/4</p>
    </div>
    <div class="l-hero-panel l-hero-panel-04 panel" id="panel04">
      <p class="l-hero-panel__contents">Panel 4/4</p>
    </div>
  </div>
</div>
.l-hero {
	overflow: hidden;
	position: relative;
	.l-hero-wrapper {
		width: 400%;
		height: 100vh;
		display: flex;
		flex-wrap: nowrap;
		will-change: auto;
	}
	.l-hero-panel {
		width: 100%;
		height: 100%;
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
		background-size: cover;
		background-position: center center;
		background-repeat: no-repeat;
		&-01 {
			background-image: url('https://source.unsplash.com/KU1DYLV3tQE');
		}
		&-02 {
			background-image: url('https://source.unsplash.com/RfH5sOHTOek');
		}
		&-03 {
			background-image: url('https://source.unsplash.com/uOcQUMXaUz8');
		}
		&-04 {
			background-image: url('https://source.unsplash.com/aYLTPUkGZ-8');
		}
		&::after {
			content: '';
			width: 100%;
			height: 100%;
			position: absolute;
			top: 0;
			left: 0;
			z-index: 2;
			background-color: rgba(#000, .25);
		}
	}
	.l-hero-panel__contents {
		flex: 0 0 100%;
		text-align: center;
		font-weight: 700;
		color: #fff;
		font-size: 24px;
		position: relative;
		text-transform: uppercase;
		z-index: 3;
	}
	@media all and (min-width: 1280px) {
		.l-hero-panel__contents {
			font-size: 36px;
		}
	}
}

ポイントは、panelを囲うwrapperの長さは、panelの数に応じて調整する必要があることです。今回は4枚のpanelが入っているため、[width: 400%;]としました。

JavaScript

続いて、JavaScriptです。

const wrapper = document.querySelector('#wrapper');
if(wrapper) {
    // gsap.registerPlugin(ScrollTrigger); // npm/yarnの際に必要
    const panels = gsap.utils.toArray('.panel');
    const wrapperWidth = wrapper.offsetWidth;
    /**
    * 横スクロール開始
    */
    gsap.to( panels, {
        xPercent: -100 * (panels.length - 1), // transformX
        ease: "none", // easingの設定
        scrollTrigger: { // scrollTrigger
            trigger: wrapper, // アニメーションの対象となる要素
            pin: true, // 要素を固定する
            scrub: 1, // スクロールとアニメーションを同期させる。数値で秒数の設定に
            snap: { // スナップスクロールにする
                snapTo: 1 / ( panels.length - 1 ), // スナップで移動させる位置
                duration: {min: .4, max: .6}, // スナップで移動する際の遅延時間
                ease: "none" // easing
            },
            end: () => "+=" + wrapperWidth // アニメーションの終了タイミング
        }
    })
}

panelを指定するにはdocument.querySelectorAll() でもいいと思うのですが、GSAPには便利な.utils.toArray()関数があるため、そちらを使わせていただきました。

const panels = gsap.utils.toArray('.panel');

GSAP内の記述に触れていきます。
まず、xPercentでは、各panelのtranslateXの値を設定しており、画像が横並びになるように調整しています。

gsap.to( panels, {
		xPercent: -100 * (panels.length - 1), // transformX
		ease: "none", // easingの設定

scrollTrigger内では、wrapper内をスクロールしている際の処理を記述しています。

scrollTrigger: { // scrollTrigger
		trigger: wrapper, // アニメーションの対象となる要素
		pin: true, // 要素を固定する
		scrub: 1, // スクロールとアニメーションを同期させる。数値で秒数の設定に
		snap: { // スナップスクロールにする
				snapTo: 1 / ( panels.length - 1 ), // スナップで移動させる位置
				duration: {min: .4, max: .6}, // スナップで移動する際の遅延時間
				ease: "none" // easing
		},
		end: () => "+=" + wrapperWidth // アニメーションの終了タイミング
}

pin機能でwrapperを画面内に固定することで、スクロールに応じて横並びとなったpanelたちが流れてくる、といった仕様です。

最後のpanelが画面左端に届いたタイミングで固定は解除したいため、アニメーションの終了タイミング(end)はwrapperの長さとしています。

アンカーリンクを設置する

ここまでの実装でも十分だとは思いますが、各panelへのアンカーリンクがあると色々と便利ですよね。
アンカーリンクを実装するために、新たにScrollToPluginプラグインを用いたいと思います。

<!-- CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/ScrollToPlugin.min.js">
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
gsap.registerPlugin(ScrollToPlugin);

実装方法はこちらになります。
※動作が不安定になるため、スナップスクロールはOFFにしています。

<div class="l-nav-anchor">
  <div class="l-nav-anchor__wrapper">
    <ul class="l-nav-anchor__list">
      <li class="l-nav-anchor__list-col anchor">
        <a href="#panel01" class="l-nav-anchor__list-item">01</a>
      </li>
      <li class="l-nav-anchor__list-col anchor">
        <a href="#panel02" class="l-nav-anchor__list-item">02</a>
      </li>
      <li class="l-nav-anchor__list-col anchor">
        <a href="#panel03" class="l-nav-anchor__list-item">03</a>
      </li>
      <li class="l-nav-anchor__list-col anchor">
        <a href="#panel04" class="l-nav-anchor__list-item">04</a>
      </li>
    </ul>
  </div>
</div>
const wrapper = document.querySelector(this.wrapper);
if(wrapper) {
    // gsap.registerPlugin(ScrollToPlugin, ScrollTrigger); // npm/yarnの際に必要
    const anchors = document.querySelectorAll(this.anchor);
    let index = '';
    anchors.forEach( (anchor) => {
        anchor.addEventListener( 'click', (e) => {
            e.preventDefault();
            index = [].slice.call(anchors).indexOf(anchor); // 何番目のアンカーリンクをクリックしたか取得
            const target = document.querySelector(e.currentTarget.querySelector('a').getAttribute('href')); // クリックしたアンカーリンクに紐づくpanelを取得
            const scrollbarWidth = window.innerWidth - document.body.clientWidth; // スクロールバーの長さを取得
            const wrapperOffset = target.offsetLeft * ( wrapper.clientWidth / ( wrapper.clientWidth - window.innerWidth ) ) + scrollbarWidth * index; // 移動位置を取得
            gsap.to(window, {
                scrollTo: {
                    y: wrapperOffset,
                    autoKill: false
                },
                duration: 1
            });
        });
    });
}

リンクの対象となるpanelの位置を取得し、その値の分だけscrollToで遷移させている仕組みになっています。

まとめ

実装が複雑そうに見える横スクロールアニメーションですが、GSAPを用いることで比較的簡単に組むことができました。
導入の見極めが難しい横スクロールですが、適切なタイミングで用いることができれば、非常に効果的なアニメーションだと思います。
是非皆さんも実装にチャレンジしてみてください。

この記事の執筆者

Masaki Yamada
Masaki Yamada
大阪・京都にあるWebサイト制作の株式会社TANE-beのデザイナー・エンジニアです。