effects#animation#interactive

Grid Distortion

A dot grid that warps and distorts based on cursor proximity

Implementation Guide
# Grid Distortion Effect

> A dot grid that warps and distorts based on cursor proximity. Creates an interactive "magnetic" effect that responds to mouse movement.

## Quick Start

```tsx
import GridDistortion from '@/components/effects/grid-distortion';

export default function InteractiveSection() {
  return (
    <div className="relative h-[500px]">
      <GridDistortion dotColor="#007AFF" />
      <div className="relative z-10 flex h-full items-center justify-center">
        <h1 className="text-4xl font-bold text-white">Interactive Grid</h1>
      </div>
    </div>
  );
}
```

## Props

| Prop            | Type   | Default   | Description                          |
| --------------- | ------ | --------- | ------------------------------------ |
| gridSize        | number | 30        | Spacing between dots in pixels       |
| dotSize         | number | 3         | Diameter of each dot in pixels       |
| dotColor        | string | '#007AFF' | Color of the dots (hex or CSS color) |
| maxDisplacement | number | 20        | Maximum pixel distance dots can move |
| influenceRadius | number | 150       | Radius of mouse influence in pixels  |

## Full Implementation

```tsx
'use client';

import { useEffect, useRef, useState } from 'react';

interface GridDistortionProps {
  gridSize?: number;
  dotSize?: number;
  dotColor?: string;
  maxDisplacement?: number;
  influenceRadius?: number;
}

export default function GridDistortion({
  gridSize = 30,
  dotSize = 3,
  dotColor = '#007AFF',
  maxDisplacement = 20,
  influenceRadius = 150,
}: GridDistortionProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [mousePos, setMousePos] = useState({ x: -1000, y: -1000 });
  const [dots, setDots] = useState<{ x: number; y: number }[]>([]);

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

    const rect = container.getBoundingClientRect();
    const cols = Math.ceil(rect.width / gridSize) + 1;
    const rows = Math.ceil(rect.height / gridSize) + 1;

    const newDots: { x: number; y: number }[] = [];
    for (let row = 0; row < rows; row++) {
      for (let col = 0; col < cols; col++) {
        newDots.push({
          x: col * gridSize,
          y: row * gridSize,
        });
      }
    }
    setDots(newDots);

    const handleResize = () => {
      const newRect = container.getBoundingClientRect();
      const newCols = Math.ceil(newRect.width / gridSize) + 1;
      const newRows = Math.ceil(newRect.height / gridSize) + 1;

      const resizedDots: { x: number; y: number }[] = [];
      for (let row = 0; row < newRows; row++) {
        for (let col = 0; col < newCols; col++) {
          resizedDots.push({
            x: col * gridSize,
            y: row * gridSize,
          });
        }
      }
      setDots(resizedDots);
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [gridSize]);

  const handleMouseMove = (event: React.MouseEvent) => {
    const rect = containerRef.current?.getBoundingClientRect();
    if (!rect) return;

    setMousePos({
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    });
  };

  const handleMouseLeave = () => {
    setMousePos({ x: -1000, y: -1000 });
  };

  const getDisplacement = (dotX: number, dotY: number) => {
    const dx = mousePos.x - dotX;
    const dy = mousePos.y - dotY;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance > influenceRadius || distance === 0) {
      return { x: 0, y: 0 };
    }

    const force = (1 - distance / influenceRadius) * maxDisplacement;
    const angle = Math.atan2(dy, dx);

    return {
      x: -Math.cos(angle) * force,
      y: -Math.sin(angle) * force,
    };
  };

  // Respect reduced motion preference
  const prefersReducedMotion =
    typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  return (
    <div
      ref={containerRef}
      className="relative h-full w-full overflow-hidden"
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      style={{ background: 'linear-gradient(135deg, #111111 0%, #1a1a2e 100%)' }}
    >
      {dots.map((dot, index) => {
        const displacement = prefersReducedMotion ? { x: 0, y: 0 } : getDisplacement(dot.x, dot.y);
        const distance = Math.sqrt(
          Math.pow(mousePos.x - dot.x, 2) + Math.pow(mousePos.y - dot.y, 2)
        );
        const scale = distance < influenceRadius ? 1 + (1 - distance / influenceRadius) * 0.5 : 1;
        const opacity =
          distance < influenceRadius ? 0.8 + (1 - distance / influenceRadius) * 0.2 : 0.3;

        return (
          <div
            key={index}
            className="absolute rounded-full transition-transform duration-75"
            style={{
              width: dotSize,
              height: dotSize,
              backgroundColor: dotColor,
              left: dot.x - dotSize / 2,
              top: dot.y - dotSize / 2,
              transform: `translate(${displacement.x}px, ${displacement.y}px) scale(${scale})`,
              opacity,
            }}
          />
        );
      })}
    </div>
  );
}
```

## Customization

### Dense Grid

For a more detailed, intricate look:

```tsx
<GridDistortion gridSize={15} dotSize={2} maxDisplacement={15} influenceRadius={100} />
```

### Sparse Grid

For a minimal, spacious feel:

```tsx
<GridDistortion gridSize={50} dotSize={4} maxDisplacement={30} influenceRadius={200} />
```

### Brand Colors

Match your brand palette:

```tsx
<GridDistortion dotColor="#FF3B30" />
```

### Light Background

For light themes, invert the colors in the component's background style:

```tsx
style={{ background: 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)' }}
```

And use a darker dot color like `dotColor="#333333"`.

## Performance Considerations

- **Grid density**: Higher `gridSize` values = fewer dots = better performance
- **Mobile optimization**: Consider increasing `gridSize` to 40-50 on mobile
- **Transition duration**: The `duration-75` class keeps animations snappy without lag
- **Virtual rendering**: For very large grids, consider implementing virtualization

## Accessibility

- Respects `prefers-reduced-motion` - disables displacement animations
- Effect is purely visual decoration
- Ensure any overlaid content has sufficient contrast

## Variations

### Attraction Mode

Change the displacement direction for an "attraction" effect:

```tsx
return {
  x: Math.cos(angle) * force, // Remove the negative
  y: Math.sin(angle) * force,
};
```

### Color Gradient

Add distance-based color intensity:

```tsx
const intensity =
  distance < influenceRadius ? Math.floor(255 * (1 - distance / influenceRadius)) : 0;
const dynamicColor = `rgb(${intensity}, 122, 255)`;
```

### Line Grid

Connect dots with lines for a mesh effect (additional complexity, requires canvas):

```tsx
// Draw lines between adjacent dots
ctx.beginPath();
ctx.moveTo(dots[i].x, dots[i].y);
ctx.lineTo(dots[i + 1].x, dots[i + 1].y);
ctx.stroke();
```