Files
Skyheal/app/components/canvas/NeuronsGravity.tsx

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>
);
}