109 lines
3.4 KiB
JavaScript
109 lines
3.4 KiB
JavaScript
/**
|
||
* 文件功能:聊天室下雨特效
|
||
*
|
||
* 使用 Canvas 绘制斜向雨线,模拟真实降雨视觉效果。
|
||
* 特效总时长约 8 秒,结束后自动清理并回调。
|
||
*/
|
||
|
||
const RainEffect = (() => {
|
||
// 雨滴类:一条从顶部往下落的斜线
|
||
class Drop {
|
||
constructor(w, h) {
|
||
this.reset(w, h);
|
||
}
|
||
|
||
/**
|
||
* 重置/初始化雨滴位置
|
||
*
|
||
* @param {number} w Canvas 宽度
|
||
* @param {number} h Canvas 高度
|
||
*/
|
||
reset(w, h) {
|
||
this.x = Math.random() * w;
|
||
this.y = Math.random() * -h; // 从屏幕上方随机位置开始
|
||
this.len = Math.random() * 20 + 10; // 雨线长度
|
||
this.speed = Math.random() * 8 + 6; // 下落速度
|
||
this.angle = (Math.PI / 180) * (75 + Math.random() * 10); // 倾斜角(接近竖直偏右)
|
||
this.alpha = Math.random() * 0.3 + 0.2; // 透明度
|
||
this.w = w;
|
||
this.h = h;
|
||
}
|
||
|
||
/** 每帧更新雨滴位置 */
|
||
update() {
|
||
this.x += Math.cos(this.angle) * this.speed * 0.3;
|
||
this.y += Math.sin(this.angle) * this.speed;
|
||
// 落出屏幕后重置
|
||
if (this.y > this.h + this.len) {
|
||
this.reset(this.w, this.h);
|
||
}
|
||
}
|
||
|
||
/** 绘制雨滴线段 */
|
||
draw(ctx) {
|
||
ctx.save();
|
||
ctx.strokeStyle = `rgba(155, 200, 255, ${this.alpha})`;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath();
|
||
ctx.moveTo(this.x, this.y);
|
||
ctx.lineTo(
|
||
this.x + Math.cos(this.angle) * this.len,
|
||
this.y + Math.sin(this.angle) * this.len,
|
||
);
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启动下雨特效
|
||
*
|
||
* @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 = 8000; // 总时长(ms)
|
||
const DROP_COUNT = 180; // 雨滴数量
|
||
|
||
// 初始化所有雨滴,随机分布在屏幕各处(避免开始时从顶部一起落)
|
||
const drops = Array.from({ length: DROP_COUNT }, () => {
|
||
const d = new Drop(w, h);
|
||
d.y = Math.random() * h; // 初始 Y 随机,不全部从顶部开始
|
||
return d;
|
||
});
|
||
|
||
let animId = null;
|
||
const startTime = performance.now();
|
||
|
||
// 画"乌云"背景遮罩(让画面有阴暗感但不完全遮住聊天)
|
||
ctx.fillStyle = "rgba(30, 40, 60, 0.18)";
|
||
ctx.fillRect(0, 0, w, h);
|
||
|
||
function animate(now) {
|
||
// 用极轻微的透明背景刷新(保留少量拖尾感)
|
||
ctx.fillStyle = "rgba(30, 40, 60, 0.08)";
|
||
ctx.fillRect(0, 0, w, h);
|
||
|
||
drops.forEach((d) => {
|
||
d.update();
|
||
d.draw(ctx);
|
||
});
|
||
|
||
if (now - startTime < DURATION) {
|
||
animId = requestAnimationFrame(animate);
|
||
} else {
|
||
cancelAnimationFrame(animId);
|
||
ctx.clearRect(0, 0, w, h);
|
||
onEnd();
|
||
}
|
||
}
|
||
|
||
animId = requestAnimationFrame(animate);
|
||
}
|
||
|
||
return { start };
|
||
})();
|