183 lines
5.9 KiB
TypeScript
183 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useRef } from "react";
|
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
|
import * as THREE from "three";
|
|
import { Preload } from "@react-three/drei";
|
|
|
|
// CONFIGURATION
|
|
const PARTICLE_COUNT = 80;
|
|
const CONNECT_DISTANCE = 3.5;
|
|
const MOUSE_INFLUENCE_RADIUS = 6; // Radius of the mouse wave
|
|
const WAVE_AMPLITUDE = 1.5; // How high the wave ripples (Z-axis)
|
|
|
|
function NeuralMesh() {
|
|
const groupRef = useRef<THREE.Group>(null);
|
|
const particlesRef = useRef<THREE.Points>(null);
|
|
const linesRef = useRef<THREE.LineSegments>(null);
|
|
|
|
// Get viewport to map mouse coordinates correctly to world space
|
|
const { viewport } = useThree();
|
|
|
|
// 1. Generate Initial Random Positions & Velocities
|
|
// We store "original positions" to calculate the wave offset from
|
|
const [positions, velocities, originalPositions] = useMemo(() => {
|
|
const pos = new Float32Array(PARTICLE_COUNT * 3);
|
|
const origPos = new Float32Array(PARTICLE_COUNT * 3);
|
|
const vel = [];
|
|
|
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
const x = (Math.random() - 0.5) * 18;
|
|
const y = (Math.random() - 0.5) * 18;
|
|
const z = (Math.random() - 0.5) * 10;
|
|
|
|
pos[i * 3] = x;
|
|
pos[i * 3 + 1] = y;
|
|
pos[i * 3 + 2] = z;
|
|
|
|
origPos[i * 3] = x;
|
|
origPos[i * 3 + 1] = y;
|
|
origPos[i * 3 + 2] = z;
|
|
|
|
vel.push({
|
|
x: (Math.random() - 0.5) * 0.05, // Slower natural drift
|
|
y: (Math.random() - 0.5) * 0.05,
|
|
z: (Math.random() - 0.5) * 0.05,
|
|
});
|
|
}
|
|
return [pos, vel, origPos];
|
|
}, []);
|
|
|
|
useFrame((state, delta) => {
|
|
// 2. Map Mouse to World Coordinates
|
|
// state.pointer is normalized (-1 to 1). Convert to world units using viewport.
|
|
const mouseX = (state.pointer.x * viewport.width) / 2;
|
|
const mouseY = (state.pointer.y * viewport.height) / 2;
|
|
|
|
if (particlesRef.current && linesRef.current) {
|
|
const positionsAttr = particlesRef.current.geometry.attributes.position;
|
|
const currentPositions = positionsAttr.array as Float32Array;
|
|
|
|
// 3. Update Particles
|
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
const i3 = i * 3;
|
|
|
|
// A. Natural Drift (Gravity)
|
|
// We use the "original" position as an anchor to prevent them from flying away too far
|
|
// but we add the velocity to keep them moving organically.
|
|
originalPositions[i3] += velocities[i].x * delta * 2;
|
|
originalPositions[i3 + 1] += velocities[i].y * delta * 2;
|
|
|
|
// Bounce logic for the anchor points
|
|
if (Math.abs(originalPositions[i3]) > 10) velocities[i].x *= -1;
|
|
if (Math.abs(originalPositions[i3 + 1]) > 10) velocities[i].y *= -1;
|
|
|
|
// B. Mouse Interaction (The Wave)
|
|
// Calculate distance from particle to mouse
|
|
const dx = mouseX - originalPositions[i3];
|
|
const dy = mouseY - originalPositions[i3 + 1];
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// Apply Wave Effect
|
|
// If the mouse is close, we disturb the position
|
|
let xOffset = 0;
|
|
let yOffset = 0;
|
|
let zOffset = 0;
|
|
|
|
if (dist < MOUSE_INFLUENCE_RADIUS) {
|
|
// 1. Repulsion force (XY Plane) - pushes them slightly aside
|
|
const force = (MOUSE_INFLUENCE_RADIUS - dist) / MOUSE_INFLUENCE_RADIUS;
|
|
const angle = Math.atan2(dy, dx);
|
|
|
|
xOffset = -Math.cos(angle) * force * 2; // Push away X
|
|
yOffset = -Math.sin(angle) * force * 2; // Push away Y
|
|
|
|
// 2. Wave Ripple (Z Plane) - Sine wave based on distance and time
|
|
// This creates the "water ripple" effect in 3D depth
|
|
zOffset = Math.sin(dist * 1.5 - state.clock.elapsedTime * 3) * WAVE_AMPLITUDE * force;
|
|
}
|
|
|
|
// Apply calculated positions
|
|
currentPositions[i3] = originalPositions[i3] + xOffset;
|
|
currentPositions[i3 + 1] = originalPositions[i3 + 1] + yOffset;
|
|
currentPositions[i3 + 2] = originalPositions[i3 + 2] + zOffset;
|
|
}
|
|
positionsAttr.needsUpdate = true;
|
|
|
|
// 4. Update Connections (Plexus)
|
|
// Re-calculate lines based on the NEW modified positions
|
|
const linePositions: number[] = [];
|
|
|
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
for (let j = i + 1; j < PARTICLE_COUNT; j++) {
|
|
const x1 = currentPositions[i * 3];
|
|
const y1 = currentPositions[i * 3 + 1];
|
|
const z1 = currentPositions[i * 3 + 2];
|
|
|
|
const x2 = currentPositions[j * 3];
|
|
const y2 = currentPositions[j * 3 + 1];
|
|
const z2 = currentPositions[j * 3 + 2];
|
|
|
|
const dist = Math.sqrt(
|
|
Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) + Math.pow(z2 - z1, 2)
|
|
);
|
|
|
|
if (dist < CONNECT_DISTANCE) {
|
|
linePositions.push(x1, y1, z1, x2, y2, z2);
|
|
}
|
|
}
|
|
}
|
|
|
|
linesRef.current.geometry.setAttribute(
|
|
"position",
|
|
new THREE.Float32BufferAttribute(linePositions, 3)
|
|
);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<group ref={groupRef}>
|
|
<points ref={particlesRef}>
|
|
<bufferGeometry>
|
|
<bufferAttribute
|
|
attach="attributes-position"
|
|
count={PARTICLE_COUNT}
|
|
array={positions}
|
|
itemSize={3}
|
|
/>
|
|
</bufferGeometry>
|
|
<pointsMaterial
|
|
size={0.15}
|
|
color="#22d3ee" // Cyan
|
|
sizeAttenuation={true}
|
|
transparent
|
|
opacity={0.8}
|
|
/>
|
|
</points>
|
|
|
|
<lineSegments ref={linesRef}>
|
|
<bufferGeometry />
|
|
<lineBasicMaterial
|
|
color="#0891b2" // Cyan-600
|
|
transparent
|
|
opacity={0.15}
|
|
/>
|
|
</lineSegments>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default function NeuronsGravity() {
|
|
return (
|
|
<div className="absolute inset-0 z-0 h-full w-full">
|
|
<Canvas
|
|
camera={{ position: [0, 0, 12], fov: 50 }}
|
|
gl={{ alpha: true, antialias: true }}
|
|
dpr={[1, 2]}
|
|
>
|
|
<NeuralMesh />
|
|
<Preload all />
|
|
</Canvas>
|
|
</div>
|
|
);
|
|
} |