2026-02-27 14:14:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 文件功能:聊天室下雨特效
|
|
|
|
|
|
*
|
|
|
|
|
|
* 使用 Canvas 绘制斜向雨线,模拟真实降雨视觉效果。
|
2026-02-27 14:22:13 +08:00
|
|
|
|
* 加粗加深雨线颜色,在浅色背景上清晰可见。
|
2026-02-27 14:14:35 +08:00
|
|
|
|
* 特效总时长约 8 秒,结束后自动清理并回调。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const RainEffect = (() => {
|
|
|
|
|
|
// 雨滴类:一条从顶部往下落的斜线
|
|
|
|
|
|
class Drop {
|
|
|
|
|
|
constructor(w, h) {
|
|
|
|
|
|
this.reset(w, h);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置/初始化雨滴位置
|
|
|
|
|
|
*/
|
|
|
|
|
|
reset(w, h) {
|
|
|
|
|
|
this.x = Math.random() * w;
|
2026-02-27 14:22:13 +08:00
|
|
|
|
this.y = Math.random() * -h;
|
|
|
|
|
|
this.len = Math.random() * 25 + 12; // 雨线长度(稍加长)
|
|
|
|
|
|
this.speed = Math.random() * 10 + 7; // 下落速度(加快)
|
|
|
|
|
|
this.angle = (Math.PI / 180) * (75 + Math.random() * 10);
|
|
|
|
|
|
this.alpha = Math.random() * 0.5 + 0.4; // 提高透明度上限 (0.4-0.9,原 0.2-0.5)
|
|
|
|
|
|
this.strokeW = Math.random() * 1.5 + 0.8; // 线条宽度随机(原 0.8 固定)
|
2026-02-27 14:14:35 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 14:22:13 +08:00
|
|
|
|
/** 绘制雨滴线段(加深蓝色,在浅色背景上更明显) */
|
2026-02-27 14:14:35 +08:00
|
|
|
|
draw(ctx) {
|
|
|
|
|
|
ctx.save();
|
2026-02-27 14:22:13 +08:00
|
|
|
|
ctx.strokeStyle = `rgba(50, 130, 220, ${this.alpha})`; // 加深蓝色(原浅蓝 155,200,255)
|
|
|
|
|
|
ctx.lineWidth = this.strokeW;
|
|
|
|
|
|
ctx.shadowColor = "rgba(30, 100, 200, 0.4)";
|
|
|
|
|
|
ctx.shadowBlur = 2;
|
2026-02-27 14:14:35 +08:00
|
|
|
|
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;
|
2026-02-27 14:22:13 +08:00
|
|
|
|
const DURATION = 8000;
|
2026-04-25 03:34:19 +08:00
|
|
|
|
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
|
|
|
|
|
|
const DROP_COUNT = isMobile ? 130 : 200; // 移动端降低雨线数量,避免满屏线段拖慢滚动和输入。
|
2026-02-27 14:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
const drops = Array.from({ length: DROP_COUNT }, () => {
|
|
|
|
|
|
const d = new Drop(w, h);
|
2026-02-27 14:22:13 +08:00
|
|
|
|
d.y = Math.random() * h;
|
2026-02-27 14:14:35 +08:00
|
|
|
|
return d;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let animId = null;
|
2026-04-25 02:52:30 +08:00
|
|
|
|
let finished = false;
|
2026-02-27 14:14:35 +08:00
|
|
|
|
const startTime = performance.now();
|
|
|
|
|
|
|
2026-04-25 02:52:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 统一结束雨滴动画,手动取消时不触发队列续播。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {boolean} canceled 是否为手动取消
|
|
|
|
|
|
*/
|
|
|
|
|
|
function finish(canceled) {
|
|
|
|
|
|
if (finished) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
finished = true;
|
|
|
|
|
|
if (animId) {
|
|
|
|
|
|
cancelAnimationFrame(animId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
|
|
if (!canceled) {
|
|
|
|
|
|
onEnd();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 14:14:35 +08:00
|
|
|
|
function animate(now) {
|
2026-02-27 14:22:13 +08:00
|
|
|
|
// 清除画布(透明,不遮挡聊天背景)
|
2026-02-27 14:17:56 +08:00
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
2026-02-27 14:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
drops.forEach((d) => {
|
|
|
|
|
|
d.update();
|
|
|
|
|
|
d.draw(ctx);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (now - startTime < DURATION) {
|
|
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
|
|
|
|
} else {
|
2026-04-25 02:52:30 +08:00
|
|
|
|
finish(false);
|
2026-02-27 14:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
animId = requestAnimationFrame(animate);
|
2026-04-25 02:52:30 +08:00
|
|
|
|
return {
|
|
|
|
|
|
cancel() {
|
|
|
|
|
|
finish(true);
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-02-27 14:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { start };
|
|
|
|
|
|
})();
|
2026-04-25 03:02:56 +08:00
|
|
|
|
|
|
|
|
|
|
window.RainEffect = RainEffect;
|