タグ: Gsap

  • React + Gsap

    要素にマウスを重ねて動かすと、その要素内の画像がマウスの動きに合わせてわずかに傾いたり移動するマウスオーバーアニメーションをGSAPとuseGSAPで実装する。

    環境

    vite@7.1.11
    react@19.2.0
    gsap@3.13.0
    gsap/react@2.1.2

    リポジトリはこちら

    コードの主要な機能と説明

    import { useRef } from 'react';
    import { gsap } from 'gsap';
    import { useGSAP } from '@gsap/react';
    import './App.scss';
    
    function AnimatedComponent() {
    
      const container = useRef(null);
      
      useGSAP(() => {
    
        const targets = gsap.utils.toArray('.hover--01', container.current);// すべての.hover--05要素を配列として取得する
    
        targets.forEach(target => {// 取得した各要素に対してイベントリスナーとアニメーションロジックを設定する
    
          const image = target.querySelector('.hover-image');// 各.hover--05の子要素である.hover-imageを取得する
    
          if (!image) return;// 画像要素がなければスキップ
    
          const onMouseMove = (e) => {// マウス移動時のイベントハンドラ
    
            const {offsetX, offsetY} = e;// e.offsetXとe.offsetYは、ターゲット要素の左上からの相対座標
            const clientWidth = target.clientWidth;// クライアント幅を取得する
            const clientHeight = target.clientHeight;// クライアント高さを取得する
            const x = (offsetX - clientWidth * .5) / (Math.PI * 3);// マウス位置を中央からのオフセットに変換し、回転角度/移動量に調整する
            const y = -(offsetY - clientHeight * .5) / (Math.PI * 4);// マウス位置を中央からのオフセットに変換し、回転角度/移動量に調整する
    
            gsap.to(image, {// GSAPアニメーションを実行(今回は個別の要素に適用)
              rotateX: y * .3,
              rotateY: x * .3,
              x: x,
              y: -y,
              duration: 0.5,
              ease: 'power4.out'
            });
          };
    
          const onMouseLeave = () => {// マウスが離れた時のイベントハンドラ
    
            gsap.to(image, {// 画像を初期状態に戻す
              rotateX: 0,
              rotateY: 0,
              x: 0,
              y: 0,
              duration: 1,
              ease: 'power4.out'
            });
          };
    
          target.addEventListener('mousemove', onMouseMove);// 各要素にイベントリスナーを追加する
          target.addEventListener('mouseleave', onMouseLeave);// 各要素にイベントリスナーを追加する
    
          target.cleanup = () => {// クリーンアップ関数を配列に格納
    
            target.removeEventListener('mousemove', onMouseMove);// 各要素のイベントリスナーを削除する
            target.removeEventListener('mouseleave', onMouseLeave);// 各要素のイベントリスナーを削除する
          };
        });
    
        return () => {// useGSAPのクリーンアップ関数として、すべての要素のクリーンアップを実行
    
          targets.forEach(target => {
    
            if (target.cleanup) target.cleanup();
          });
        };
      }, {scope: container, dependencies: []});
    
      return (
        <>
          <div ref={container} className="container--01">
            <div className="container-contents">
              <div className="hover--01"> 
                <div className="hover-inner">
                  <div className="hover-image">
                    <img src="/img/aaron-burden-TBjlyNzZ8H4-unsplash.jpg" alt="" />
                  </div>
                </div>
              </div>
              <div className="hover--01"> 
                <div className="hover-inner">
                  <div className="hover-image">
                    <img src="/img/aaron-burden-TBjlyNzZ8H4-unsplash.jpg" alt="" />
                  </div>
                </div>
              </div>
              <div className="hover--01"> 
                <div className="hover-inner">
                  <div className="hover-image">
                    <img src="/img/aaron-burden-TBjlyNzZ8H4-unsplash.jpg" alt="" />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </>
      );
    }
    
    export default AnimatedComponent;

    準備と設定

    import { useRef } from 'react';
    ReactのHook。DOM要素への参照(リファレンス)を保持するために使用する。

    import { gsap } from 'gsap';
    GSAPのコアライブラリをインポートする。

    import { useGSAP } from '@gsap/react';
    React環境でGSAPを安全かつ効率的に使用するためのHook。コンポーネントのマウント時や更新時にアニメーションを設定し、アンマウント時に自動的にクリーンアップ(破棄)する。

    const container = useRef(null);
    アニメーションの対象となる要素全体(<div className="animation-container">)への参照を保持する。useGSAPのスコープを設定するために使われる。

    アニメーションロジック (useGSAP)

    const targets = gsap.utils.toArray('.hover--05', container.current);
    GSAPのユーティリティを使用してコンポーネント全体(container.current)の中からクラス名が.hover--05の要素をすべて探し出して配列として取得する。

    targets.forEach(target => { ... });
    取得した各.hover--05要素に対してマウスイベントのリスナーを設定するループ。

    const image = target.querySelector('.hover-image');
    .hover--05要素の子要素から実際にアニメーションさせる対象となる画像コンテナ(.hover-image)を取得する。

    const onMouseMove = () => { ... }
    マウスがtarget要素(.hover--05)の上で移動したときに実行される関数。

    位置計算
    マウスの相対座標(offsetX, offsetY)を基に要素の中央からのオフセットを計算し、その値をGSAPのx, y, rotateX, rotateYプロパティに適用するための値(x, y)に変換する。この計算により、マウスが中央に近づくと動きが小さく、端に近づくと動きが大きくなる。

    gsap.to(image, { ... });
    GSAPのtoメソッドを使ってマウス位置に応じて.hover-image要素をアニメーションさせる。rotateXrotateYxyプロパティを変化させ立体的な傾きとわずかな移動のエフェクトを実現する。

    const onMouseLeave = () => { ... }
    マウスがtarget要素から離れたときに実行される関数。

    gsap.to(image, { rotateX: 0, rotateY: 0, x: 0, y: 0, ... });
    画像コンテナのrotatetranslatex, y)プロパティを初期値(0)に戻してエフェクトをリセットする。

    イベントリスナーの登録とクリーンアップ
    target.addEventListenermousemovemouseleaveのイベントハンドラを登録する。useGSAPの戻り値の関数内(クリーンアップ)で、removeEventListenerを実行する処理を用意することでコンポーネントが画面から消えるときにメモリリークを防ぎイベントリスナーを正しく解除する。

    レンダリング

    ref={container}により、このdivが前述のcontainer.currentとなり、アニメーションのスコープ(範囲)を定義する。

    クラス名.hover--05を持つ複数の要素が並んでおり、これらがそれぞれ独立したマウスホバーエフェクトのターゲットとなる。

    .hover-imageクラスは、マウスの動きに合わせて実際に動く画像のコンテナ。

    useGSAP の主な機能

    • アニメーションの自動クリーンアップ
      Reactコンポーネントがアンマウントされる際にGSAPで作成されたアニメーションやタイムライン、ScrollTriggerインスタンスなどは手動で破棄しないとメモリに残り続ける可能性がある。useGSAP は、コンポーネントのアンマウント時にHook内で設定した全てのアニメーション(トゥイーン、タイムライン、ScrollTriggerなど)を自動的にクリーンアップ(停止・破棄)してメモリリークや意図しないアニメーションの実行を防ぐ。
    • スコープとコンテキストの管理
      useGSAP は、scope オプションを通じてアニメーションの対象範囲(コンポーネントのDOM要素)を定義する。
    • StrictModeへの対応
      ReactのStrictModeは、開発中にコンポーネントを意図的に二度レンダリングすることがあり、従来の useEffect だけでGSAPをセットアップすると、アニメーションが二重に作成されてしまう問題がある。useGSAP はこの StrictMode の動作を考慮して設計されており、開発中の二重実行を防ぎアニメーションのセットアップを一貫して保証する。
    • 依存配列 (Dependencies)
      useEffect と同様に、useGSAP も依存配列(dependencies)を持つ。配列内の値が変更された場合、GSAPのアニメーションが一旦破棄(クリーンアップ)された後に再作成される。これにより、動的な値に基づいてアニメーションを更新することが容易になる。
    useGSAP(
      () => {
        // ここにGSAPのアニメーションロジックを記述する
        gsap.to('.box', { rotation: 360 });
      },
      {
        // オプション
        scope: containerRef, // 必須: アニメーションの対象範囲
        dependencies: [value], // 任意: 依存する値
        revertOnUpdate: true,  // 任意: 再実行時に以前のアニメーションをリセットするか
      }
    );