Add interactive constellation widget - 400x400 particle network
Some checks failed
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-pink-900 flex items-center justify-center p-4">
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-pink-900 flex flex-col items-center justify-center p-4 gap-8">
|
||||
{/* Creative Widget - 400x400 Interactive Constellation */}
|
||||
<ConstellationWidget />
|
||||
|
||||
{/* Hello World Card */}
|
||||
<Card className="w-full max-w-md border-slate-700 bg-slate-800/50 backdrop-blur">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-4xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
|
||||
153
src/components/ConstellationWidget.tsx
Normal file
153
src/components/ConstellationWidget.tsx
Normal file
@@ -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<HTMLCanvasElement>(null);
|
||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const particlesRef = useRef<Particle[]>([]);
|
||||
const animationRef = useRef<number>();
|
||||
|
||||
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<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
setMousePos({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="relative group">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={400}
|
||||
height={400}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={() => 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)' }}
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 right-4 text-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<p className="text-xs text-slate-400 bg-slate-900/80 px-3 py-1 rounded-full inline-block">
|
||||
Move your mouse to interact with particles
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">
|
||||
✨ Interactive Constellation Widget • {isHovering ? 'Mouse active' : 'Hover to interact'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user