🎊

GSAPを使ったパララックスの新たな実装アプローチ

に公開

直近のプロジェクトでパララックスについて色々考えることがあったのと、個人的なベストプラクティスを発見できたので、今後誰かのお役に立てればと思い記事にしました。

今回実装したもの試したい方はこちらからコードをクローンし、pnpmコマンドで起動してください
https://github.com/shin-coder/parallaxImage

パララックスとは?

本稿の主旨は実装アプローチではありますが、概念的な理解をした上で読んでもらった方が応用を効かせやすいと感じるので、軽く説明します。
パララックス(視差効果)とは、異なる距離にある物体が、観察者の移動に対して異なる速度で移動し
て見える現象のことです。Webサイトにおいては、スクロールに応じて背景画像や要素を異なる速度で
移動させることで、奥行き感や立体感を演出する技術として活用されています。

要するに、スクロール自体は進んでいるが、画像の進行軸を変えることで、視差効果(パララックス)を生み出すことになります。
これを実現するために、一般的には初期値でマイナスY軸方向に設定し、終点では同等のプラスY軸方向に設定(例えば、-10%〜10%)することで、枠の中にある画像があたかも静止しているかのように錯覚させます。

よく見る実装アプローチ

①画像を縦幅だけデザインカンプのサイズより少し大きくトリミングする
この手法では、画像を事前に縦方向に拡張してトリミングします。
これにより画像の縦幅に余裕が生まれるため、パララックスの実装が可能となります。ただし、Y軸をどのくらい移動させるかを考える必要があるのと、都度トリミングを行う必要があるため、あまり効率的ではありません。

page.tsx
'use client';
import Image from 'next/image';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export default function ParallaxImageTrimmed() {
  const imageRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    if (!imageRef.current) return;

    gsap.fromTo(
      imageRef.current,
      { y: '-10%' },
      {
        y: '10%',
        scrollTrigger: {
          trigger: imageRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: true,
        },
      }
    );
  });

  return (
    <>
      <div className="grid place-items-center w-full h-[150vh]">
        <div className="relative w-[50rem] h-[40rem] overflow-hidden">
          <div ref={imageRef}>
            <Image
              src="/parallax-image-extended.jpg"
              width={1920}
              height={1200}
              alt="縦にリサイズした画像"
              className="w-full h-auto object-cover"
            />
          </div>
        </div>
      </div>
    </>
  );
}

②画像をスケールで拡大させる
この方法であれば画像を都度トリミングする必要はありませんが、移動幅を考えたスケール計算が必要となるので、計算ミスといったヒューマンエラーが発生しやすくなります。
例えば、移動幅が5%であれば、上下合わせて10%移動することになるため、スケールは110%にするといった計算が発生します。

page.tsx
'use client';

import Image from 'next/image';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export default function ParallaxImageScaled() {
  const imageRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    if (!imageRef.current) return;

    gsap.fromTo(
      imageRef.current,
      { y: '-5%' },
      {
        y: '5%',
        scrollTrigger: {
          trigger: imageRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: true,
        },
      }
    );
  });

  return (
    <>
      <div className="grid place-items-center w-full h-[150vh]">
        <div className="relative w-[50rem] h-[40rem] overflow-hidden">
          <div className="scale-110" ref={imageRef}>
            <Image
              src="/parallax-image.jpg"
              alt=""
              className="w-full h-full object-cover"
            />
          </div>
        </div>
      </div>
    </>
  );
}

新しいアプローチ

どちらのアプローチも正しいことは大前提として、もう少し簡単で保守しやすいものはないかと考えたのが以下のアプローチです。
まず画像の枠となる要素にrelativeを設定し、画像を囲うdivタグに対してabsoluteを設定します。画像要素に対して、insetを0にした後、topとbottomにはそれぞれ移動幅×(-1)の値を追記します。
こうすることで、デザインカンプから再度トリミングを行う必要もなければ、移動幅と画像サイズが連動しているため、コードの保守性も高くなります。
また、移動値を変数としておくことができるため、こういった面からでもメンテナンスがしやすいコードと言えます。

page.tsx
'use client';

import { useRef } from 'react';
import Image from 'next/image';

import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/dist/ScrollTrigger';
import { useGSAP } from '@gsap/react';

gsap.registerPlugin(ScrollTrigger);

export default function Home() {
  const imageItemRef = useRef<HTMLDivElement>(null);
  const yAxiDistance = 10;

  useGSAP(() => {
    if (imageItemRef.current) {
      gsap.fromTo(
        imageItemRef.current,
        {
          yPercent: yAxiDistance,
        },
        {
          yPercent: -yAxiDistance,
          ease: 'none',
          scrollTrigger: {
            trigger: imageItemRef.current,
            start: 'top bottom',
            scrub: true,
          },
        }
      );
    }
  }, [yAxiDistance]);

  return (
    <>
      <div className="grid place-items-center w-full h-[150vh]">
        <div className="w-[50rem] h-[40rem] overflow-hidden relative">
          <div
            className="absolute inset-0"
            ref={imageItemRef}
            style={{ top: `-${yAxiDistance}%`, bottom: `-${yAxiDistance}%` }}
          >
            <Image
              src="/images/img01.jpg"
              width={1920}
              height={1200}
              alt=""
              className="w-full h-auto object-cover"
            />
          </div>
        </div>
      </div>
    </>
  );
}

今回紹介したinsetを活用したアプローチは、従来の手法と比較して以下のような利点があります:

  • 開発効率の向上: 画像の事前加工やスケール計算が不要
  • 保守性の高さ: 移動幅を変数で管理でき、コードの可読性が向上
  • 柔軟性: デザイン変更時の対応が容易

パララックス効果は、適切に実装すればユーザー体験を大きく向上させる強力な手法です。
ぜひこのアプローチを活用して、効率的で保守しやすいパララックス実装を試してみてください。

Discussion