/** * 文件功能:聊天室萤火虫特效 * * 使用高亮柔光粒子模拟夜色中的萤火虫,让光点在屏幕中缓慢游走、 * 呼吸闪烁,适合常驻氛围和安静主题房间。 */ const FirefliesEffect = (() => { class Firefly { constructor(w, h) { this.w = w; this.h = h; this.reset(); } /** * 重置萤火虫位置与呼吸参数。 */ reset() { this.x = Math.random() * this.w; this.y = Math.random() * this.h; this.baseRadius = Math.random() * 3 + 2.4; this.alpha = Math.random() * 0.24 + 0.36; this.phase = Math.random() * Math.PI * 2; this.phaseSpeed = Math.random() * 0.05 + 0.015; this.angle = Math.random() * Math.PI * 2; this.speed = Math.random() * 0.55 + 0.22; this.flutter = Math.random() * Math.PI * 2; this.flutterSpeed = Math.random() * 0.18 + 0.08; this.trail = []; } /** * 更新萤火虫轨迹。 */ update() { this.phase += this.phaseSpeed; this.flutter += this.flutterSpeed; this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > 7) { this.trail.shift(); } this.angle += (Math.random() - 0.5) * 0.08 + Math.sin(this.flutter) * 0.012; this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; if (this.x < -20) this.x = this.w + 20; if (this.x > this.w + 20) this.x = -20; if (this.y < -20) this.y = this.h + 20; if (this.y > this.h + 20) this.y = -20; } /** * 绘制发光萤火虫。 * * @param {CanvasRenderingContext2D} ctx */ draw(ctx) { const pulse = 0.38 + (Math.sin(this.phase) + 1) * 0.42; const radius = this.baseRadius * pulse; const wingSwing = Math.sin(this.flutter) * 1.9; this.trail.forEach((point, index) => { const alpha = ((index + 1) / this.trail.length) * 0.08; ctx.save(); ctx.fillStyle = `rgba(250, 204, 21, ${alpha})`; ctx.shadowColor = "rgba(250, 204, 21, 0.45)"; ctx.shadowBlur = 6; ctx.beginPath(); ctx.arc(point.x, point.y, Math.max(0.8, radius * 0.55), 0, Math.PI * 2); ctx.fill(); ctx.restore(); }); ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle); // 先画半透明翅膀,再画虫身和发光尾部,让画面更像“萤火虫”而不是单纯光点。 ctx.globalAlpha = 0.18 + pulse * 0.08; ctx.fillStyle = "#f8fafc"; ctx.beginPath(); ctx.ellipse(-radius * 1.15, -radius * 0.45, radius * 1.05, radius * 0.55, wingSwing * 0.05, 0, Math.PI * 2); ctx.ellipse(-radius * 0.1, radius * 0.45, radius * 1.05, radius * 0.55, -wingSwing * 0.05, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = this.alpha + pulse * 0.3; ctx.fillStyle = "#fde047"; ctx.shadowColor = "rgba(250, 204, 21, 0.85)"; ctx.shadowBlur = 18 + pulse * 12; ctx.beginPath(); ctx.arc(radius * 0.75, 0, radius, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "rgba(51, 65, 85, 0.9)"; ctx.shadowBlur = 0; ctx.beginPath(); ctx.ellipse(-radius * 0.25, 0, radius * 1.05, Math.max(1.4, radius * 0.5), 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "rgba(248, 250, 252, 0.35)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-radius * 1.1, -radius * 0.2); ctx.lineTo(-radius * 1.65, -radius * 0.8); ctx.moveTo(-radius * 1.1, radius * 0.2); ctx.lineTo(-radius * 1.65, radius * 0.8); ctx.stroke(); 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 = 10500; const COUNT = Math.min(52, Math.max(24, Math.floor(w / 34))); const fireflies = Array.from({ length: COUNT }, () => new Firefly(w, h)); const startTime = performance.now(); let animId = null; function animate(now) { ctx.clearRect(0, 0, w, h); fireflies.forEach((firefly) => { firefly.update(); firefly.draw(ctx); }); if (now - startTime < DURATION) { animId = requestAnimationFrame(animate); } else { cancelAnimationFrame(animId); ctx.clearRect(0, 0, w, h); onEnd(); } } animId = requestAnimationFrame(animate); } return { start }; })();