爆発する3Dボタン!? React×Three.jsで驚きのUIを作ってみた!

  • URLをコピーしました!

「WebサイトのUIにちょっとした驚きを加えたい…」と思ったことはありませんか?
シンプルなボタンでは物足りない、でも複雑すぎるアニメーションは扱いにくい…。そんな時にぴったりのアイデアが「爆発する3Dボタン」です!

このボタンは、クリックするとパーティクルに分解され、まるで爆発するかのような動きを見せます。しかも、一定時間後には元の形に戻るという面白い仕組み。
ReactとThree.jsを使えば、このようなインタラクティブで美しいUIを簡単に実現できます。

TypeScript x React Three Fiberのレッスンや作品は、今後もどんどん作成していきます!
YouTubeで告知致しますので、ぜひYouTubeのチャンネル登録をして通知をお待ちください!

📺 YouTubeを見る:こちらのリンクから視聴できます。

TypeScript x React Three Fiber

React Three Fiberで何ができるのか知りたい方は、下記を参考にしてみてください!
簡単にできる作品を用意しております!

目次

爆発する3Dボタンのデモ

クリックすると爆発する驚きの3Dボタン、ぜひその動きを実際にご覧ください!
以下の動画で、完成形のデモを見ることができます。

📺 YouTubeでデモを見る:こちらのリンクから視聴できます。

さらに、このプロジェクトのソースコードはGitHubに公開しています。
ぜひコードをダウンロードして試してみてください!

💾 GitHubリポジトリ:こちらのリンクでソースコードをチェック!

「見てみたけどどうやって作るの?」と思った方、安心してください!
この記事では、この3Dボタンをゼロから作る方法をステップバイステップで解説していきます。

必要な技術と主要ライブラリ

このプロジェクトでは、以下の技術とライブラリを使用しています。それぞれの役割を簡単に説明します。

  • React
    • フロントエンド開発のためのライブラリ
    • コンポーネントベースの設計でUIを効率的に構築できます
  • Three.js
    • 3DグラフィックスをWeb上で描画するためのライブラリ
    • WebGLの複雑な処理を簡単に扱えるようにしてくれます
  • React Three Fiber
    • ReactでThree.jsを扱うためのラッパーライブラリ
    • Reactの開発スタイルでThree.jsの強力な機能を利用できます
  • React Thee Drei
    • React Three Fiberの拡張ライブラリ
    • React Three Fiberの拡張ライブラリ

実装の全体設計

このプロジェクトでは、「パーティクルが集まってボタンを形成し、クリックすると爆発し、元の形に戻る」というシンプルな流れを実現します。

設計図(簡易フロー)
初期状態(Idle)
  • パーティクルが規則的に配置され、ボタンの形を形成します。
  • 表面には「Click Me!!」のテキストが表示されます。
クリックイベント発生(Trigger)
  • ボタンをクリックすると、すべてのパーティクルが爆発的に飛び散る動作を開始します。
  • この動作を「explode」と呼びます。
爆発状態(Explode)
  • パーティクルがランダムな方向に移動します。
  • 速度や方向は、パーティクルごとにランダムに設定されています。
元の形に戻る(Reset)
  • 一定時間後、パーティクルが元の位置に戻り、再びボタンの形を形成します。
  • 戻る動きは滑らかに補間され、自然なアニメーションを実現します。

環境準備

このセクションでは、プロジェクトの初期設定を行います。npxコマンドでReactアプリを作成し、必要なライブラリをインストールしてフォルダ構成を整えます。

Reactアプリの作成

まず、npxコマンドを使用してReactアプリを作成します。

npx create-react-app exploding-button --template typescript
  • exploding-button はプロジェクトの名前です
  • --template typescript を指定することで、TypeScript対応のテンプレートを使用します

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

React Three Fiberやその他のライブラリをインストールします。

cd exploding-button
npm install three @react-three/fiber @react-three/drei
  • three: Three.js本体
  • @react-three/fiber: ReactでThree.jsを使うためのラッパー
  • @react-three/drei: カメラコントロールやテキスト描画など、便利なヘルパー

フォルダ構成の見直しと不要ファイルの削除

初期状態から以下のようにフォルダを整理・追加します。
基本的には初期値だったりしますが、わからないファイルなどは、GitHubを見てみてください。

exploding-button/
├── node_modules/
├── public/
├── src/
│   ├── components/                 // 他のコンポーネントを追加する場合のディレクトリ
│   ├── data/                       // 必要ならデータを管理するファイル用
│   ├── pages/                      // ページ単位で管理するコンポーネント
│   │   ├── ExplodingButtonPage/
│   │   │   ├── ExplodingButtonPage.tsx  // メインページのコード
│   │   │   └── ExplodingButtonPage.css  // スタイルシート
│   ├── App.tsx                     // アプリのエントリーポイント
│   ├── index.tsx                   // Reactのレンダリング処理
│   ├── App.css                     // グローバルスタイル
│   ├── index.css                   // グローバルCSS
├── package.json
├── tsconfig.json                   // TypeScriptの設定
└── README.md                       // プロジェクト概要

今回手を加えるファイルは、下記となります。

  • App.tsx: アプリのエントリーポイント
  • pages/ExplodingButtonPage: 今回作成対象のフォルダ
    • ExplodingButtonPage.tsx: メインページのコンポーネント。
    • ExplodingButtonPage.css: ページ固有のスタイル。

各ステップ毎にソースコードの詳細解説

以下のセクションでは、下記の5つのPARTを順番に詳しく解説していきます。

PART
STEP
定数定義
  • ボタンやパーティクルのサイズ、速度、動作時間などの基本的な設定値を定義します
  • これらの定数はプロジェクト全体で使用され、変更することで簡単に挙動を調整できます
STEP
型定義
  • ボタンを構成する各パーティクルのデータ構造を定義します
  • TypeScriptを使うことで、コードの可読性と保守性を向上させます
STEP
パーティクル生成
  • ボタンを構成する立方体のパーティクルとテキストのパーティクルを生成します
  • 各パーティクルの初期位置と爆発時の速度ベクトルをランダムに設定
  • 「Click Me!!」の文字をボタン表面に配置するロジックもここで実装します
STEP
パーティクル描画
  • 生成したパーティクルを3D空間に描画します
  • 爆発時のアニメーション(飛び出す動き)や、元の位置に戻るアニメーションをuseFrameで制御
  • テキストと立方体をそれぞれ適切な描画方法で分けて処理します
STEP
メインコンポーネント
  • 全体を統括するコンポーネントです
  • 状態管理(idleexplode)を行い、クリックイベントで爆発をトリガー
  • 背景やカメラコントロールの設定もここで実装します
  • パーティクルを表示するExplodingParticlesコンポーネントを組み込み、完成形に仕上げます

全体ソースを確認

以下は、この記事で作成する爆発する3Dボタンの完成版ソースコードです。すべてのコードをまとめていますので、動作のイメージをつかみやすいと思います。

これ以外のソースを見たい方は、GitHubで確認してください。

import './App.css';
import { ExplodingButtonPage } from './pages';

function App() {
  return (
    <div className="App">
      <ExplodingButtonPage />
    </div>
  );
}

export default App;
import React, { useRef, useState } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls, Stars, Text } from "@react-three/drei";
import * as THREE from "three";

// ================================
// PART 1: 定数定義
// ================================
const PARTICLE_SIZE = 0.1; // 1パーティクルのサイズ
const PARTICLE_NUM_WIDTH = 40; // パーティクル数(横)
const PARTICLE_NUM_HEIGHT = 20; // パーティクル数(横)
const PARTICLE_NUM_THICKNESS = 5; // パーティクル数(横)
const BUTTON_SIZE_WIDTH = PARTICLE_SIZE * PARTICLE_NUM_WIDTH; // ボタンサイズ=1パーティクル*パーティクル数
const BUTTON_SIZE_HEIGHT = PARTICLE_SIZE * PARTICLE_NUM_HEIGHT; // ボタンサイズ=1パーティクル*パーティクル数
const BUTTON_SIZE_THICKNESS = PARTICLE_SIZE * PARTICLE_NUM_THICKNESS; // ボタンサイズ=1パーティクル*パーティクル数

const TEXT = "Click Me!!"; // 表示するテキスト
const TEXT_SIZE = BUTTON_SIZE_WIDTH / TEXT.length; // 1文字あたりのサイズ
const TEXT_FLOAT_OFFSET = 0.01; // テキストをボタン表面から浮かせる距離

const PARTICLE_SPEED = 2.0; // 爆発時の速度
const PARTICLE_TIME = 5 * 1000; // 爆発の継続時間(秒)
const RESET_SPEED = 0.05; // 元の位置に戻る速度

// ================================
// PART 2: 型定義
// ================================
type Particle = {
    id: number; // 一意のID
    startPosition: THREE.Vector3; // 初期位置
    position: THREE.Vector3; // 現在位置
    velocity: THREE.Vector3; // 爆発時の速度ベクトル
    scale: number; // サイズ
    char?: string; // テキストパーティクルの場合の文字
};

// ================================
// PART 3: パーティクル生成関数
// ================================
const generateButtonParticles = (): Particle[] => {
    const particles: Particle[] = [];

    // --- ボタンのパーティクルを生成 ---
    for (let x = 0; x < PARTICLE_NUM_WIDTH; x++) {
        for (let y = 0; y < PARTICLE_NUM_HEIGHT; y++) {
            for (let z = 0; z < PARTICLE_NUM_THICKNESS; z++) {
                const position = new THREE.Vector3(
                    x * PARTICLE_SIZE - BUTTON_SIZE_WIDTH / 2,
                    y * PARTICLE_SIZE - BUTTON_SIZE_HEIGHT / 2,
                    z * PARTICLE_SIZE
                );
                particles.push({
                    id: particles.length,
                    startPosition: position.clone(),
                    position: position.clone(),
                    velocity: new THREE.Vector3(
                        (Math.random() - 0.5) * PARTICLE_SPEED,
                        (Math.random() - 0.5) * PARTICLE_SPEED,
                        (Math.random() - 0.5) * PARTICLE_SPEED
                    ),
                    scale: PARTICLE_SIZE,
                });
            }
        }
    }

    // --- テキストのパーティクルを生成 ---
    TEXT.split("").forEach((char, i) => {
        const position = new THREE.Vector3(
            i * TEXT_SIZE - BUTTON_SIZE_WIDTH / 2 + TEXT_SIZE / 2,
            0, // Y軸中央
            BUTTON_SIZE_THICKNESS + TEXT_FLOAT_OFFSET // Z軸: 表面から少し浮かせる
        );
        particles.push({
            id: particles.length,
            startPosition: position.clone(),
            position: position.clone(),
            velocity: new THREE.Vector3(
                (Math.random() - 0.5) * PARTICLE_SPEED,
                (Math.random() - 0.5) * PARTICLE_SPEED,
                (Math.random() - 0.5) * PARTICLE_SPEED
            ),
            scale: TEXT_SIZE,
            char,
        });
    });

    return particles;
};

// ================================
// PART 4: パーティクル描画コンポーネント
// ================================
const ExplodingParticles: React.FC<{ particles: Particle[]; state: string }> = ({ particles, state }) => {
    const groupRef = useRef<THREE.Group>(null);

    // useFrame: 毎フレームの処理(爆発 or 初期位置に戻す)
    useFrame((_, delta) => {
        groupRef.current?.children.forEach((child) => {
            const particle = child.userData.particle as Particle;

            if (state === "explode") {
                // 爆発: 速度ベクトルに基づいて移動
                child.position.add(particle.velocity.clone().multiplyScalar(delta));
            } else {
                // 初期位置に戻す
                child.position.lerp(particle.startPosition, RESET_SPEED);
            }
        });
    });

    return (
        <group ref={groupRef}>
            {particles.map((particle) =>
                particle.char ? (
                    // テキストパーティクル
                    <group key={particle.id} position={particle.position} userData={{ particle }}>
                        <Text fontSize={particle.scale} color="white">
                            {particle.char}
                        </Text>
                    </group>
                ) : (
                    // ボタン形状のパーティクル
                    <mesh key={particle.id} position={particle.position} userData={{ particle }}>
                        <boxGeometry args={[particle.scale, particle.scale, particle.scale]} />
                        <meshStandardMaterial color={"#3498db"} />
                    </mesh>
                )
            )}
        </group>
    );
};

// ================================
// PART 5: メインコンポーネント
// ================================
const ExplodingButtonPage: React.FC = () => {
    const [particles] = useState<Particle[]>(generateButtonParticles());
    const [state, setState] = useState<"idle" | "explode">("idle");

    const handleClick = () => {
        if (state === "idle") {
            setState("explode");
            setTimeout(() => setState("idle"), PARTICLE_TIME);
        }
    };

    return (
        <div style={{ width: "100vw", height: "100vh", background: "black" }}>
            <Canvas camera={{ position: [0, 0, 8] }}>
                {/* 背景効果 */}
                <Stars radius={100} depth={50} count={1000} factor={4} fade />
                <ambientLight intensity={0.8} />
                <spotLight position={[10, 10, 10]} intensity={2} castShadow />

                {/* パーティクル表示 */}
                <group onClick={handleClick}>
                    <ExplodingParticles particles={particles} state={state} />
                </group>

                {/* カメラ操作 */}
                <OrbitControls />
            </Canvas>
        </div>
    );
};

export default ExplodingButtonPage;

PART 1: 定数定義

まずは、各ライブラリのimportと、定数定義です。

import React, { useRef, useState } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls, Stars, Text } from "@react-three/drei";
import * as THREE from "three";

// ================================
// PART 1: 定数定義
// ================================
const PARTICLE_SIZE = 0.1; // 1パーティクルのサイズ
const PARTICLE_NUM_WIDTH = 40; // パーティクル数(横)
const PARTICLE_NUM_HEIGHT = 20; // パーティクル数(横)
const PARTICLE_NUM_THICKNESS = 5; // パーティクル数(横)
const BUTTON_SIZE_WIDTH = PARTICLE_SIZE * PARTICLE_NUM_WIDTH; // ボタンサイズ=1パーティクル*パーティクル数
const BUTTON_SIZE_HEIGHT = PARTICLE_SIZE * PARTICLE_NUM_HEIGHT; // ボタンサイズ=1パーティクル*パーティクル数
const BUTTON_SIZE_THICKNESS = PARTICLE_SIZE * PARTICLE_NUM_THICKNESS; // ボタンサイズ=1パーティクル*パーティクル数

const TEXT = "Click Me!!"; // 表示するテキスト
const TEXT_SIZE = BUTTON_SIZE_WIDTH / TEXT.length; // 1文字あたりのサイズ
const TEXT_FLOAT_OFFSET = 0.01; // テキストをボタン表面から浮かせる距離

const PARTICLE_SPEED = 2.0; // 爆発時の速度
const PARTICLE_TIME = 5 * 1000; // 爆発の継続時間(秒)
const RESET_SPEED = 0.05; // 元の位置に戻る速度
  • サイズの定義
    • ボタン全体の大きさを決めるために、パーティクルのサイズとその数を掛け合わせて計算
    • PARTICLE_SIZE は1つのパーティクルの立方体のサイズ
    • 各方向のパーティクル数(横、縦、奥行き)を個別に設定
  • 速度と時間の定義
    • PARTICLE_SPEED は爆発時の移動速度
    • PARTICLE_TIME は爆発のアニメーションが続く時間

PART 2: 型定義

次は、型定義ですね。
JavaScriptなら不要なのですが、今回はTypeScriptで作っているため、型定義が必要になります。

type Particle = {
    id: number; // 各パーティクルの一意の識別子
    startPosition: THREE.Vector3; // 初期位置
    position: THREE.Vector3; // 現在の位置
    velocity: THREE.Vector3; // 移動速度
    scale: number; // サイズ
    char?: string; // テキストパーティクルの場合の文字
};
  • 型定義の目的
    • パーティクルに必要なデータを明確に整理
    • THREE.Vector3 は3次元空間で位置や速度を表現するThree.jsの型
    • char はオプション型(?)として定義し、テキストパーティクルにのみ使用

PART 3: パーティクル生成

ボタンとテキストのパーティクルを生成する関数を定義します。
パーティクルとは、ボタンだと分裂したときの1つ1つの粒のことになります。文字だと1文字づつがパーティクルですね。

ボタンは、1つが分裂しているというよりは、複数の分子が集まって1つのボタンになっているイメージです。
これはその1つ1つを並べて、ボタンを作っている関数になります。

const generateButtonParticles = (): Particle[] => {
    const particles: Particle[] = [];

    // ボタンの立方体パーティクルを生成
    for (let x = 0; x < PARTICLE_NUM_WIDTH; x++) {
        for (let y = 0; y < PARTICLE_NUM_HEIGHT; y++) {
            for (let z = 0; z < PARTICLE_NUM_THICKNESS; z++) {
                const position = new THREE.Vector3(
                    x * PARTICLE_SIZE - BUTTON_SIZE_WIDTH / 2,
                    y * PARTICLE_SIZE - BUTTON_SIZE_HEIGHT / 2,
                    z * PARTICLE_SIZE
                );
                particles.push({
                    id: particles.length,
                    startPosition: position.clone(),
                    position: position.clone(),
                    velocity: new THREE.Vector3(
                        (Math.random() - 0.5) * PARTICLE_SPEED,
                        (Math.random() - 0.5) * PARTICLE_SPEED,
                        (Math.random() - 0.5) * PARTICLE_SPEED
                    ),
                    scale: PARTICLE_SIZE,
                });
            }
        }
    }

    // テキストパーティクルを生成
    TEXT.split("").forEach((char, i) => {
        const position = new THREE.Vector3(
            i * TEXT_SIZE - BUTTON_SIZE_WIDTH / 2 + TEXT_SIZE / 2,
            0,
            BUTTON_SIZE_THICKNESS + TEXT_FLOAT_OFFSET
        );
        particles.push({
            id: particles.length,
            startPosition: position.clone(),
            position: position.clone(),
            velocity: new THREE.Vector3(
                (Math.random() - 0.5) * PARTICLE_SPEED,
                (Math.random() - 0.5) * PARTICLE_SPEED,
                (Math.random() - 0.5) * PARTICLE_SPEED
            ),
            scale: TEXT_SIZE,
            char,
        });
    });

    return particles;
};
  • ボタンパーティクルの生成
    • 3重ループを使って、パーティクルをボタン形状に配置(縦・横・幅)
    • 各パーティクルの初期位置(startPosition)と現在位置(position)を設定
    • velocity はランダムな方向に設定
  • テキストパーティクルの生成
    • TEXT の各文字をループで処理
    • 各文字をボタン表面に配置し、パーティクルとして扱う

PART 4: パーティクル描画

パーティクルを実際に描画する関数を定義しています。

const ExplodingParticles: React.FC<{ particles: Particle[]; state: string }> = ({ particles, state }) => {
    const groupRef = useRef<THREE.Group>(null);

    useFrame((_, delta) => {
        groupRef.current?.children.forEach((child) => {
            const particle = child.userData.particle as Particle;

            if (state === "explode") {
                child.position.add(particle.velocity.clone().multiplyScalar(delta));
            } else {
                child.position.lerp(particle.startPosition, RESET_SPEED);
            }
        });
    });

    return (
        <group ref={groupRef}>
            {particles.map((particle) =>
                particle.char ? (
                    <group key={particle.id} position={particle.position} userData={{ particle }}>
                        <Text fontSize={particle.scale} color="white">
                            {particle.char}
                        </Text>
                    </group>
                ) : (
                    <mesh key={particle.id} position={particle.position} userData={{ particle }}>
                        <boxGeometry args={[particle.scale, particle.scale, particle.scale]} />
                        <meshStandardMaterial color={"#3498db"} />
                    </mesh>
                )
            )}
        </group>
    );
};
  • useFrameを使ったアニメーション制御
    • useFrame@react-three/fiberが提供するReactフックで、3D空間内で毎フレーム行いたい処理を記述します
    • delta: 前回のフレームからの経過時間。これを用いてアニメーションの動きをフレームレートに依存しない形にします
    • child.position.add()
      • 爆発時には、velocity(速度ベクトル)にdeltaを掛け算し、パーティクルの位置を更新します
      • clone()を使うことで元の速度ベクトルを破壊しないようにしています
    • lerp(線形補間)
      • 元の位置(startPosition)に徐々に近づくようにします。RESET_SPEEDが速度の指標です
  • パーティクルの種類(立方体とテキスト)の描画
    • 立方体パーティクル(ボタン形状部分)
      • <mesh>タグを使い、立方体のジオメトリ(<boxGeometry>)を描画
      • meshStandardMaterialを用いて色や表面の見た目を設定
      • userDataparticleのデータを渡し、useFrameで位置を更新可能にしています
    • テキストパーティクル
      • @react-three/drei<Text>コンポーネントを使用
      • フォントサイズをパーティクルのスケール(scale)に基づいて設定
      • 各文字が爆発に追随するよう、group内で個別に管理
  • groupコンポーネントを活用したパーティクル管理
    • <group>は、Three.jsのグループコンポーネントで、複数のオブジェクトをまとめて管理できます
    • この場合、すべてのパーティクルを1つの<group>内にまとめ、親要素(groupRef)を使って操作します
    • groupRef.current.children
      • group内のすべてのパーティクル(子要素)にアクセスします
      • 各パーティクルのuserDataを参照して位置や速度を更新

PART 5: メインコンポーネント

最後は、メインコンポーネントになります。
背景やカメラ(OrbitControls)などを定義してます。

const ExplodingButtonPage: React.FC = () => {
    const [particles] = useState<Particle[]>(generateButtonParticles());
    const [state, setState] = useState<"idle" | "explode">("idle");

    const handleClick = () => {
        if (state === "idle") {
            setState("explode");
            setTimeout(() => setState("idle"), PARTICLE_TIME);
        }
    };

    return (
        <div style={{ width: "100vw", height: "100vh", background: "black" }}>
            <Canvas camera={{ position: [0, 0, 8] }}>
                <Stars radius={100} depth={50} count={1000} factor={4} fade />
                <ambientLight intensity={0.8} />
                <spotLight position={[10, 10, 10]} intensity={2} castShadow />

                <group onClick={handleClick}>
                    <ExplodingParticles particles={particles} state={state} />
                </group>

                <OrbitControls />
            </Canvas>
        </div>
    );
};
  • 状態管理
    • state によって爆発と初期位置へのリセットを切り替え
  • クリックイベント
    • 爆発をトリガーし、一定時間後に元の状態に戻す

最後に

今回の記事では、ReactとThree.jsを組み合わせて「爆発する3Dボタン」を作成しました!このプロジェクトを通じて、以下のようなポイントを学べたのではないでしょうか?

  • React Three Fiberを使った3D空間の操作方法
  • パーティクルを使ったインタラクティブなアニメーションの実装
  • 状態管理(useState)とアニメーション制御(useFrame)の組み合わせ

完成した3Dボタンをぜひ実際に触ってみてください!

📺 YouTubeでデモを見る:こちらのリンクから視聴できます。

さらに、このプロジェクトのソースコードはGitHubに公開しています。
ぜひコードをダウンロードして試してみてください!

💾 GitHubリポジトリ:こちらのリンクでソースコードをチェック!

参考になった方は、是非チャンネル登録をお願いします!

TypeScript x React Three Fiberのレッスンや作品は、今後もどんどん作成していきます!
YouTubeで告知致しますので、ぜひYouTubeのチャンネル登録をして通知をお待ちください!

📺 YouTubeを見る:こちらのリンクから視聴できます。

TypeScript x React Three Fiber

React Three Fiberで何ができるのか知りたい方は、下記を参考にしてみてください!
簡単にできる作品を用意しております!

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

情報セキュリティを勉強するために始めたブログです。
新人のため、広い心を持って見ていただけると嬉しく思います。
楽しくプログラミングを勉強するために、「Teech Lab.」もありますので、ソフトウェア開発にも興味があればぜひ覗いて見てください!

目次