アプリのコード
APP.js
const handleRun = () => {
const sqlLower = sql.toLowerCase();
if (
sqlLower.includes("select") &&
sqlLower.includes("age") &&
sqlLower.includes(">=") &&
sqlLower.includes("30")
) {
// Step 1: Three.js 爆発演出開始
setShowExplosionThree(true);
setTimeout(() => {
// Step 2: Three.js 終了、Explosion.js 開始
setShowExplosionThree(false);
setShowExplosion(true);
// Step 3: Explosion.js 終了
setTimeout(() => setShowExplosion(false), 1000);
}, 1000);
} else {
alert("❌ 条件が合っていません");
}
APP.js
return (
//実行するボタンでhandleRunを実行。
<button
onClick={handleRun}
className="w-full bg-orange-500 text-white py-2 rounded hover:bg-orange-600 transition"
>
▶️ 実行する
</button>
);
爆発のコード
Explosion.js
import React, { useEffect, useRef } from 'react';
// Three.jsライブラリ全体を THREE としてインポートします。
import * as THREE from 'three';
const Explosion = () => {
//DOMへの参照を取得するための ref。ここにThree.jsのcanvas要素をマウントします。
const mountRef = useRef(null);
useEffect(() => {
// ここでローカル変数にコピー
const mount = mountRef.current;
//Three.jsで描画される「シーン」を作成します。
const scene = new THREE.Scene();
// カメラの設定
//視野角75度、画面サイズに合わせたアスペクト比、0.1〜1000の視点距離範囲のカメラを作成。
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
//カメラをZ軸方向に15離して、オブジェクト全体が見えるようにします。
camera.position.z = 15;
// レンダラー
// WebGLレンダラーを生成。アンチエイリアスで滑らかに、透過は無効。
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
//レンダラーのサイズを画面全体にします。
renderer.setSize(window.innerWidth, window.innerHeight);
//背景色を黒に設定(不透明)。
renderer.setClearColor(0x000000, 1);
//canvas(DOM)を実際のHTML要素に追加して表示。
mount.appendChild(renderer.domElement);
// 粒子群を作成
// パーティクル(爆発の粒子)200個を格納する配列と、下向きの重力ベクトルを定義。
const particles = [];
const particleCount = 200;
const gravity = new THREE.Vector3(0, -0.03, 0); // 少し強めの重力
//200個のパーティクルを1つずつ作成。
for (let i = 0; i < particleCount; i++) {
// 小さな球体(爆発の粒)の形状を定義。
const geometry = new THREE.SphereGeometry(0.12, 8, 8);
//明るめのオレンジ〜黄色の色相をもつ素材(透明度も設定)を作成。
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color(`hsl(${30 + Math.random() * 30}, 100%, 50%)`),
transparent: true,
opacity: 1.0,
});
//形状と素材から1つの粒子メッシュを生成。
const particle = new THREE.Mesh(geometry, material);
//初期位置を画面中央に。
particle.position.set(0, 0, 0);
// ランダムな球面方向に速度を与える
//球面座標系で、ランダム方向に速度ベクトルを与えるための角度と速度を生成。
const theta = Math.random() * 2 * Math.PI;
const phi = Math.acos(2 * Math.random() - 1);
const speed = 4 + Math.random() * 6;
//速度ベクトルを3次元で作成し、速度倍率をかけて実際の移動量を決定。
const velocity = new THREE.Vector3(
Math.sin(phi) * Math.cos(theta),
Math.sin(phi) * Math.sin(theta),
Math.cos(phi)
).multiplyScalar(speed);
//粒子のデータに速度と「生存時間」(アニメーション経過時間)を追加。
particle.userData.velocity = velocity;
particle.userData.aliveTime = 0; // 生存時間カウント
//シーンに追加し、管理配列にも保存。
scene.add(particle);
particles.push(particle);
}
// 爆発のフラッシュ光
//フラッシュ(爆発時の光)用の半透明の黄色っぽいマテリアル。
const flashMaterial = new THREE.MeshBasicMaterial({
color: 0xffffcc,
transparent: true,
opacity: 1.0,
side: THREE.DoubleSide,
});
//中央に発光球体を置き、シーンに追加。
const flash = new THREE.Mesh(new THREE.SphereGeometry(1.5, 32, 32), flashMaterial);
scene.add(flash);
// アニメーション開始時刻
let startTime = null;
const maxDuration = 3500; // 3.5秒でフェードアウト終了
// アニメーション関数と、その呼び出しIDを定義。
let frameId;
const animate = (time) => {
// 初回のみ開始時間を保存し、現在までの経過時間を計算。
if (!startTime) startTime = time;
const elapsed = time - startTime;
particles.forEach((p) => {
// 重力加算
p.userData.velocity.add(gravity);
// 位置更新
p.position.add(p.userData.velocity);
// 生存時間アップデート
p.userData.aliveTime += 16;
// 徐々に透明にし縮小
p.material.opacity = THREE.MathUtils.clamp(1 - p.userData.aliveTime / maxDuration, 0, 1);
const scaleVal = THREE.MathUtils.lerp(1, 0, p.userData.aliveTime / maxDuration);
p.scale.setScalar(scaleVal);
});
// フラッシュも徐々に大きく、透明に
flash.scale.multiplyScalar(1.1);
flash.material.opacity = THREE.MathUtils.clamp(1 - elapsed / maxDuration, 0, 1);
//毎フレームの描画処理。
renderer.render(scene, camera);
//アニメーションが時間内であれば、次のフレームを予約。
if (elapsed < maxDuration) {
frameId = requestAnimationFrame(animate);
}
};
//最初のフレームを呼び出してアニメーションを開始。
frameId = requestAnimationFrame(animate);
//クリーンアップ関数。Reactのアンマウント時に呼ばれる。
return () => {
//アニメーション停止、canvas削除、メモリ解放。
cancelAnimationFrame(frameId);
if (mount && renderer.domElement) {
mount.removeChild(renderer.domElement);
}
renderer.dispose();
};
//useEffectがマウント時に一度だけ実行されることを意味します。
}, []);
//Three.jsのcanvasを挿入するための空のdiv。全画面に固定表示され、クリックなどのイベントをブロックしません。
return (
<div
ref={mountRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
pointerEvents: 'none',
zIndex: 9999,
}}
/>
);
};
export default Explosion;
爆発コードの不明点まとめ
- Three.jsライブラリ
Webブラウザ上で3Dグラフィックスを表示・操作するためのJavaScriptライブラリです。 - canvas要素
HTML5で導入された描画専用のタグです。これを使うことで、JavaScriptを使って図形・画像・アニメーション・3D表現などを直接描画できる領域をウェブページ上に作れます。 - WebGLレンダラー
WebGLの複雑なAPIを隠し、直感的な記述で3Dを実現する - アンチエイリアス(WebGLレンダラー)
3Dオブジェクトのエッジ(境界線)をなめらかに描画する処理のことです。 - canvas(DOM)
HTML5で導入された描画専用のタグです。これを使うことで、JavaScriptを使って図形・画像・アニメーション・3D表現などを直接描画できる領域をウェブページ上に作れます。 - Math.sin
三角関数のサイン (sin) 計算するための JavaScript 組み込み関数。
xラジアンのsin値(y座標) - Math.cos
三角関数のコサイン (cos) 計算するための JavaScript 組み込み関数。
xラジアンのcos値(x座標)
爆弾のコード
ExplosionThree.js
import React, { useEffect, useRef } from "react";
import * as THREE from "three";
const ExplosionThree = () => {
const mountRef = useRef(null);
useEffect(() => {
const mount = mountRef.current;
// シーンセットアップ
const scene = new THREE.Scene();
// カメラセットアップ
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
// レンダラーセットアップ
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
//rendererのcanvas要素を div に追加(DOMに表示)。
mount.appendChild(renderer.domElement);
// 🧨 爆弾本体(黒い球体)
// 半径0.5の球体を作成。頂点数が多めで滑らか。
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
//真っ黒なマテリアル。透明度を操作可能にしています。
const sphereMaterial = new THREE.MeshBasicMaterial({
color: 0x111111,
transparent: true,
opacity: 1,
});
//ジオメトリとマテリアルを結合して「爆弾メッシュ」を作成。
const bomb = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(bomb);
// 🧵 導火線(シリンダー)
const fuseGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.3, 8);
const fuseMaterial = new THREE.MeshBasicMaterial({ color: 0xffa500 }); // オレンジ
const fuse = new THREE.Mesh(fuseGeometry, fuseMaterial);
fuse.position.y = 0.6; // 球の上に乗せる
bomb.add(fuse); // 導火線を爆弾に「子オブジェクト」として追加(爆弾と一緒に動くように)。
// 🔥 火花(オプション: 点滅)
const sparkGeometry = new THREE.SphereGeometry(0.05, 8, 8);
//黄色のマテリアル。
const sparkMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
//火花のメッシュを作成。
const spark = new THREE.Mesh(sparkGeometry, sparkMaterial);
spark.position.y = 0.2;
fuse.add(spark);
// ウィンドウのリサイズ時に、カメラとレンダラーを再調整。
const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
//イベントリスナーとして登録。
window.addEventListener("resize", handleResize);
//requestAnimationFrame のID保存用。
let reqId;
// アニメーション関数
//毎フレーム描画するループ処理(再帰的に自分を呼ぶ)。
const animate = () => {
reqId = requestAnimationFrame(animate);
// スケールアップ
bomb.scale.x += 0.01;
bomb.scale.y += 0.01;
bomb.scale.z += 0.01;
// フェードアウト
if (bomb.material.opacity > 0) {
bomb.material.opacity -= 0.01;
if (bomb.material.opacity < 0) bomb.material.opacity = 0;
}
// 点滅(火花用)
//火花が点滅するように、ランダムに表示/非表示を切り替える。
spark.visible = Math.random() > 0.5;
renderer.render(scene, camera);
};
animate();
// クリーンアップ
return () => {
//アニメーションループ停止。
cancelAnimationFrame(reqId);
//リサイズイベント解除。
window.removeEventListener("resize", handleResize);
if (mount && renderer.domElement) {
mount.removeChild(renderer.domElement);
}
//レンダラーを破棄してメモリ解放。
renderer.dispose();
};
}, []);
//refを関連付けた透明なdiv。ここにThree.jsのcanvasが描画される。
//画面全体に固定表示(fixed)、クリックなどを受け付けない(pointerEvents: none)。
return (
<div
ref={mountRef}
style={{
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
pointerEvents: "none",
zIndex: 9999,
}}
/>
);
};
export default ExplosionThree;