Next.jsで魅せる！V0を使って動くパーティクルアニメーションの作ってみた

![スクリーンショット 2024-10-29 23.47.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/b2bc57cb-fcc5-c3c8-5a78-e872d7a1f96c.png)

## はじめに

ウェブサイトに動きのある要素を加えることで、ユーザーの注目を集め、印象的なユーザー体験を提供することができます。今回は、V0と共にNext.jsとReactを使用して、ハート型に集まるパーティクルアニメーションを作成する方法を詳しく解説します。

このチュートリアルでは、以下の技術やコンセプトを扱います：

- Next.jsのApp Router
- Reactのuseeffectとuseref
- HTML5 Canvas API
- JavaScriptのアニメーション


## プロジェクトのセットアップ

まず、新しいNext.jsプロジェクトを作成します：

```shellscript
npx create-next-app@latest heart-particles-app
cd heart-particles-app
```
以下のファイル構造です。

```plaintext
heart-particles-app/
├── app/
│   ├── layout.tsx
│   └── heart-particles/
│       └── page.tsx
├── package.json
└── tsconfig.json
```

## 必要なファイルの作成と編集

### 1. app/layout.tsx

このファイルは、アプリケーション全体のレイアウトを定義します。

```typescriptreact
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className="bg-black">{children}</body>
    </html>
  )
}
```

### 2. app/heart-particles/page.tsx

このファイルにハートパーティクルアニメーションのコンポーネントを実装します。

```typescriptreact
"use client"

import React, { useRef, useEffect } from 'react'

export default function Component() {
  const canvasRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const ctx = canvas.getContext('2d')
    if (!ctx) return

    const setCanvasSize = () => {
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
    }
    setCanvasSize()

    class Particle {
      x: number
      y: number
      size: number
      targetX: number
      targetY: number
      color: string
      speed: number
      angle: number
      rising: boolean

      constructor(x: number, y: number, targetX: number, targetY: number) {
        this.x = x
        this.y = y
        this.size = Math.random() * 2 + 1
        this.targetX = targetX
        this.targetY = targetY
        this.color = `rgba(0, ${Math.floor(Math.random() * 55 + 200)}, ${Math.floor(Math.random() * 55 + 200)}, ${Math.random() * 0.3 + 0.5})`
        this.speed = Math.random() * 0.5 + 0.2
        this.angle = Math.random() * Math.PI * 2
        this.rising = false
      }

      update(time: number) {
        if (!this.rising && Math.random() < 0.01) {
          this.rising = true
        }

        if (this.rising) {
          const dx = this.targetX - this.x
          const dy = this.targetY - this.y
          const distance = Math.sqrt(dx * dx + dy * dy)

          if (distance > 1) {
            this.x += dx * this.speed * 0.01
            this.y += dy * this.speed * 0.01
          } else {
            this.x = this.targetX
            this.y = this.targetY
          }
        } else {
          this.y += Math.sin(this.angle + time * 2) * 0.2
        }
      }

      draw() {
        ctx!.fillStyle = this.color
        ctx!.beginPath()
        ctx!.arc(this.x, this.y, this.size, 0, Math.PI * 2)
        ctx!.fill()
      }
    }

    function createHeart() {
      const particles: Particle[] = []
      const centerX = canvas.width / 2
      const centerY = canvas.height / 2
      const size = Math.min(canvas.width, canvas.height) * 0.3

      for (let i = 0; i < 3000; i++) {
        const t = i / 3000 * Math.PI * 2
        const x = 16 * Math.pow(Math.sin(t), 3)
        const y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t))
        const targetX = centerX + x * size / 16
        const targetY = centerY + y * size / 16

        const startX = Math.random() * canvas.width
        const startY = canvas.height + Math.random() * 50 // Start below the canvas

        particles.push(new Particle(startX, startY, targetX, targetY))
      }

      return particles
    }

    let particles = createHeart()
    let time = 0

    function animate() {
      ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'
      ctx.fillRect(0, 0, canvas.width, canvas.height)

      time += 0.01
      particles.forEach(particle => {
        particle.update(time)
        particle.draw()
      })

      requestAnimationFrame(animate)
    }

    animate()

    const handleResize = () => {
      setCanvasSize()
      particles = createHeart()
    }

    window.addEventListener('resize', handleResize)

    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return (
    <canvas
      ref={canvasRef}
      className="w-full h-screen bg-black"
    />
  )
}
```

## コードの詳細解説

### 1. app/layout.tsx

```typescriptreact
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className="bg-black">{children}</body>
    </html>
  )
}
```

このファイルは、アプリケーション全体のレイアウトを定義します。`body`タグに`bg-black`クラスを追加することで、背景を黒に設定しています。

### 2. app/heart-particles/page.tsx

#### コンポーネントの基本構造

```typescriptreact
"use client"

import React, { useRef, useEffect } from 'react'

export default function Component() {
  const canvasRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    // ...
  }, [])

  return (
    <canvas
      ref={canvasRef}
      className="w-full h-screen bg-black"
    />
  )
}
```

- `"use client"`ディレクティブは、このコンポーネントがクライアントサイドでレンダリングされることを示します。
- `useRef`フックを使用して、canvasへの参照を作成します。
- `useEffect`フックは、コンポーネントのマウント時に一度だけ実行されます。
- canvasは画面全体を覆うように設定されています。


#### Particleクラス

```typescriptreact
class Particle {
  // ...

  constructor(x: number, y: number, targetX: number, targetY: number) {
    // ...
  }

  update(time: number) {
    // ...
  }

  draw() {
    // ...
  }
}
```

このクラスは各パーティクルの動作を定義します：

- `constructor`: パーティクルの初期位置、目標位置、色、速度などを設定します。
- `update`: パーティクルの位置を更新します。最初はゆっくり上昇し、その後ハートの形に向かって移動します。
- `draw`: パーティクルをキャンバス上に描画します。


#### ハートの形状生成

```typescriptreact
function createHeart() {
  // ...
  for (let i = 0; i < 3000; i++) {
    const t = i / 3000 * Math.PI * 2
    const x = 16 * Math.pow(Math.sin(t), 3)
    const y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t))
    // ...
  }
  // ...
}
```

この関数は、数学的な方程式を使用してハート型の座標を生成します。3000個のパーティクルを作成し、それぞれにハート型の目標位置を割り当てます。

#### アニメーション

```typescriptreact
function animate() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'
  ctx.fillRect(0, 0, canvas.width, canvas.height)

  time += 0.01
  particles.forEach(particle => {
    particle.update(time)
    particle.draw()
  })

  requestAnimationFrame(animate)
}
```

`animate`関数は、各フレームでパーティクルの位置を更新し、再描画します。`requestAnimationFrame`を使用することで、ブラウザの描画タイミングに合わせて効率的にアニメーションを実行します。

#### リサイズ対応

```typescriptreact
const handleResize = () => {
  setCanvasSize()
  particles = createHeart()
}

window.addEventListener('resize', handleResize)
```

ウィンドウのリサイズイベントをリッスンし、キャンバスのサイズとハートの形を適切に調整します。

## パフォーマンスの考慮点

- パーティクルの数（3000個）は、多くのデバイスで滑らかなアニメーションを実現できる数として選ばれていますが、必要に応じて調整可能です。
- `requestAnimationFrame`を使用することで、ブラウザのレンダリングサイクルに合わせて効率的にアニメーションを実行しています。
- 背景を完全に消去せず、半透明の黒で塗りつぶすことで、パーティクルの軌跡を表現しつつ、描画コストを抑えています。


## カスタマイズの可能性

このコードは様々な方法でカスタマイズできます：

- パーティクルの色や数を変更
- ハートの形状を他の形に変更（数式を調整することで）
- パーティクルの動きのパターンを変更（例：螺旋状の動きを追加）
- 背景色や効果の追加（グラデーションなど）


## 詳細な実行方法

1. Next.jsプロジェクトを作成します：

```shellscript
npx create-next-app@latest heart-particles-app
cd heart-particles-app
```


2. 必要なファイルを作成します：

```shellscript
mkdir -p app/heart-particles
touch app/heart-particles/page.tsx
```


3. `app/layout.tsx`と`app/heart-particles/page.tsx`の内容を、上記で提供したコードで置き換えます。
4. 開発サーバーを起動します：

```shellscript
npm run dev
```


5. ブラウザで`http://localhost:3000/heart-particles`にアクセスします。
6. ハートの形に集まるパーティクルアニメーションが表示されるはずです。


## 注意点

- このコンポーネントは、モダンブラウザでのみ正常に動作します。
- 大量のパーティクルを描画するため、低スペックのデバイスでは動作が遅くなる可能性があります。

## まとめ

このチュートリアルでは、Next.jsとReactを使用して、魅力的なハートパーティクルアニメーションを作成する方法を学びました。HTML5 Canvasを活用し、数学的な方程式を用いてハート型を生成し、パーティクルの動きを制御することで、印象的なビジュアル効果を実現しました。

このような動的なグラフィックスは、ウェブサイトのランディングページや特別なイベントページなどで効果的に使用できます。ぜひ、このコードを基に、独自のアイデアを加えて、さらに魅力的なアニメーションを作成してみてください。