ML-Learner / frontend /src /components /layout /FloatingStudyHero.tsx
VashuTheGreat2's picture
Upload folder using huggingface_hub
c01955c verified
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<HTMLCanvasElement>(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 (
<div className="w-full h-full relative">
<canvas
ref={canvasRef}
className="w-full h-full"
style={{ display: "block" }}
/>
</div>
);
};