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>
);
}