152 lines
5.2 KiB
JavaScript
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 };
|
|
})();
|