mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-27 12:44:13 +00:00
362 lines
9.1 KiB
TypeScript
362 lines
9.1 KiB
TypeScript
import { useRef, useMemo } from "react";
|
|
import { Canvas, useFrame } from "@react-three/fiber";
|
|
import { Float } from "@react-three/drei";
|
|
import * as THREE from "three";
|
|
|
|
const NODE_COUNT = 32;
|
|
const GRID_SPACING = 1.1;
|
|
const EDGE_MAX_DIST = 1.6;
|
|
const PACKET_COUNT = 8;
|
|
|
|
interface GridNode {
|
|
position: THREE.Vector3;
|
|
phase: number;
|
|
baseScale: number;
|
|
}
|
|
|
|
interface Edge {
|
|
from: number;
|
|
to: number;
|
|
}
|
|
|
|
interface Packet {
|
|
edgeIndex: number;
|
|
progress: number;
|
|
speed: number;
|
|
direction: 1 | -1;
|
|
}
|
|
|
|
/* Generate nodes on a gently curved grid */
|
|
function generateGridNodes(): GridNode[] {
|
|
const nodes: GridNode[] = [];
|
|
const cols = 6;
|
|
const rows = 5;
|
|
|
|
for (let row = 0; row < rows; row++) {
|
|
// Offset every other row for a hex-like feel
|
|
const colsInRow = row % 2 === 0 ? cols : cols - 1;
|
|
const offsetX = row % 2 === 0 ? 0 : GRID_SPACING * 0.5;
|
|
|
|
for (let col = 0; col < colsInRow; col++) {
|
|
if (nodes.length >= NODE_COUNT) break;
|
|
|
|
const x =
|
|
(col - (colsInRow - 1) / 2) * GRID_SPACING + offsetX;
|
|
const z = (row - (rows - 1) / 2) * (GRID_SPACING * 0.85);
|
|
|
|
// Gentle curve — bowl shape
|
|
const dist = Math.sqrt(x * x + z * z);
|
|
const y = dist * dist * 0.03;
|
|
|
|
// Slight random jitter
|
|
const jx = (Math.random() - 0.5) * 0.15;
|
|
const jz = (Math.random() - 0.5) * 0.15;
|
|
|
|
nodes.push({
|
|
position: new THREE.Vector3(x + jx, y, z + jz),
|
|
phase: Math.random() * Math.PI * 2,
|
|
baseScale: 0.03 + Math.random() * 0.015,
|
|
});
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
/* Generate edges between nearby nodes */
|
|
function generateEdges(nodes: GridNode[]): Edge[] {
|
|
const edges: Edge[] = [];
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
for (let j = i + 1; j < nodes.length; j++) {
|
|
if (
|
|
nodes[i].position.distanceTo(nodes[j].position) < EDGE_MAX_DIST
|
|
) {
|
|
edges.push({ from: i, to: j });
|
|
}
|
|
}
|
|
}
|
|
return edges;
|
|
}
|
|
|
|
/* ─── Grid node (sphere with pulse) ─── */
|
|
function GridNodes({ nodes }: { nodes: GridNode[] }) {
|
|
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
|
|
|
|
useFrame(({ clock }) => {
|
|
const t = clock.getElapsedTime();
|
|
meshRefs.current.forEach((mesh, i) => {
|
|
if (!mesh) return;
|
|
const node = nodes[i];
|
|
const pulse = Math.sin(t * 1.5 + node.phase);
|
|
const mat = mesh.material as THREE.MeshBasicMaterial;
|
|
mat.opacity = 0.25 + 0.35 * Math.max(0, pulse);
|
|
const s =
|
|
node.baseScale * (1 + 0.4 * Math.max(0, pulse));
|
|
mesh.scale.setScalar(s / node.baseScale);
|
|
});
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{nodes.map((node, i) => (
|
|
<mesh
|
|
key={i}
|
|
ref={(el) => {
|
|
meshRefs.current[i] = el;
|
|
}}
|
|
position={node.position}
|
|
>
|
|
<sphereGeometry args={[node.baseScale, 10, 10]} />
|
|
<meshBasicMaterial
|
|
color="#d4d4d8"
|
|
transparent
|
|
opacity={0.3}
|
|
/>
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/* ─── Connection lines between nodes ─── */
|
|
function ConnectionLines({
|
|
nodes,
|
|
edges,
|
|
}: {
|
|
nodes: GridNode[];
|
|
edges: Edge[];
|
|
}) {
|
|
const lines = useMemo(() => {
|
|
return edges.map((edge) => {
|
|
const geo = new THREE.BufferGeometry().setFromPoints([
|
|
nodes[edge.from].position,
|
|
nodes[edge.to].position,
|
|
]);
|
|
const mat = new THREE.LineBasicMaterial({
|
|
color: "#a1a1aa",
|
|
transparent: true,
|
|
opacity: 0.06,
|
|
});
|
|
return new THREE.Line(geo, mat);
|
|
});
|
|
}, [nodes, edges]);
|
|
|
|
return (
|
|
<group>
|
|
{lines.map((line, i) => (
|
|
<primitive key={i} object={line} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/* ─── Light packets traveling along edges ─── */
|
|
function Packets({
|
|
nodes,
|
|
edges,
|
|
}: {
|
|
nodes: GridNode[];
|
|
edges: Edge[];
|
|
}) {
|
|
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
|
|
const glowRefs = useRef<(THREE.Mesh | null)[]>([]);
|
|
const packetsRef = useRef<Packet[]>([]);
|
|
|
|
// Initialize packets
|
|
if (packetsRef.current.length === 0 && edges.length > 0) {
|
|
packetsRef.current = Array.from(
|
|
{ length: PACKET_COUNT },
|
|
() => ({
|
|
edgeIndex: Math.floor(Math.random() * edges.length),
|
|
progress: Math.random(),
|
|
speed: 0.003 + Math.random() * 0.004,
|
|
direction: (Math.random() > 0.5 ? 1 : -1) as 1 | -1,
|
|
}),
|
|
);
|
|
}
|
|
|
|
useFrame(() => {
|
|
packetsRef.current.forEach((packet, i) => {
|
|
const mesh = meshRefs.current[i];
|
|
const glow = glowRefs.current[i];
|
|
if (!mesh || !glow) return;
|
|
|
|
// Advance
|
|
packet.progress += packet.speed * packet.direction;
|
|
|
|
// When reaching end, jump to a connected edge
|
|
if (packet.progress > 1 || packet.progress < 0) {
|
|
const currentEdge = edges[packet.edgeIndex];
|
|
const endNode =
|
|
packet.direction === 1
|
|
? currentEdge.to
|
|
: currentEdge.from;
|
|
|
|
// Find edges connected to end node
|
|
const connected = edges
|
|
.map((e, idx) => ({ e, idx }))
|
|
.filter(
|
|
({ e, idx }) =>
|
|
idx !== packet.edgeIndex &&
|
|
(e.from === endNode || e.to === endNode),
|
|
);
|
|
|
|
if (connected.length > 0) {
|
|
const next =
|
|
connected[Math.floor(Math.random() * connected.length)];
|
|
packet.edgeIndex = next.idx;
|
|
packet.direction =
|
|
next.e.from === endNode ? 1 : -1;
|
|
packet.progress = packet.direction === 1 ? 0 : 1;
|
|
} else {
|
|
packet.direction *= -1 as 1 | -1;
|
|
packet.progress = THREE.MathUtils.clamp(
|
|
packet.progress,
|
|
0,
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Position along edge
|
|
const edge = edges[packet.edgeIndex];
|
|
const from = nodes[edge.from].position;
|
|
const to = nodes[edge.to].position;
|
|
const pos = new THREE.Vector3().lerpVectors(
|
|
from,
|
|
to,
|
|
packet.progress,
|
|
);
|
|
|
|
mesh.position.copy(pos);
|
|
glow.position.copy(pos);
|
|
});
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{Array.from({ length: PACKET_COUNT }, (_, i) => (
|
|
<group key={i}>
|
|
{/* Core */}
|
|
<mesh
|
|
ref={(el) => {
|
|
meshRefs.current[i] = el;
|
|
}}
|
|
>
|
|
<sphereGeometry args={[0.02, 8, 8]} />
|
|
<meshBasicMaterial
|
|
color="#ffffff"
|
|
transparent
|
|
opacity={0.7}
|
|
/>
|
|
</mesh>
|
|
{/* Glow */}
|
|
<mesh
|
|
ref={(el) => {
|
|
glowRefs.current[i] = el;
|
|
}}
|
|
>
|
|
<sphereGeometry args={[0.06, 8, 8]} />
|
|
<meshBasicMaterial
|
|
color="#d4d4d8"
|
|
transparent
|
|
opacity={0.08}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/* ─── Deploy burst — periodic flare on a random node ─── */
|
|
function DeployBursts({ nodes }: { nodes: GridNode[] }) {
|
|
const ringRef = useRef<THREE.Mesh>(null);
|
|
const burstNodeRef = useRef(0);
|
|
const lastBurstRef = useRef(0);
|
|
|
|
useFrame(({ clock }) => {
|
|
const t = clock.getElapsedTime();
|
|
|
|
// Trigger a burst every ~5 seconds
|
|
if (t - lastBurstRef.current > 5) {
|
|
burstNodeRef.current = Math.floor(
|
|
Math.random() * nodes.length,
|
|
);
|
|
lastBurstRef.current = t;
|
|
}
|
|
|
|
if (ringRef.current) {
|
|
const elapsed = t - lastBurstRef.current;
|
|
const node = nodes[burstNodeRef.current];
|
|
ringRef.current.position.copy(node.position);
|
|
|
|
if (elapsed < 1.5) {
|
|
const scale = 1 + elapsed * 2;
|
|
ringRef.current.scale.setScalar(scale);
|
|
const mat = ringRef.current
|
|
.material as THREE.MeshBasicMaterial;
|
|
mat.opacity = 0.12 * (1 - elapsed / 1.5);
|
|
} else {
|
|
const mat = ringRef.current
|
|
.material as THREE.MeshBasicMaterial;
|
|
mat.opacity = 0;
|
|
}
|
|
}
|
|
});
|
|
|
|
return (
|
|
<mesh ref={ringRef} rotation={[-Math.PI / 2, 0, 0]}>
|
|
<ringGeometry args={[0.08, 0.1, 24]} />
|
|
<meshBasicMaterial
|
|
color="#d4d4d8"
|
|
transparent
|
|
opacity={0}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
);
|
|
}
|
|
|
|
/* ─── Full scene ─── */
|
|
function ComputeMesh() {
|
|
const groupRef = useRef<THREE.Group>(null);
|
|
|
|
const nodes = useMemo(() => generateGridNodes(), []);
|
|
const edges = useMemo(() => generateEdges(nodes), [nodes]);
|
|
|
|
// Static orientation — no spinning
|
|
|
|
return (
|
|
<Float speed={0.5} rotationIntensity={0.01} floatIntensity={0.04}>
|
|
<group ref={groupRef}>
|
|
<GridNodes nodes={nodes} />
|
|
<ConnectionLines nodes={nodes} edges={edges} />
|
|
<Packets nodes={nodes} edges={edges} />
|
|
<DeployBursts nodes={nodes} />
|
|
</group>
|
|
</Float>
|
|
);
|
|
}
|
|
|
|
export function ComputeMeshScene() {
|
|
return (
|
|
<div className="w-full h-[550px] -mt-[400px]">
|
|
<Canvas
|
|
camera={{ position: [0, 3, 3.5], fov: 38 }}
|
|
dpr={[1, 2]}
|
|
gl={{
|
|
antialias: true,
|
|
alpha: true,
|
|
toneMapping: THREE.ACESFilmicToneMapping,
|
|
toneMappingExposure: 1,
|
|
}}
|
|
style={{ background: "transparent" }}
|
|
>
|
|
<ambientLight intensity={0.1} />
|
|
<ComputeMesh />
|
|
</Canvas>
|
|
</div>
|
|
);
|
|
}
|