Aurora Dots

PreviousNext

A captivating background effect featuring softly glowing, animated dots that mimic the ethereal beauty of the aurora borealis.

Docs
scrollxuicomponent

Preview

Loading preview…
components/ui/aurora-dots.tsx
"use client";
import React, { useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';

type Cluster = {
  cx: number;
  cy: number;
  radius: number;
  count: number;
};

type Particle = {
  x: number;
  y: number;
  baseOpacity: number;
  phase: number;
};

type AuroraDotsProps = {
  particleColor?: string;
  particleSize?: number;
  glowIntensity?: number;
  hoverGlowIntensity?: number;
  animationSpeed?: number;
  hoverRadius?: number;
  interactive?: boolean;
  clusters?: Cluster[];
  className?: string;
  children?: React.ReactNode;
};

export function AuroraDots({
  particleColor = "34, 211, 238",
  particleSize = 2,
  glowIntensity = 0.3,
  hoverGlowIntensity = 0.5,
  animationSpeed = 3,
  hoverRadius = 10,
  interactive = true,
  clusters = [
    { cx: 20, cy: 15, radius: 8, count: 45 },
    { cx: 45, cy: 12, radius: 6, count: 35 },
    { cx: 70, cy: 18, radius: 10, count: 60 },
    { cx: 85, cy: 14, radius: 7, count: 40 },
    { cx: 15, cy: 35, radius: 9, count: 50 },
    { cx: 35, cy: 40, radius: 7, count: 42 },
    { cx: 55, cy: 38, radius: 8, count: 48 },
    { cx: 75, cy: 35, radius: 6, count: 38 },
    { cx: 88, cy: 40, radius: 7, count: 40 },
    { cx: 10, cy: 60, radius: 8, count: 45 },
    { cx: 30, cy: 58, radius: 9, count: 52 },
    { cx: 50, cy: 62, radius: 7, count: 42 },
    { cx: 68, cy: 60, radius: 10, count: 58 },
    { cx: 85, cy: 65, radius: 8, count: 46 },
    { cx: 18, cy: 82, radius: 7, count: 40 },
    { cx: 42, cy: 85, radius: 8, count: 48 },
    { cx: 65, cy: 80, radius: 9, count: 50 },
    { cx: 82, cy: 88, radius: 6, count: 35 },
  ],
  className,
  children,
}: AuroraDotsProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const particlesRef = useRef<Particle[]>([]);
  const mouseRef = useRef({ x: -1000, y: -1000 });
  const animationRef = useRef<number>();
  const autoHoverPosRef = useRef({ x: 0.5, y: 0.5, angle: 0 });

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

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const resizeCanvas = () => {
      const rect = container.getBoundingClientRect();
      canvas.width = rect.width;
      canvas.height = rect.height;
    };

    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    const particles: Particle[] = [];
    clusters.forEach(cluster => {
      for (let i = 0; i < cluster.count; i++) {
        const angle = (i / cluster.count) * Math.PI * 2;
        const radiusVariation = Math.random() * cluster.radius;
        const angleOffset = (Math.random() - 0.5) * 0.5;
        
        const x = (cluster.cx + Math.cos(angle + angleOffset) * radiusVariation) / 100;
        const y = (cluster.cy + Math.sin(angle + angleOffset) * radiusVariation) / 100;
        
        particles.push({
          x,
          y,
          baseOpacity: 0.2 + Math.random() * 0.1,
          phase: Math.random() * Math.PI * 2,
        });
      }
    });
    particlesRef.current = particles;

    const [r, g, b] = particleColor.split(',').map(Number);
    const startTime = Date.now();

    const animate = () => {
      const now = Date.now();
      const elapsed = (now - startTime) / 1000;

      autoHoverPosRef.current.angle += 0.01;
      const radius = 0.3;
      autoHoverPosRef.current.x = 0.5 + Math.cos(autoHoverPosRef.current.angle) * radius;
      autoHoverPosRef.current.y = 0.5 + Math.sin(autoHoverPosRef.current.angle) * radius;

      ctx.clearRect(0, 0, canvas.width, canvas.height);

      particles.forEach(particle => {
        const px = particle.x * canvas.width;
        const py = particle.y * canvas.height;

        const wave = Math.sin(elapsed / animationSpeed + particle.phase);
        let opacity = particle.baseOpacity + wave * glowIntensity + glowIntensity;
        let size = particleSize;
        let blur = particleSize * 3;
        let glowAlpha = glowIntensity;

        const autoHoverX = autoHoverPosRef.current.x * canvas.width;
        const autoHoverY = autoHoverPosRef.current.y * canvas.height;
        const autoDx = px - autoHoverX;
        const autoDy = py - autoHoverY;
        const autoDistance = Math.sqrt(autoDx * autoDx + autoDy * autoDy);
        const autoNormalizedDistance = autoDistance / (canvas.width * hoverRadius / 100);

        if (autoNormalizedDistance < 1) {
          const factor = 1 - autoNormalizedDistance;
          opacity = Math.min(1, opacity + factor * 0.6);
          size *= 1 + factor * 0.5;
          blur = size * 5;
          glowAlpha = Math.min(1, glowAlpha + factor * hoverGlowIntensity);
        }

        if (interactive) {
          const dx = px - mouseRef.current.x;
          const dy = py - mouseRef.current.y;
          const distance = Math.sqrt(dx * dx + dy * dy);
          const normalizedDistance = distance / (canvas.width * hoverRadius / 100);

          if (normalizedDistance < 1) {
            const factor = 1 - normalizedDistance;
            opacity = Math.min(1, opacity + factor * 0.5);
            size *= 1 + factor * 0.4;
            blur = size * 5;
            glowAlpha = hoverGlowIntensity;
          }
        }

        ctx.save();
        ctx.shadowBlur = blur;
        ctx.shadowColor = `rgba(${r}, ${g}, ${b}, ${glowAlpha})`;
        ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${Math.min(1, Math.max(0, opacity))})`;
        
        ctx.beginPath();
        ctx.arc(px, py, size / 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      });

      animationRef.current = requestAnimationFrame(animate);
    };

    animate();

    return () => {
      window.removeEventListener('resize', resizeCanvas);
      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current);
      }
    };
  }, [clusters, particleColor, particleSize, glowIntensity, hoverGlowIntensity, animationSpeed, hoverRadius, interactive]);

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!interactive || !containerRef.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    mouseRef.current = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
    };
  };

  const handleMouseLeave = () => {
    mouseRef.current = { x: -1000, y: -1000 };
  };

  return (
    <div 
      ref={containerRef}
      className={cn("relative overflow-hidden bg-white dark:bg-black", className)}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
    >
      <canvas
        ref={canvasRef}
        className="absolute inset-0 w-full h-full"
      />
      {children && <div className="relative z-10">{children}</div>}
    </div>
  );
}

Installation

npx shadcn@latest add @scrollxui/aurora-dots

Usage

import { AuroraDots } from "@/components/aurora-dots"
<AuroraDots />