Todos os componentes

Helix Chrono Matrix

Helix Chrono Matrix é um componente de fundo para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.

NedDev backgrounds

O que é

Helix Chrono Matrix é um componente de fundo 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

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i lucide-react

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.

Helix Chrono Matrix

components/ui/helix-chrono-matrix.tsx
'use client';

import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Play, Pause } from 'lucide-react';
import { cn } from '@/lib/utils';

interface FiberPoint {
    x: number;
    y: number;
    baseY: number;
    vy: number;
    excitation: number;
}

interface FiberRing {
    points: FiberPoint[];
    radius: number;
    baseRadius: number;
    yOffset: number;
    rotationSpeed: number;
    angle: number;
    harmonicOffset: number;
}

interface Particle {
    ringIndex: number;
    progress: number; // 0 to 1 along the ring
    speed: number;
    size: number;
}

export interface HelixChronoMatrixProps {
    headline?: string;
    className?: string;
}

type TopologyMode = 'DOUBLE_HELIX' | 'NEURAL_STRATA' | 'QUANTUM_RIBBONS';

export function HelixChronoMatrix({
    headline = "STRATA",
    className = "",
}: HelixChronoMatrixProps) {
    const containerRef = useRef<HTMLDivElement | null>(null);
    const canvasRef = useRef<HTMLCanvasElement | null>(null);

    const [isDarkMode, setIsDarkMode] = useState(true);
    const [isRunning, setIsRunning] = useState(true);
    const [topology, setTopology] = useState<TopologyMode>('DOUBLE_HELIX');

    // Sync theme
    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);
    }, []);

    // Smooth pointer ref
    const pointerRef = useRef({
        x: -2000,
        y: -2000,
        targetX: -2000,
        targetY: -2000,
        radius: 220,
    });

    const ringsRef = useRef<FiberRing[]>([]);
    const particlesRef = useRef<Particle[]>([]);
    const dimensionsRef = useRef({ width: 0, height: 0 });
    const topologyTransitionRef = useRef({ progress: 1, from: 'DOUBLE_HELIX' as TopologyMode, to: 'DOUBLE_HELIX' as TopologyMode });

    // Initialize stratified 3D ribbon fibers and traveling particles
    const initTopology = useCallback((width: number, height: number) => {
        const rings: FiberRing[] = [];
        const ringCount = 28;
        const pointsPerRing = 120;

        for (let r = 0; r < ringCount; r++) {
            const progress = r / ringCount;
            const points: FiberPoint[] = [];
            const baseRadius = Math.min(width, height) * 0.35 * (0.4 + progress * 0.6);
            const yOffset = (progress - 0.5) * (height * 0.45);

            for (let p = 0; p < pointsPerRing; p++) {
                points.push({
                    x: 0,
                    y: 0,
                    baseY: yOffset,
                    vy: 0,
                    excitation: 0,
                });
            }

            rings.push({
                points,
                radius: baseRadius,
                baseRadius,
                yOffset,
                rotationSpeed: (r % 2 === 0 ? 1 : -1) * (0.002 + (r / ringCount) * 0.0025),
                angle: (r * Math.PI) / ringCount,
                harmonicOffset: r * 0.2,
            });
        }

        ringsRef.current = rings;

        // Initialize moving particles along the lines
        const particles: Particle[] = [];
        const particleCount = 45;
        for (let i = 0; i < particleCount; i++) {
            particles.push({
                ringIndex: Math.floor(Math.random() * ringCount),
                progress: Math.random(),
                speed: (Math.random() * 0.003 + 0.001) * (Math.random() > 0.5 ? 1 : -1),
                size: Math.random() * 1.5 + 1.5,
            });
        }
        particlesRef.current = particles;
    }, []);

    // Canvas Resize Observer
    useEffect(() => {
        const container = containerRef.current;
        const canvas = canvasRef.current;
        if (!container || !canvas) return;

        const ctx = canvas.getContext('2d', { alpha: false });
        if (!ctx) return;

        const resizeObserver = new ResizeObserver((entries) => {
            for (const entry of entries) {
                const rect = entry.contentRect;
                const dpr = Math.min(window.devicePixelRatio || 1, 2);

                dimensionsRef.current = { width: rect.width, height: rect.height };
                canvas.width = Math.floor(rect.width * dpr);
                canvas.height = Math.floor(rect.height * dpr);
                canvas.style.width = `${rect.width}px`;
                canvas.style.height = `${rect.height}px`;

                ctx.setTransform(1, 0, 0, 1, 0, 0);
                ctx.scale(dpr, dpr);
                initTopology(rect.width, rect.height);
            }
        });

        resizeObserver.observe(container);
        return () => resizeObserver.disconnect();
    }, [initTopology]);

    // Handle topology transition
    const handleTopologyChange = (newMode: TopologyMode) => {
        if (newMode === topology) return;
        topologyTransitionRef.current = {
            progress: 0,
            from: topology,
            to: newMode,
        };
        setTopology(newMode);
    };

    // Main Render & Smooth Physics Loop
    useEffect(() => {
        const canvas = canvasRef.current;
        if (!canvas) return;

        const ctx = canvas.getContext('2d', { alpha: false });
        if (!ctx) return;

        let animId = 0;
        let time = 0;

        const render = () => {
            if (!isRunning) {
                animId = requestAnimationFrame(render);
                return;
            }

            time += 0.012;
            const { width, height } = dimensionsRef.current;
            const pointer = pointerRef.current;
            const rings = ringsRef.current;
            const particles = particlesRef.current;
            const trans = topologyTransitionRef.current;

            if (trans.progress < 1) {
                trans.progress = Math.min(1, trans.progress + 0.05);
            }

            // Silky smooth mouse interpolation (Lerp)
            pointer.x += (pointer.targetX - pointer.x) * 0.1;
            pointer.y += (pointer.targetY - pointer.y) * 0.1;

            const isDark = document.documentElement.classList.contains('dark') || isDarkMode;
            const bgColor = isDark ? '#090a0f' : '#f8fafc';
            const strokeBase = isDark ? '255, 255, 255' : '15, 23, 42';

            ctx.fillStyle = bgColor;
            ctx.fillRect(0, 0, width, height);

            const centerX = width / 2;
            const centerY = height / 2;

            // Render fibers
            for (let rIdx = 0; rIdx < rings.length; rIdx++) {
                const ring = rings[rIdx];
                ring.angle += ring.rotationSpeed;

                const points = ring.points;
                const numPoints = points.length;

                ctx.beginPath();
                let firstProjX = 0;
                let firstProjY = 0;
                let avgExcitation = 0;

                for (let pIdx = 0; pIdx < numPoints; pIdx++) {
                    const pt = points[pIdx];
                    const theta = (pIdx / numPoints) * Math.PI * 2 + ring.angle;

                    const getPos = (mode: TopologyMode) => {
                        let x = Math.cos(theta) * ring.radius;
                        let z = Math.sin(theta) * ring.radius;
                        let y = ring.yOffset;

                        if (mode === 'DOUBLE_HELIX') {
                            y += Math.sin(theta * 2 + time * 2 + ring.harmonicOffset) * 45;
                        } else if (mode === 'NEURAL_STRATA') {
                            x += Math.sin(y * 0.02 + time * 1.5) * 35;
                            y += Math.cos(theta * 3 + time) * 30;
                        } else {
                            x *= 1 + Math.sin(theta * 4 + time * 1.2) * 0.15;
                            y += Math.sin(x * 0.008 + time * 2) * 50;
                        }
                        return { x, y, z };
                    };

                    const posFrom = getPos(trans.from);
                    const posTo = getPos(trans.to);
                    const easeProgress = trans.progress < 0.5
                        ? 2 * trans.progress * trans.progress
                        : -1 + (4 - 2 * trans.progress) * trans.progress;

                    const x3D = posFrom.x + (posTo.x - posFrom.x) * easeProgress;
                    const y3D = posFrom.y + (posTo.y - posFrom.y) * easeProgress;
                    const z3D = posFrom.z + (posTo.z - posFrom.z) * easeProgress;

                    const fov = 600;
                    const cameraDist = 550;
                    const scale = fov / (cameraDist + z3D);

                    const projX = centerX + x3D * scale;
                    const projY = centerY + (y3D + pt.vy) * scale;

                    // Smooth pointer attraction field
                    const dx = projX - pointer.x;
                    const dy = projY - pointer.y;
                    const dist = Math.sqrt(dx * dx + dy * dy);

                    if (dist < pointer.radius && dist > 0) {
                        const ratio = 1 - dist / pointer.radius;
                        const targetVy = Math.sin(theta + time) * ratio * 15;
                        pt.vy += (targetVy - pt.vy) * 0.1;
                        pt.excitation = Math.max(pt.excitation, ratio);
                    } else {
                        pt.vy *= 0.92;
                    }

                    pt.excitation *= 0.92;
                    avgExcitation += pt.excitation;

                    if (pIdx === 0) {
                        firstProjX = projX;
                        firstProjY = projY;
                        ctx.moveTo(projX, projY);
                    } else {
                        ctx.lineTo(projX, projY);
                    }
                }

                ctx.lineTo(firstProjX, firstProjY);

                avgExcitation /= numPoints;
                const depthAlpha = 0.15 + (rIdx / rings.length) * 0.45;
                const isExcited = avgExcitation > 0.05;

                if (isExcited) {
                    ctx.strokeStyle = isDark
                        ? `rgba(255, 255, 255, ${Math.min(1, 0.4 + avgExcitation * 0.6)})`
                        : `rgba(0, 0, 0, ${Math.min(1, 0.4 + avgExcitation * 0.6)})`;
                    ctx.lineWidth = 1.2 + avgExcitation * 1.5;
                } else {
                    ctx.strokeStyle = `rgba(${strokeBase}, ${depthAlpha * 0.6})`;
                    ctx.lineWidth = 0.75;
                }

                ctx.stroke();
            }

            // Render Traveling Points along the Lines (Black normally, White when hovered/excited)
            for (let i = 0; i < particles.length; i++) {
                const p = particles[i];
                p.progress = (p.progress + p.speed + 1) % 1;

                const ring = rings[p.ringIndex];
                if (!ring) continue;

                const numPoints = ring.points.length;
                const exactIndex = p.progress * numPoints;
                const pIdx1 = Math.floor(exactIndex) % numPoints;
                const pIdx2 = (pIdx1 + 1) % numPoints;
                const blend = exactIndex - Math.floor(exactIndex);

                const theta1 = (pIdx1 / numPoints) * Math.PI * 2 + ring.angle;
                const theta2 = (pIdx2 / numPoints) * Math.PI * 2 + ring.angle;

                // Simple interpolation for particle positioning
                const x1 = Math.cos(theta1) * ring.radius;
                const z1 = Math.sin(theta1) * ring.radius;
                const x2 = Math.cos(theta2) * ring.radius;
                const z2 = Math.sin(theta2) * ring.radius;

                const x3D = x1 + (x2 - x1) * blend;
                const z3D = z1 + (z2 - z1) * blend;
                const y3D = ring.yOffset;

                const fov = 600;
                const cameraDist = 550;
                const scale = fov / (cameraDist + z3D);

                const projX = centerX + x3D * scale;
                const projY = centerY + y3D * scale;

                // Check proximity to pointer for hover color inversion (Black -> White)
                const dx = projX - pointer.x;
                const dy = projY - pointer.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                const isNearHover = dist < pointer.radius;

                ctx.beginPath();
                ctx.arc(projX, projY, p.size * scale, 0, Math.PI * 2);

                if (isNearHover) {
                    // Inverted to white (or high contrast) when hovered
                    ctx.fillStyle = isDark ? '#ffffff' : '#000000';
                } else {
                    // Standard color opposite to lines (Black by default)
                    ctx.fillStyle = isDark ? '#000000' : '#ffffff';
                }
                ctx.fill();

                // Optional soft border outline for high definition clarity
                ctx.lineWidth = 0.5;
                ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)';
                ctx.stroke();
            }

            animId = requestAnimationFrame(render);
        };

        animId = requestAnimationFrame(render);
        return () => cancelAnimationFrame(animId);
    }, [isRunning, topology, isDarkMode]);

    const handlePointerMove = (e: React.MouseEvent<HTMLDivElement>) => {
        const container = containerRef.current;
        if (!container) return;
        const rect = container.getBoundingClientRect();
        pointerRef.current.targetX = e.clientX - rect.left;
        pointerRef.current.targetY = e.clientY - rect.top;
    };

    const handlePointerLeave = () => {
        pointerRef.current.targetX = -2000;
        pointerRef.current.targetY = -2000;
    };

    return (
        <div
            ref={containerRef}
            onMouseMove={handlePointerMove}
            onMouseLeave={handlePointerLeave}
            className={cn(
                "group relative flex h-full w-full select-none flex-col justify-between overflow-hidden bg-slate-50 transition-colors duration-700 dark:bg-[#090a0f]",
                className
            )}
        >
            <canvas
                ref={canvasRef}
                className="absolute inset-0 block h-full w-full cursor-crosshair"
            />

            <div className="relative z-20 flex h-full w-full flex-col justify-between p-6 md:p-10">
                {/* Top Header Deck */}
                <header className="flex w-full flex-wrap items-center justify-between gap-4 font-mono text-[11px] text-neutral-500 dark:text-neutral-400">
                    <div className="flex items-center gap-2">
                        {/* Topology Selector */}
                        <div className="flex items-center gap-1 rounded-xl border border-neutral-200 bg-white/80 p-1 backdrop-blur-md dark:border-neutral-800 dark:bg-neutral-900/80 shadow-sm">
                            {(['DOUBLE_HELIX', 'NEURAL_STRATA', 'QUANTUM_RIBBONS'] as TopologyMode[]).map((mode) => (
                                <button
                                    key={mode}
                                    onClick={() => handleTopologyChange(mode)}
                                    className={cn(
                                        "rounded-lg px-2.5 py-1 text-[10px] font-medium tracking-wider transition-all duration-300",
                                        topology === mode
                                            ? "bg-neutral-900 text-white shadow-sm dark:bg-white dark:text-black"
                                            : "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white"
                                    )}
                                >
                                    {mode.replace('_', ' ')}
                                </button>
                            ))}
                        </div>

                        <button
                            onClick={() => setIsRunning((prev) => !prev)}
                            className="flex items-center gap-1.5 rounded-xl border border-neutral-200 bg-white/80 px-3 py-1.5 backdrop-blur-md transition-all hover:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900/80 dark:hover:bg-neutral-800 shadow-sm"
                        >
                            {isRunning ? <Pause className="size-3" /> : <Play className="size-3" />}
                            <span className="font-mono text-[10px]">{isRunning ? "FREEZE" : "RUN"}</span>
                        </button>
                    </div>
                </header>

                {/* Center Stencil Typography */}
                <main className="pointer-events-none flex flex-col items-center justify-center text-center">
                    <h1 className="font-mono text-6xl font-black tracking-tighter uppercase sm:text-7xl md:text-9xl text-neutral-900/90 dark:text-white/90">
                        {headline}
                    </h1>
                </main>

                <div />
            </div>
        </div>
    );
}

export default HelixChronoMatrix;

Componentes parecidos