【React/GSAP】useGSAPで作るテキストアニメーション

はじめに

この記事の概要

こんにちは、株式会社TOKOSのスギタです!
今回はReact/GSAPuseGSAPを使用してテキストアニメーションを作ってみました。
また今回概要も調べてみたので、概要の説明もしていきます。

対象読者

  • WEB制作を行っている方

今回の完成予定

今回は以前作ったテキストアニメーションをReact/GSAPで再現したいと思います。

以前作ったテキストアニメーションは下記です。

開発環境

  • react: 18.2.0,
  • gsap: 3.12.5,

実装

React/GSAPの導入

今回はyarnを使用します。

まずはGSAPのイントールをします。

$yarn add gsap

次にReact/GSAPのパッケージのインストールです。

$yarn add @gsap/react

React/GSAPの概要

まずは基本的な構文からです。
例は下記になります。

import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";

const container = useRef();

useGSAP(() => {
  //ここにgsapの記述
  gsap.to(".box", {x: 360}); 

}, { scope: container });

基本的にReact/GSAPではuseGSAP内にgsap構文で書いていきます。

今回のメインのuseGSAPというフックですが、これが何をしているかというと、useEffectを踏襲したフックみたいです。
このuseEffectではクリーンアップ関数の記述が重要ですが、useGSAPにはgsap.context()が内包されていてgsapで記述されている関数にはクリーンアップ関数の記述をせずDOMの操作ができるみたいです。

またuseGSAPの第二引数にはconfigObjectというオブジェクトを入れることができます。

このconfigObjectの概要を説明します。

プロパティ名概要
dependenciesArray/null
デフォルト値: [ ]
内部のuseEffectに渡される依存配列
scopeReact refuseGSAP()フック内のすべてのGSAPセレクタテキストがそのコンテナの子孫にスコープされるように、configオブジェクトのスコープとしてコンテナを定義します。
revertOnUpdateboolean
デフォルト値: false
依存関係の配列を定義して依存関係が変更されても、GSAP 関連のオブジェクト(アニメーション、ScrollTriggers など)は元に戻りません。コンポーネントがアンマウントされ、フックが破棄されたときのみ、それらは元に戻ります。フックが再同期するたびに(依存関係が変更されるたびに)コンテキストを元に戻したい場合は、revertOnUpdate: true を設定します。

scopeがめちゃくちゃ便利なので別途解説します。

refをスコープとして渡すと、useGSAP()フック内のGSAP関連コードで使われるすべてのセレクタテキストもそれに応じてスコープされます。つまり子孫にuseRefでアクセスせずによくなります。

1.スコープにRefを渡さない例

const container = useRef();
const box1 = useRef();
const box2 = useRef();
const box3 = useRef();

useGSAP(() => {
  gsap.from([box1, box2, box3], {opacity: 0, stagger: 0.1});
});

return (
  //各要素にuseRefを用いてアクセスする
  <div ref={container}>
    <div ref={box1} className="box"></div>
    <div ref={box2} className="box"></div>
    <div ref={box3} className="box"></div>
  </div>
);

2.スコープにRefを渡す例

const container = useRef();

useGSAP(() => {
    gsap.from(".box", {opacity: 0, stagger: 0.1});
}, { scope: container }) // 親のRefをスコープにわたす

return (
  <div ref={container}>
    //子孫はuseRefを用いなくてもよい
    <div className="box"></div>
    <div className="box"></div>
    <div className="box"></div>
  </div>
);

このように親要素のrefを渡すだけで子要素のrefにアクセスせずに済みます。

またuseGSAP()には参照し使用できる機能があります。

プロパティ名概要
contextgsap.context()すべてのアニメーションを追跡するインスタンス。
contextSafe関数の実行中に作成された GSAP 関連のオブジェクトは、Context が元に戻されたときに元に戻ります(クリーンアップ)。コンテキストセーフ関数内のセレクタテキストも Context のスコープを使用します。 contextSafe() は関数を受け取り、その関数の新しいコンテキストセーフバージョンを返します。
コンテキストセーフについて

useGSAPの内部のgsap.contextに追加されている状態、クリーンアップされる状態という解釈。(違ったらすみません)

コンテキストセーフでは無い例

const container = useRef();

//下記はコンテキストセーフ
useGSAP(() => {
  gsap.to(".good", {x: 100}); 
}, {scope: container})

//下記が悪い例、クリーンアップできていない
const onClickBad = () => {
  gsap.to(".bad", {y: 100});
};

return (
  <div ref={container}>
    <div className="good"></div>
    <button onClick={onClickBad} className="bad"></button>
  </div>
);

コンテキストセーフさせるにはcontextSafe()でwrappしなければいけません。

contextSafe()の使用の仕方は2種類あります。

1.デストラクチャリングを用いて関数を抽出する

const container = useRef();

//デストラクチャリングを用いて関数を抽出する
const { contextSafe } = useGSAP({scope: container}); 

//contextSafe()でwrapp
const onClickGood = contextSafe(() => {
 gsap.to(".good", {rotation: 180});
});

return (
  <div ref={container}>
    <button onClick={onClickGood} className="good"></button>
  </div>
);

2.引数を使用

const container = useRef();
const badRef = useRef();
const goodRef = useRef();

useGSAP((context, contextSafe) => {
    
  gsap.to(goodRef.current, {x: 100});

  //コンテキストセーフではない例
  badRef.current.addEventListener("click", () => {
    gsap.to(badRef.current, {y: 100}); 
  });

  //第二引数のcontextSafe()でwrapp
  const onClickGood = contextSafe(() => {
    gsap.to(goodRef.current, {rotation: 180});
  });

  goodRef.current.addEventListener("click", onClickGood);

  //イベントリスナーのクリーンアップ関数
  return () => { 
    goodRef.current.removeEventListener("click", onClickGood);
  };

}, {scope: container});
return (
  <div ref={container}>
    <button ref={badRef}></button>
    <button ref={goodRef}></button>
  </div>
);

以上がcontextSafe()の使用方法です。

詳しく知りたい方は下記を参照してください。

テキストアニメーションの実装

HTML

今回は前回とは違いSplitTextは使用しません。

<div ref={text} className="text-wrapper">
  <span className="text">P</span>
  <span className="text">L</span>
  <span className="text">A</span>
  <span className="text">Y</span>
  <span className="text">T</span>
  <span className="text">O</span>
  <span className="text">E</span>
  <span className="text">N</span>
  <span className="text">J</span>
  <span className="text">O</span>
  <span className="text">Y</span>
</div>

CSS

.text-wrapper {
  color: #fff; /*お好きなフォントカラーを選んでください*/
  font-size: 80px; /*お好きなフォントサイズを選んでください*/
  font-family: "Times New Roman", Times, serif; /*お好きなフォントを選んでください*/
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
  line-height: 1; /*line-heightなくしたほうがカッコ良くなります*/
}

.text {
  transform: translateY(100px);
  display: inline-block; //インライン要素の為
  transition: 0.5s;
}

GSAP

export const TextAnimation = () => {
  const textWrapper = useRef()

  useGSAP(
    () => {
      gsap.to(".text", {
        delay: 0.2,
        duration: 0.5,
        ease: "power2.out", //型がリテラル型なので注意
        stagger: 0.05,
        y: 0,
      })
    },
    //scopeで親を指定すると子孫はuseRefでアクセスせずそのまま操作できる
    { scope: textWrapper },
  )

  return (
   <div ref={textWrapper} className="text-wrapper">
     <span className="text">P</span>
     <span className="text">L</span>
     <span className="text">A</span>
     <span className="text">Y</span>
     <span className="text">T</span>
     <span className="text">O</span>
     <span className="text">E</span>
     <span className="text">N</span>
     <span className="text">J</span>
     <span className="text">O</span>
     <span className="text">Y</span>
  </div>
  )
}

これで完成になります。 第二引数のscopeに親要素のrefを渡すだけでめちゃくちゃ簡単にかけます。

一応参考動画も載せておきます。

使用しているメソッド等がわからない方は下記を参照してください。

最後に

今回はReact/GSAPを紹介しました。
このuseGSAP()を用いた記述のが直感的で簡単にかけとてもかなり良いと感じました。
私としてはReactライクな記述方法のが書きやすく感じました!
また関係ないのですが、公式ドキュメントが更新されていてめちゃくちゃ読みやすくなっていることに感動しました。
ライブラリを使用する際はドキュメントの読みやすさも重要だなと改めて感じました。