import { useEffect, useRef } from "react"; import { useTheme } from "@/contexts/ThemeContext"; /* FloatingStudyHero — Canvas-powered animated study-themed visual Features: glowing orbs, floating knowledge nodes (code, math, AI topics), animated connecting lines like a neural/knowledge graph. */ const TOPICS = [ { label: "Python", color: "#3b82f6" }, { label: "ML", color: "#8b5cf6" }, { label: "NLP", color: "#ec4899" }, { label: "∑ Math", color: "#f59e0b" }, { label: "AI", color: "#10b981" }, { label: "Code", color: "#06b6d4" }, { label: "Neural", color: "#f43f5e" }, { label: "Data", color: "#a855f7" }, { label: "GPT", color: "#22c55e" }, ]; interface Node { x: number; y: number; vx: number; vy: number; label: string; color: string; radius: number; phase: number; } export const FloatingStudyHero = () => { const canvasRef = useRef(null); const { isDark } = useTheme(); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let raf: number; let t = 0; // Build nodes const nodes: Node[] = TOPICS.map((topic, i) => ({ x: (canvas.width / (TOPICS.length + 1)) * (i + 1) + (Math.random() - 0.5) * 80, y: canvas.height / 2 + (Math.random() - 0.5) * canvas.height * 0.6, vx: (Math.random() - 0.5) * 0.4, vy: (Math.random() - 0.5) * 0.4, label: topic.label, color: topic.color, radius: 32 + Math.random() * 12, phase: Math.random() * Math.PI * 2, })); const resize = () => { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; // Reposition nodes on resize nodes.forEach((n, i) => { n.x = (canvas.width / (nodes.length + 1)) * (i + 1); n.y = canvas.height / 2 + (Math.random() - 0.5) * canvas.height * 0.5; }); }; resize(); window.addEventListener("resize", resize); const drawGlowCircle = (x: number, y: number, r: number, color: string, alpha: number) => { const grad = ctx.createRadialGradient(x, y, 0, x, y, r * 2.5); grad.addColorStop(0, color + "cc"); grad.addColorStop(0.4, color + "55"); grad.addColorStop(1, color + "00"); ctx.beginPath(); ctx.arc(x, y, r * 2.5, 0, Math.PI * 2); ctx.fillStyle = grad; ctx.globalAlpha = alpha; ctx.fill(); ctx.globalAlpha = 1; }; const drawNode = (node: Node, t: number) => { const bob = Math.sin(t * 0.8 + node.phase) * 6; const x = node.x; const y = node.y + bob; const r = node.radius; // Outer glow drawGlowCircle(x, y, r, node.color, 0.7); // Inner circle with gradient const grad = ctx.createRadialGradient(x - r * 0.3, y - r * 0.3, r * 0.1, x, y, r); grad.addColorStop(0, node.color + "ff"); grad.addColorStop(0.6, node.color + "cc"); grad.addColorStop(1, node.color + "88"); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = grad; ctx.shadowColor = node.color; ctx.shadowBlur = 20; ctx.fill(); ctx.shadowBlur = 0; // Border ring ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.strokeStyle = node.color + "ff"; ctx.lineWidth = 2; ctx.stroke(); // Label ctx.font = `bold ${Math.floor(r * 0.38)}px 'Inter', sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = "#ffffff"; ctx.shadowColor = "#00000099"; ctx.shadowBlur = 6; ctx.fillText(node.label, x, y); ctx.shadowBlur = 0; }; const drawEdge = (a: Node, b: Node, t: number) => { const dx = b.x - a.x; const dy = b.y - a.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 240) return; const alpha = (1 - dist / 240) * 0.4; const grad = ctx.createLinearGradient(a.x, a.y, b.x, b.y); grad.addColorStop(0, a.color + Math.round(alpha * 255).toString(16).padStart(2, "0")); grad.addColorStop(1, b.color + Math.round(alpha * 255).toString(16).padStart(2, "0")); ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.strokeStyle = grad; ctx.lineWidth = 1.5; ctx.setLineDash([6, 8]); ctx.lineDashOffset = -t * 15; ctx.stroke(); ctx.setLineDash([]); }; // Interaction state const mouse = { x: -1000, y: -1000, active: false }; let draggedNode: Node | null = null; const handleMouseMove = (e: MouseEvent) => { const rect = canvas.getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top; mouse.active = true; }; const handleMouseDown = () => { // Find node under mouse for (const node of nodes) { const dx = node.x - mouse.x; const dy = node.y - mouse.y; if (Math.sqrt(dx * dx + dy * dy) < node.radius * 1.5) { draggedNode = node; break; } } }; const handleMouseUp = () => { draggedNode = null; }; const handleMouseLeave = () => { mouse.active = false; }; canvas.addEventListener("mousemove", handleMouseMove); canvas.addEventListener("mousedown", handleMouseDown); window.addEventListener("mouseup", handleMouseUp); canvas.addEventListener("mouseleave", handleMouseLeave); const tick = () => { t += 0.016; ctx.clearRect(0, 0, canvas.width, canvas.height); // Background ambient blobs [[canvas.width * 0.2, canvas.height * 0.3, "#6366f1"], [canvas.width * 0.8, canvas.height * 0.7, "#10b981"], [canvas.width * 0.5, canvas.height * 0.5, "#ec4899"]].forEach(([bx, by, bc]) => { drawGlowCircle(bx as number, by as number, 80, bc as string, 0.15); }); // Physics: move nodes nodes.forEach((n) => { if (n === draggedNode) { n.x = mouse.x; n.y = mouse.y; n.vx = 0; n.vy = 0; } else { // Magnetic attraction to mouse if (mouse.active) { const dx = mouse.x - n.x; const dy = mouse.y - n.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 400) { const force = (1 - dist / 400) * 0.02; n.vx += dx * force; n.vy += dy * force; } } n.x += n.vx; n.y += n.vy; // Damping to keep things stable n.vx *= 0.98; n.vy *= 0.98; if (n.x < n.radius) { n.x = n.radius; n.vx *= -1; } if (n.x > canvas.width - n.radius) { n.x = canvas.width - n.radius; n.vx *= -1; } if (n.y < n.radius) { n.y = n.radius; n.vy *= -1; } if (n.y > canvas.height - n.radius) { n.y = canvas.height - n.radius; n.vy *= -1; } } }); // Draw edges for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { drawEdge(nodes[i], nodes[j], t); } } // Draw nodes on top nodes.forEach((n) => drawNode(n, t)); raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); canvas.removeEventListener("mousemove", handleMouseMove); canvas.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mouseup", handleMouseUp); canvas.removeEventListener("mouseleave", handleMouseLeave); }; }, [isDark]); return (
); };