2026-02-27 14:14:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 文件功能:聊天室烟花特效
|
|
|
|
|
|
*
|
|
|
|
|
|
* 使用 Canvas 粒子系统在全屏播放多发烟花爆炸动画。
|
|
|
|
|
|
* 特效总时长约 4 秒,结束后自动清理并回调。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const FireworksEffect = (() => {
|
|
|
|
|
|
// 粒子类:模拟一个爆炸后的发光粒子
|
|
|
|
|
|
class Particle {
|
|
|
|
|
|
constructor(x, y, color) {
|
|
|
|
|
|
this.x = x;
|
|
|
|
|
|
this.y = y;
|
|
|
|
|
|
this.color = color;
|
|
|
|
|
|
// 随机方向和速度
|
|
|
|
|
|
const angle = Math.random() * Math.PI * 2;
|
|
|
|
|
|
const speed = Math.random() * 6 + 2;
|
|
|
|
|
|
this.vx = Math.cos(angle) * speed;
|
|
|
|
|
|
this.vy = Math.sin(angle) * speed;
|
|
|
|
|
|
this.alpha = 1;
|
|
|
|
|
|
this.gravity = 0.12;
|
|
|
|
|
|
this.decay = Math.random() * 0.012 + 0.012; // 透明度每帧衰减量
|
|
|
|
|
|
this.radius = Math.random() * 3 + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 每帧更新粒子位置和状态 */
|
|
|
|
|
|
update() {
|
|
|
|
|
|
this.vy += this.gravity;
|
|
|
|
|
|
this.x += this.vx;
|
|
|
|
|
|
this.y += this.vy;
|
|
|
|
|
|
this.vx *= 0.98; // 空气阻力
|
|
|
|
|
|
this.vy *= 0.98;
|
|
|
|
|
|
this.alpha -= this.decay;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 绘制粒子 */
|
|
|
|
|
|
draw(ctx) {
|
|
|
|
|
|
ctx.save();
|
|
|
|
|
|
ctx.globalAlpha = Math.max(0, this.alpha);
|
|
|
|
|
|
ctx.fillStyle = this.color;
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
ctx.restore();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 预定义烟花颜色组
|
|
|
|
|
|
const COLORS = [
|
|
|
|
|
|
"#ff4444",
|
|
|
|
|
|
"#ff8800",
|
|
|
|
|
|
"#ffdd00",
|
|
|
|
|
|
"#44ff44",
|
|
|
|
|
|
"#44ddff",
|
|
|
|
|
|
"#8844ff",
|
|
|
|
|
|
"#ff44cc",
|
|
|
|
|
|
"#ffffff",
|
|
|
|
|
|
"#ffaaaa",
|
|
|
|
|
|
"#aaffaa",
|
|
|
|
|
|
"#aaaaff",
|
|
|
|
|
|
"#ffffaa",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发射一枚烟花,返回粒子数组
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number} x 爆炸中心 x
|
|
|
|
|
|
* @param {number} y 爆炸中心 y
|
|
|
|
|
|
* @param {number} count 粒子数量
|
|
|
|
|
|
* @returns {Particle[]}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _burst(x, y, count) {
|
|
|
|
|
|
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
|
|
|
|
|
const particles = [];
|
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
|
particles.push(new Particle(x, y, color));
|
|
|
|
|
|
}
|
|
|
|
|
|
return particles;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动烟花特效
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLCanvasElement} canvas 全屏 Canvas
|
|
|
|
|
|
* @param {Function} onEnd 特效结束回调
|
|
|
|
|
|
*/
|
|
|
|
|
|
function start(canvas, onEnd) {
|
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
const w = canvas.width;
|
|
|
|
|
|
const h = canvas.height;
|
|
|
|
|
|
const DURATION = 4500; // 总时长(ms)
|
|
|
|
|
|
|
|
|
|
|
|
let particles = [];
|
|
|
|
|
|
let animId = null;
|
|
|
|
|
|
let launchCount = 0;
|
|
|
|
|
|
const MAX_LAUNCHES = 8; // 总共发射几枚烟花
|
|
|
|
|
|
|
|
|
|
|
|
// 定时发射烟花
|
|
|
|
|
|
const launchInterval = setInterval(() => {
|
|
|
|
|
|
if (launchCount >= MAX_LAUNCHES) {
|
|
|
|
|
|
clearInterval(launchInterval);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const x = w * (0.15 + Math.random() * 0.7); // 避免贴近边缘
|
|
|
|
|
|
const y = h * (0.1 + Math.random() * 0.5); // 在屏幕上半区爆炸
|
|
|
|
|
|
const count = Math.floor(Math.random() * 40) + 60;
|
|
|
|
|
|
particles = particles.concat(_burst(x, y, count));
|
|
|
|
|
|
launchCount++;
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
|
|
const startTime = performance.now();
|
|
|
|
|
|
|
|
|
|
|
|
// 动画循环
|
|
|
|
|
|
function animate(now) {
|
2026-02-27 14:17:56 +08:00
|
|
|
|
// 清除画布(保持透明,不遮挡聊天背景)
|
|
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
2026-02-27 14:14:35 +08:00
|
|
|
|
|
2026-02-27 14:17:56 +08:00
|
|
|
|
// 更新并绘制存活粒子(粒子自带 alpha 衰减,视觉上有淡出效果)
|
2026-02-27 14:14:35 +08:00
|
|
|
|
particles = particles.filter((p) => p.alpha > 0.02);
|
|
|
|
|
|
particles.forEach((p) => {
|
|
|
|
|
|
p.update();
|
|
|
|
|
|
p.draw(ctx);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (now - startTime < DURATION) {
|
|
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 特效结束:清空 canvas 后回调
|
|
|
|
|
|
clearInterval(launchInterval);
|
|
|
|
|
|
cancelAnimationFrame(animId);
|
|
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
|
|
onEnd();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { start };
|
|
|
|
|
|
})();
|