GSAPのScrollTriggerで、スクロールに合わせてキャラクターを動かす手順

こんにちは!アントロンのNaokoです。
ページをスクロールすると、キャラクターが一緒に動くサイトを見たことはありませんか?
このサイトに付ける予定のチャットボットでは、呼び出しボタンに妖精のキャラクターを使うことにしています。その妖精を、スクロールに合わせて飛ばす試作をしました。使ったのは、GSAP(ジーサップ)というJavaScriptのライブラリと、そのプラグインのScrollTriggerです。
コードは、AIのClaude Code に「こう動かしたい」と頼んで書いてもらいました。私はブラウザで動きを見て、直してほしい所を伝える役です。10月1日の夜、約2時間で、練習の丸から妖精の完成まで進みました(Claude Code の使い方はこちらの記事にまとめています)。
この記事では、最初の練習から妖精が羽ばたくまでの手順と、途中でつまずいたところをまとめます。本番のサイトにこの動きを入れるかは、まだ決めていません。

GSAPとScrollTriggerでできること
GSAPは、Webページの要素を動かすためのJavaScriptのライブラリです。ScrollTriggerはGSAPのプラグイン(機能を足すための追加の部品)の1つで、動きをスクロールの位置と結びつけられます。
今回は、HTML・CSS・JavaScriptだけのページに、次の2つのファイルを jsDelivr(ライブラリを配信しているCDN)から読み込みました。バージョンは3.13.0です。
- gsap.min.js(GSAP本体)
- ScrollTrigger.min.js(ScrollTrigger)
指定できる項目の説明は、公式のドキュメント(ScrollTrigger)にあります。
手順1:まず丸を1つ動かす
いきなりキャラクターを動かす前に、オレンジの丸で練習しました。500pxスクロールする間に、丸が左へ200px動き、1.5倍に大きくなる動きです。
gsap.registerPlugin(ScrollTrigger);
gsap.set(".character", { xPercent: -50, yPercent: -50 }); // 画面の中央に置く
gsap.to(".character", {
x: -200, // 左へ200px
scale: 1.5, // 1.5倍
ease: "none", // スクロール量に比例させる
scrollTrigger: {
trigger: ".scroll-area",
start: "top top", // ページの一番上から
end: "+=500", // 500pxスクロールしたら終わり
scrub: true, // スクロールに連動。戻すと逆に動く
markers: true, // 開始と終了の位置を画面に出す
},
});ポイントは scrub: true です。これを付けると、スクロールを戻したときに動きも逆向きに戻ります。
markers: true にすると、画面の右に start・end(動かす要素の側の位置)と scroller-start・scroller-end(画面の側の位置)の線が出ます。思った位置で動き始めないときは、まずこれを付けて確かめると原因を探しやすくなります。

中央寄せは、CSSの transform: translate(-50%, -50%) ではなく、GSAPの xPercent と yPercent で指定しました。GSAPも要素の transform を書き換えるので、動かす指定と中央寄せをGSAP側にそろえています。
手順2:妖精を浮かせて、上から下へ動かす
練習のあと、Canvaの画像生成で作った妖精の絵(背景が透明なPNG)に置き換えました。動きは2つです。
- ふわふわ浮く:スクロールと関係なく、上下12pxの動きをずっと繰り返す
- ページの進み具合に合わせて、画面の上から下へ移動する
// ふわふわ浮く(yoyo で行って戻る、repeat: -1 でずっと繰り返す)
gsap.to(bob, { y: -12, duration: 1.4, ease: "sine.inOut", yoyo: true, repeat: -1 });
// ページの一番上から一番下までで、画面の上から下へ
gsap.fromTo(guide,
{ y: () => innerHeight * 0.06 },
{
y: () => innerHeight - guide.offsetHeight - 24,
ease: "none",
scrollTrigger: { trigger: ".page", start: "top top", end: "bottom bottom", scrub: 0.6, invalidateOnRefresh: true },
}
);scrub に数字(ここでは0.6)を入れると、その秒数をかけてスクロールの位置に追いつくので、動きがなめらかになります。invalidateOnRefresh: true は、画面の大きさが変わったときに位置を計算し直すための指定です。
手順3:羽を分けて、羽ばたかせる
静止画1枚のままだと、羽だけを動かせません。そこで、Claude Code に頼んで、1枚の絵を「体」「左の羽」「右の羽」の3枚の画像に切り分けてもらいました。3枚とも同じ大きさ(360×365px)で、同じ位置に重ねると元の絵に戻ります。

羽は、肩の付け根を軸にして横幅を縮めると、羽ばたいて見えます。軸はCSSの transform-origin で、左の羽は47% 45%、右の羽は53% 45%にしました。
const flap = gsap.timeline({ repeat: -1, yoyo: true });
flap.to(".wing", { scaleX: 0.6, duration: 0.14, ease: "sine.inOut" });
flap.timeScale(0.3); // 止まっているときはゆっくり
最初は止まっているときも速く羽ばたいていましたが、試作を見て「止まっているときはもっとゆっくりがいい」と頼んで変えてもらいました。いまは止まっているときは0.3倍、スクロール中は速さに合わせて最大2.5倍です。
手順4:進む向きを見せる
スクロールの向きと速さは、ScrollTriggerの onUpdate で受け取れます。self.direction が向き(下へ進むと1、上へ戻ると-1)、self.getVelocity() が速さです。これを使って、次の3つを足しました。
- 速さに合わせて前後に傾ける(下へ進むと前のめり、上へ戻るとのけぞる。最大35度)
- 向きが変わった瞬間に、進む方向へ14px跳ねる
- キラキラ光る粒を、進む向きと反対へ流す(8px進むごとに1粒)

つまずいたところ:速くスクロールすると絵がにじむ
最初の試作では、上下の向きが変わったときに、妖精を左右に反転させていました。ところが、速くスクロールすると絵がにじみ、とくに向きを切り替えた直後はにじんだままになりました。
反転は、横幅を1から-1へ0.3秒かけて変える動きにしていました。途中で横幅が0に近い状態を通るので、そのときの粗い絵が引き伸ばされて残っていた、というのが Claude Code の見立てです。私が確かめたのは、直したあとの画面でにじみが出なくなったことです。向きを切り替えた直後のにじみは、反転を一瞬で切り替える形に変えたら出なくなりました。
羽ばたきも横幅を縮める動きなので、次の2つを入れています。
- 羽の横幅は0.6より細くしない
- 羽の画像に、CSSの
will-change: transformを付ける
なお、左右の反転はそのあとやめました。正面を向いた絵なので、反転しても、どちらを向いているのか分かりづらかったためです。代わりに、手順4のキラキラ光る粒で向きを見せています。
妖精を押すとチャット画面が開く
最後に、妖精を押すと少し跳ねてからチャット画面が開き、妖精は隠れるようにしました。チャット画面を閉じると、妖精が戻ってきます。チャット画面の見た目は、チャットボットの試作で決めたものと同じです。いまは見た目だけで、送信しても返事は来ません。画面の中の会話は、見た目を確かめるために先に入れておいた見本です。

あわせて、パソコンやスマホで「視差効果を減らす」などの、動きを減らす設定をしている人には、浮く・羽ばたく・傾く・跳ねる動きと、キラキラ光る粒を止めるようにしています。JavaScriptの matchMedia で、prefers-reduced-motion の設定を確かめています。
まずは丸1つから試してみてください
スクロールに合わせた動きは、いきなりキャラクターで作るより、丸1つで start・end・scrub の動きを確かめてからのほうが、うまく動かないときに原因を探しやすくなります。手順1のコードを、スクロールできる長さ(例:高さ300vh)の箱と、画面に固定した丸を1つ置いたHTMLに入れて、x や end の数字を変えたときに動きがどう変わるかを見てみてください。
サイトにチャットボットを付けたい方は、AIチャットボット導入サービスのページもご覧ください。
最後まで読んでくださりありがとうございました!




コメント