返回动效库
微交互高级

彩纸爆炸

点击时从触发点向四周射出彩色纸片粒子,适合成功状态、庆祝和游戏化场景的视觉反馈。

点击时1.2s

交互预览

动效定义与代码

{
  "slug": "confetti-burst",
  "name": "彩纸爆炸",
  "nameEn": "Confetti Burst",
  "description": "点击时从触发点向四周射出彩色纸片粒子,适合成功状态、庆祝和游戏化场景的视觉反馈。",
  "descriptionEn": "Colorful confetti particles burst outward from the click point and float down with gravity. Perfect for success states, celebrations, and gamification feedback.",
  "category": "micro-interaction",
  "tags": [
    "confetti",
    "particle",
    "celebration",
    "click",
    "success",
    "gamification"
  ],
  "trigger": "on-click",
  "difficulty": "advanced",
  "duration": "1.2s",
  "easing": "cubic-bezier(0, 0.9, 0.57, 1)",
  "cssProperties": [
    "transform",
    "opacity"
  ],
  "isGPUAccelerated": true,
  "previewBg": "light",
  "keywords": [
    "confetti",
    "celebration",
    "particle",
    "burst",
    "success",
    "reward",
    "gamification"
  ],
  "useCases": [
    "Form submission success",
    "Achievement unlocked",
    "Payment completed",
    "Goal reached indicator"
  ],
  "relatedAnimations": [
    "ripple-click",
    "pulse-ring",
    "elastic-snap"
  ],
  "recommendedStyles": [
    "dopamine-design",
    "kawaii-minimal",
    "memphis",
    "pop-art"
  ],
  "codeSnippets": [
    {
      "label": "CSS Keyframes",
      "language": "css",
      "code": "@keyframes confetti-fall {\n  0% {\n    opacity: 1;\n    transform: translate(var(--x), var(--y)) rotate(0deg) scale(1);\n  }\n  100% {\n    opacity: 0;\n    transform: translate(\n      calc(var(--x) + var(--drift)),\n      calc(var(--y) + 120px)\n    ) rotate(var(--spin)) scale(0.4);\n  }\n}\n\n.confetti-particle {\n  position: absolute;\n  width: 8px;\n  height: 8px;\n  will-change: transform, opacity;\n  animation: confetti-fall 1.2s cubic-bezier(0, 0.9, 0.57, 1) forwards;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .confetti-particle {\n    animation: none;\n    display: none;\n  }\n}"
    },
    {
      "label": "Tailwind CSS",
      "language": "css",
      "code": "/* Confetti burst — JS generates particles, CSS animates them. */\n\n@keyframes confetti-fall {\n  0% {\n    opacity: 1;\n    transform: translate(var(--x), var(--y)) rotate(0deg) scale(1);\n  }\n  100% {\n    opacity: 0;\n    transform: translate(\n      calc(var(--x) + var(--drift)),\n      calc(var(--y) + 120px)\n    ) rotate(var(--spin)) scale(0.4);\n  }\n}\n\n@utility animate-confetti {\n  position: absolute;\n  width: 8px;\n  height: 8px;\n  will-change: transform, opacity;\n  animation: confetti-fall 1.2s cubic-bezier(0, 0.9, 0.57, 1) forwards;\n  pointer-events: none;\n}"
    },
    {
      "label": "AnimeJS",
      "language": "tsx",
      "code": "\"use client\";\n\nimport { useEffect, useRef, type MouseEvent, type ReactNode } from \"react\";\n\nconst COLORS = [\"#ff006e\", \"#fb5607\", \"#ffbe0b\", \"#8338ec\", \"#3a86ff\", \"#06d6a0\"];\nconst PARTICLE_COUNT = 26;\n\ntype AnimeModule = typeof import(\"animejs\");\ntype AnimeAnimation = {\n  cancel(): unknown;\n};\n\nfunction prefersReducedMotion() {\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\nexport function ConfettiButton({ children }: { children: ReactNode }) {\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const animeRef = useRef<Promise<AnimeModule> | null>(null);\n  const activeParticlesRef = useRef<Array<{ animation: AnimeAnimation; element: HTMLSpanElement }>>([]);\n\n  useEffect(() => {\n    return () => {\n      activeParticlesRef.current.forEach(({ animation, element }) => {\n        animation.cancel();\n        element.remove();\n      });\n      activeParticlesRef.current = [];\n    };\n  }, []);\n\n  function getAnime() {\n    animeRef.current ??= import(\"animejs\");\n    return animeRef.current;\n  }\n\n  function createParticle(button: HTMLButtonElement, x: number, y: number, index: number) {\n    const particle = document.createElement(\"span\");\n    particle.className = \"pointer-events-none absolute block will-change-transform\";\n    Object.assign(particle.style, {\n      background: COLORS[index % COLORS.length],\n      borderRadius: Math.random() > 0.45 ? \"50%\" : \"1px\",\n      height: \"8px\",\n      left: x - 4 + \"px\",\n      top: y - 4 + \"px\",\n      width: \"8px\",\n    });\n    button.appendChild(particle);\n    return particle;\n  }\n\n  function removeParticle(particle: HTMLSpanElement) {\n    activeParticlesRef.current = activeParticlesRef.current.filter(\n      (entry) => entry.element !== particle\n    );\n    particle.remove();\n  }\n\n  async function handleClick(event: MouseEvent<HTMLButtonElement>) {\n    const button = event.currentTarget;\n    const rect = button.getBoundingClientRect();\n    const originX = event.clientX - rect.left;\n    const originY = event.clientY - rect.top;\n    if (prefersReducedMotion()) return;\n\n    const { animate } = await getAnime();\n    if (buttonRef.current !== button) return;\n\n    animate(button, { scale: [0.97, 1], duration: 220, ease: \"out(3)\" });\n\n    Array.from({ length: PARTICLE_COUNT }, (_, index) => {\n      const angle = (Math.PI * 2 * index) / PARTICLE_COUNT + (Math.random() - 0.5) * 0.5;\n      const velocity = 58 + Math.random() * 90;\n      const particle = createParticle(button, originX, originY, index);\n      const animation = animate(particle, {\n        x: [0, Math.cos(angle) * velocity],\n        y: [0, Math.sin(angle) * velocity + 104],\n        rotate: [\"0deg\", Math.random() * 720 - 360 + \"deg\"],\n        scale: [1, 0.35],\n        opacity: [1, 0],\n        duration: 1050 + Math.random() * 220,\n        delay: index * 5,\n        ease: \"out(3)\",\n        onComplete: () => removeParticle(particle),\n      });\n      activeParticlesRef.current.push({ animation, element: particle });\n    });\n  }\n\n  return (\n    <button\n      ref={buttonRef}\n      onClick={handleClick}\n      className=\"relative overflow-visible px-6 py-3 bg-zinc-900 text-white\"\n    >\n      {children}\n    </button>\n  );\n}"
    },
    {
      "label": "Framer Motion",
      "language": "tsx",
      "code": "\"use client\";\n\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useState, type MouseEvent } from \"react\";\n\nconst COLORS = [\"#ff006e\", \"#fb5607\", \"#ffbe0b\", \"#8338ec\", \"#3a86ff\", \"#06d6a0\"];\n\ninterface Particle {\n  id: number;\n  x: number;\n  y: number;\n  color: string;\n  angle: number;\n  velocity: number;\n}\n\nfunction spawnParticles(x: number, y: number, count = 20): Particle[] {\n  return Array.from({ length: count }, (_, i) => ({\n    id: Date.now() + i,\n    x,\n    y,\n    color: COLORS[Math.floor(Math.random() * COLORS.length)],\n    angle: (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5,\n    velocity: 60 + Math.random() * 80,\n  }));\n}\n\nexport function ConfettiButton({ children }: { children: React.ReactNode }) {\n  const [particles, setParticles] = useState<Particle[]>([]);\n\n  function handleClick(e: MouseEvent<HTMLButtonElement>) {\n    const rect = e.currentTarget.getBoundingClientRect();\n    const x = e.clientX - rect.left;\n    const y = e.clientY - rect.top;\n    setParticles((prev) => [...prev, ...spawnParticles(x, y)]);\n  }\n\n  return (\n    <button onClick={handleClick} style={{ position: \"relative\", overflow: \"visible\" }}>\n      {children}\n      <AnimatePresence>\n        {particles.map((p) => (\n          <motion.span\n            key={p.id}\n            initial={{\n              x: p.x,\n              y: p.y,\n              scale: 1,\n              opacity: 1,\n            }}\n            animate={{\n              x: p.x + Math.cos(p.angle) * p.velocity,\n              y: p.y + Math.sin(p.angle) * p.velocity + 120,\n              scale: 0.4,\n              opacity: 0,\n              rotate: Math.random() * 720 - 360,\n            }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 1.2, ease: [0, 0.9, 0.57, 1] }}\n            onAnimationComplete={() =>\n              setParticles((prev) => prev.filter((pp) => pp.id !== p.id))\n            }\n            style={{\n              position: \"absolute\",\n              width: 8,\n              height: 8,\n              borderRadius: Math.random() > 0.5 ? \"50%\" : \"1px\",\n              background: p.color,\n              pointerEvents: \"none\",\n            }}\n          />\n        ))}\n      </AnimatePresence>\n    </button>\n  );\n}"
    }
  ]
}