← Back
Canvas

Confetti

Canvas confetti burst with physics — launch from the button or click the canvas.

canvas · click button or anywhere in the canvas · gravity physics

Confetti.tsx144 lines
"use client";

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

interface Piece {
  x: number; y: number;
  vx: number; vy: number;
  life: number;
  color: string;
  size: number;
  rot: number;
  rotSpeed: number;
  shape: "rect" | "circle";
}

const COLORS = ["#f59e0b", "#ef4444", "#8b5cf6", "#10b981", "#3b82f6", "#ec4899", "#f97316", "#06b6d4"];

function createPiece(cx: number, cy: number): Piece {
  const angle = Math.random() * Math.PI * 2;
  const speed = Math.random() * 9 + 2;
  return {
    x: cx, y: cy,
    vx: Math.cos(angle) * speed,
    vy: Math.sin(angle) * speed - 5,
    life: 1,
    color: COLORS[Math.floor(Math.random() * COLORS.length)],
    size: Math.random() * 9 + 4,
    rot: Math.random() * 360,
    rotSpeed: (Math.random() - 0.5) * 12,
    shape: Math.random() > 0.4 ? "rect" : "circle",
  };
}

export default function Confetti() {
  const canvasRef  = useRef<HTMLCanvasElement>(null);
  const piecesRef  = useRef<Piece[]>([]);
  const rafRef     = useRef<number>(0);
  const runningRef = useRef(false);
  const [count, setCount] = useState(0);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const resize = () => {
      canvas.width  = canvas.offsetWidth;
      canvas.height = canvas.offsetHeight;
    };
    resize();
    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, []);

  function launch(fromX?: number, fromY?: number) {
    const canvas = canvasRef.current!;
    const cx = fromX ?? canvas.width  / 2;
    const cy = fromY ?? canvas.height / 2;

    for (let i = 0; i < 120; i++) {
      piecesRef.current.push(createPiece(cx, cy));
    }

    setCount((c) => c + 1);

    if (!runningRef.current) {
      runningRef.current = true;
      animate();
    }
  }

  function animate() {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx    = canvas.getContext("2d");
    if (!ctx) return;
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    piecesRef.current = piecesRef.current.filter((p) => p.life > 0 && p.y < canvas.height + 60);

    for (const p of piecesRef.current) {
      p.x  += p.vx;
      p.y  += p.vy;
      p.vy += 0.22;
      p.vx *= 0.99;
      p.rot += p.rotSpeed;
      p.life -= 0.012;

      ctx.save();
      ctx.globalAlpha = Math.max(0, p.life);
      ctx.fillStyle   = p.color;
      ctx.translate(p.x, p.y);
      ctx.rotate((p.rot * Math.PI) / 180);

      if (p.shape === "rect") {
        ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
      } else {
        ctx.beginPath();
        ctx.ellipse(0, 0, p.size / 2, p.size / 4, 0, 0, Math.PI * 2);
        ctx.fill();
      }

      ctx.restore();
    }

    if (piecesRef.current.length > 0) {
      rafRef.current = requestAnimationFrame(animate);
    } else {
      runningRef.current = false;
    }
  }

  function handleCanvasClick(e: React.MouseEvent<HTMLCanvasElement>) {
    const r = e.currentTarget.getBoundingClientRect();
    launch(e.clientX - r.left, e.clientY - r.top);
  }

  return (
    <div className="relative w-full flex flex-col items-center gap-6 py-8">
      <canvas
        ref={canvasRef}
        onClick={handleCanvasClick}
        className="w-full h-64 rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 cursor-crosshair block"
      />

      <div className="flex items-center gap-4">
        <button
          onClick={() => launch()}
          className="px-8 py-3 rounded-full bg-gradient-to-r from-amber-500 via-orange-500 to-red-500 text-white font-semibold text-sm shadow-lg hover:shadow-orange-500/30 hover:scale-105 active:scale-95 transition-all duration-200"
        >
          🎉 Launch Confetti
        </button>
        {count > 0 && (
          <span className="text-xs font-mono text-zinc-400 dark:text-zinc-600">
            {count}× launched
          </span>
        )}
      </div>

      <p className="text-[10px] font-mono text-zinc-400 dark:text-zinc-600">
        canvas · click button or anywhere in the canvas · gravity physics
      </p>
    </div>
  );
}

More in Canvas