Docs
Swarm Effect

Swarm Effect

An artistic swarm effect component with customizable particle styles and hover interactions.

Installation

Copy and paste the following code into your project.

"use client";
 
import React, { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import type { StaticImageData } from "next/image";
 
export interface ParticleImageProps
  extends React.HTMLAttributes<HTMLDivElement> {
  src: string | StaticImageData;
  particleSize?: number;
  particleSpacing?: number;
  particleColor?: string;
  displacementRadius?: number;
  hoverEffect?: "scatter" | "gather" | "none";
  className?: string;
}
 
export function SwarmEffect({
  src,
  particleSize = 2,
  particleSpacing = 4,
  particleColor = "hsl(280, 100%, 60%)",
  displacementRadius = 50,
  hoverEffect = "scatter",
  className,
  ...props
}: ParticleImageProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [particles, setParticles] = useState<
    Array<{ x: number; y: number; originX: number; originY: number }>
  >([]);
  const [mousePosition, setMousePosition] = useState<{
    x: number;
    y: number;
  } | null>(null);
  const frameRef = useRef<number | null>(null);
  const imageRef = useRef<HTMLImageElement | null>(null);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
 
    const ctx = canvas.getContext("2d", { willReadFrequently: true });
    if (!ctx) return;
 
    const image = new Image();
    image.crossOrigin = "anonymous";
    image.src = typeof src === "string" ? src : src.src;
    imageRef.current = image;
 
    image.onload = () => {
      const tempCanvas = document.createElement("canvas");
      const tempCtx = tempCanvas.getContext("2d");
      if (!tempCtx) return;
 
      const containerWidth = canvas.clientWidth;
      const containerHeight =
        canvas.clientHeight || (containerWidth * image.height) / image.width;
 
      canvas.width = containerWidth;
      canvas.height = containerHeight;
 
      tempCanvas.width = containerWidth;
      tempCanvas.height = containerHeight;
      tempCtx.drawImage(image, 0, 0, containerWidth, containerHeight);
 
      const imageData = tempCtx.getImageData(
        0,
        0,
        containerWidth,
        containerHeight,
      );
      const particles: Array<{
        x: number;
        y: number;
        originX: number;
        originY: number;
      }> = [];
 
      for (let y = 0; y < containerHeight; y += particleSpacing) {
        for (let x = 0; x < containerWidth; x += particleSpacing) {
          const i = (y * containerWidth + x) * 4;
          const alpha = imageData.data[i + 3];
          const brightness =
            (imageData.data[i] +
              imageData.data[i + 1] +
              imageData.data[i + 2]) /
            3;
          if (alpha > 128 && brightness > 20) {
            particles.push({
              x,
              y,
              originX: x,
              originY: y,
            });
          }
        }
      }
 
      setParticles(particles);
    };
 
    return () => {
      if (frameRef.current) {
        cancelAnimationFrame(frameRef.current);
      }
    };
  }, [src, particleSpacing]);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || !particles.length) return;
 
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
 
    const animate = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = particleColor;
 
      particles.forEach((particle) => {
        let dx = 0;
        let dy = 0;
 
        if (mousePosition && hoverEffect !== "none") {
          const distance = Math.sqrt(
            Math.pow(mousePosition.x - particle.x, 2) +
              Math.pow(mousePosition.y - particle.y, 2),
          );
 
          if (distance < displacementRadius) {
            const force = Math.pow(
              (displacementRadius - distance) / displacementRadius,
              1.5,
            );
            if (hoverEffect === "scatter") {
              dx = (particle.x - mousePosition.x) * force * 1.2;
              dy = (particle.y - mousePosition.y) * force * 1.2;
            } else {
              dx = (mousePosition.x - particle.x) * force * 1.2;
              dy = (mousePosition.y - particle.y) * force * 1.2;
            }
          }
        }
 
        const targetX = particle.originX + dx;
        const targetY = particle.originY + dy;
 
        particle.x += (targetX - particle.x) * 0.25;
        particle.y += (targetY - particle.y) * 0.25;
 
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, particleSize, 0, Math.PI * 2);
        ctx.fill();
      });
 
      frameRef.current = requestAnimationFrame(animate);
    };
 
    animate();
 
    const handleMouseMove = (e: MouseEvent) => {
      const rect = canvas.getBoundingClientRect();
      setMousePosition({
        x: ((e.clientX - rect.left) * canvas.width) / rect.width,
        y: ((e.clientY - rect.top) * canvas.height) / rect.height,
      });
    };
 
    const handleMouseLeave = () => {
      setMousePosition(null);
    };
 
    canvas.addEventListener("mousemove", handleMouseMove);
    canvas.addEventListener("mouseleave", handleMouseLeave);
 
    return () => {
      if (frameRef.current) {
        cancelAnimationFrame(frameRef.current);
      }
      canvas.removeEventListener("mousemove", handleMouseMove);
      canvas.removeEventListener("mouseleave", handleMouseLeave);
    };
  }, [
    particles,
    particleSize,
    particleColor,
    displacementRadius,
    hoverEffect,
    mousePosition,
  ]);
 
  return (
    <div className={cn("relative inline-block", className)} {...props}>
      <canvas
        ref={canvasRef}
        className="h-full w-full max-w-full"
        style={{ opacity: particles.length ? 1 : 0 }}
      />
      <img
        src={typeof src === "string" ? src : src.src}
        alt=""
        className="absolute left-0 top-0 h-full w-full opacity-0"
        style={{ visibility: "hidden" }}
      />
    </div>
  );
}

Update the import paths to match your project setup.

Usage

import SwarmEffect from "@/components/ui/swarm-effect";
 
export default function Demo() {
  return (
    <SwarmEffect
      src="/your-image.jpg"
      particleSize={2}
      particleSpacing={4}
      particleColor="#000000"
      displacementRadius={50}
      hoverEffect="scatter"
    />
  );
}

Props

PropTypeDescriptionDefault
srcstringThe source URL of the imageRequired
particleSizenumberSize of individual particles2
particleSpacingnumberSpace between particles4
particleColorstringColor of the particles"#000000"
displacementRadiusnumberRadius of the hover effect50
hoverEffect"scatter" | "gather" | "none"Type of interaction when hovering"scatter"
classNamestringAdditional CSS classesundefined

Examples

Basic Usage

<SwarmEffect src="/image.jpg" className="w-[300px]" />

Custom Particle Style

<SwarmEffect
  src="/placeholder.svg"
  particleSize={2}
  particleSpacing={4}
  particleColor="hsl(280, 100%, 60%)"
  displacementRadius={50}
  hoverEffect="scatter"
/>

Gather Effect

<SwarmEffect
  src="/image.jpg"
  particleSize={2}
  particleSpacing={4}
  particleColor="#000000"
  hoverEffect="gather"
  displacementRadius={60}
  className="w-[300px]"
/>