Skip to content
Back to skills

Particle Systems

ASecurity

Create Canvas and WebGL particle effects as React components for shadcn/ui projects. Confetti, starfields, pixel trails, fire, smoke, snow, matrix rain, aurora, interactive particle swarms, and constellation networks. Use when: user wants particle effects, confetti, starfields, digital rain, connected dots, or any particle-based visual.

  • 6 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added June 1, 2026
ai-agentstypescriptreactnextjsrailsgitperformance

Works with

  • cli

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add KevinZai/commander --skill particle-systems --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Particle Systems?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Particle Systems
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kevinzai-particle-systems/badge)](https://www.skillsdirectory.com/skills/kevinzai-particle-systems)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: particle-systems
description: "Create Canvas and WebGL particle effects as React components for shadcn/ui projects. Confetti, starfields, pixel trails, fire, smoke, snow, matrix rain, aurora, interactive particle swarms, and constellation networks. Use when: user wants particle effects, confetti, starfields, digital rain, connected dots, or any particle-based visual."
---

# Particle Systems Skill

Create particle effect components for **Next.js + shadcn/ui + Tailwind CSS + TypeScript** projects.

All output MUST be:
- React functional components with TypeScript
- Canvas/WebGL wrapped in shadcn-compatible containers
- Using shadcn CSS variables for particle colors
- Handling resize, visibility, and cleanup properly
- Providing static fallback for `prefers-reduced-motion`

## Core Canvas Particle Engine

```tsx
"use client"
import { useRef, useEffect, useCallback } from "react"
import { cn } from "@/lib/utils"

interface Particle {
  x: number; y: number
  vx: number; vy: number
  life: number; maxLife: number
  size: number; color: string
}

interface ParticleFieldProps {
  className?: string
  count?: number
  color?: string
}

export function ParticleField({ className, count = 100, color }: ParticleFieldProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const particlesRef = useRef<Particle[]>([])
  const animationRef = useRef<number>(0)

  const createParticle = useCallback((width: number, height: number): Particle => ({
    x: Math.random() * width,
    y: Math.random() * height,
    vx: (Math.random() - 0.5) * 0.5,
    vy: (Math.random() - 0.5) * 0.5,
    life: Math.random() * 100,
    maxLife: 100 + Math.random() * 100,
    size: 1 + Math.random() * 2,
    color: color ?? "hsl(var(--primary))",
  }), [color])

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return

    const ctx = canvas.getContext("2d")!
    const resize = () => {
      canvas.width = canvas.clientWidth * Math.min(devicePixelRatio, 2)
      canvas.height = canvas.clientHeight * Math.min(devicePixelRatio, 2)
      ctx.scale(Math.min(devicePixelRatio, 2), Math.min(devicePixelRatio, 2))
    }
    resize()
    window.addEventListener("resize", resize)

    particlesRef.current = Array.from({ length: count }, () => createParticle(canvas.clientWidth, canvas.clientHeight))

    const animate = () => {
      ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
      for (const p of particlesRef.current) {
        p.x += p.vx; p.y += p.vy; p.life++
        if (p.life > p.maxLife) Object.assign(p, createParticle(canvas.clientWidth, canvas.clientHeight))
        const alpha = 1 - p.life / p.maxLife
        ctx.globalAlpha = alpha
        ctx.fillStyle = p.color
        ctx.beginPath()
        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
        ctx.fill()
      }
      animationRef.current = requestAnimationFrame(animate)
    }
    animate()

    return () => {
      cancelAnimationFrame(animationRef.current)
      window.removeEventListener("resize", resize)
    }
  }, [count, createParticle])

  return <canvas ref={canvasRef} className={cn("absolute inset-0 -z-10 h-full w-full", className)} />
}
```

## Effect Recipes

### Connected Particles (Constellation)
```tsx
// After drawing particles, connect nearby ones:
for (let i = 0; i < particles.length; i++) {
  for (let j = i + 1; j < particles.length; j++) {
    const dx = particles[i].x - particles[j].x
    const dy = particles[i].y - particles[j].y
    const dist = Math.sqrt(dx * dx + dy * dy)
    if (dist < 120) {
      ctx.strokeStyle = `rgba(255,255,255,${1 - dist / 120})`
      ctx.lineWidth = 0.5
      ctx.beginPath()
      ctx.moveTo(particles[i].x, particles[i].y)
      ctx.lineTo(particles[j].x, particles[j].y)
      ctx.stroke()
    }
  }
}
// Optimize with spatial grid for >200 particles
```

### Mouse-Interactive Swarm
```tsx
const mouseRef = useRef({ x: 0, y: 0 })
// In particle update:
const dx = mouseRef.current.x - p.x
const dy = mouseRef.current.y - p.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 150) {
  // Attract: p.vx += dx / dist * 0.2
  // Repel: p.vx -= dx / dist * 0.5
}
```

### Matrix Rain
```tsx
interface RainDrop { x: number; y: number; speed: number; chars: string[] }
// Grid of columns, each dropping characters
// Use monospace font, green color palette
// Random katakana: String.fromCharCode(0x30A0 + Math.random() * 96)
// Trail: draw with decreasing opacity for each char above
```

### Starfield / Warp Speed
```tsx
interface Star { x: number; y: number; z: number }
// Project 3D to 2D: screenX = (star.x / star.z) * centerX + centerX
// Decrease z each frame for warp effect
// Draw streak: line from current to previous position
// Streak length proportional to speed (1/z)
```

### Confetti Burst
```tsx
// On trigger: spawn N particles at point with high initial velocity
// Add gravity: p.vy += 0.1
// Add rotation: p.angle += p.rotationSpeed
// Draw rectangles with rotation for confetti shape
// Various colors from shadcn palette
```

### Snow / Rain
```tsx
// Snow: slow fall, lateral drift with sin(time), small circles
// Rain: fast fall, slight angle, streaks (lines not circles)
// Both: recycle particles at bottom -> top
p.y += p.speed
p.x += Math.sin(p.life * 0.01) * 0.3 // snow drift
if (p.y > height) { p.y = -10; p.x = Math.random() * width }
```

### Fire / Sparks
```tsx
// Emit from bottom center, upward velocity with spread
// Color gradient: white -> yellow -> orange -> red -> transparent
// Particles shrink as they age
// Add turbulence: p.vx += (Math.random() - 0.5) * 0.3
const t = p.life / p.maxLife
const color = t < 0.2 ? "#fff" : t < 0.5 ? "#ffa500" : "#ff4500"
```

### Pixel Dissolution
```tsx
// Sample image pixels into particle positions
// On trigger: add random velocity to each particle
// Reverse: particles lerp back to original positions
// Use imageData.data to get pixel colors
```

## Performance Optimization

### Object Pooling
```tsx
class ParticlePool {
  private pool: Particle[] = []
  acquire(): Particle { return this.pool.pop() ?? createDefault() }
  release(p: Particle) { this.pool.push(p) }
}
```

### Spatial Grid
```tsx
const CELL = 100
const grid = new Map<string, Particle[]>()
const key = (p: Particle) => `${Math.floor(p.x/CELL)},${Math.floor(p.y/CELL)}`
```

### WebGL Instanced (10k+)
```tsx
// THREE.InstancedMesh or raw WebGL instancing
// Pass particle data via InstancedBufferAttribute
```

## Performance Rules

1. Object pool particles — never `new` in the animation loop
2. Use `ctx.clearRect` not `canvas.width = canvas.width`
3. Batch draw calls: single `beginPath` + multiple `arc` + single `fill`
4. Spatial partitioning for O(n) proximity checks instead of O(n^2)
5. Skip frames on low-end: check `performance.now()` delta
6. Pause when `document.hidden === true`
7. Cap particle count on mobile (`navigator.hardwareConcurrency`)
8. Use OffscreenCanvas + Web Worker for >5000 particles

## Dependencies

| Package | Purpose | Install |
|---------|---------|---------|
| (none required) | Canvas 2D is built-in | — |
| three | WebGL instancing for 10k+ | `npm i three @types/three` |
| @tsparticles/react | Pre-built configs | `npm i @tsparticles/react tsparticles` |

**Version:** 1.0.0
**Last Updated:** 2026-03-17

Files in this skill

  • SKILL.md7.2 KB
  • references/webgl-instanced.md8.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.