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 ( {nodes.map((node, i) => ( { meshRefs.current[i] = el; }} position={node.position} > ))} ); } /* ─── 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 ( {lines.map((line, i) => ( ))} ); } /* ─── 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([]); // 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 ( {Array.from({ length: PACKET_COUNT }, (_, i) => ( {/* Core */} { meshRefs.current[i] = el; }} > {/* Glow */} { glowRefs.current[i] = el; }} > ))} ); } /* ─── Deploy burst — periodic flare on a random node ─── */ function DeployBursts({ nodes }: { nodes: GridNode[] }) { const ringRef = useRef(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 ( ); } /* ─── Full scene ─── */ function ComputeMesh() { const groupRef = useRef(null); const nodes = useMemo(() => generateGridNodes(), []); const edges = useMemo(() => generateEdges(nodes), [nodes]); // Static orientation — no spinning return ( ); } export function ComputeMeshScene() { return (
); }