259 lines
8.1 KiB
TypeScript
259 lines
8.1 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useRef } from "react";
|
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
|
import { MeshDistortMaterial, Sphere, Preload, Float } from "@react-three/drei";
|
|
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
|
|
import * as THREE from "three";
|
|
|
|
// --- CONFIGURATION ---
|
|
const NEURON_COUNT = 15; // Keep low for high-quality geometry
|
|
const CONNECTION_DISTANCE = 6.5;
|
|
const PULSE_SPEED = 1.8;
|
|
|
|
// 1. REALISTIC CELL (Soma + Nucleus)
|
|
function NeuronCell({ position }: { position: [number, number, number] }) {
|
|
const membraneRef = useRef<THREE.Mesh>(null!);
|
|
const nucleusRef = useRef<THREE.Mesh>(null!);
|
|
|
|
// Randomize biology
|
|
const size = useMemo(() => 0.5 + Math.random() * 0.3, []);
|
|
|
|
useFrame((state) => {
|
|
const t = state.clock.getElapsedTime();
|
|
|
|
// Biological breathing/pulsing
|
|
if (membraneRef.current) {
|
|
const scale = size + Math.sin(t * 2 + position[0]) * 0.05;
|
|
membraneRef.current.scale.setScalar(scale);
|
|
}
|
|
|
|
// Nucleus gentle pulse (faster)
|
|
if (nucleusRef.current) {
|
|
const nScale = (size * 0.4) + Math.sin(t * 4) * 0.02;
|
|
nucleusRef.current.scale.setScalar(nScale);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<Float speed={1.5} rotationIntensity={0.6} floatIntensity={0.8}>
|
|
<group position={position}>
|
|
|
|
{/* A. The Membrane (Outer Shell) */}
|
|
<Sphere ref={membraneRef} args={[1, 64, 64]}>
|
|
<MeshDistortMaterial
|
|
color="#083344" // Deep blue-black (organic tissue)
|
|
emissive="#155e75" // Cyan glow from within
|
|
emissiveIntensity={0.2}
|
|
roughness={0.1}
|
|
metalness={0.8}
|
|
distort={0.5} // High distortion for "blobby" look
|
|
speed={2}
|
|
transparent
|
|
opacity={0.7}
|
|
/>
|
|
</Sphere>
|
|
|
|
{/* B. The Nucleus (Inner Core) */}
|
|
<Sphere ref={nucleusRef} args={[1, 32, 32]}>
|
|
<meshStandardMaterial
|
|
color="#a5f3fc" // Bright Cyan
|
|
emissive="#22d3ee" // Strong Glow
|
|
emissiveIntensity={2} // Push this high for Bloom to pick it up
|
|
toneMapped={false}
|
|
/>
|
|
<pointLight distance={4} intensity={2} color="#22d3ee" decay={2} />
|
|
</Sphere>
|
|
|
|
</group>
|
|
</Float>
|
|
);
|
|
}
|
|
|
|
// 2. SYNAPSE (Axon + Electric Pulse)
|
|
function Synapse({ start, end }: { start: THREE.Vector3; end: THREE.Vector3 }) {
|
|
const curve = useMemo(() => {
|
|
// Create organic curve
|
|
const mid = new THREE.Vector3().lerpVectors(start, end, 0.5);
|
|
// Randomize control point for "tendon" shape
|
|
mid.add(new THREE.Vector3(
|
|
(Math.random() - 0.5) * 3,
|
|
(Math.random() - 0.5) * 3,
|
|
(Math.random() - 0.5) * 3
|
|
));
|
|
return new THREE.QuadraticBezierCurve3(start, mid, end);
|
|
}, [start, end]);
|
|
|
|
const points = useMemo(() => curve.getPoints(30), [curve]);
|
|
const impulseRef = useRef<THREE.Mesh>(null!);
|
|
|
|
useFrame((state) => {
|
|
// Move electric pulse
|
|
const t = (state.clock.getElapsedTime() * PULSE_SPEED) % 1;
|
|
if (impulseRef.current) {
|
|
const pos = curve.getPointAt(t);
|
|
impulseRef.current.position.copy(pos);
|
|
|
|
// Stretch effect for speed illusion
|
|
const tangent = curve.getTangent(t).normalize();
|
|
impulseRef.current.lookAt(pos.clone().add(tangent));
|
|
impulseRef.current.scale.set(0.6, 0.6, 2.5); // Stretch Z
|
|
}
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{/* The physical connection (Axon) */}
|
|
<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="#0e7490" transparent opacity={0.15} />
|
|
</line>
|
|
|
|
{/* The Traveling Spark (Electricity) */}
|
|
<mesh ref={impulseRef}>
|
|
<sphereGeometry args={[0.08, 8, 8]} />
|
|
<meshBasicMaterial color="#ccfbf1" />
|
|
<pointLight distance={3} intensity={3} color="#22d3ee" decay={2} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// 3. BACKGROUND PARTICLES (Neurotransmitters)
|
|
function NeuroDust() {
|
|
const count = 200;
|
|
const mesh = useRef<THREE.InstancedMesh>(null!);
|
|
|
|
const particles = useMemo(() => {
|
|
const temp = [];
|
|
for(let i=0; i<count; i++) {
|
|
const t = Math.random() * 100;
|
|
const factor = 20 + Math.random() * 10;
|
|
const speed = 0.01 + Math.random() / 200;
|
|
const x = (Math.random() - 0.5) * 30;
|
|
const y = (Math.random() - 0.5) * 30;
|
|
const z = (Math.random() - 0.5) * 15;
|
|
temp.push({ t, factor, speed, x, y, z, mx: 0, my: 0 });
|
|
}
|
|
return temp;
|
|
}, []);
|
|
|
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
|
|
|
useFrame((state) => {
|
|
particles.forEach((particle, i) => {
|
|
let { t, factor, speed, x, y, z } = particle;
|
|
t = particle.t += speed / 2;
|
|
const a = Math.cos(t) + Math.sin(t * 1) / 10;
|
|
const b = Math.sin(t) + Math.cos(t * 2) / 10;
|
|
const s = Math.cos(t);
|
|
|
|
dummy.position.set(
|
|
x + Math.cos(t) + Math.sin(t) * 2,
|
|
y + Math.sin(t) + Math.cos(t) * 2,
|
|
z + Math.cos(t)
|
|
);
|
|
dummy.scale.setScalar(s * 0.03); // Tiny dust
|
|
dummy.rotation.set(s * 5, s * 5, s * 5);
|
|
dummy.updateMatrix();
|
|
|
|
if (mesh.current) mesh.current.setMatrixAt(i, dummy.matrix);
|
|
});
|
|
if (mesh.current) mesh.current.instanceMatrix.needsUpdate = true;
|
|
});
|
|
|
|
return (
|
|
<instancedMesh ref={mesh} args={[undefined, undefined, count]}>
|
|
<dodecahedronGeometry args={[0.2, 0]} />
|
|
<meshPhongMaterial color="#0891b2" emissive="#06b6d4" transparent opacity={0.4} />
|
|
</instancedMesh>
|
|
);
|
|
}
|
|
|
|
// 4. MAIN SCENE
|
|
function Scene() {
|
|
const { viewport } = useThree();
|
|
|
|
// Generate random positions for neurons
|
|
const neurons = useMemo(() => {
|
|
const temp = [];
|
|
for (let i = 0; i < NEURON_COUNT; i++) {
|
|
temp.push({
|
|
position: new THREE.Vector3(
|
|
(Math.random() - 0.5) * 22,
|
|
(Math.random() - 0.5) * 22,
|
|
(Math.random() - 0.5) * 10
|
|
),
|
|
id: i
|
|
});
|
|
}
|
|
return temp;
|
|
}, []);
|
|
|
|
// Connect them
|
|
const connections = useMemo(() => {
|
|
const conns = [];
|
|
for(let i=0; i<neurons.length; i++) {
|
|
for(let j=i+1; j<neurons.length; j++) {
|
|
if(neurons[i].position.distanceTo(neurons[j].position) < CONNECTION_DISTANCE) {
|
|
conns.push({ start: neurons[i].position, end: neurons[j].position, key: `${i}-${j}` });
|
|
}
|
|
}
|
|
}
|
|
return conns;
|
|
}, [neurons]);
|
|
|
|
useFrame((state) => {
|
|
// Subtle Camera Parallax based on Mouse
|
|
const x = (state.pointer.x * viewport.width) / 12;
|
|
const y = (state.pointer.y * viewport.height) / 12;
|
|
state.camera.position.x = THREE.MathUtils.lerp(state.camera.position.x, x, 0.02);
|
|
state.camera.position.y = THREE.MathUtils.lerp(state.camera.position.y, y, 0.02);
|
|
state.camera.lookAt(0,0,0);
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<color attach="background" args={["#020617"]} /> {/* Match gray-950 */}
|
|
<ambientLight intensity={0.2} />
|
|
<pointLight position={[10, 10, 10]} intensity={1} color="#22d3ee" />
|
|
|
|
{neurons.map(n => <NeuronCell key={n.id} position={[n.position.x, n.position.y, n.position.z]} />)}
|
|
{connections.map(c => <Synapse key={c.key} start={c.start} end={c.end} />)}
|
|
<NeuroDust />
|
|
|
|
{/* POST PROCESSING - The Secret Sauce for Realism */}
|
|
<EffectComposer disableNormalPass>
|
|
<Bloom
|
|
luminanceThreshold={1} // Only very bright things glow
|
|
mipmapBlur
|
|
intensity={1.5} // Strength of the glow
|
|
radius={0.6}
|
|
/>
|
|
<Vignette eskil={false} offset={0.1} darkness={1.1} />
|
|
</EffectComposer>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function RealisticNeurons() {
|
|
return (
|
|
<div className="absolute inset-0 z-0 h-full w-full">
|
|
<Canvas
|
|
camera={{ position: [0, 0, 16], fov: 40 }}
|
|
gl={{ alpha: false, antialias: false, toneMapping: THREE.ReinhardToneMapping }}
|
|
dpr={[1, 1.5]} // Limit DPR for performance with post-processing
|
|
>
|
|
<Scene />
|
|
<Preload all />
|
|
</Canvas>
|
|
</div>
|
|
);
|
|
} |