Animation Added and Gif file updated
This commit is contained in:
192
app/components/canvas/BioNeurons.tsx
Normal file
192
app/components/canvas/BioNeurons.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
110
app/components/canvas/DNAHelix.tsx
Normal file
110
app/components/canvas/DNAHelix.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DNAHelix: React.FC<Props> = ({ className }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Dimensions based on parent container
|
||||
let width = canvas.width = canvas.parentElement?.clientWidth || window.innerWidth;
|
||||
let height = canvas.height = canvas.parentElement?.clientHeight || window.innerHeight;
|
||||
|
||||
let animationFrameId: number;
|
||||
let time = 0;
|
||||
|
||||
const handleResize = () => {
|
||||
if (canvas.parentElement) {
|
||||
width = canvas.width = canvas.parentElement.clientWidth;
|
||||
height = canvas.height = canvas.parentElement.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
time += 0.02; // Rotation speed
|
||||
|
||||
const strandGap = 40; // Vertical distance between "base pairs"
|
||||
const amplitude = 50; // Width of the helix
|
||||
const frequency = 0.015; // Tightness of the loops
|
||||
|
||||
// Position helix on the right side (85% of width) to keep content clear
|
||||
const centerX = width * 0.85;
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Calculate number of points based on container height
|
||||
const numPoints = Math.ceil(height / strandGap) + 5;
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
// Scroll the helix slowly downward
|
||||
const y = i * strandGap - (time * 10) % strandGap;
|
||||
|
||||
// Calculate rotation phase
|
||||
const phase = y * frequency + time;
|
||||
|
||||
// Strand 1 (Cyan)
|
||||
const x1 = centerX + Math.sin(phase) * amplitude;
|
||||
// Strand 2 (Emerald) - Offset by PI
|
||||
const x2 = centerX + Math.sin(phase + Math.PI) * amplitude;
|
||||
|
||||
// Depth calculation for 3D effect (fade when "back")
|
||||
const depth1 = Math.cos(phase);
|
||||
const depth2 = Math.cos(phase + Math.PI);
|
||||
|
||||
// Map depth (-1 to 1) to opacity (0.2 to 0.8)
|
||||
const alpha1 = (depth1 + 1) / 2 * 0.6 + 0.2;
|
||||
const alpha2 = (depth2 + 1) / 2 * 0.6 + 0.2;
|
||||
|
||||
// Draw Connector Line (Base Pair)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y);
|
||||
ctx.lineTo(x2, y);
|
||||
ctx.strokeStyle = `rgba(34, 211, 238, 0.05)`; // Very faint connector
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Strand 1 Particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(x1, y, 4 + depth1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(34, 211, 238, ${alpha1})`; // Cyan
|
||||
ctx.fill();
|
||||
|
||||
// Draw Strand 2 Particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(x2, y, 4 + depth2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(52, 211, 153, ${alpha2})`; // Emerald
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`block w-full h-full ${className}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DNAHelix;
|
||||
123
app/components/canvas/GravityWave.tsx
Normal file
123
app/components/canvas/GravityWave.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const GravityWave: React.FC<Props> = ({ className }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Initialize dimensions based on parent container
|
||||
let width = canvas.width = canvas.parentElement?.clientWidth || window.innerWidth;
|
||||
let height = canvas.height = canvas.parentElement?.clientHeight || window.innerHeight;
|
||||
|
||||
let animationFrameId: number;
|
||||
let time = 0;
|
||||
|
||||
// Configuration
|
||||
const gap = 50;
|
||||
const rows = Math.ceil(height / gap) + 4;
|
||||
const cols = Math.ceil(width / gap) + 4;
|
||||
|
||||
// Mouse state
|
||||
const mouse = { x: -500, y: -500 };
|
||||
|
||||
const handleResize = () => {
|
||||
// Resize based on parent element
|
||||
if (canvas.parentElement) {
|
||||
width = canvas.width = canvas.parentElement.clientWidth;
|
||||
height = canvas.height = canvas.parentElement.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouse.x = e.clientX - rect.left;
|
||||
mouse.y = e.clientY - rect.top;
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
// Attach mouse move to the specific canvas/container, not window, if you want localized interaction
|
||||
// But keeping it on window usually feels smoother for "approaching" the section
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
time += 0.02;
|
||||
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.shadowColor = "rgba(34, 211, 238, 0.5)";
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
||||
gradient.addColorStop(0, "rgba(34, 211, 238, 0.4)");
|
||||
gradient.addColorStop(1, "rgba(52, 211, 153, 0.4)");
|
||||
|
||||
ctx.strokeStyle = gradient;
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
// Re-calculate rows/cols in case of resize
|
||||
const currentRows = Math.ceil(height / gap) + 4;
|
||||
const currentCols = Math.ceil(width / gap) + 4;
|
||||
|
||||
for (let iy = 0; iy < currentRows; iy++) {
|
||||
const yBase = iy * gap;
|
||||
ctx.beginPath();
|
||||
for (let ix = 0; ix < currentCols; ix++) {
|
||||
const xBase = ix * gap;
|
||||
|
||||
const waveOffset = Math.sin(ix * 0.2 + time) * 8 + Math.cos(iy * 0.3 + time) * 8;
|
||||
const dx = xBase - mouse.x;
|
||||
const dy = yBase - mouse.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const maxDist = 400;
|
||||
let gravityX = 0;
|
||||
let gravityY = 0;
|
||||
|
||||
if (dist < maxDist) {
|
||||
const force = (maxDist - dist) / maxDist;
|
||||
const power = force * 60;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
gravityX = Math.cos(angle) * power;
|
||||
gravityY = Math.sin(angle) * power;
|
||||
}
|
||||
|
||||
const xFinal = xBase + gravityX;
|
||||
const yFinal = yBase + waveOffset + gravityY;
|
||||
|
||||
if (ix === 0) ctx.moveTo(xFinal, yFinal);
|
||||
else ctx.lineTo(xFinal, yFinal);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`block w-full h-full ${className}`}
|
||||
style={{ touchAction: "none" }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GravityWave;
|
||||
65
app/components/canvas/NeuralNetwork.tsx
Normal file
65
app/components/canvas/NeuralNetwork.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useMemo } from "react";
|
||||
import { Canvas, useFrame } from "@react-three/fiber";
|
||||
import { Points, PointMaterial, Preload } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
|
||||
function Particles(props: any) {
|
||||
const ref = useRef<THREE.Points>(null!);
|
||||
|
||||
// Generate 5000 random points inside a sphere
|
||||
const sphere = useMemo(() => {
|
||||
const coords = new Float32Array(5000 * 3);
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const r = 1.2 * Math.cbrt(Math.random()); // Radius
|
||||
const theta = Math.random() * 2 * Math.PI; // Theta
|
||||
const phi = Math.acos(2 * Math.random() - 1); // Phi
|
||||
|
||||
const x = r * Math.sin(phi) * Math.cos(theta);
|
||||
const y = r * Math.sin(phi) * Math.sin(theta);
|
||||
const z = r * Math.cos(phi);
|
||||
|
||||
coords[i * 3] = x;
|
||||
coords[i * 3 + 1] = y;
|
||||
coords[i * 3 + 2] = z;
|
||||
}
|
||||
return coords;
|
||||
}, []);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (ref.current) {
|
||||
// Rotate the entire cloud slowly
|
||||
ref.current.rotation.x -= delta / 10;
|
||||
ref.current.rotation.y -= delta / 15;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group rotation={[0, 0, Math.PI / 4]}>
|
||||
<Points ref={ref} positions={sphere} stride={3} frustumCulled={false} {...props}>
|
||||
<PointMaterial
|
||||
transparent
|
||||
color="#22d3ee" // Cyan-400
|
||||
size={0.005}
|
||||
sizeAttenuation={true}
|
||||
depthWrite={false}
|
||||
opacity={0.6}
|
||||
/>
|
||||
</Points>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
const NeuralNetwork = () => {
|
||||
return (
|
||||
<div className="absolute inset-0 z-0 h-full w-full">
|
||||
<Canvas camera={{ position: [0, 0, 1] }}>
|
||||
<Particles />
|
||||
<Preload all />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NeuralNetwork;
|
||||
81
app/components/canvas/NeuronNetwork.tsx
Normal file
81
app/components/canvas/NeuronNetwork.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
// src/components/canvas/NeuronNetwork.tsx
|
||||
"use client";
|
||||
|
||||
import { useRef, useMemo } from "react";
|
||||
import { Canvas, useFrame } from "@react-three/fiber";
|
||||
import { Points, PointMaterial } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
|
||||
function NeuronParticles({ count = 200 }) {
|
||||
// Create a ref for the points to animate them
|
||||
const points = useRef<THREE.Points>(null!);
|
||||
|
||||
// Generate random positions for the neurons
|
||||
const positions = useMemo(() => {
|
||||
const pos = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Spread particles across a wide area (x, y, z)
|
||||
pos[i * 3] = (Math.random() - 0.5) * 25; // x
|
||||
pos[i * 3 + 1] = (Math.random() - 0.5) * 25; // y
|
||||
pos[i * 3 + 2] = (Math.random() - 0.5) * 10; // z
|
||||
}
|
||||
return pos;
|
||||
}, [count]);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (points.current) {
|
||||
// subtle rotation of the entire system
|
||||
points.current.rotation.x -= delta / 50;
|
||||
points.current.rotation.y -= delta / 60;
|
||||
|
||||
// Optional: Gentle wave motion could be added here
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group rotation={[0, 0, Math.PI / 4]}>
|
||||
<Points
|
||||
ref={points}
|
||||
positions={positions}
|
||||
stride={3}
|
||||
frustumCulled={false}
|
||||
>
|
||||
<PointMaterial
|
||||
transparent
|
||||
color="#22d3ee" // Cyan-400
|
||||
size={0.08}
|
||||
sizeAttenuation={true}
|
||||
depthWrite={false}
|
||||
opacity={0.6}
|
||||
/>
|
||||
</Points>
|
||||
{/* For true "connections" (lines), calculating distance between 200 points
|
||||
every frame is heavy. A visual trick is to render a second layer
|
||||
of slightly larger, fainter particles or use a custom shader.
|
||||
|
||||
However, for a clean "Neuron" look, we often just need the floating nodes
|
||||
and a faint fog, or we can use the <Line> component from drei
|
||||
if the count is low (<50).
|
||||
*/}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// A wrapper to handle the Canvas settings
|
||||
export default function NeuronNetwork() {
|
||||
return (
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Canvas
|
||||
camera={{ position: [0, 0, 10], fov: 60 }}
|
||||
gl={{ alpha: true, antialias: true }}
|
||||
dpr={[1, 2]} // Handle high-res screens
|
||||
>
|
||||
<color attach="background" args={["transparent"]} />
|
||||
<ambientLight intensity={0.5} />
|
||||
<NeuronParticles count={300} />
|
||||
{/* Fog creates depth, fading distant neurons */}
|
||||
<fog attach="fog" args={['#030712', 5, 20]} />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
app/components/canvas/NeuronsGravity.tsx
Normal file
183
app/components/canvas/NeuronsGravity.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
259
app/components/canvas/RealisticNeeurons.tsx
Normal file
259
app/components/canvas/RealisticNeeurons.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user