gsap + Next.jsでpin-animationを作成する

# はじめに
フロントエンドエンジニア歴3年目、現在は[80&Company](https://80and.co/ "80&Company")でフロントエンドを担当しています。
**Next.js**と**gsap**を使用するプロジェクトがあったので、備忘録を兼ねて記録します。

## 読者対象
gsapについて導入から基本的な使用方法を知りたい方
gsapを使用して[pin-animation](https://greensock.com/forums/topic/30098-how-to-pin-section-in-scrolltrigger/ "pin-animation")を作成したい方
応用編でgsapのtext-animationも作成しているので、text-animationに興味がある方

## 作りたいもの
### pin-animation
![Next-with-Gsap-Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286746/4326b2f5-f20f-f2b5-5b13-d78be2624974.gif)

section2にpin-animationを適応しています。
しばらく画面にsection2が止まり、**backgroud-color**と**text-color**がスクロールと共に変化している様子が確認いただけます。

## 開発環境
```package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@types/node": "18.15.3",
    "@types/react": "18.0.28",
    "@types/react-dom": "18.0.11",
    "autoprefixer": "^10.4.14",
    "eslint": "8.36.0",
    "eslint-config-next": "13.2.4",
    "gsap": "^3.11.2",
    "next": "13.2.4",
    "postcss": "^8.4.21",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "^3.2.7",
    "tailwindcss-gradients": "^3.0.0",
    "typescript": "4.9.5"
  }
}

```

## 手順
Next.js、Tailwind CSSのセットアップは省略し、gsapの導入から始めます。

### gsapの導入
``` bash
npm install gsap
```
普段はnpmを使用しているので、npmコマンドを使用します。
CDNなどでも導入可能です、詳しくは[gsapの公式サイト](https://greensock.com "gsap")を参照ください。

### レイアウト作成
今回はTailwind CSSで指定していますが、上記の動画にあったように3つのセクションの箱を作成し、それぞれheightを100vhで指定し、画面いっぱいにして縦に並べただけです。

```tsx
	<div className="h-screen bg-blue w-full flex justify-center items-center">
		<p className="text-[100px]">section 1</p>
	</div>
	<div className="h-screen w-full bg-dark flex items-center justify-center">
		<p className="text-[100px]">section 2</p>　 //背景色白、テキストカラー黒
	</div>
	<div className="h-screen bg-green w-full flex justify-center items-center">
		<p className="text-[100px]">section 3</p>
	</div>
```

### Refの適応
Reactでgsapを使用する場合は、refでdomを操作します。
そのためuseRefで作成したrefを操作したいタグに適応します。
section2を囲んでいるdivにrefをつけました。

```tsx
import { useRef } from "react";

const pinRef = useRef(null);
```

```tsx
	<div className="h-screen bg-blue w-full flex justify-center items-center">
		<p className="text-[100px]">section 1</p>
	</div>
	<div ref={pinRef} className="h-screen w-full bg-dark flex items-center justify-center"> 
        // refの追加↑
		<p className="text-[100px]">section 2</p>
	</div>
	<div className="h-screen bg-green w-full flex justify-center items-center">
		<p className="text-[100px]">section 3</p>
	</div>
```

### scrollTriggerプラグインの追加
今回使用するpin-animationはgsapの**scrollTrigger**の機能で、**scrollTriggerはプラグイン**なので追加する必要があります。
```tsx
import ScrollTrigger from "gsap/dist/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);
```
⚠️ページ自体に追加してもいいですが、どのページでも使いたい場合は、HeaderなどLayout系(どのページでも読み込まれる要素)に入れておくと、どこでも使えるので便利です。

### gsapで操作
domを操作する準備が整ったので、gsapを使用します。

```tsx
import gsap from "gsap";

useEffect(()=>{
    gsap.to(pinRef.current, {
		background: "#fff", // 背景色白
		color: "#000", // テキストカラー黒
		scrollTrigger: {
			trigger: pinRef.current,
			pin: true,
			scrub: true,
		},
	});
  },[])
```
上から見ていきます。

gsapの関数はuseEffectで囲います。

gsap.toの第一引数には変更したいターゲットを指定します。
今回はrefを使っているので**pinRef.current**を指定します。

**[gsap.to](https://greensock.com/docs/v3/GSAP/gsap.to() "gsap.to")は今の状態からgsapで指定した状態に変化させる**という意味です。
今回の場合、今の状態(背景色が黒、テキストカラーが白)から
gsapで第二引数に指定している状態(背景色が白,テキストカラーが黒)に変化するということになります。

scrollTriggerの内容は、
triggerはアニメーションを発生させる場所の指定です、第一引数と同じくpinRef.currentを指定します。
pinはpin-animationする場合に、trueを指定します
scrubはスクロールに連動してアニメーションさせる場合に、trueを指定します。

![Next-with-Gsap-Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286746/4326b2f5-f20f-f2b5-5b13-d78be2624974.gif)

完成しました。

```tsx
import { useEffect, useRef } from "react";
import gsap from "gsap";
import ScrollTrigger from "gsap/dist/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const PinAnimation = () => {
	const pinRef = useRef(null);

	useEffect(() => {
		gsap.to(pinRef.current, {
			background: "#fff",
			color: "#000",
			scrollTrigger: {
				trigger: pinRef.current,
				scrub: true,
				pin: true,
				markers: true,
			},
		});
	}, []);

	return (
		<>
			<div className="h-screen bg-blue w-full flex justify-center items-center">
				<p className="text-[100px]">section 1</p>
			</div>
			<div ref={pinRef} className="h-screen w-full bg-dark flex items-center justify-center">
				<p className="text-[100px]">section 2</p>
			</div>
			<div className="h-screen bg-green w-full flex justify-center items-center">
				<p className="text-[100px]">section 3</p>
			</div>
		</>
	);
};

export default PinAnimation;

```
ページ全体のコードはこんな感じです。

## 応用編 (text-animation)
pin-animationに合わせて、[text-animation](https://greensock.com/docs/v3/Plugins/TextPlugin "text-animation")も追加してみます。

### Textプラグイン追加
text-animationもプラグインが必要なので追加します。
``` tsx
import TextPlugin from "gsap/dist/TextPlugin";

gsap.registerPlugin(TextPlugin);
```

### refの追加
同じくrefで操作するので、追加します。

```tsx
    const textRef = useRef(null);
``` 
``` tsx
    <div className="h-screen bg-blue w-full flex justify-center items-center">
		<p className="text-[100px]">section 1</p>
	</div>
	<div ref={pinRef} className="h-screen w-full bg-dark flex items-center justify-center">
		<p ref={textRef} className="text-[100px]">section 2</p>
           // refの追加↑
	</div>
	<div className="h-screen bg-green w-full flex justify-center items-center">
		<p className="text-[100px]">section 3</p>
	</div>
```

### gsapで操作
スクロールに応じて、テキストをアニメーションさせます。
その準備として、タグ内にあった"section2"の文字を消し、pタグの箱のみにします。
なぜ消すかはあとで説明があります。
```tsx
	<div className="h-screen bg-blue w-full flex justify-center items-center">
		<p className="text-[100px]">section 1</p>
	</div>
	<div ref={pinRef} className="h-screen w-full bg-dark flex items-center justify-center">
		<p ref={textRef} className="text-[100px]" /> // "section 2" 削除
	</div>
	<div className="h-screen bg-green w-full flex justify-center items-center">
		<p className="text-[100px]">section 3</p>
	</div>
```

```tsx
useEffect(()=>{
	gsap.to(textRef.current, {
		autoAlpha: 1,
		text: "section 2",
		scrollTrigger: {
			trigger: pinRef.current,
			scrub: true,
		},
	});
},[])
```

同じようにgsap.toで第一引数にターゲット（textRef.current）を指定。
[autoAlpha](https://greensock.com/forums/topic/15361-understanding-autoalpha/ "autoAlpha")はアニメーションスタート時点opacity-0からアニメーションの終わりにopacity-100に徐々に変化するための指定です。

textには表示したいテキストを入れます、今回は先ほど削除した"**section 2**"を入れました。

scrollTriggerのtriggerをpinRef.currentにしているのはアニメーションさせたい範囲の指定なので、
pinRefで指定しているdomの範囲内でアニメーションさせたいからです、その他はスクロール連動を指定しています。

![Next-with-Gsap-Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286746/c4417c00-39ff-44d9-39d7-bc720dfa5852.gif)

動かしてみるこんな感じです。
テキストがスクロールに応じてアニメーションしています。
widthが指定されていないので、文字が出てくるたびにwidthが変わっているため真ん中からテキストが出てきています。
動きが少し気持ち悪いので、修正します。

#### 微修正
アニメーションが終わる時点のsection2が入っているpタグのwidthは380pxなので、それを前もって要素に持たせてしまいます。
```tsx
	<div className="h-screen bg-blue w-full flex justify-center items-center">
		<p className="text-[100px]">section 1</p>
	</div>
	<div ref={pinRef} className="h-screen w-full bg-dark flex items-center justify-center">
		<p ref={textRef} className="text-[100px] w-[380px]" />
                                            // widthを事前に設定↑
	</div>
	<div className="h-screen bg-green w-full flex justify-center items-center">
		<p className="text-[100px]">section 3</p>
	</div>
```
![Next-with-Gsap-Animation-_1_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286746/0b6e621b-3d42-33ea-3d80-937f012f9036.gif)

pタグには元からwidthがあるので、左から順番にテキストが出てきているのが確認できます。

```tsx
import { useEffect, useRef } from "react";
import gsap from "gsap";
import ScrollTrigger from "gsap/dist/ScrollTrigger";
import TextPlugin from "gsap/dist/TextPlugin";

gsap.registerPlugin(ScrollTrigger);
gsap.registerPlugin(TextPlugin);

const PinAnimation = () => {
	const textRef = useRef(null);
	const pinRef = useRef(null);

	useEffect(() => {
		gsap.to(pinRef.current, {
			background: "#fff",
			color: "#000",
			scrollTrigger: {
				trigger: pinRef.current,
				scrub: true,
				pin: true,
				markers: true,
			},
		});
		gsap.to(textRef.current, {
			autoAlpha: 1,
			duration: 10,
			text: "section 2",
			scrollTrigger: {
				trigger: pinRef.current,
				scrub: true,
				pin: true,
				markers: true,
			},
		});
	}, []);

	return (
		<>
			<div className="h-screen bg-blue w-full flex justify-center items-center">
				<p className="text-[100px]">section 1</p>
			</div>
			<div ref={pinRef} className="h-screen w-full bg-dark flex items-center justify-center">
				<p ref={textRef} className="text-[100px] w-[380px]" />
			</div>
			<div className="h-screen bg-green w-full flex justify-center items-center">
				<p className="text-[100px]">section 3</p>
			</div>
		</>
	);
};

export default PinAnimation;

```
全体はこんな感じです。
gsap.toを関数に切り出すと、もっとスッキリしますが、また別の機会にします。

## 最後に
読んでいただきありがとうございました、今回はgsapのpin-animationについて解説しました。
今後も機会があれば、情報発信していきます。

## 参考資料

公式ページ

https://greensock.com/gsap/

scrollTrigger

https://greensock.com/scrolltrigger/

textPlugin

https://greensock.com/docs/v3/Plugins/TextPlugin