192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useRef, useState } from "react";
|
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
|
import { MeshDistortMaterial, Sphere, Preload, Float } from "@react-three/drei";
|
|
import * as THREE from "three";
|
|
|
|
// --- CONFIGURATION ---
|
|
const NEURON_COUNT = 18; // Fewer, but more detailed neurons
|
|
const CONNECTION_DISTANCE = 5.5; // Distance to form a synapse
|
|
const PULSE_SPEED = 2.5; // Speed of the electric signal
|
|
|
|
// 1. SINGLE NEURON CELL (The Soma)
|
|
// We use a distorted sphere to make it look like organic tissue
|
|
function NeuronCell({ position }: { position: [number, number, number] }) {
|
|
const meshRef = useRef<THREE.Mesh>(null!);
|
|
const [hovered, setHover] = useState(false);
|
|
|
|
// Randomize size slightly for variety
|
|
const scale = useMemo(() => 0.6 + Math.random() * 0.4, []);
|
|
|
|
useFrame((state) => {
|
|
// Gentle heartbeat pulsation
|
|
const t = state.clock.getElapsedTime();
|
|
const pulse = Math.sin(t * 3) * 0.05 + 1;
|
|
if (meshRef.current) {
|
|
meshRef.current.scale.set(scale * pulse, scale * pulse, scale * pulse);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<Float speed={2} rotationIntensity={0.5} floatIntensity={0.5}>
|
|
<Sphere
|
|
ref={meshRef}
|
|
args={[1, 32, 32]} // Geometry args
|
|
position={position}
|
|
onPointerOver={() => setHover(true)}
|
|
onPointerOut={() => setHover(false)}
|
|
>
|
|
<MeshDistortMaterial
|
|
color={hovered ? "#06b6d4" : "#22d3ee"} // Cyan 500 to 400
|
|
emissive={hovered ? "#0891b2" : "#0e7490"}
|
|
emissiveIntensity={0.6}
|
|
roughness={0.2}
|
|
metalness={0.1}
|
|
distort={0.4} // The "wobble" amount (0-1)
|
|
speed={2} // Speed of the wobble
|
|
/>
|
|
<pointLight distance={3} intensity={2} color="#22d3ee" />
|
|
</Sphere>
|
|
</Float>
|
|
);
|
|
}
|
|
|
|
// 2. SYNAPSE (The Connection & The Impulse)
|
|
// Draws a curved organic line and a traveling light pulse
|
|
function Synapse({ start, end }: { start: THREE.Vector3; end: THREE.Vector3 }) {
|
|
const curve = useMemo(() => {
|
|
// Create a quadratic bezier curve for a natural "tendon" look
|
|
// Control point is midway but offset randomly to create the curve
|
|
const mid = new THREE.Vector3().lerpVectors(start, end, 0.5);
|
|
mid.x += (Math.random() - 0.5) * 2;
|
|
mid.y += (Math.random() - 0.5) * 2;
|
|
mid.z += (Math.random() - 0.5) * 2;
|
|
return new THREE.QuadraticBezierCurve3(start, mid, end);
|
|
}, [start, end]);
|
|
|
|
// Create points for the line geometry
|
|
const points = useMemo(() => curve.getPoints(20), [curve]);
|
|
|
|
// The traveling impulse ref
|
|
const impulseRef = useRef<THREE.Mesh>(null!);
|
|
|
|
useFrame((state) => {
|
|
// Move the impulse along the curve
|
|
const t = (state.clock.getElapsedTime() * PULSE_SPEED) % 1; // 0 to 1 loop
|
|
if (impulseRef.current) {
|
|
const pos = curve.getPointAt(t);
|
|
impulseRef.current.position.copy(pos);
|
|
|
|
// Scale impulse based on position (fade in/out at ends)
|
|
const scale = Math.sin(t * Math.PI);
|
|
impulseRef.current.scale.setScalar(scale * 0.15);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{/* The Axon (Line) */}
|
|
<line>
|
|
<bufferGeometry>
|
|
<bufferAttribute
|
|
attach="attributes-position"
|
|
count={points.length}
|
|
array={new Float32Array(points.flatMap((p) => [p.x, p.y, p.z]))}
|
|
itemSize={3}
|
|
/>
|
|
</bufferGeometry>
|
|
<lineBasicMaterial
|
|
color="#155e75" // Dark Cyan
|
|
transparent
|
|
opacity={0.3}
|
|
linewidth={1}
|
|
/>
|
|
</line>
|
|
|
|
{/* The Electric Impulse (Glowing Dot) */}
|
|
<mesh ref={impulseRef}>
|
|
<sphereGeometry args={[1, 8, 8]} />
|
|
<meshBasicMaterial color="#ecfeff" toneMapped={false} />
|
|
<pointLight distance={2} intensity={2} color="#22d3ee" decay={2} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// 3. MAIN SCENE CONTROLLER
|
|
function NeuralNetworkScene() {
|
|
const { viewport } = useThree();
|
|
const mouse = useRef(new THREE.Vector2());
|
|
|
|
// Generate Neurons
|
|
const neurons = useMemo(() => {
|
|
const temp = [];
|
|
for (let i = 0; i < NEURON_COUNT; i++) {
|
|
temp.push({
|
|
position: new THREE.Vector3(
|
|
(Math.random() - 0.5) * 20,
|
|
(Math.random() - 0.5) * 20,
|
|
(Math.random() - 0.5) * 10
|
|
),
|
|
id: i,
|
|
});
|
|
}
|
|
return temp;
|
|
}, []);
|
|
|
|
// Generate Connections (Proximity based)
|
|
const connections = useMemo(() => {
|
|
const conns = [];
|
|
for (let i = 0; i < neurons.length; i++) {
|
|
for (let j = i + 1; j < neurons.length; j++) {
|
|
const dist = neurons[i].position.distanceTo(neurons[j].position);
|
|
if (dist < CONNECTION_DISTANCE) {
|
|
conns.push({ start: neurons[i].position, end: neurons[j].position, key: `${i}-${j}` });
|
|
}
|
|
}
|
|
}
|
|
return conns;
|
|
}, [neurons]);
|
|
|
|
useFrame((state) => {
|
|
// Soft camera movement based on mouse
|
|
const x = (state.pointer.x * viewport.width) / 10;
|
|
const y = (state.pointer.y * viewport.height) / 10;
|
|
|
|
state.camera.position.x = THREE.MathUtils.lerp(state.camera.position.x, x, 0.05);
|
|
state.camera.position.y = THREE.MathUtils.lerp(state.camera.position.y, y, 0.05);
|
|
state.camera.lookAt(0, 0, 0);
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{/* Draw Neurons */}
|
|
{neurons.map((n) => (
|
|
<NeuronCell key={n.id} position={[n.position.x, n.position.y, n.position.z]} />
|
|
))}
|
|
|
|
{/* Draw Synapses */}
|
|
{connections.map((c) => (
|
|
<Synapse key={c.key} start={c.start} end={c.end} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default function BioNeurons() {
|
|
return (
|
|
<div className="absolute inset-0 z-0 h-full w-full">
|
|
<Canvas
|
|
camera={{ position: [0, 0, 14], fov: 45 }}
|
|
gl={{ alpha: true, antialias: true }}
|
|
dpr={[1, 2]}
|
|
>
|
|
<ambientLight intensity={0.2} />
|
|
<pointLight position={[10, 10, 10]} intensity={1} color="#06b6d4" />
|
|
<NeuralNetworkScene />
|
|
<Preload all />
|
|
</Canvas>
|
|
</div>
|
|
);
|
|
} |