From 58b01331ad7aecb4d95cf5290272f6bf5d7fcbc4 Mon Sep 17 00:00:00 2001 From: schlaus Date: Sun, 8 Feb 2026 14:43:25 +0000 Subject: [PATCH] Add interactive constellation widget - 400x400 particle network --- src/App.tsx | 7 +- src/components/ConstellationWidget.tsx | 153 +++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/components/ConstellationWidget.tsx diff --git a/src/App.tsx b/src/App.tsx index b965f67..1e53690 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,17 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { useState } from "react" +import { ConstellationWidget } from "./components/ConstellationWidget" function App() { const [count, setCount] = useState(0) return ( -
+
+ {/* Creative Widget - 400x400 Interactive Constellation */} + + + {/* Hello World Card */} diff --git a/src/components/ConstellationWidget.tsx b/src/components/ConstellationWidget.tsx new file mode 100644 index 0000000..9f35ba5 --- /dev/null +++ b/src/components/ConstellationWidget.tsx @@ -0,0 +1,153 @@ +import { useEffect, useRef, useState } from 'react'; + +interface Particle { + x: number; + y: number; + vx: number; + vy: number; + radius: number; + color: string; +} + +export function ConstellationWidget() { + const canvasRef = useRef(null); + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); + const [isHovering, setIsHovering] = useState(false); + const particlesRef = useRef([]); + const animationRef = useRef(); + + const colors = [ + '#60A5FA', // blue-400 + '#A78BFA', // violet-400 + '#F472B6', // pink-400 + '#34D399', // emerald-400 + '#FBBF24', // amber-400 + ]; + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Set canvas size + canvas.width = 400; + canvas.height = 400; + + // Initialize particles + if (particlesRef.current.length === 0) { + for (let i = 0; i < 25; i++) { + particlesRef.current.push({ + x: Math.random() * 400, + y: Math.random() * 400, + vx: (Math.random() - 0.5) * 0.5, + vy: (Math.random() - 0.5) * 0.5, + radius: Math.random() * 3 + 2, + color: colors[Math.floor(Math.random() * colors.length)], + }); + } + } + + const animate = () => { + ctx.fillStyle = 'rgba(15, 23, 42, 0.1)'; + ctx.fillRect(0, 0, 400, 400); + + const particles = particlesRef.current; + + // Update and draw particles + particles.forEach((particle, i) => { + // Mouse interaction + if (isHovering) { + const dx = mousePos.x - particle.x; + const dy = mousePos.y - particle.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 100) { + particle.vx += dx * 0.0001; + particle.vy += dy * 0.0001; + } + } + + // Update position + particle.x += particle.vx; + particle.y += particle.vy; + + // Bounce off walls + if (particle.x < 0 || particle.x > 400) particle.vx *= -1; + if (particle.y < 0 || particle.y > 400) particle.vy *= -1; + + // Keep in bounds + particle.x = Math.max(0, Math.min(400, particle.x)); + particle.y = Math.max(0, Math.min(400, particle.y)); + + // Draw particle + ctx.beginPath(); + ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); + ctx.fillStyle = particle.color; + ctx.fill(); + + // Draw connections + for (let j = i + 1; j < particles.length; j++) { + const other = particles[j]; + const dx = particle.x - other.x; + const dy = particle.y - other.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 100) { + ctx.beginPath(); + ctx.moveTo(particle.x, particle.y); + ctx.lineTo(other.x, other.y); + ctx.strokeStyle = `rgba(96, 165, 250, ${0.2 * (1 - dist / 100)})`; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + }); + + animationRef.current = requestAnimationFrame(animate); + }; + + animate(); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [mousePos, isHovering]); + + const handleMouseMove = (e: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + const rect = canvas.getBoundingClientRect(); + setMousePos({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + }; + + return ( +
+
+ setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + className="rounded-2xl shadow-2xl cursor-crosshair border border-slate-700/50" + style={{ background: 'rgba(15, 23, 42, 0.8)' }} + /> +
+

+ Move your mouse to interact with particles +

+
+
+

+ ✨ Interactive Constellation Widget • {isHovering ? 'Mouse active' : 'Hover to interact'} +

+
+ ); +} \ No newline at end of file