Todos os componentes

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.

Abrir no catálogo

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

components/ui/constellation-grid.tsx
    '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>
        );
    }

Componentes parecidos