effects#animation#interactive

Floating Orbs

Mesmerizing floating orbs that drift across the screen and respond to mouse movement

Implementation Guide
# Floating Orbs Effect

> Mesmerizing floating orbs that drift across the screen and respond to mouse movement. Creates a dynamic, ambient background effect.

## Quick Start

```tsx
import FloatingOrbs from '@/components/effects/floating-orbs';

export default function Hero() {
  return (
    <div className="relative h-screen">
      <FloatingOrbs colors={['#007AFF', '#FF3B30']} />
      <div className="relative z-10">{/* Your content goes here */}</div>
    </div>
  );
}
```

## Props

| Prop           | Type     | Default                                      | Description                                 |
| -------------- | -------- | -------------------------------------------- | ------------------------------------------- |
| orbCount       | number   | 15                                           | Number of floating orbs                     |
| colors         | string[] | ['#007AFF', '#FF3B30', '#5856D6', '#FF9500'] | Array of hex colors for orbs                |
| speed          | number   | 0.5                                          | Base animation speed (0.1 - 2.0)            |
| mouseInfluence | number   | 0.3                                          | How much mouse affects orb movement (0 - 1) |
| blur           | number   | 60                                           | Gaussian blur radius in pixels              |

## Full Implementation

```tsx
'use client';

import { useEffect, useRef } from 'react';

interface Orb {
  x: number;
  y: number;
  radius: number;
  color: string;
  vx: number;
  vy: number;
  targetX: number;
  targetY: number;
}

interface FloatingOrbsProps {
  orbCount?: number;
  colors?: string[];
  speed?: number;
  mouseInfluence?: number;
  blur?: number;
}

export default function FloatingOrbs({
  orbCount = 15,
  colors = ['#007AFF', '#FF3B30', '#5856D6', '#FF9500'],
  speed = 0.5,
  mouseInfluence = 0.3,
  blur = 60,
}: FloatingOrbsProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const orbsRef = useRef<Orb[]>([]);
  const mouseRef = useRef({ x: 0, y: 0 });
  const animationRef = useRef<number>(0);

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

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

    const resizeCanvas = () => {
      const rect = canvas.getBoundingClientRect();
      canvas.width = rect.width * window.devicePixelRatio;
      canvas.height = rect.height * window.devicePixelRatio;
      ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    };

    const initOrbs = () => {
      const rect = canvas.getBoundingClientRect();
      orbsRef.current = Array.from({ length: orbCount }, () => {
        const radius = 50 + Math.random() * 150;
        return {
          x: Math.random() * rect.width,
          y: Math.random() * rect.height,
          radius,
          color: colors[Math.floor(Math.random() * colors.length)],
          vx: (Math.random() - 0.5) * speed,
          vy: (Math.random() - 0.5) * speed,
          targetX: Math.random() * rect.width,
          targetY: Math.random() * rect.height,
        };
      });
    };

    const handleMouseMove = (event: MouseEvent) => {
      const rect = canvas.getBoundingClientRect();
      mouseRef.current = {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top,
      };
    };

    const animate = () => {
      const rect = canvas.getBoundingClientRect();
      ctx.clearRect(0, 0, rect.width, rect.height);

      orbsRef.current.forEach((orb) => {
        // Mouse attraction
        const dx = mouseRef.current.x - orb.x;
        const dy = mouseRef.current.y - orb.y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        const maxDistance = 300;

        if (distance < maxDistance) {
          const force = ((maxDistance - distance) / maxDistance) * mouseInfluence;
          orb.vx += (dx / distance) * force * 0.5;
          orb.vy += (dy / distance) * force * 0.5;
        }

        // Random target movement
        if (Math.random() < 0.01) {
          orb.targetX = Math.random() * rect.width;
          orb.targetY = Math.random() * rect.height;
        }

        // Drift toward target
        orb.vx += (orb.targetX - orb.x) * 0.0005 * speed;
        orb.vy += (orb.targetY - orb.y) * 0.0005 * speed;

        // Apply friction
        orb.vx *= 0.99;
        orb.vy *= 0.99;

        // Update position
        orb.x += orb.vx;
        orb.y += orb.vy;

        // Wrap around edges
        if (orb.x < -orb.radius) orb.x = rect.width + orb.radius;
        if (orb.x > rect.width + orb.radius) orb.x = -orb.radius;
        if (orb.y < -orb.radius) orb.y = rect.height + orb.radius;
        if (orb.y > rect.height + orb.radius) orb.y = -orb.radius;

        // Draw orb with radial gradient
        const gradient = ctx.createRadialGradient(orb.x, orb.y, 0, orb.x, orb.y, orb.radius);
        const alpha = 0.6;
        gradient.addColorStop(
          0,
          orb.color +
            Math.round(alpha * 255)
              .toString(16)
              .padStart(2, '0')
        );
        gradient.addColorStop(0.5, orb.color + '40');
        gradient.addColorStop(1, orb.color + '00');

        ctx.fillStyle = gradient;
        ctx.beginPath();
        ctx.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
        ctx.fill();
      });

      animationRef.current = requestAnimationFrame(animate);
    };

    resizeCanvas();
    initOrbs();
    animate();

    window.addEventListener('resize', () => {
      resizeCanvas();
      initOrbs();
    });
    canvas.addEventListener('mousemove', handleMouseMove);

    // Respect reduced motion preference
    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (prefersReducedMotion) {
      cancelAnimationFrame(animationRef.current);
    }

    return () => {
      cancelAnimationFrame(animationRef.current);
      window.removeEventListener('resize', resizeCanvas);
      canvas.removeEventListener('mousemove', handleMouseMove);
    };
  }, [orbCount, colors, speed, mouseInfluence]);

  return (
    <canvas
      ref={canvasRef}
      className="h-full w-full"
      style={{
        filter: `blur(${blur}px)`,
        background: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)',
      }}
    />
  );
}
```

## Customization

### Brand Colors

Replace the `colors` array with your brand palette:

```tsx
<FloatingOrbs colors={['#yourPrimary', '#yourSecondary', '#yourAccent']} />
```

### Subtle Background

For a more subtle ambient effect:

```tsx
<FloatingOrbs orbCount={8} speed={0.2} mouseInfluence={0.1} blur={80} />
```

### High Energy

For a more dynamic, energetic feel:

```tsx
<FloatingOrbs orbCount={25} speed={1.0} mouseInfluence={0.6} blur={40} />
```

### Dark vs Light Backgrounds

The component uses a dark gradient by default. For light backgrounds, modify the canvas style:

```tsx
style={{
  filter: `blur(${blur}px)`,
  background: 'linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%)',
}}
```

## Performance Considerations

- **Reduce `orbCount`** on mobile devices (8-10 recommended)
- **Increase `blur`** value can be GPU-intensive on older devices
- **Use `will-change: transform`** on parent container for smoother compositing
- Canvas is automatically scaled for retina displays

## Accessibility

- Respects `prefers-reduced-motion` - animation pauses when user prefers reduced motion
- Effect is purely decorative - ensure content above has proper contrast
- Content should have `position: relative; z-index: 10` to appear above the canvas

## Browser Support

- Modern browsers with Canvas 2D support
- requestAnimationFrame support required
- CSS filter blur support required