React Three Fiberで作る画像カルーセル

目次

デモ

はじめに

この記事では、React Three FiberとThree.jsを使用して、インタラクティブな画像カルーセルを作成する方法を紹介します。

React Three Fiberは、ReactコンポーネントとしてThree.jsを使用できるライブラリで、3Dグラフィックスを簡単に扱うことができます。

簡単に導入できる点そして素のThree.jsと比べて、より直感的にコードを書くことができる点が魅力です!

必要なライブラリのインストール

まず、必要なライブラリをインストールします。以下のコマンドを実行してください。

npm install @react-three/fiber @react-three/drei three @use-gesture/react

カードコンポーネントの作成

次に、画像カルーセルの各カードを表すコンポーネントを作成します。


// カードコンポーネント
function Card({ index, rotationY, ...props }: CardProps) {
  const mesh = useRef<THREE.Mesh>(null!); // メッシュの参照を作成
  const texture = useTexture(`/images/sample${index + 1}.jpg`); // テクスチャを読み込む

  // フレームごとに呼ばれる関数
  useFrame(() => {
    // Y軸の回転を設定
    mesh.current.rotation.y = rotationY + (index * (Math.PI * 2)) / CARD_COUNT;
    // X座標を計算
    mesh.current.position.x = Math.sin(mesh.current.rotation.y) * RADIUS;
    // Z座標を計算
    mesh.current.position.z = Math.cos(mesh.current.rotation.y) * RADIUS;
  });

  return (
    <mesh {...props} ref={mesh}>
      {/* 平面ジオメトリを作成 */}
      <planeGeometry args={[2, 3]} />
      {/* テクスチャを適用 */}
      <meshBasicMaterial map={texture} side={THREE.DoubleSide} />
    </mesh>
  );
}

メインコンポーネントの作成

次に、カルーセル全体を管理するメインコンポーネントを作成します。

export default function ImageCarousel() {
  const [rotationY, setRotationY] = useState(0); // Y軸の回転角度の状態
  const [isDragging, setIsDragging] = useState(false); // ドラッグ中かどうかの状態

  // エフェクトフック
  useEffect(() => {
    let animationId: number; // アニメーションID
    const autoRotate = () => {
      // ドラッグ中でない場合、自動回転を実行
      if (!isDragging) {
        setRotationY((prevRotation) => prevRotation + AUTO_ROTATE_SPEED);
      }
      animationId = requestAnimationFrame(autoRotate); // 次のフレームをリクエスト
    };
    autoRotate(); // 自動回転を開始
    return () => cancelAnimationFrame(animationId); // クリーンアップ
  }, [isDragging]);

  // ドラッグのバインディング
  const bind = useDrag(
    ({ movement: [mx], active }) => {
      setIsDragging(active); // ドラッグ状態を更新
      if (active) {
        setRotationY((rotationY) => rotationY - mx * 0.01); // ドラッグに応じて回転を更新
      }
    },
    { filterTaps: true }, // タップをフィルタリング
  ) as unknown as () => {
    onPointerDown: (e: ThreeEvent<PointerEvent>) => void; // ポインターのダウンイベント
  };

  return (
    <div className="h-screen w-full">
      <Canvas>
        <PerspectiveCamera makeDefault position={[0, 0, 15]} fov={70} />{" "}
        {/* カメラを設定 */}
        <OrbitControls enableZoom={false} enablePan={false} />{" "}
        {/* オービットコントロールを設定 */}
        <ambientLight intensity={0.5} /> {/* 環境光を設定 */}
        {/* <pointLight position={[10, 10, 10]} /> */}
        <group {...bind()} onPointerDown={(e) => e.stopPropagation()}>
          {/* ドラッグのバインディングを適用 */}
          {Array.from({ length: CARD_COUNT }).map((_, index) => (
            <Card key={index} index={index} rotationY={rotationY} /> // カードを描画
          ))}
        </group>
      </Canvas>
    </div>
  );
}

コード

TypeScript
import React, { useRef, useState, useEffect } from "react";
import { Canvas, useFrame, type ThreeEvent } from "@react-three/fiber";
import { useDrag } from "@use-gesture/react";
import * as THREE from "three";
import {
OrbitControls,
useTexture,
PerspectiveCamera,
} from "@react-three/drei";

// カードの数を定義
const CARD_COUNT = 5;
// カードの半径を定義
const RADIUS = 5;
// 自動回転の速度を定義
const AUTO_ROTATE_SPEED = 0.005;

// カードのプロパティを定義する型
type CardProps = Omit<THREE.Mesh, keyof JSX.IntrinsicElements["mesh"]> & {
index: number; // カードのインデックス
rotationY: number; // Y軸の回転角度
};

// カードコンポーネント
function Card({ index, rotationY, ...props }: CardProps) {
const mesh = useRef<THREE.Mesh>(null!); // メッシュの参照を作成
const texture = useTexture(`/images/sample${index + 1}.jpg`); // テクスチャを読み込む

// フレームごとに呼ばれる関数
useFrame(() => {
// Y軸の回転を設定
mesh.current.rotation.y = rotationY + (index _ (Math.PI _ 2)) / CARD*COUNT;
// X座標を計算
mesh.current.position.x = Math.sin(mesh.current.rotation.y) * RADIUS;
// Z座標を計算
mesh.current.position.z = Math.cos(mesh.current.rotation.y) \_ RADIUS;
});

return (

<mesh {...props} ref={mesh}>
{/* 平面ジオメトリを作成 */}
<planeGeometry args={[2, 3]} />
{/* テクスチャを適用 */}
<meshBasicMaterial map={texture} side={THREE.DoubleSide} />
</mesh>
); }

// メインコンポーネント
export default function ImageCarousel() {
const [rotationY, setRotationY] = useState(0); // Y軸の回転角度の状態
const [isDragging, setIsDragging] = useState(false); // ドラッグ中かどうかの状態

// エフェクトフック
useEffect(() => {
let animationId: number; // アニメーションID
const autoRotate = () => {
// ドラッグ中でない場合、自動回転を実行
if (!isDragging) {
setRotationY((prevRotation) => prevRotation + AUTO_ROTATE_SPEED);
}
animationId = requestAnimationFrame(autoRotate); // 次のフレームをリクエスト
};
autoRotate(); // 自動回転を開始
return () => cancelAnimationFrame(animationId); // クリーンアップ
}, [isDragging]);

// ドラッグのバインディング
const bind = useDrag(
({ movement: [mx], active }) => {
setIsDragging(active); // ドラッグ状態を更新
if (active) {
setRotationY((rotationY) => rotationY - mx \* 0.01); // ドラッグに応じて回転を更新
}
},
{ filterTaps: true }, // タップをフィルタリング
) as unknown as () => {
onPointerDown: (e: ThreeEvent<PointerEvent>) => void; // ポインターのダウンイベント
};

return (

<div className="h-screen w-full">
<Canvas>
<PerspectiveCamera makeDefault position={[0, 0, 15]} fov={70} />{" "}
{/* カメラを設定 */}
<OrbitControls enableZoom={false} enablePan={false} />{" "}
{/* オービットコントロールを設定 */}
<ambientLight intensity={0.5} /> {/* 環境光を設定 */}
{/* <pointLight position={[10, 10, 10]} /> */}
<group {...bind()} onPointerDown={(e) => e.stopPropagation()}>
{/* ドラッグのバインディングを適用 */}
{Array.from({ length: CARD*COUNT }).map((*, index) => (
<Card key={index} index={index} rotationY={rotationY} /> // カードを描画
))}
</group>
</Canvas>
</div>
);
}

参考

React Three FiberとDreiについての簡単な説明記事