返回资源库
撒花效果
动画
特效
点击按钮查看撒花效果
组件代码
'use client';
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
interface ConfettiProps {
count?: number;
colors?: string[];
autoStart?: boolean;
speed?: number;
}
export interface ConfettiHandle {
restart: () => void;
}
interface ConfettiParticle {
x: number;
y: number;
r: number;
d: number;
color: string;
tilt: number;
tiltAngleIncremental: number;
tiltAngle: number;
draw: (ctx: CanvasRenderingContext2D) => void;
}
interface ColorManager {
colorOptions: string[];
colorIndex: number;
colorIncrementer: number;
getColor: () => string;
}
const createParticle = (
color: string,
width: number,
height: number,
count: number
): ConfettiParticle => {
const randomFromTo = (from: number, to: number) => {
return Math.floor(Math.random() * (to - from + 1) + from);
};
const x = Math.random() * width;
const y = Math.random() * height - height;
const r = randomFromTo(5, 15);
const d = Math.random() * count + 10;
const tilt = Math.floor(10 * Math.random()) - 10;
const tiltAngleIncremental = 0.07 * Math.random() + 0.05;
return {
x,
y,
r,
d,
color,
tilt,
tiltAngleIncremental,
tiltAngle: 0,
draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.lineWidth = this.r / 2;
ctx.strokeStyle = this.color;
ctx.moveTo(this.x + this.tilt + this.r / 4, this.y);
ctx.lineTo(this.x + this.tilt, this.y + this.tilt + this.r / 4);
ctx.stroke();
}
};
};
const Confetti = forwardRef<ConfettiHandle, ConfettiProps>(({
count = 300,
colors = [
"DodgerBlue",
"OliveDrab",
"Gold",
"pink",
"SlateBlue",
"lightblue",
"Violet",
"PaleGreen",
"SteelBlue",
"SandyBrown",
"Chocolate",
"Crimson"
],
autoStart = false,
speed = 1
}, ref) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const particlesRef = useRef<ConfettiParticle[]>([]);
const animationHandlerRef = useRef<number>();
const angleRef = useRef(0);
const tiltAngleRef = useRef(0);
const [confettiActive, setConfettiActive] = useState(false);
const [animationComplete, setAnimationComplete] = useState(false);
// 使用useMemo创建颜色管理器,避免重复创建
const particleColorsRef = useRef<ColorManager>(useMemo(() => ({
colorOptions: colors,
colorIndex: 0,
colorIncrementer: 0,
getColor: function() {
if (this.colorIncrementer >= 10) {
this.colorIncrementer = 0;
this.colorIndex++;
if (this.colorIndex >= this.colorOptions.length) {
this.colorIndex = 0;
}
}
this.colorIncrementer++;
return this.colorOptions[this.colorIndex];
}
}), [colors]));
// 获取容器尺寸
const getContainerSize = useCallback(() => {
if (!containerRef.current) return { width: 400, height: 400 };
const rect = containerRef.current.getBoundingClientRect();
return { width: rect.width || 400, height: rect.height || 400 };
}, []);
// 更新粒子位置
const stepParticle = useCallback((particle: ConfettiParticle, index: number) => {
particle.tiltAngle += particle.tiltAngleIncremental;
particle.y += ((Math.cos(angleRef.current + particle.d) + 3 + particle.r / 2) / 2) * speed;
particle.x += Math.sin(angleRef.current) * speed;
particle.tilt = 15 * Math.sin(particle.tiltAngle - index / 3);
}, [speed]);
// 重定位粒子
const repositionParticle = useCallback((
particle: ConfettiParticle,
x: number,
y: number,
tilt: number
) => {
particle.x = x;
particle.y = y;
particle.tilt = tilt;
}, []);
// 检查是否需要重新定位粒子
const checkForReposition = useCallback((
particle: ConfettiParticle,
index: number,
width: number,
height: number
) => {
if ((particle.x > width + 20 || particle.x < -20 || particle.y > height) && confettiActive) {
if (index % 5 > 0 || index % 2 === 0) {
repositionParticle(
particle,
Math.random() * width,
-10,
Math.floor(10 * Math.random()) - 10
);
} else if (Math.sin(angleRef.current) > 0) {
repositionParticle(
particle,
-5,
Math.random() * height,
Math.floor(10 * Math.random()) - 10
);
} else {
repositionParticle(
particle,
width + 5,
Math.random() * height,
Math.floor(10 * Math.random()) - 10
);
}
}
}, [confettiActive, repositionParticle]);
const clearTimers = useCallback(() => {
if (animationHandlerRef.current) {
cancelAnimationFrame(animationHandlerRef.current);
animationHandlerRef.current = undefined;
}
}, []);
const stopConfetti = useCallback(() => {
setAnimationComplete(true);
setConfettiActive(false);
if (canvasRef.current) {
const ctx = canvasRef.current.getContext('2d');
if (ctx) {
const { width, height } = getContainerSize();
ctx.clearRect(0, 0, width, height);
}
}
clearTimers();
}, [clearTimers, getContainerSize]);
// 绘制函数
const draw = useCallback(() => {
if (!canvasRef.current) return;
const ctx = canvasRef.current.getContext('2d');
if (!ctx) return;
const { width, height } = getContainerSize();
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < particlesRef.current.length; i++) {
particlesRef.current[i].draw(ctx);
}
update(width, height);
}, [getContainerSize]);
// 更新所有粒子
const update = useCallback((width: number, height: number) => {
let remainingParticles = 0;
angleRef.current += 0.01 * speed;
tiltAngleRef.current += 0.1 * speed;
for (let i = 0; i < particlesRef.current.length; i++) {
const particle = particlesRef.current[i];
if (animationComplete) return;
if (!confettiActive && particle.y < -15) {
particle.y = height + 100;
} else {
stepParticle(particle, i);
if (particle.y <= height) {
remainingParticles++;
}
checkForReposition(particle, i, width, height);
}
}
// 当所有粒子离开视窗,自动重新开始新的撒花效果,而不是停止
if (remainingParticles === 0) {
// 重置粒子和位置
particlesRef.current = [];
angleRef.current = 0;
tiltAngleRef.current = 0;
// 重新初始化新的粒子
const particleCount = Math.min(count, width > 300 ? count : count / 2);
for (let i = 0; i < particleCount; i++) {
const color = particleColorsRef.current.getColor();
particlesRef.current.push(createParticle(color, width, height, count));
}
}
}, [animationComplete, checkForReposition, confettiActive, count, stepParticle, speed]);
// 初始化撒花效果
const initializeConfetti = useCallback(() => {
const { width, height } = getContainerSize();
if (width <= 0 || height <= 0) {
setTimeout(initializeConfetti, 100); // 如果尺寸无效,稍后重试
return;
}
// 根据容器大小调整粒子数量
const particleCount = Math.min(count, width > 300 ? count : count / 2);
particlesRef.current = [];
for (let i = 0; i < particleCount; i++) {
const color = particleColorsRef.current.getColor();
particlesRef.current.push(createParticle(color, width, height, count));
}
startConfetti();
}, [count, getContainerSize]);
const startConfetti = useCallback(() => {
if (!canvasRef.current || !containerRef.current) return;
const { width, height } = getContainerSize();
if (width <= 0 || height <= 0) {
setTimeout(startConfetti, 100);
return;
}
// 确保canvas尺寸正确设置
canvasRef.current.width = width;
canvasRef.current.height = height;
setConfettiActive(true);
setAnimationComplete(false);
const animate = () => {
if (animationComplete) {
return;
}
animationHandlerRef.current = requestAnimationFrame(animate);
draw();
};
animate();
}, [animationComplete, draw, getContainerSize]);
// 重启撒花
const restartConfetti = useCallback(() => {
// 先清除所有计时器和动画
clearTimers();
// 重置所有状态
setAnimationComplete(false);
setConfettiActive(false);
// 清除画布
if (canvasRef.current) {
const ctx = canvasRef.current.getContext('2d');
if (ctx) {
const { width, height } = getContainerSize();
ctx.clearRect(0, 0, width, height);
}
}
// 重置粒子数组
particlesRef.current = [];
// 重置角度
angleRef.current = 0;
tiltAngleRef.current = 0;
// 重置颜色管理器
particleColorsRef.current.colorIndex = 0;
particleColorsRef.current.colorIncrementer = 0;
// 延迟一点时间确保状态已重置
setTimeout(() => {
setConfettiActive(true);
setAnimationComplete(false);
initializeConfetti();
}, 100);
}, [clearTimers, getContainerSize, initializeConfetti]);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
restart: restartConfetti
}), [restartConfetti]);
// 处理容器尺寸变化
useEffect(() => {
const handleResize = () => {
if (canvasRef.current && containerRef.current && !animationComplete) {
const { width, height } = getContainerSize();
if (width > 0 && height > 0) {
canvasRef.current.width = width;
canvasRef.current.height = height;
}
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimers();
};
}, [animationComplete, clearTimers, getContainerSize]);
// 确保在DOM渲染完成后再初始化
useEffect(() => {
//
const initTimer = setTimeout(() => {
if (autoStart) {
restartConfetti();
}
}, 300); // 增加延迟确保DOM已完全渲染
return () => {
clearTimeout(initTimer);
clearTimers();
stopConfetti();
};
}, [autoStart, clearTimers, restartConfetti, stopConfetti]);
return (
<div ref={containerRef} className="w-full h-full relative">
<canvas
ref={canvasRef}
id="canvas-strips"
className="absolute inset-0 pointer-events-none z-[999]"
style={{ width: '100%', height: '100%' }}
/>
</div>
);
});
export default Confetti;