138 lines
4.1 KiB
JavaScript
138 lines
4.1 KiB
JavaScript
/**
|
||
* 文件功能:聊天室烟花特效
|
||
*
|
||
* 使用 Canvas 粒子系统在全屏播放多发烟花爆炸动画。
|
||
* 粒子加大、加发光描边,在浅色背景上也清晰可见。
|
||
* 特效总时长约 5 秒,结束后自动清理并回调。
|
||
*/
|
||
|
||
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() * 7 + 3;
|
||
this.vx = Math.cos(angle) * speed;
|
||
this.vy = Math.sin(angle) * speed;
|
||
this.alpha = 1;
|
||
this.gravity = 0.12;
|
||
this.decay = Math.random() * 0.01 + 0.01; // 衰减略慢,显色更久
|
||
this.radius = Math.random() * 4 + 2; // 增大粒子半径
|
||
}
|
||
|
||
/** 每帧更新粒子位置和状态 */
|
||
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.shadowColor = this.color;
|
||
ctx.shadowBlur = 8;
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
// 预定义烟花颜色组(饱和度高,避免和浅蓝背景撞色)
|
||
const COLORS = [
|
||
"#ff2200",
|
||
"#ff7700",
|
||
"#ffcc00",
|
||
"#00cc33",
|
||
"#cc00ff",
|
||
"#ff0088",
|
||
"#00aaff",
|
||
"#ff4488",
|
||
"#ff6600",
|
||
"#aaff00",
|
||
"#ff2255",
|
||
"#ffaa00",
|
||
];
|
||
|
||
/**
|
||
* 发射一枚烟花,返回粒子数组
|
||
*/
|
||
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 = 5000; // 总时长(ms)
|
||
|
||
let particles = [];
|
||
let animId = null;
|
||
let launchCount = 0;
|
||
const MAX_LAUNCHES = 10; // 总发射枚数(增加)
|
||
|
||
// 定时发射烟花
|
||
const launchInterval = setInterval(() => {
|
||
if (launchCount >= MAX_LAUNCHES) {
|
||
clearInterval(launchInterval);
|
||
return;
|
||
}
|
||
const x = w * (0.1 + Math.random() * 0.8);
|
||
const y = h * (0.05 + Math.random() * 0.5);
|
||
const cnt = Math.floor(Math.random() * 50) + 80; // 每枚 80-130 粒子(增多)
|
||
particles = particles.concat(_burst(x, y, cnt));
|
||
launchCount++;
|
||
}, 450);
|
||
|
||
const startTime = performance.now();
|
||
|
||
// 动画循环
|
||
function animate(now) {
|
||
// 清除画布(透明,不遮挡聊天背景)
|
||
ctx.clearRect(0, 0, w, h);
|
||
|
||
// 更新并绘制存活粒子
|
||
particles = particles.filter((p) => p.alpha > 0.02);
|
||
particles.forEach((p) => {
|
||
p.update();
|
||
p.draw(ctx);
|
||
});
|
||
|
||
if (now - startTime < DURATION) {
|
||
animId = requestAnimationFrame(animate);
|
||
} else {
|
||
clearInterval(launchInterval);
|
||
cancelAnimationFrame(animId);
|
||
ctx.clearRect(0, 0, w, h);
|
||
onEnd();
|
||
}
|
||
}
|
||
|
||
animId = requestAnimationFrame(animate);
|
||
}
|
||
|
||
return { start };
|
||
})();
|