🎹

デザイナーとの共通言語!音楽系Webサイト向けアニメーション実装と技術スタックの検証

に公開

概要と制作の背景

このたび、Webページ制作の依頼を受ける際、デザイナーに対しどのようなアニメーション表現が可能かを示すための技術デモ用モックアップを制作しました。

ターゲットを音楽系制作会社に設定したため、視覚的な訴求力と先進性を追求すべく、モダンなWebアニメーション技術を深く掘り下げました。本格的なアニメーション制作は初体験であり、本プロジェクトは技術的な習得と表現の可能性の探求を目的としています。

完成したモックアップページはこちらです。
https://wave-hp-pearl.vercel.app/


採用技術スタックと学習ポイント

基盤にNext.js、アニメーションにFramer Motion、スタイリングにTailwind CSSを採用し、コンポーネント指向で開発効率の高いモダンな環境を構築しました。

特にFramer Motionの学習に注力し、複雑なアニメーションロジックを再利用可能なReactコンポーネントとして抽象化する手法を体得しました。


ページを彩るアニメーションコンポーネントの解剖

モックアップでは、すべてのアニメーションをカスタムコンポーネントにカプセル化し、メインのページファイル(page.tsx)から呼び出すことで、コードの可読性と再利用性を高めています。ここでは、その主要な3つのコンポーネントを詳細に解説します。

1. FadeIn.tsx: 汎用的なスクロールイン出現アニメーション

FadeInは、要素が画面内に入った(In-View)時に、フェードインしながら指定方向へスライドする、最も汎用性の高いコンポーネントです。

📄 コード解説と学習ポイント

// components/animations/FadeIn.tsx
export default function FadeIn({
  children,
  delay = 0,
  direction = 'up', // デフォルトは下から上へのスライド
  duration = 0.5,
}: FadeInProps) {
  // directionに応じて初期位置(オフセット)を決定
  const directionOffset = { up: { y: 40 }, down: { y: -40 }, /* ... */ }; 

  return (
    <motion.div
      initial={{ opacity: 0, ...directionOffset[direction] }} // 1. 初期状態: 透明かつオフセット
      whileInView={{ opacity: 1, x: 0, y: 0 }}               // 2. 最終状態: 完全に表示され、元の位置へ
      viewport={{ once: true, margin: '-100px' }}            // 3. ビューポート設定
      transition={{ duration, delay, ease: [0.21, 0.47, 0.32, 0.98] }} // 4. トランジション設定
      // ...
    >
      {children}
    </motion.div>
  );
}
ポイント Framer Motionの機能 意図/効果
初期/最終状態 initial / whileInView 宣言的に開始と終了を定義。要素が画面内に入るのをトリガー(whileInView)に動作。
遅延の活用 delay prop メインページでの利用時(例:制作実績)にdelay={i * 0.1}のように連続適用することで、要素を時間差で出現させる(スタッガー効果)
カスタムの動き ease: [...] デフォルトのイージングではなく、カスタムのベジェ曲線を適用することで、より滑らかでプロフェッショナルな「らしさ」を追求。

2. SlideIn.tsx: スクロール連動のパララックス効果

このコンポーネントは、要素の出現ではなく、ユーザーのスクロール量に完全に連動して要素が横方向に動き続けるパララックス効果を提供します。

📄 コード解説と学習ポイント

// components/animations/SlideIn.tsx
export default function SlideIn({
  children,
  direction = 'left', // 左にスライド(スクロールに応じて)
}: SlideInProps) {
  const ref = useRef<HTMLDivElement>(null);
  // 1. ターゲットが画面に入り始めてから、画面から出始めるまでのスクロール進捗を測定
  const { scrollYProgress } = useScroll({ target: ref, offset: ['start end', 'end start'] });

  // 2. スクロール進捗(0〜1)をX座標(-100px〜100px)に変換
  const x = useTransform(
    scrollYProgress,
    [0, 1], 
    direction === 'left' ? [-100, 100] : [100, -100]
  );

  return (
    <motion.div
      ref={ref}
      style={{ x }} // 3. 計算されたX座標を直接スタイルに適用
      className={cn('relative will-change-transform', className)}
    >
      {children}
    </motion.div>
  );
}
ポイント Framer Motionの機能 意図/効果
スクロール量取得 useScroll 要素基準でスクロールの進捗(scrollYProgress)を0から1の範囲で取得。
数値変換 useTransform スクロール進捗をピクセル値に変換。これにより、スムーズで連続的なパララックス移動を実現し、Webページに奥行きを生み出す。

3. Hero.tsx: 複合的なスクロール連動演出

ヒーローセクションでは、Framer Motionのフックを組み合わせることで、スクロール時にメインコンテンツが「遠ざかるように消える」複雑な没入感のある演出を実現しています。

📄 コード解説(抜粋)と学習ポイント

// components/ui/Hero.tsx
export default function Hero() {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start start', 'end start'], // 1. ヒーローセクション全体が画面を通過する範囲を測定
  });

  // 2. 一つのスクロール進捗から、3つの異なるCSSプロパティを生成
  const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);   // 50%スクロールで透明度0
  const scale = useTransform(scrollYProgress, [0, 0.5], [1, 0.8]);   // 50%スクロールで縮小
  const y = useTransform(scrollYProgress, [0, 1], [0, 200]);        // 100%スクロールで下に200px移動

  return (
    <section ref={ref} className="relative h-screen overflow-hidden bg-black">
      {/* ... 背景 ... */}
      <motion.div
        style={{ opacity, scale, y }} // 3. 複数の計算値をstyleに適用して同期
        // ...
      >
        {/* ... メインコンテンツ(WA/VEタイトルなど) ... */}
      </motion.div>
      
      {/* スクロールインジケーターのバウンスアニメーション */}
      <motion.div 
        animate={{ y: [0, 10, 0] }} 
        transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
        // ...
      >
        {/* ... */}
      </motion.div>
    </section>
  );
}
ポイント Framer Motionの機能 意図/効果
アニメーションの同期 複数のuseTransform 一つのscrollYProgressをインプットに、不透明度、スケール、位置を連動させることで、コンテンツが遠くへ消えていくようなリッチな没入感を演出。
キーフレーム animate={{ y: [0, 10, 0] }} スクロールインジケーターに適用し、シンプルなバウンス動作を無限に繰り返すことで、ユーザーにスクロールを促す。

まとめ

普段はバックエンドの実装が多いので、全然違う技術だなあと改めて感じました。

Discussion