/** * 文件功能:聊天室烟花特效(真实感版本) * * 完整模拟真实烟花流程: * 1. 火箭从底部带着尾迹飞向目标高度 * 2. 到达位置后爆炸,产生 3 种形态:球形/柳叶/星形 * 3. 每颗粒子带历史轨迹尾巴,随重力下坠并消散 * * 全程透明 Canvas,不遮挡聊天背景。 */ const FireworksEffect = (() => { // ─── 火箭类 ────────────────────────────────────────── class Rocket { /** * @param {number} x 发射 x 位置 * @param {number} targetY 爆炸目标高度(距顶部比例) * @param {string} color 爆炸颜色 * @param {string} type 爆炸类型:sphere / willow / ring */ constructor(x, targetY, color, type) { this.x = x; this.y = 9999; // 在 start() 里设置为 canvas.height this.targetY = targetY; this.color = color; this.type = type; this.vy = -(12 + Math.random() * 4); // 上升速度 this.trail = []; // 尾迹历史坐标 this.exploded = false; this.done = false; } /** 更新火箭位置,到达目标高度后标记为已爆炸 */ update() { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > 10) this.trail.shift(); this.y += this.vy; this.vy *= 0.98; // 轻微减速(仿真阻力) if (this.y <= this.targetY) { this.exploded = true; this.done = true; } } /** 绘制火箭(白色头部 + 橙色尾迹) */ draw(ctx) { // 尾迹(由新到旧逐渐变淡) for (let i = 0; i < this.trail.length; i++) { const a = (i / this.trail.length) * 0.6; ctx.save(); ctx.globalAlpha = a; ctx.fillStyle = "#ffaa44"; ctx.shadowColor = "#ff6600"; ctx.shadowBlur = 6; ctx.beginPath(); ctx.arc(this.trail[i].x, this.trail[i].y, 2, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } // 火箭头部(亮白色光点) ctx.save(); ctx.fillStyle = "#ffffff"; ctx.shadowColor = "#ffcc88"; ctx.shadowBlur = 10; ctx.beginPath(); ctx.arc(this.x, this.y, 3, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } // ─── 爆炸粒子类 ────────────────────────────────────── class Particle { /** * @param {number} x 爆炸中心 x * @param {number} y 爆炸中心 y * @param {string} color 粒子颜色 * @param {string} type 爆炸类型 * @param {number} angle 发射角度(ring 类型用) */ constructor(x, y, color, type, angle) { this.x = x; this.y = y; this.color = color; this.trail = []; let speed; if (type === "ring") { // 环形:均匀角度,固定速度 speed = 5 + Math.random() * 2; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; this.gravity = 0.06; this.decay = 0.014; this.radius = 2; } else if (type === "willow") { // 柳叶:慢速,在空中下垂 speed = Math.random() * 3 + 1; const a = Math.random() * Math.PI * 2; this.vx = Math.cos(a) * speed; this.vy = Math.sin(a) * speed - 2; // 初速稍微向上 this.gravity = 0.07; this.decay = 0.009; // 衰减慢,拖出长尾 this.radius = 1.5; } else { // sphere:标准球形爆炸 speed = Math.random() * 6 + 2; const a = Math.random() * Math.PI * 2; this.vx = Math.cos(a) * speed; this.vy = Math.sin(a) * speed; this.gravity = 0.1; this.decay = 0.016; this.radius = Math.random() * 2 + 1.5; } this.alpha = 1; // 部分粒子有闪烁效果 this.sparkle = Math.random() > 0.6; this.frame = 0; } /** 更新粒子物理状态 */ update() { this.frame++; // 保存轨迹历史(尾迹长度由透明度控制) this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > 6) this.trail.shift(); this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.vx *= 0.985; this.vy *= 0.985; // 闪烁:每隔几帧透明度轻微抖动 if (this.sparkle && this.frame % 4 === 0) { this.alpha = Math.max( 0, this.alpha - this.decay * (0.5 + Math.random()), ); } else { this.alpha -= this.decay; } } /** 绘制粒子及其运动轨迹尾迹 */ draw(ctx) { // 绘制尾迹 for (let i = 0; i < this.trail.length; i++) { const a = (i / this.trail.length) * this.alpha * 0.45; ctx.save(); ctx.globalAlpha = a; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc( this.trail[i].x, this.trail[i].y, this.radius * 0.55, 0, Math.PI * 2, ); ctx.fill(); ctx.restore(); } // 绘制粒子主体(带发光) 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(); } get alive() { return this.alpha > 0.01; } } // ─── 预定义颜色 / 类型 ─────────────────────────────── const COLORS = [ "#ff2200", "#ff7700", "#ffcc00", "#00dd44", "#cc00ff", "#ff0088", "#00bbff", "#ff4488", "#ffaa00", ]; const TYPES = ["sphere", "willow", "ring"]; /** * 生成一批爆炸粒子 * * @param {number} x 爆炸中心 x * @param {number} y 爆炸中心 y * @param {string} color 颜色 * @param {string} type 爆炸类型 * @returns {Particle[]} */ function _burst(x, y, color, type) { const particles = []; const count = type === "ring" ? 80 : type === "willow" ? 120 : 100; if (type === "ring") { // 环形:均匀分布 for (let i = 0; i < count; i++) { const angle = ((Math.PI * 2) / count) * i; particles.push(new Particle(x, y, color, type, angle)); } } else { for (let i = 0; i < count; i++) { particles.push(new Particle(x, y, color, type, 0)); } // willow/sphere 中心加一颗白色闪光核心粒子 for (let i = 0; i < 10; i++) { const p = new Particle(x, y, "#ffffff", "sphere", 0); p.decay *= 2; p.radius = 1; particles.push(p); } } 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 = 10000; let rockets = []; let particles = []; let animId = null; let launchCnt = 0; const MAX_LAUNCHES = 12; // 定时发射火箭 const launchInterval = setInterval(() => { if (launchCnt >= MAX_LAUNCHES) { clearInterval(launchInterval); return; } const x = w * (0.15 + Math.random() * 0.7); const ty = h * (0.08 + Math.random() * 0.45); const color = COLORS[Math.floor(Math.random() * COLORS.length)]; const type = TYPES[Math.floor(Math.random() * TYPES.length)]; const rocket = new Rocket(x, ty, color, type); rocket.y = h; // 从底部发射 rockets.push(rocket); launchCnt++; }, 600); const startTime = performance.now(); function animate(now) { ctx.clearRect(0, 0, w, h); // 更新和绘制火箭 for (let i = rockets.length - 1; i >= 0; i--) { const r = rockets[i]; if (r.done) { // 火箭爆炸:生成粒子 const burst = _burst(r.x, r.y, r.color, r.type); particles = particles.concat(burst); rockets.splice(i, 1); } else { r.update(); r.draw(ctx); } } // 更新和绘制粒子 particles = particles.filter((p) => p.alive); 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 }; })();