【React + Framer Motion】viewportに映ったタイミングでアニメーションを発火させる

# はじめに

サイト制作で記事のリンク一覧を作成した。  
各リンク一覧がviewportに映ったら、サムネイル画像が下から上に表示されるようにしたい。

そのためには、サムネイル画像の上に同じ形の黒い長方形を用意して、  
**映ったタイミングでそれを上へスライドさせる**という実装をする必要があった。

これを `Framer Motion` を使って実装する。



## Framer Motionとは？

[Framer Motion](https://www.framer.com/motion/) は React 向けのアニメーションライブラリ。

`initial` や `animate` などの予約語を使うことで、**状態ごとにアニメーションを管理できる**のがとても便利。

まずはインストール。
```ssh
npm install framer-motion
```


## 実装の概要

やりたいことは次の通り：

1. サムネイル画像の上に黒いオーバーレイ（div）を重ねる  
2. `Framer Motion` で、オーバーレイが下から上にスライドするアニメーションを定義  
3. そのオーバーレイが viewport に入ったタイミングでアニメーション発火


## 実装コード（React + Framer Motion）

AnimationLink.tsxというコンポーネントを作成した
これに記事データのpropsを渡して使用している
```tsx
'use client'

import { motion } from 'framer-motion'
import styles from './articleLink.module.scss'

const overlayVariants = {
  hidden: { y: '0%' },
  visible: (custom: number) => ({
    y: '-100%',
    transition: {
      delay: custom % 2 === 0 ? 0 : 0.2,
      duration: 0.6,
      ease: [0.2, 0.34, 0.24, 1],
    },
  }),
}

type Props = {
  id: string
  title: string
  thumbnail_path: string
  custom: number
}

export default function ArticleLink({ id, title, thumbnail_path, custom }: Props) {

  return (
    <div className={styles.item}>
      <div className={styles.imageWrapper}>
        <Link href={`/production/${id}`}>
          <div
            className={styles.thumbnailContainer}
          >
            <img
              src={thumbnail_path}
              alt={title}
            />
          </div>
        </Link>

        <motion.div
          className={styles.overlay}
          variants={overlayVariants}
          initial="hidden"
          whileInView="visible"
          viewport={{ once: true, amount: 0.8 }}
          custom={custom}
        />
      </div>
      <h2 className={styles.title}>{title}</h2>
    </div>
  )
}
```


## ポイント解説

### `variants` でアニメーション状態を定義

```ts
const overlayVariants = {
  hidden: { y: '0%' },
  visible: (custom: number) => ({
    y: '-100%',
    transition: {
      delay: custom % 2 === 0 ? 0 : 0.2,
      duration: 0.6,
      ease: [0.2, 0.34, 0.24, 1],
    },
  }),
}
```

このように、状態を「名前付き」で管理できる
`hidden → visible` に切り替えることで、要素の動きを制御している


### `whileInView` でスクロール時の表示検知

```tsx
<motion.div
  initial="hidden"
  whileInView="visible"
  viewport={{ once: true, amount: 0.8 }}
/>
```

- `initial="hidden"`：初期状態
- `whileInView="visible"`：viewport に 80% 入ったら `visible` 状態へ切り替え
- `once: true`：一度だけアニメーションする

80%に設定した理由は、少しでもビューポートに映るとすぐアニメーションが開始されてしまうため


## おわりに

直感的にアニメーション実装ができたので今後も使っていきたい

備忘録として使用頻度が高そうなものをまとめた

| プロパティ名     | カテゴリ       | 説明 |
|------------------|----------------|------|
| `initial`        | 基本           | 初期状態のスタイル（アニメーション開始前） |
| `animate`        | 基本           | アニメーションの最終（または途中）状態 |
| `exit`           | 基本           | コンポーネントが消える時の状態（`AnimatePresence`と併用） |
| `transition`     | 基本           | アニメーションの速度・タイミング設定 |
| `whileHover`     | ジェスチャー   | ホバー中の状態に変化させる |
| `whileTap`       | ジェスチャー   | クリック/タップ中の状態に変化させる |
| `whileFocus`     | ジェスチャー   | フォーカス中の状態（アクセシビリティ対応） |
| `whileDrag`      | ジェスチャー   | ドラッグ中の状態に変化させる |
| `variants`       | 状態制御       | 状態（初期・アニメ・終了）をまとめて定義できるオブジェクト |
| `drag`           | 動作制御       | 要素をドラッグ可能にする（`true` / `x` / `y`） |