Files

128 lines
3.8 KiB
JavaScript

/**
* 文件功能:聊天室彩带庆典特效
*
* 通过大量彩纸碎片与飘带在空中散落、翻转,形成明显的庆典氛围,
* 适合活动开始、中奖提示和管理员公告等场景。
*/
const ConfettiEffect = (() => {
class Piece {
constructor(w, h) {
this.w = w;
this.h = h;
this.reset();
}
/**
* 初始化彩带碎片参数。
*/
reset() {
this.x = this.w * (0.15 + Math.random() * 0.7);
this.y = -Math.random() * this.h * 0.2;
this.vx = Math.random() * 4.4 - 2.2;
this.vy = Math.random() * 1.6 + 0.8;
this.gravity = Math.random() * 0.03 + 0.025;
this.rot = Math.random() * Math.PI * 2;
this.rotSpeed = (Math.random() - 0.5) * 0.16;
this.width = Math.random() * 10 + 6;
this.height = Math.random() * 18 + 8;
this.wave = Math.random() * Math.PI * 2;
this.waveSpeed = Math.random() * 0.06 + 0.03;
this.alpha = Math.random() * 0.2 + 0.75;
this.color = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899"][
Math.floor(Math.random() * 6)
];
this.isRibbon = Math.random() > 0.72;
}
/**
* 更新碎片运动状态。
*/
update() {
this.wave += this.waveSpeed;
this.vy += this.gravity;
this.vx *= 0.995;
this.x += this.vx + Math.sin(this.wave) * 0.65;
this.y += this.vy;
this.rot += this.rotSpeed;
}
/**
* 判断彩带是否仍在画布内。
*/
get alive() {
return this.y < this.h + 60;
}
/**
* 绘制单个彩带碎片。
*
* @param {CanvasRenderingContext2D} ctx
*/
draw(ctx) {
const scaleX = Math.max(0.18, Math.abs(Math.cos(this.rot)));
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rot);
ctx.scale(scaleX, 1);
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.shadowColor = `${this.color}66`;
ctx.shadowBlur = 6;
if (this.isRibbon) {
ctx.fillRect(-this.width * 0.2, -this.height / 2, this.width * 0.4, this.height);
} else {
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
ctx.restore();
}
}
/**
* 启动彩带庆典特效。
*
* @param {HTMLCanvasElement} canvas
* @param {Function} onEnd
*/
function start(canvas, onEnd) {
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const DURATION = 7800;
let pieces = Array.from({ length: 90 }, () => new Piece(w, h));
const startTime = performance.now();
let lastSpawnAt = startTime;
let animId = null;
function animate(now) {
ctx.clearRect(0, 0, w, h);
pieces = pieces.filter((piece) => {
piece.update();
piece.draw(ctx);
return piece.alive;
});
if (now - startTime < DURATION * 0.9 && now - lastSpawnAt >= 120) {
pieces.push(...Array.from({ length: 10 }, () => new Piece(w, h)));
lastSpawnAt = now;
}
if (now - startTime < DURATION) {
animId = requestAnimationFrame(animate);
} else {
cancelAnimationFrame(animId);
ctx.clearRect(0, 0, w, h);
onEnd();
}
}
animId = requestAnimationFrame(animate);
}
return { start };
})();