AgentSkillsCN

Interactive Effects

交互式特效

SKILL.md
skill
---
name: interactive-effects
description: Interactive visual effects including particle systems, cursor effects, scroll-triggered animations, hover interactions, and mouse-following elements. Apply when adding interactivity to landing pages, creating engaging hover states, or implementing scroll-based animations.
license: MIT
metadata:
  author: all-the-vibes
  version: "1.0.0"
---

# Interactive Effects Skill

Comprehensive guide for implementing interactive visual effects in React/Next.js applications. Covers particle systems, cursor effects, scroll animations, and mouse interactions.

## When to Apply

Reference these guidelines when:
- Adding particle backgrounds or decorative elements
- Creating custom cursor effects
- Implementing scroll-triggered animations
- Building hover interactions
- Adding mouse-following elements
- Creating interactive backgrounds

## Core Pattern Categories

| Category | Impact | Primary Use |
|----------|--------|-------------|
| Particle Systems | HIGH | Background atmosphere |
| Cursor Effects | MEDIUM | User engagement |
| Scroll Animations | HIGH | Content reveal |
| Hover Interactions | MEDIUM | Interactive elements |
| Mouse Tracking | LOW | Decorative effects |

## Quick Reference

### 1. Particle System (Canvas)

```tsx
"use client"

import { useEffect, useRef, useCallback } from "react"

interface Particle {
  x: number
  y: number
  vx: number
  vy: number
  size: number
  color: string
  alpha: number
}

export function ParticleBackground({ count = 100 }: { count?: number }) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  
  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return
    
    const ctx = canvas.getContext('2d')
    if (!ctx) return
    
    // Setup
    const resize = () => {
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
    }
    resize()
    window.addEventListener('resize', resize)
    
    // Create particles
    const particles: Particle[] = Array.from({ length: count }, () => ({
      x: Math.random() * canvas.width,
      y: Math.random() * canvas.height,
      vx: (Math.random() - 0.5) * 0.5,
      vy: (Math.random() - 0.5) * 0.5,
      size: Math.random() * 2 + 1,
      color: Math.random() > 0.5 ? '#00ffff' : '#a855f7',
      alpha: Math.random() * 0.5 + 0.2
    }))
    
    let animationId: number
    
    const animate = () => {
      ctx.fillStyle = 'rgba(5, 0, 16, 0.1)'
      ctx.fillRect(0, 0, canvas.width, canvas.height)
      
      particles.forEach(p => {
        // Update position
        p.x += p.vx
        p.y += p.vy
        
        // Wrap around
        if (p.x < 0) p.x = canvas.width
        if (p.x > canvas.width) p.x = 0
        if (p.y < 0) p.y = canvas.height
        if (p.y > canvas.height) p.y = 0
        
        // Draw
        ctx.beginPath()
        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
        ctx.fillStyle = p.color
        ctx.globalAlpha = p.alpha
        ctx.fill()
        ctx.globalAlpha = 1
      })
      
      // Draw connections
      particles.forEach((p1, i) => {
        particles.slice(i + 1).forEach(p2 => {
          const dist = Math.hypot(p1.x - p2.x, p1.y - p2.y)
          if (dist < 100) {
            ctx.beginPath()
            ctx.moveTo(p1.x, p1.y)
            ctx.lineTo(p2.x, p2.y)
            ctx.strokeStyle = `rgba(0, 255, 255, ${0.2 * (1 - dist / 100)})`
            ctx.stroke()
          }
        })
      })
      
      animationId = requestAnimationFrame(animate)
    }
    
    animate()
    
    return () => {
      cancelAnimationFrame(animationId)
      window.removeEventListener('resize', resize)
    }
  }, [count])
  
  return <canvas ref={canvasRef} className="fixed inset-0 pointer-events-none" />
}
```

### 2. Custom Cursor

```tsx
"use client"

import { useEffect, useState } from "react"
import { motion } from "framer-motion"

export function CustomCursor() {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  const [isHovering, setIsHovering] = useState(false)
  const [isClicking, setIsClicking] = useState(false)
  
  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }
    
    const handleMouseDown = () => setIsClicking(true)
    const handleMouseUp = () => setIsClicking(false)
    
    // Detect hoverable elements
    const handleMouseOver = (e: MouseEvent) => {
      const target = e.target as HTMLElement
      if (target.matches('a, button, [role="button"], input, textarea, [data-cursor="pointer"]')) {
        setIsHovering(true)
      }
    }
    
    const handleMouseOut = () => setIsHovering(false)
    
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mousedown', handleMouseDown)
    document.addEventListener('mouseup', handleMouseUp)
    document.addEventListener('mouseover', handleMouseOver)
    document.addEventListener('mouseout', handleMouseOut)
    
    return () => {
      document.removeEventListener('mousemove', handleMouseMove)
      document.removeEventListener('mousedown', handleMouseDown)
      document.removeEventListener('mouseup', handleMouseUp)
      document.removeEventListener('mouseover', handleMouseOver)
      document.removeEventListener('mouseout', handleMouseOut)
    }
  }, [])
  
  return (
    <>
      {/* Main cursor */}
      <motion.div
        className="fixed top-0 left-0 w-4 h-4 rounded-full bg-cyan-400 pointer-events-none z-[9999] mix-blend-difference"
        animate={{
          x: position.x - 8,
          y: position.y - 8,
          scale: isClicking ? 0.8 : isHovering ? 1.5 : 1
        }}
        transition={{ type: "spring", stiffness: 500, damping: 28 }}
      />
      
      {/* Trailing circle */}
      <motion.div
        className="fixed top-0 left-0 w-10 h-10 rounded-full border border-cyan-400/50 pointer-events-none z-[9998]"
        animate={{
          x: position.x - 20,
          y: position.y - 20,
          scale: isHovering ? 1.5 : 1,
          opacity: isHovering ? 0.5 : 0.3
        }}
        transition={{ type: "spring", stiffness: 150, damping: 15 }}
      />
    </>
  )
}
```

### 3. Scroll-Triggered Animation

```tsx
"use client"

import { useRef } from "react"
import { motion, useScroll, useTransform } from "framer-motion"

interface ScrollRevealProps {
  children: React.ReactNode
  direction?: "up" | "down" | "left" | "right"
}

export function ScrollReveal({ children, direction = "up" }: ScrollRevealProps) {
  const ref = useRef<HTMLDivElement>(null)
  
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"]
  })
  
  const transforms = {
    up: useTransform(scrollYProgress, [0, 0.3], [50, 0]),
    down: useTransform(scrollYProgress, [0, 0.3], [-50, 0]),
    left: useTransform(scrollYProgress, [0, 0.3], [50, 0]),
    right: useTransform(scrollYProgress, [0, 0.3], [-50, 0])
  }
  
  const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
  const isHorizontal = direction === "left" || direction === "right"
  
  return (
    <motion.div
      ref={ref}
      style={{
        opacity,
        x: isHorizontal ? transforms[direction] : 0,
        y: !isHorizontal ? transforms[direction] : 0
      }}
    >
      {children}
    </motion.div>
  )
}
```

### 4. Mouse Tracking Element

```tsx
"use client"

import { useRef, useEffect, useState } from "react"

export function MouseTracker({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null)
  const [position, setPosition] = useState({ x: 0, y: 0 })
  
  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (!ref.current) return
      
      const rect = ref.current.getBoundingClientRect()
      const x = (e.clientX - rect.left - rect.width / 2) / 20
      const y = (e.clientY - rect.top - rect.height / 2) / 20
      
      setPosition({ x, y })
    }
    
    window.addEventListener('mousemove', handleMouseMove)
    return () => window.removeEventListener('mousemove', handleMouseMove)
  }, [])
  
  return (
    <div
      ref={ref}
      style={{
        transform: `translate(${position.x}px, ${position.y}px)`,
        transition: 'transform 0.1s ease-out'
      }}
    >
      {children}
    </div>
  )
}
```

### 5. Magnetic Button

```tsx
"use client"

import { useRef, useState } from "react"
import { motion } from "framer-motion"

interface MagneticButtonProps {
  children: React.ReactNode
  className?: string
  strength?: number
}

export function MagneticButton({ children, className = "", strength = 0.3 }: MagneticButtonProps) {
  const ref = useRef<HTMLButtonElement>(null)
  const [position, setPosition] = useState({ x: 0, y: 0 })
  
  const handleMouseMove = (e: React.MouseEvent) => {
    if (!ref.current) return
    
    const rect = ref.current.getBoundingClientRect()
    const centerX = rect.left + rect.width / 2
    const centerY = rect.top + rect.height / 2
    
    setPosition({
      x: (e.clientX - centerX) * strength,
      y: (e.clientY - centerY) * strength
    })
  }
  
  const handleMouseLeave = () => {
    setPosition({ x: 0, y: 0 })
  }
  
  return (
    <motion.button
      ref={ref}
      className={className}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      animate={{ x: position.x, y: position.y }}
      transition={{ type: "spring", stiffness: 150, damping: 15 }}
    >
      {children}
    </motion.button>
  )
}
```

### 6. Parallax Layer

```tsx
"use client"

import { useRef } from "react"
import { motion, useScroll, useTransform } from "framer-motion"

interface ParallaxLayerProps {
  children: React.ReactNode
  speed?: number
  className?: string
}

export function ParallaxLayer({ children, speed = 0.5, className = "" }: ParallaxLayerProps) {
  const ref = useRef<HTMLDivElement>(null)
  
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"]
  })
  
  const y = useTransform(scrollYProgress, [0, 1], [-100 * speed, 100 * speed])
  
  return (
    <motion.div ref={ref} style={{ y }} className={className}>
      {children}
    </motion.div>
  )
}
```

## Performance Guidelines

1. **Canvas Animations**: Use `requestAnimationFrame`, never `setInterval`
2. **Event Throttling**: Throttle mouse/scroll events to 60fps
3. **GPU Acceleration**: Use `transform` and `opacity` only
4. **Lazy Loading**: Dynamic import heavy animation components
5. **Visibility Check**: Pause off-screen animations
6. **Mobile Fallbacks**: Simplify or disable on mobile

## Common Mistakes

- ❌ Not cleaning up event listeners
- ❌ Creating new objects in animation loops
- ❌ Animating layout-triggering properties
- ❌ Too many particles on low-end devices
- ❌ Not respecting reduced motion preferences

## Full Implementation Guide

See `AGENTS.md` for complete implementation examples and advanced patterns.