T-CREATOR

Tailwind CSS × Three.js でインタラクティブな 3D 表現を実装

Tailwind CSS × Three.js でインタラクティブな 3D 表現を実装

現代の Web サイトでは、ユーザーの注意を引く魅力的な視覚体験が求められています。Three.js の 3D 表現力と Tailwind CSS のスタイリング効率性を組み合わせることで、従来のフラットな Web サイトを超えた、インタラクティブで美しい 3D 体験を実現できるでしょう。

3D 表現は単なる見た目の華やかさだけでなく、ユーザーがサイトに長く滞在し、深く関与したくなるような体験を提供します。マウスの動きに反応する 3D オブジェクトや、スクロールに連動するアニメーションは、ユーザーの好奇心を刺激し、サイトとの関係性を深めてくれますね。

背景

現代の Web において、リッチな UI/UX への需要は日々高まっています。Apple や Google などの大手企業をはじめ、多くの Web サイトが平面的なデザインから立体的で動的な表現へと移行しているのが現状です。

ユーザーはもはや静的なコンテンツだけでは満足せず、操作に対する即座のフィードバックや、直感的で楽しいインタラクションを期待しています。特にポートフォリオサイトやプロダクト紹介サイトでは、競合他社との差別化が重要な要素となっているでしょう。

以下の図は、現代 Web における技術トレンドの変遷を示しています。

mermaidflowchart LR
  static[静的HTML/CSS] -->|進化| responsive[レスポンシブデザイン]
  responsive -->|発展| interactive[インタラクティブUI]
  interactive -->|最新| three_d[3D表現+インタラクション]

  subgraph tools[現在主流の技術]
    tailwind[Tailwind CSS]
    threejs[Three.js]
    webgl[WebGL]
  end

  three_d --> tools

Three.js は WebGL 技術をベースとした 3D ライブラリとして、ブラウザ上で高品質な 3D 表現を実現する強力なツールです。一方、Tailwind CSS はユーティリティファーストのアプローチにより、迅速で一貫性のあるスタイリングを可能にします。

課題

しかし、3D ライブラリとユーティリティファースト CSS フレームワークの統合には、いくつかの技術的課題が存在します。最も大きな問題は、異なるレンダリング方式の統合の難しさです。

Three.js は Canvas 要素や WebGL コンテキストを使用して 3D オブジェクトをレンダリングしますが、Tailwind CSS は通常の HTML 要素に対するスタイリングを前提としています。この両者を効果的に連携させるには、適切なアーキテクチャと実装戦略が必要になるでしょう。

また、パフォーマンスとデザイン性の両立も重要な課題です。3D レンダリングは計算リソースを多く消費するため、スムーズなアニメーションと高品質な見た目を両立させながら、モバイルデバイスでも快適に動作させる必要があります。

以下の図は、統合時に発生する主な課題を整理したものです。

mermaidflowchart TD
  challenges[統合時の課題] --> rendering[レンダリング方式の違い]
  challenges --> performance[パフォーマンス最適化]
  challenges --> responsive[レスポンシブ対応]
  challenges --> maintenance[メンテナンス性]

  rendering --> canvas[Canvas vs DOM]
  rendering --> event[イベント処理の分離]

  performance --> mobile[モバイル対応]
  performance --> animation[アニメーション最適化]

  responsive --> breakpoints[ブレークポイント対応]
  responsive --> touch[タッチ操作対応]

これらの課題を解決するためには、段階的なアプローチと適切な設計パターンが不可欠です。

解決策

Three.js と Tailwind CSS の効果的な連携を実現するため、以下の戦略的アプローチを提案します。

まず、レイヤー分離の概念を導入することが重要です。3D レンダリング部分と UI 要素を明確に分離し、それぞれに最適化された技術を適用します。Three.js は 3D シーンのレンダリングに専念し、Tailwind CSS はコントロールパネルやオーバーレイ要素のスタイリングを担当するという役割分担ですね。

次に、イベント統合システムの構築により、3D 空間での操作と HTML 要素での操作をシームレスに連携させます。マウスやタッチイベントを統一的に管理し、3D オブジェクトの状態変化を Tailwind CSS クラスの動的切り替えに反映させる仕組みを作ります。

レスポンシブな 3D 表現の実装では、Tailwind CSS のブレークポイントシステムを活用して、デバイスサイズに応じた 3D シーンの最適化を行います。モバイルデバイスでは 3D の複雑さを減らし、デスクトップでは豊富なインタラクションを提供するアダプティブアプローチを採用するでしょう。

以下の図は、提案する解決アーキテクチャを示しています。

mermaidflowchart TB
  subgraph solution[解決アーキテクチャ]
    ui_layer[UI レイヤー<br/>Tailwind CSS]
    three_layer[3D レイヤー<br/>Three.js]
    event_system[統合イベントシステム]
  end

  user_input[ユーザー入力] --> event_system
  event_system --> ui_layer
  event_system --> three_layer

  ui_layer -.->|状態同期| three_layer
  three_layer -.->|レンダリング結果| ui_layer

このアーキテクチャにより、各技術の長所を最大限に活用しながら、統合時の課題を効果的に解決できます。

具体例

基本セットアップ

まずは、Next.js プロジェクトに Three.js と Tailwind CSS を導入するところから始めましょう。現代的な Web アプリケーション開発では、TypeScript の型安全性も重要な要素になります。

プロジェクトの初期化から行っていきます。

bash# プロジェクトの作成
yarn create next-app threejs-tailwind-demo --typescript
cd threejs-tailwind-demo

必要なライブラリをインストールします。Three.js とその型定義、そして Tailwind CSS を追加しましょう。

bash# Three.js関連のライブラリ
yarn add three @types/three

# Tailwind CSS関連
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p

Tailwind CSS の設定ファイルを更新して、3D 表現に必要なユーティリティクラスを追加できるようにします。

javascript// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      animation: {
        float: 'float 6s ease-in-out infinite',
        'rotate-slow': 'rotate 10s linear infinite',
      },
      keyframes: {
        float: {
          '0%, 100%': { transform: 'translateY(0px)' },
          '50%': { transform: 'translateY(-20px)' },
        },
      },
    },
  },
  plugins: [],
};

基本的な 3D シーンコンポーネントを作成します。このコンポーネントでは、Three.js による 3D レンダリングと react の状態管理を連携させます。

typescript// components/ThreeScene.tsx
import { useEffect, useRef } from 'react';
import * as THREE from 'three';

interface ThreeSceneProps {
  className?: string;
}

export const ThreeScene: React.FC<ThreeSceneProps> = ({
  className,
}) => {
  const mountRef = useRef<HTMLDivElement>(null);
  const sceneRef = useRef<THREE.Scene>();
  const rendererRef = useRef<THREE.WebGLRenderer>();
  const cameraRef = useRef<THREE.PerspectiveCamera>();

  useEffect(() => {
    if (!mountRef.current) return;

    // シーンの初期化
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true, // 背景を透明に
    });

    // レンダラーの設定
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor(0x000000, 0); // 透明背景
    mountRef.current.appendChild(renderer.domElement);

    // 参照を保存
    sceneRef.current = scene;
    rendererRef.current = renderer;
    cameraRef.current = camera;

    return () => {
      if (mountRef.current && renderer.domElement) {
        mountRef.current.removeChild(renderer.domElement);
      }
      renderer.dispose();
    };
  }, []);

  return (
    <div
      ref={mountRef}
      className={`w-full h-full ${className}`}
    />
  );
};

次に、基本的な 3D オブジェクト(立方体)を追加して、シーンを完成させます。

typescript// components/ThreeScene.tsx(続き)
export const ThreeScene: React.FC<ThreeSceneProps> = ({
  className,
}) => {
  // 前の部分は同じ...

  useEffect(() => {
    if (
      !sceneRef.current ||
      !rendererRef.current ||
      !cameraRef.current
    )
      return;

    const scene = sceneRef.current;
    const renderer = rendererRef.current;
    const camera = cameraRef.current;

    // 立方体の作成
    const geometry = new THREE.BoxGeometry(2, 2, 2);
    const material = new THREE.MeshBasicMaterial({
      color: 0x06b6d4, // Tailwind CSSのcyan-500
      wireframe: false,
    });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    // ライトの追加
    const ambientLight = new THREE.AmbientLight(
      0x404040,
      0.6
    );
    const directionalLight = new THREE.DirectionalLight(
      0xffffff,
      0.8
    );
    directionalLight.position.set(1, 1, 1);
    scene.add(ambientLight);
    scene.add(directionalLight);

    // カメラの位置設定
    camera.position.z = 5;

    // アニメーションループ
    const animate = () => {
      requestAnimationFrame(animate);

      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;

      renderer.render(scene, camera);
    };
    animate();
  }, []);

  // 残りは同じ...
};

インタラクティブ要素の実装

次に、マウス操作によるインタラクションと Tailwind CSS での UI コントロールを実装していきます。ユーザーがマウスを動かすと 3D オブジェクトが反応し、同時に UI も連動して変化する仕組みを作りましょう。

まず、マウス位置を追跡するカスタム hook を作成します。

typescript// hooks/useMouse.ts
import { useState, useEffect } from 'react';

interface MousePosition {
  x: number;
  y: number;
  normalizedX: number; // -1 to 1
  normalizedY: number; // -1 to 1
}

export const useMouse = () => {
  const [position, setPosition] = useState<MousePosition>({
    x: 0,
    y: 0,
    normalizedX: 0,
    normalizedY: 0,
  });

  useEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      const x = event.clientX;
      const y = event.clientY;

      // -1 から 1 の範囲に正規化
      const normalizedX = (x / window.innerWidth) * 2 - 1;
      const normalizedY = -(y / window.innerHeight) * 2 + 1;

      setPosition({ x, y, normalizedX, normalizedY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () =>
      window.removeEventListener(
        'mousemove',
        handleMouseMove
      );
  }, []);

  return position;
};

インタラクティブな 3D コンポーネントを拡張して、マウスの動きに反応するようにします。

typescript// components/InteractiveThreeScene.tsx
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { useMouse } from '../hooks/useMouse';

interface InteractiveThreeSceneProps {
  className?: string;
  onObjectHover?: (isHovered: boolean) => void;
}

export const InteractiveThreeScene: React.FC<
  InteractiveThreeSceneProps
> = ({ className, onObjectHover }) => {
  const mountRef = useRef<HTMLDivElement>(null);
  const cubeRef = useRef<THREE.Mesh>();
  const mousePosition = useMouse();

  useEffect(() => {
    if (!mountRef.current) return;

    // シーンの基本設定(前回と同様)
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
    });

    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor(0x000000, 0);
    mountRef.current.appendChild(renderer.domElement);

    // 立方体の作成
    const geometry = new THREE.BoxGeometry(2, 2, 2);
    const material = new THREE.MeshLambertMaterial({
      color: 0x06b6d4,
    });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);
    cubeRef.current = cube;

    // ライトの設定
    const ambientLight = new THREE.AmbientLight(
      0x404040,
      0.6
    );
    const directionalLight = new THREE.DirectionalLight(
      0xffffff,
      0.8
    );
    directionalLight.position.set(1, 1, 1);
    scene.add(ambientLight, directionalLight);

    camera.position.z = 5;

    // レイキャスターでホバー検出
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    const checkIntersection = (
      clientX: number,
      clientY: number
    ) => {
      mouse.x = (clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(clientY / window.innerHeight) * 2 + 1;

      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObject(cube);

      const isHovered = intersects.length > 0;
      onObjectHover?.(isHovered);

      // ホバー時の色変更
      if (isHovered) {
        material.color.setHex(0xf59e0b); // Tailwind amber-500
      } else {
        material.color.setHex(0x06b6d4); // Tailwind cyan-500
      }
    };

    const handleMouseMove = (event: MouseEvent) => {
      checkIntersection(event.clientX, event.clientY);
    };

    renderer.domElement.addEventListener(
      'mousemove',
      handleMouseMove
    );

    // アニメーションループ
    const animate = () => {
      requestAnimationFrame(animate);

      // マウス位置に基づく回転
      if (cubeRef.current) {
        cubeRef.current.rotation.y =
          mousePosition.normalizedX * 0.5;
        cubeRef.current.rotation.x =
          mousePosition.normalizedY * 0.5;
      }

      renderer.render(scene, camera);
    };
    animate();

    return () => {
      renderer.domElement.removeEventListener(
        'mousemove',
        handleMouseMove
      );
      if (mountRef.current && renderer.domElement) {
        mountRef.current.removeChild(renderer.domElement);
      }
      renderer.dispose();
    };
  }, [mousePosition, onObjectHover]);

  return (
    <div
      ref={mountRef}
      className={`w-full h-full ${className}`}
    />
  );
};

UI コントロールパネルを Tailwind CSS で作成し、3D シーンと連携させます。

typescript// components/ControlPanel.tsx
import { useState } from 'react';

interface ControlPanelProps {
  isObjectHovered: boolean;
  onColorChange: (color: string) => void;
  onAnimationToggle: (enabled: boolean) => void;
}

export const ControlPanel: React.FC<ControlPanelProps> = ({
  isObjectHovered,
  onColorChange,
  onAnimationToggle,
}) => {
  const [isAnimating, setIsAnimating] = useState(true);

  const colors = [
    {
      name: 'Cyan',
      value: '#06b6d4',
      class: 'bg-cyan-500',
    },
    {
      name: 'Purple',
      value: '#8b5cf6',
      class: 'bg-violet-500',
    },
    {
      name: 'Green',
      value: '#10b981',
      class: 'bg-emerald-500',
    },
    {
      name: 'Orange',
      value: '#f59e0b',
      class: 'bg-amber-500',
    },
  ];

  const handleAnimationToggle = () => {
    const newState = !isAnimating;
    setIsAnimating(newState);
    onAnimationToggle(newState);
  };

  return (
    <div
      className={`
      fixed top-4 left-4 p-6 bg-white/90 backdrop-blur-md rounded-xl shadow-lg
      transition-all duration-300 transform
      ${
        isObjectHovered
          ? 'scale-105 shadow-xl'
          : 'scale-100'
      }
    `}
    >
      <h3 className='text-lg font-semibold text-gray-800 mb-4'>
        3D コントロール
      </h3>

      {/* アニメーション切り替え */}
      <div className='mb-4'>
        <button
          onClick={handleAnimationToggle}
          className={`
            px-4 py-2 rounded-lg font-medium transition-all duration-200
            ${
              isAnimating
                ? 'bg-green-500 text-white shadow-md hover:bg-green-600'
                : 'bg-gray-300 text-gray-700 hover:bg-gray-400'
            }
          `}
        >
          {isAnimating ? '停止' : '開始'}
        </button>
      </div>

      {/* カラーピッカー */}
      <div className='space-y-2'>
        <p className='text-sm font-medium text-gray-600'>
          カラー選択:
        </p>
        <div className='grid grid-cols-2 gap-2'>
          {colors.map((color) => (
            <button
              key={color.name}
              onClick={() => onColorChange(color.value)}
              className={`
                ${color.class} w-full h-10 rounded-lg border-2 border-white
                hover:scale-105 transition-transform duration-200
                shadow-md hover:shadow-lg
              `}
              title={color.name}
            />
          ))}
        </div>
      </div>

      {/* ホバー状態の表示 */}
      <div className='mt-4 text-sm'>
        <span
          className={`
          inline-block px-2 py-1 rounded text-white font-medium
          ${
            isObjectHovered ? 'bg-green-500' : 'bg-gray-400'
          }
        `}
        >
          {isObjectHovered ? 'ホバー中' : 'ホバーなし'}
        </span>
      </div>
    </div>
  );
};

アニメーションとエフェクト

最後に、Three.js アニメーションと Tailwind CSS トランジションを連携させて、滑らかで美しいエフェクトを実装しましょう。レスポンシブ対応も同時に行います。

高度なアニメーション制御を行うコンポーネントを作成します。

typescript// components/AdvancedThreeScene.tsx
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { useMouse } from '../hooks/useMouse';

interface AdvancedThreeSceneProps {
  className?: string;
  animationEnabled: boolean;
  objectColor: string;
}

export const AdvancedThreeScene: React.FC<
  AdvancedThreeSceneProps
> = ({ className, animationEnabled, objectColor }) => {
  const mountRef = useRef<HTMLDivElement>(null);
  const sceneRef = useRef<THREE.Scene>();
  const cubeRef = useRef<THREE.Mesh>();
  const animationIdRef = useRef<number>();
  const mousePosition = useMouse();
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    if (!mountRef.current) return;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
      powerPreference: 'high-performance', // パフォーマンス最適化
    });

    // レスポンシブ設定
    const updateSize = () => {
      const width =
        mountRef.current?.clientWidth || window.innerWidth;
      const height =
        mountRef.current?.clientHeight ||
        window.innerHeight;

      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height);
    };

    updateSize();
    renderer.setClearColor(0x000000, 0);
    mountRef.current.appendChild(renderer.domElement);

    // 複数の3Dオブジェクトを作成
    const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
    const material = new THREE.MeshPhongMaterial({
      color: objectColor,
      shininess: 100,
      specular: 0x222222,
    });

    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);
    cubeRef.current = cube;

    // パーティクルシステムの追加
    const particleGeometry = new THREE.BufferGeometry();
    const particleCount = 1000;
    const positions = new Float32Array(particleCount * 3);

    for (let i = 0; i < particleCount * 3; i++) {
      positions[i] = (Math.random() - 0.5) * 20;
    }

    particleGeometry.setAttribute(
      'position',
      new THREE.BufferAttribute(positions, 3)
    );
    const particleMaterial = new THREE.PointsMaterial({
      color: 0x888888,
      size: 0.05,
    });
    const particles = new THREE.Points(
      particleGeometry,
      particleMaterial
    );
    scene.add(particles);

    // ライティング設定
    const ambientLight = new THREE.AmbientLight(
      0x404040,
      0.4
    );
    const directionalLight = new THREE.DirectionalLight(
      0xffffff,
      0.6
    );
    const pointLight = new THREE.PointLight(
      0x06b6d4,
      0.8,
      100
    );

    directionalLight.position.set(5, 5, 5);
    pointLight.position.set(0, 0, 5);

    scene.add(ambientLight, directionalLight, pointLight);

    camera.position.z = 5;
    sceneRef.current = scene;

    // リサイズイベントリスナー
    window.addEventListener('resize', updateSize);

    // ローディング完了
    setIsLoaded(true);

    return () => {
      window.removeEventListener('resize', updateSize);
      if (animationIdRef.current) {
        cancelAnimationFrame(animationIdRef.current);
      }
      if (mountRef.current && renderer.domElement) {
        mountRef.current.removeChild(renderer.domElement);
      }
      renderer.dispose();
    };
  }, []);

  // オブジェクトの色変更
  useEffect(() => {
    if (cubeRef.current) {
      const material = cubeRef.current
        .material as THREE.MeshPhongMaterial;
      material.color.set(objectColor);
    }
  }, [objectColor]);

  // アニメーションループ
  useEffect(() => {
    if (!sceneRef.current || !cubeRef.current) return;

    const scene = sceneRef.current;
    const cube = cubeRef.current;
    const camera = scene.children.find(
      (child) => child instanceof THREE.PerspectiveCamera
    ) as THREE.PerspectiveCamera;
    const renderer = mountRef.current
      ?.querySelector('canvas')
      ?.getContext('webgl2')
      ? new THREE.WebGLRenderer({
          canvas: mountRef.current.querySelector('canvas')!,
        })
      : null;

    if (!camera || !renderer) return;

    let animationSpeed = 0.01;

    const animate = (time: number) => {
      if (animationEnabled) {
        // 基本回転
        cube.rotation.x += animationSpeed;
        cube.rotation.y += animationSpeed * 1.2;

        // マウス追従
        const targetRotationY =
          mousePosition.normalizedX * 0.3;
        const targetRotationX =
          mousePosition.normalizedY * 0.3;

        cube.rotation.y +=
          (targetRotationY - cube.rotation.y) * 0.05;
        cube.rotation.x +=
          (targetRotationX - cube.rotation.x) * 0.05;

        // 浮遊効果
        cube.position.y = Math.sin(time * 0.001) * 0.3;

        // パーティクルアニメーション
        const particles = scene.children.find(
          (child) => child instanceof THREE.Points
        ) as THREE.Points;
        if (particles) {
          particles.rotation.y += 0.002;
        }
      }

      renderer.render(scene, camera);
      animationIdRef.current =
        requestAnimationFrame(animate);
    };

    animationIdRef.current = requestAnimationFrame(animate);

    return () => {
      if (animationIdRef.current) {
        cancelAnimationFrame(animationIdRef.current);
      }
    };
  }, [animationEnabled, mousePosition]);

  return (
    <div
      ref={mountRef}
      className={`
        w-full h-full relative overflow-hidden
        transition-opacity duration-1000
        ${isLoaded ? 'opacity-100' : 'opacity-0'}
        ${className}
      `}
    >
      {/* ローディング表示 */}
      {!isLoaded && (
        <div className='absolute inset-0 flex items-center justify-center bg-gray-900/50'>
          <div className='animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-500'></div>
        </div>
      )}

      {/* レスポンシブ用のオーバーレイ情報 */}
      <div className='absolute bottom-4 right-4 bg-black/50 text-white px-3 py-2 rounded-lg text-sm'>
        <div className='block sm:hidden'>モバイル表示</div>
        <div className='hidden sm:block md:hidden'>
          タブレット表示
        </div>
        <div className='hidden md:block'>
          デスクトップ表示
        </div>
      </div>
    </div>
  );
};

メインページコンポーネントで全てを統合します。

typescript// pages/index.tsx または app/page.tsx
import { useState } from 'react';
import { AdvancedThreeScene } from '../components/AdvancedThreeScene';
import { ControlPanel } from '../components/ControlPanel';

export default function Home() {
  const [isObjectHovered, setIsObjectHovered] =
    useState(false);
  const [animationEnabled, setAnimationEnabled] =
    useState(true);
  const [objectColor, setObjectColor] = useState('#06b6d4');

  return (
    <div className='min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-purple-900'>
      {/* ヘッダー */}
      <header
        className={`
        fixed top-0 left-0 right-0 z-10 p-4
        bg-white/10 backdrop-blur-md border-b border-white/20
        transition-all duration-300
        ${isObjectHovered ? 'bg-white/20' : 'bg-white/10'}
      `}
      >
        <h1 className='text-2xl font-bold text-white text-center'>
          Three.js × Tailwind CSS デモ
        </h1>
      </header>

      {/* メインコンテンツ */}
      <main className='pt-20 h-screen'>
        <AdvancedThreeScene
          className='w-full h-full'
          animationEnabled={animationEnabled}
          objectColor={objectColor}
        />

        <ControlPanel
          isObjectHovered={isObjectHovered}
          onColorChange={setObjectColor}
          onAnimationToggle={setAnimationEnabled}
        />
      </main>

      {/* フッター */}
      <div
        className={`
        fixed bottom-4 left-1/2 transform -translate-x-1/2
        px-6 py-3 bg-white/90 backdrop-blur-md rounded-full
        transition-all duration-300
        ${
          isObjectHovered
            ? 'scale-110 shadow-xl'
            : 'scale-100 shadow-lg'
        }
      `}
      >
        <p className='text-sm text-gray-700 font-medium'>
          マウスを動かして3Dオブジェクトを操作してください
        </p>
      </div>
    </div>
  );
}

これらの実装により、Three.js の 3D 表現力と Tailwind CSS のスタイリング効率性を効果的に組み合わせた、インタラクティブな Web アプリケーションが完成します。

まとめ

Three.js と Tailwind CSS の組み合わせは、現代の Web サイトに求められる高品質な 3D 体験と効率的な開発ワークフローを両立させる優れたソリューションです。

この記事で紹介した段階的アプローチにより、技術的な課題を一つずつ解決しながら、魅力的なインタラクティブ 3D 表現を実装できるでしょう。レイヤー分離の概念、統合イベントシステム、そしてレスポンシブ対応により、メンテナブルで拡張性の高いアーキテクチャを構築できます。

重要なポイントは、3D レンダリングと UI 要素を適切に分離し、それぞれの技術の強みを活かすことです。Three.js は 3D 空間での表現に集中し、Tailwind CSS はユーザーインターフェースの構築と状態表現を担当するという明確な役割分担が成功の鍵となります。

パフォーマンスとアクセシビリティにも配慮しながら、ユーザーが直感的に操作できる 3D 体験を提供することで、競合他社との差別化を図り、印象に残る Web サイトを作成できるでしょう。

今後の Web 開発において、3D 表現はますます重要な要素となっていくため、これらの技術スキルを習得することで、より魅力的で革新的な Web アプリケーションの開発が可能になります。

関連リンク