Files
chatroom/public/js/effects/fireflies.js

152 lines
5.2 KiB
JavaScript

/**
* 文件功能:聊天室萤火虫特效
*
* 使用高亮柔光粒子模拟夜色中的萤火虫,让光点在屏幕中缓慢游走、
* 呼吸闪烁,适合常驻氛围和安静主题房间。
*/
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 };
})();