【React】ライブラリーを使わないでパララックスを使って、リッチなサイトを作る

## 概要
みなさんは、パララックス（parallax）と聞いたことがあるでしょうか？

最近のサイトは、要素がふわっとフェードインしたり、要素の移動速度が違ったり、
スクロールに応じて色々なアニメーションが発火します。

このようなアニメーションを実装するのは大変です。
そのため、多くのライブラリーが公開され、簡単に実装できるようになっています。


ただ今回は、そんなライブラリーを使わずにパララックスのアニメーションを作成し、
リッチに見えるサイトを作成しました。

↓作ったサイトはこちらです。ご覧ください。

https://qiita.com/official-campaigns/engineer-festa/2022

*※ Qiita Engeer Festa 2022にご参加もお願いします。*

## パララックス（parallax）についてとは？
まずパララックス（parallax）にて解説します。

### パララックス（parallax）とは？
パララックスとは、視差効果のことで、
Webサイトにおいては、パララックスはスクロールなどの動作に応じて、
複数のレイヤー（層）にある要素を異なるスピードで動かすことで、
「**立体感や奥行きの演出**」、「**フェード・拡大縮小・回転などの視覚的エフェクト**」を演出する
アニメーションを指します。

### パララックス（parallax）を使ったサイトの例
*1. Instagramの公式サイト*
https://about.instagram.com/ja-jp/

*2. 星野リゾートの界タビ20s*
https://www.hoshinoresorts.com/sp/kaitabi20s/

*3. パララックスアニメーションのライブラリー*
https://dixonandmoe.com/rellax/

## パララックス（parallax）の作り方
### 1. パララックス（parallax）の基本・構造を理解する

パララックス（parallax）の基本・構造のポイントは以下の通りです。

:::note
**ポイント**
・ **手前にあるもの**は、**スクロール量より早く移動する**。
・ **奥にあるもの**は、**スクロール量よりゆっくり移動する**。
:::

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/764efca9-0021-b7ef-76cd-b025c608fd01.png)

**イメージしてください。🤔**

*電車に乗って窓から景色を見ている時、遠くに見える家はどのように移動して見えるでしょうか？*
→ 遠くに見える家はゆっくり移動しているようにみえますよね！？

*では反対に、近くに見える家はどのように移動して見えるでしょうか？*
→ 近くに見える家は一瞬で移動しているようにみえますよね！？

この現象をUI上に反映させるのが、パララックス（parallax）になります。

**↓ つまり...**

手前にくるような画像や要素は、スクロール量より早く移動し、
奥にくるようは背景画像や要素は、スクロール量よりゆっくり移動させることで、
基本的なHTML要素と視差が起こり、パララックスを表現することができます。


### 2. パララックス（parallax）を作成する
`パララックス（parallax）の基本・構造を理解する`で記載したように、
パララックス（parallax）を実装するためには以下の要素が必要です。

+ スクロール量を取得する
+ オブジェクトの変化量`targetFactor`を個別に設定できるようにする
+ パララックスしたいオブジェクトを操作できるようにする
+ スクロールごとに、スクロール量と変化量`targetFactor`を計算する
+ 計算した値に位置を変更する

では早速、これをもとにパララックスを作っていきましょう。


#### 1. スクロール量を取得する
`useEffect`をつかい、`document.addEventListener('scroll', onScroll)`でスクロールするごとに`onScroll`を発火させ、`const scrollY = window.pageYOffset`でスクロール量を取得します。

```typescript
const ParallaxItem = (children:Props) => {
  const onScroll = () => {
    const scrollY = window.pageYOffset // スクロール量を取得する
  }

  useEffect(() => {
    // スクロールするごとにonScrollを発火する
    document.addEventListener('scroll', onScroll)
  })

  return (
    <></>
  )
}
```

#### 2. オブジェクトの変化量`targetFactor`を個別に設定できるようにする
propsで`targetFactor`を渡せるように `ParallaxProps`を作ります。
ただ、毎回`targetFactor`を渡すのは大変なので、デフォルト値を決めておきます。

```typescript
interface ParallaxProps {
  targetFactor?: number
}

const ParallaxItem = (props: ParallaxProps) => {
  // targetFactorのデフォルト値を決めておく
  const targetFactor = props.targetFactor ? props.targetFactor : 0.15

  const onScroll = () => {
    const scrollY = window.pageYOffset
  }

  useEffect(() => {
    document.addEventListener('scroll', onScroll)
  })

  return (
    <></>
  )
}
```


#### 3. パララックスしたいオブジェクトを操作できるようにする
パララックスしたいオブジェクトのポジションを変更させるため、
useRefを使いdomを操作できるようにしておきます。


```typescript
interface ParallaxProps {
  // パララックスしたいオブジェクトを個別に設定できるようにする
  children: ReactNode
  targetFactor?: number
}

const ParallaxItem = (props: ParallaxProps) => {
  // useRefを使いdomを操作できるようにする
  const domRef = useRef<HTMLDivElement>(null)
  const targetFactor = props.targetFactor ? props.targetFactor : 0.15

  const onScroll = () => {
    // ついでにdomRef.currentがnullではなかった時にスクロール量を取得するようにする。
    if (domRef.current !== null) {
      const scrollY = window.pageYOffset
    }
  }

  useEffect(() => {
    document.addEventListener('scroll', onScroll)
  })

  return (
    <div ref={domRef}>
      {props.children}
    </div>
  )
}
```

#### 4. スクロールごとに、スクロール量と変化量`targetFactor`を計算する

translateYの位置をスクロールロール量に合わせて変更できるように計算する。

```typescript
interface ParallaxProps {
  children: ReactNode
  targetFactor?: number
}

const ParallaxItem = (props: ParallaxProps) => {
  const domRef = useRef<HTMLDivElement>(null)
  const targetFactor = props.targetFactor ? props.targetFactor : 0.15
  const [offsetY, setOffsetY] = useState(0) // 計算結果

  const onScroll = () => {
    if (domRef.current !== null) {
      const scrollY = window.pageYOffset
      setOffsetY(scrollY * targetFactor * -1)
    }
  }

  useEffect(() => {
    document.addEventListener('scroll', onScroll)
  })

  return (
    <div ref={domRef}>
      {props.children}
    </div>
  )
}
```

#### 5. 計算した値に位置を変更する
計算した値を`OffsetY`をオブジェクトのtranslateYに指定する

```typescript
interface ParallaxProps {
  children: ReactNode
  targetFactor?: number
}

const ParallaxItem = (props: ParallaxProps) => {
  const domRef = useRef<HTMLDivElement>(null)
  const targetFactor = props.targetFactor ? props.targetFactor : 0.15
  const [offsetY, setOffsetY] = useState(0)

  const onScroll = () => {
    if (domRef.current !== null) {
      const scrollY = window.pageYOffset
      setOffsetY(scrollY * targetFactor * -1)
    }
  }

  useEffect(() => {
    document.addEventListener('scroll', onScroll)
  })

  return (
    <div
      ref={domRef}
      style={{
        transform: `translateY(${offsetY}px)`,
      }}
    >
      {props.children}
    </div>
  )
}
```

#### 6. 完成
**その他の対応1**
`verticalPosition`と`verticalOffset`でY軸方向の基準の位置を決めます。
`horizontalPosition`と`horizontalOffset`でX軸方向の基準の位置を決めます。
`objectSize`オブジェクトの大きさを決める。


**その他の対応2**
スタイルで使わない要素を除く。

**その他の対応3**

```typescript
interface ParallaxProps {
  children: ReactNode
  targetFactor?: number
  // 基準の位置・大きさを決める
  verticalPosition: 'top' | 'bottom'
  verticalOffset: number
  horizontalPosition: 'left' | 'right'
  horizontalOffset: number
  objectSize: number
}

// スタイルで使わない要素を除く
type ParallaxStyleType = Omit<ParallaxProps, 'children' | 'targetFactor'>

const ParallaxItem = (props: ParallaxProps) => {
  const domRef = useRef<HTMLDivElement>(null)
  const targetFactor = props.targetFactor ? props.targetFactor : 0.15
  const [offsetY, setOffsetY] = useState(0)

  const onScroll = () => {
    if (domRef.current !== null) {
      const scrollY = window.pageYOffset // scroll量を取得
      setOffsetY(scrollY * targetFactor * -1)
    }
  }

  useEffect(() => {
    document.addEventListener('scroll', onScroll)
  })

  return (
    <div
      ref={domRef}
      css={parallaxItemStyle(props)} //propsでスタイルを渡す。
      style={{
        transform: `translateY(${offsetY}px)`,
      }}
    >
      {props.children}
    </div>
  )
}

const parallaxItemStyle = (style: ParallaxStyleType) => {
  return css({
    position: 'absolute',
    top: style.verticalPosition === 'top' ? style.verticalOffset : '',
    bottom: style.verticalPosition === 'bottom' ? style.verticalOffset : '',
    left: style.horizontalPosition === 'left' ? style.horizontalOffset : '',
    right: style.horizontalPosition === 'right' ? style.horizontalOffset : '',
    width: style.objectSize,
    height: style.objectSize,
  })
}
```

## パララックス（parallax）を使って作ったサイト
↓作ったサイトはこちらです。ご覧ください。

https://qiita.com/official-campaigns/engineer-festa/2022

*※ Qiita Engeer Festa 2022にご参加もお願いします。*



## まとめ

この記事では、パララックスの作り方と使い方について解説しました。

少しは、パララックスについての理解ができたのではないのでしょうか？？？

---

最後まで読んでくださってありがとうございます！

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

https://twitter.com/degudegu2510