import React, { useRef, useState, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { TextureLoader } from "three/src/loaders/TextureLoader";
import { useLoader } from "@react-three/fiber";
import * as THREE from "three";
function Sun() {
const texture = useLoader(TextureLoader, "/texture/sun_texture.jpg");
const ref = useRef();
useFrame(() => {
ref.current.rotation.y += 0.005;
});
return (
<mesh ref={ref} position={[0, 0, 0]}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial
map={texture}
emissive={"#ff6600"}
emissiveIntensity={1.1}
/>
</mesh>
);
}
function Earth({ G, resetTrigger }) {
const texture = useLoader(TextureLoader, "/texture/earth_texture.jpg");
const ref = useRef();
const initialPos = [10, 0, 0];
const initialVel = [0, 0, 10];
const pos = useRef([...initialPos]);
const vel = useRef([...initialVel]);
const trailPoints = useRef([]);
const trailRef = useRef();
useEffect(() => {
// 位置・速度・軌跡をリセット
pos.current = [...initialPos];
vel.current = [...initialVel];
trailPoints.current = [];
if (ref.current) {
ref.current.position.set(...initialPos);
ref.current.rotation.set(0, 0, 0);
}
if (trailRef.current) {
trailRef.current.geometry.setDrawRange(0, 0);
}
}, [G, resetTrigger]);
const M = 1000;
const dt = 0.01; // タイムスケールなし固定
useFrame(() => {
const rVec = [-pos.current[0], -pos.current[1], -pos.current[2]];
const dist = Math.sqrt(
rVec[0] ** 2 + rVec[1] ** 2 + rVec[2] ** 2
);
const acc = [
(G * M * rVec[0]) / dist ** 3,
(G * M * rVec[1]) / dist ** 3,
(G * M * rVec[2]) / dist ** 3,
];
vel.current[0] += acc[0] * dt;
vel.current[1] += acc[1] * dt;
vel.current[2] += acc[2] * dt;
pos.current[0] += vel.current[0] * dt;
pos.current[1] += vel.current[1] * dt;
pos.current[2] += vel.current[2] * dt;
if (ref.current) {
ref.current.position.set(...pos.current);
ref.current.rotation.y += 0.01;
}
trailPoints.current.push(new THREE.Vector3(...pos.current));
if (trailPoints.current.length > 200) trailPoints.current.shift();
if (trailRef.current) {
const positions = new Float32Array(trailPoints.current.length * 3);
trailPoints.current.forEach((v, i) => {
positions[i * 3] = v.x;
positions[i * 3 + 1] = v.y;
positions[i * 3 + 2] = v.z;
});
trailRef.current.geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
trailRef.current.geometry.setDrawRange(0, trailPoints.current.length);
trailRef.current.geometry.attributes.position.needsUpdate = true;
}
});
return (
<>
<mesh ref={ref}>
<sphereGeometry args={[0.3, 32, 32]} />
<meshStandardMaterial
map={texture}
emissive="#555555"
emissiveIntensity={0.3}
/>
</mesh>
<line ref={trailRef}>
<bufferGeometry />
<lineBasicMaterial color="lightblue" linewidth={2} />
</line>
</>
);
}
function CameraController() {
const { camera } = useThree();
useEffect(() => {
camera.position.set(18, 8, 8);
camera.lookAt(0, 0, 0);
}, [camera]);
return null;
}
export default function App() {
const [G, setG] = useState(1.0);
const [resetKey, setResetKey] = useState(0);
const handleReset = () => {
setG(1.0); // Gを1に戻す
setResetKey((k) => k + 1); // リセットトリガー更新
};
return (
<>
<div
style={{
position: "absolute",
top: 10,
left: 10,
zIndex: 1,
background: "rgba(0,0,0,0.6)",
color: "white",
padding: 15,
borderRadius: 8,
width: 260,
fontFamily: "Arial, sans-serif",
userSelect: "none",
}}
>
<label>
<strong>重力定数 G:</strong> {G.toFixed(2)}
<br />
<input
type="range"
min="0.1"
max="10.0"
step="0.01"
value={G}
onChange={(e) => setG(parseFloat(e.target.value))}
style={{ width: "100%" }}
/>
</label>
<br />
<br />
<button
onClick={handleReset}
style={{
width: "100%",
padding: "8px 0",
borderRadius: 5,
border: "none",
backgroundColor: "#ff6600",
color: "white",
fontWeight: "bold",
cursor: "pointer",
fontSize: "16px",
}}
>
リセット(G=1に戻す)
</button>
</div>
<Canvas
camera={{ fov: 50 }}
style={{ background: "#0b0d17", height: "100vh", width: "100vw" }}
>
<ambientLight intensity={1.3} />
<pointLight
position={[0, 0, 0]}
intensity={1.8}
decay={1}
distance={0}
color="#ffaa33"
/>
<CameraController />
<Sun />
<Earth G={G} resetTrigger={resetKey} />
</Canvas>
</>
);
}