Constellation Grid
Constellation Grid é um componente para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.
NedDev
other
O que é
Constellation Grid é um componente para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Quatro cópias grátis por semana. Sem cartão.
Não precisa de pacotes além do que o projeto já tem
Licença
Este componente foi escrito para este catálogo. É teu para usares em trabalho de cliente e em produtos que vendas, nos termos de uso.
Constellation Grid
'use client';
import React, { useEffect, useRef, useState } from 'react';
interface Node {
x: number;
y: number;
vx: number;
vy: number;
baseX: number;
baseY: number;
radius: number;
label: string;
pulse: number;
}
export default function ConstellationGrid() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [isDarkMode, setIsDarkMode] = useState<boolean>(true);
// Sync theme preference
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
setIsDarkMode(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => setIsDarkMode(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
let animationFrameId: number;
let width = 0;
let height = 0;
// Mouse velocity & inertial tracking
const mouse = {
x: -1000,
y: -1000,
prevX: -1000,
prevY: -1000,
vx: 0,
vy: 0,
radius: 220,
};
let nodes: Node[] = [];
const handleResize = () => {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);
initNodes();
};
const handleMouseMove = (e: MouseEvent) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
};
const handleMouseLeave = () => {
mouse.x = -1000;
mouse.y = -1000;
};
const initNodes = () => {
nodes = [];
const spacing = 55; // Tighter grid density for richer visual connections
const cols = Math.ceil(width / spacing) + 1;
const rows = Math.ceil(height / spacing) + 1;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const x = i * spacing;
const y = j * spacing;
nodes.push({
x,
y,
vx: 0,
vy: 0,
baseX: x,
baseY: y,
radius: Math.random() * 1.2 + 1.2,
label: `${(i * 7).toString(16).toUpperCase()}:${(j * 11).toString(16).toUpperCase()}`,
pulse: Math.random() * Math.PI * 2,
});
}
}
};
handleResize();
window.addEventListener('resize', handleResize);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
let lastTime = performance.now();
const render = (now: number) => {
// Normalize dt across high-refresh displays
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
// Mouse velocity calculation
mouse.vx = (mouse.x - mouse.prevX) / (dt * 1000 || 1);
mouse.vy = (mouse.y - mouse.prevY) / (dt * 1000 || 1);
mouse.prevX = mouse.x;
mouse.prevY = mouse.y;
const speed = Math.sqrt(mouse.vx * mouse.vx + mouse.vy * mouse.vy);
// Color paletting for dark/light seamlessness
const bgColor = isDarkMode ? '#030407' : '#f8fafc';
const nodeColor = isDarkMode ? '255, 255, 255' : '15, 23, 42';
const accentColor = isDarkMode ? '56, 189, 248' : '2, 132, 199'; // Sky Cyan Accent
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
// Node Physics Engine (Hooke's Law Spring-Mass-Damping system)
const SPRING_K = 18; // Spring stiffness
const DAMPING = 0.82; // Velocity resistance
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
n.pulse += dt * 3;
// Mouse distance vectors
const dx = mouse.x - n.x;
const dy = mouse.y - n.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Dynamic shockwave repulsion based on cursor speed
if (dist < mouse.radius && dist > 0) {
const power = (1 - dist / mouse.radius);
const force = power * (1500 + speed * 150);
const angle = Math.atan2(dy, dx);
// Impulse force pushing node away from cursor
n.vx -= Math.cos(angle) * force * dt;
n.vy -= Math.sin(angle) * force * dt;
}
// Calculate restoring force back to home anchor point (baseX, baseY)
const homeDx = n.baseX - n.x;
const homeDy = n.baseY - n.y;
n.vx += homeDx * SPRING_K * dt;
n.vy += homeDy * SPRING_K * dt;
// Apply Damping
n.vx *= DAMPING;
n.vy *= DAMPING;
// Integrate position
n.x += n.vx * dt * 60;
n.y += n.vy * dt * 60;
}
// Draw Connections (Optimized Distance Culling)
const MAX_CONN_DIST = 75;
const MAX_CONN_DIST_SQ = MAX_CONN_DIST * MAX_CONN_DIST;
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const n2 = nodes[j];
const ndx = n.x - n2.x;
const ndy = n.y - n2.y;
const distSq = ndx * ndx + ndy * ndy;
if (distSq < MAX_CONN_DIST_SQ) {
const nDist = Math.sqrt(distSq);
const alpha = (1 - nDist / MAX_CONN_DIST) * (isDarkMode ? 0.18 : 0.08);
ctx.strokeStyle = `rgba(${nodeColor}, ${alpha})`;
ctx.lineWidth = 0.7;
ctx.beginPath();
ctx.moveTo(n.x, n.y);
ctx.lineTo(n2.x, n2.y);
ctx.stroke();
}
}
}
// Render Node Points & Interactive Highlights
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
const dx = mouse.x - n.x;
const dy = mouse.y - n.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const isNear = dist < mouse.radius;
// Node base opacity pulse
const baseAlpha = isNear ? 0.95 : 0.25 + Math.sin(n.pulse) * 0.1;
ctx.fillStyle = isNear
? `rgba(${accentColor}, ${baseAlpha})`
: `rgba(${nodeColor}, ${baseAlpha})`;
const currentRadius = isNear
? n.radius * 2.2
: n.radius + Math.sin(n.pulse) * 0.3;
ctx.beginPath();
ctx.arc(n.x, n.y, Math.max(0.5, currentRadius), 0, Math.PI * 2);
ctx.fill();
// High-tech Spatial Radar Rings on active proximity
if (dist < 90) {
const pulseRing = ((n.pulse * 20) % 30) + 4;
const ringAlpha = (1 - pulseRing / 34) * 0.4;
ctx.strokeStyle = `rgba(${accentColor}, ${ringAlpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(n.x, n.y, pulseRing, 0, Math.PI * 2);
ctx.stroke();
// Hex Coordinate Readout
ctx.font = '8px ui-monospace, SFMono-Regular, Consolas, monospace';
ctx.fillStyle = `rgba(${accentColor}, 0.85)`;
ctx.fillText(n.label, n.x + 10, n.y - 10);
}
}
animationFrameId = requestAnimationFrame(render);
};
animationFrameId = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener('resize', handleResize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
};
}, [isDarkMode]);
return (
<div className="relative w-full h-screen overflow-hidden select-none bg-slate-950 dark:bg-slate-950 light:bg-slate-50">
<canvas ref={canvasRef} className="absolute inset-0 block cursor-crosshair" />
{/* Seamless overlay title */}
<div className="relative z-10 flex h-full flex-col items-center justify-center text-center px-4 pointer-events-none mix-blend-difference text-white">
<h1 className="font-mono text-6xl md:text-9xl font-black tracking-tighter uppercase leading-none">
Constellation
</h1>
<p className="mt-4 font-mono text-xs md:text-sm max-w-lg opacity-70">
High-velocity dynamic mesh. Sweep your cursor quickly across the grid to unleash kinetic shockwaves.
</p>
</div>
</div>
);
}