GSAPで無限スクロールをスクロール量に応じて速くしてみる(備忘録)
今回は、ずっと個人的に気になっていた無限スクロール部分がスクロール量に応じて速くなるというのを実装してみます。
実は過去に何度か挑戦してうまくいかなかったのですが、今回はなんとか実装することができました。
GSAPを使って無限スクロールをスクロール量に応じて速くなるように調整しています。
それではやっていきましょう。
HTMLの記述について
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GSAPで無限スクロールをスクロール量に応じて速くしてみる</title>
<link rel="stylesheet" href="../reset.css">
<link rel="stylesheet" href="./style.css">
</head>
<body>
<main>
<section></section>
<section></section>
<div class="bl_loop_inner">
<div class="bl_loop_target">
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
</div>
</div>
<section></section>
<section></section>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="./style.js"></script>
</body>
</html>
section部分は該当の無限スクロール部分まで距離をとりたいので、入れています。
該当の無限スクロール部分はdivタグのbl_loop_targetのclassで囲っている部分です。
またGSAPを使うため、下記の記述を追記しています。今回は、ScrollTriggerを使います。
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
今回はCDNを使っていますが、npmやyarnを使って対応することもできます。下記公式ドキュメントに記載してあります。
Installation | GSAP | Docs & Learning
上記ページからCDNを選択し、ScrollTriggerにチェックを入れ、赤枠に表示されたscriptのコードをHTMLへ貼り付ければOKです。
CSSの記述について
@charset "utf-8";
/* ==========================
初期設定
========================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
position: relative;
word-wrap: break-word;
}
img {
width: 100%;
}
/* レイアウト設定 */
section {
height: 100vh;
}
section:nth-child(1) {
background-color: #ccc;
}
section:nth-child(2) {
background-color: #444;
}
section:nth-child(3) {
background-color: #ccc;
}
section:nth-child(4) {
background-color: #444;
}
section {
background-color: #ccc;
}
img {
width: 100%;
height: auto;
vertical-align: bottom;
margin: 0;
}
.bl_loop_inner {
overflow: hidden;
display: flex;
}
.bl_loop_target {
display: flex;
gap: 24px;
}
.bl_loop_target img {
width: 400px;
}
.bl_loop_target + .bl_loop_target {
margin-left: 24px;
}
bl_loop_innerの箇所で、overflowをhiddenにしてはみ出した部分が表示されないように設定しています。
あとはbl_loop_targetの箇所で、displayをflexにして画像を並べているだけです。
bl_loop_targetにmargin-leftを設定しているのは、JavaScript側で要素を複製するためです。
JavaScriptの記述について
"use strict";
// GSAPライブラリを初期化し、ScrollTriggerプラグインを使用可能にする
gsap.registerPlugin(ScrollTrigger);
// 無限スクロールの対象となる要素を取得し、その複製を作成してDOMに追加する
const loopTarget = document.querySelector(".bl_loop_target");
const clone = loopTarget.cloneNode(true);
loopTarget.parentNode.appendChild(clone);
// 要素間のギャップとマージンを定義し、複製された要素の全体幅を計算する
const gap = 24; // .bl_loop_target img要素間のギャップ
const marginLeft = 24; // 複製された要素間の左マージン
const targetWidth = loopTarget.offsetWidth + gap + marginLeft; // 全体の幅を計算
// 要素とその複製に幅を設定し、無限スクロールアニメーションを定義する
gsap.set([loopTarget, clone], {
width: targetWidth,
});
const loopAnimation = gsap
.timeline({
repeat: -1, // アニメーションを無限に繰り返す
defaults: {
ease: "none",
}, // イージングを無しに設定
})
.to([loopTarget, clone], {
x: -targetWidth, // 要素を左に移動し、複製された要素が表示されるようにする
duration: 30, // 移動にかかる時間(無限スクロールの基本速度)
ease: "none",
})
.to([loopTarget, clone], {
x: 0, // 位置をリセット
duration: 0,
});
// スクロールイベントを検出し、スクロール速度に基づいてアニメーションの速度を調整する
let lastScrollTop = 0;
let lastTimestamp = 0;
let scrollTimeout;
window.addEventListener("scroll", () => {
const currentScrollTop =
window.pageYOffset || document.documentElement.scrollTop;
const currentTimestamp = Date.now();
// 前回のスクロールイベントからの時間と距離を計算し、スクロール速度を求める
if (lastTimestamp !== 0) {
const deltaTime = currentTimestamp - lastTimestamp;
const deltaScroll = currentScrollTop - lastScrollTop;
const scrollSpeed = Math.abs(deltaScroll) / deltaTime;
// スクロール速度に応じてアニメーションの速度(timeScale)を調整
loopAnimation.timeScale(Math.max(1, scrollSpeed * 10));
}
lastScrollTop = currentScrollTop;
lastTimestamp = currentTimestamp;
// スクロールが終了したとみなされる場合、アニメーションの速度をリセット
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
loopAnimation.timeScale(1); // アニメーションの速度を元に戻す
}, 200); // スクロールイベントが200ms以上発生しなければスクロール終了とみなす
});
下記公式ドキュメントに基づいて、対応していきます。
Installation | GSAP | Docs & Learning
まずは下記画像の赤枠のgsap.registerPlugin(ScrollTrigger)を追記します。
// GSAPライブラリを初期化し、ScrollTriggerプラグインを使用可能にする
gsap.registerPlugin(ScrollTrigger);
無限スクロールの対象となる要素を取得し、その複製を作成してDOMに追加します。
// 無限スクロールの対象となる要素を取得し、その複製を作成してDOMに追加する
const loopTarget = document.querySelector(".bl_loop_target");
const clone = loopTarget.cloneNode(true);
loopTarget.parentNode.appendChild(clone);
これで下記HTMLの箇所が1つから2つになります。
<div class="bl_loop_target">
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
</div>
<div class="bl_loop_target">
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
<img src="./images/img_01.webp" alt="" />
<img src="./images/img_02.webp" alt="" />
</div>
要素間のギャップとマージンを定義し、複製された要素の全体幅を計算します。これで無限スクロール時のずれを無くします。
// 要素間のギャップとマージンを定義し、複製された要素の全体幅を計算する
const gap = 24; // .bl_loop_target img要素間のギャップ
const marginLeft = 24; // 複製された要素間の左マージン
const targetWidth = loopTarget.offsetWidth + gap + marginLeft; // 全体の幅を計算
要素とその複製に幅を設定し、無限スクロールアニメーションを定義します。
repeatを−1にしてアニメーション無限に繰り返すようにして、easeは一定にしたいのでnoneにします。
今回は無限スクロールの移動方向は左側にします。xを-targetWidthにして設定します。
durationは30に設定していますが、ここは任意の値に変えていただいて大丈夫です。
最後に位置をリセットするため、xやdurationの値を0にしています。
// 要素とその複製に幅を設定
gsap.set([loopTarget, clone], {
width: targetWidth,
});
// 無限スクロールアニメーションを定義する
const loopAnimation = gsap
.timeline({
repeat: -1, // アニメーションを無限に繰り返す
defaults: {
ease: "none",
}, // イージングを無しに設定
})
.to([loopTarget, clone], {
x: -targetWidth, // 要素を左に移動し、複製された要素が表示されるようにする
duration: 30, // 移動にかかる時間(無限スクロールの基本速度)
ease: "none",
})
.to([loopTarget, clone], {
x: 0, // 位置をリセット
duration: 0,
});
スクロールイベントを検出し、スクロール速度に基づいてアニメーションの速度を調整します。
あとはスクロールしたときに、無限スクロールの速度を調整していきます。
addEventListenerのscrollイベントの中にコードを書いていきます。
スクロール位置、現在までの経過時間を取得し、前回のスクロールイベントからの時間と距離を計算し、スクロール速度を求めます。
その後、lastScrollTopとlastTimestampの情報を現在の情報に書き換え、スクロールが終了したとみなされる場合、アニメーションの速度をリセットしています。
// スクロールイベントを検出し、スクロール速度に基づいてアニメーションの速度を調整する
let lastScrollTop = 0;
let lastTimestamp = 0;
let scrollTimeout;
window.addEventListener("scroll", () => {
const currentScrollTop =
window.pageYOffset || document.documentElement.scrollTop;
const currentTimestamp = Date.now();
// 前回のスクロールイベントからの時間と距離を計算し、スクロール速度を求める
if (lastTimestamp !== 0) {
const deltaTime = currentTimestamp - lastTimestamp;
const deltaScroll = currentScrollTop - lastScrollTop;
const scrollSpeed = Math.abs(deltaScroll) / deltaTime;
// スクロール速度に応じてアニメーションの速度(timeScale)を調整
loopAnimation.timeScale(Math.max(1, scrollSpeed * 10));
}
lastScrollTop = currentScrollTop;
lastTimestamp = currentTimestamp;
// スクロールが終了したとみなされる場合、アニメーションの速度をリセット
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
loopAnimation.timeScale(1); // アニメーションの速度を元に戻す
}, 200); // スクロールイベントが200ms以上発生しなければスクロール終了とみなす
});
これにて実装完了です。
正攻法がどのようなものかはわかりませんが、とりあえず動いてよかったです。最初はCSSのkeyframesを使うなど、いろいろ試してみたのですが実現ができず諦めかけていました。
デザインギャラリーを見ていると、よく見かけると思いましたので、紹介してみました。