108 lines
3.4 KiB
JavaScript
108 lines
3.4 KiB
JavaScript
|
|
/**
|
|||
|
|
* 文件功能:聊天室下雪特效
|
|||
|
|
*
|
|||
|
|
* 使用 Canvas 绘制随机飘落的雪花圆点,模拟冬日飘雪效果。
|
|||
|
|
* 雪花大小、速度、飘动幅度随机,在浅色背景上以白色+深描边显示。
|
|||
|
|
* 特效总时长约 10 秒,结束后自动清理并回调。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const SnowEffect = (() => {
|
|||
|
|
// 雪花类
|
|||
|
|
class Flake {
|
|||
|
|
constructor(w, h) {
|
|||
|
|
this.w = w;
|
|||
|
|
this.h = h;
|
|||
|
|
this.reset(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置雪花位置
|
|||
|
|
*
|
|||
|
|
* @param {boolean} initial 是否初始化(初始化时 Y 随机分布全屏,否则从顶部重生)
|
|||
|
|
*/
|
|||
|
|
reset(initial = false) {
|
|||
|
|
this.x = Math.random() * this.w;
|
|||
|
|
this.y = initial ? Math.random() * this.h : -10;
|
|||
|
|
this.r = Math.random() * 4 + 2; // 半径 2-6
|
|||
|
|
this.speed = Math.random() * 1.5 + 0.5; // 下落速度
|
|||
|
|
this.drift = Math.random() * 0.8 - 0.4; // 水平漂移
|
|||
|
|
this.alpha = Math.random() * 0.4 + 0.6; // 透明度 0.6-1.0
|
|||
|
|
this.angle = 0;
|
|||
|
|
this.wobble = Math.random() * 0.04 + 0.01; // 左右摇摆频率
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 每帧更新雪花位置 */
|
|||
|
|
update() {
|
|||
|
|
this.angle += this.wobble;
|
|||
|
|
this.x += Math.sin(this.angle) * this.drift + this.drift * 0.3;
|
|||
|
|
this.y += this.speed;
|
|||
|
|
if (this.y > this.h + 10) {
|
|||
|
|
this.reset(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 绘制雪花(白色圆点 + 深色描边,在浅色背景上可见) */
|
|||
|
|
draw(ctx) {
|
|||
|
|
ctx.save();
|
|||
|
|
ctx.globalAlpha = this.alpha;
|
|||
|
|
// 外圈:半透明蓝灰描边
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
|
|||
|
|
ctx.strokeStyle = "rgba(80, 120, 180, 0.6)";
|
|||
|
|
ctx.lineWidth = 0.8;
|
|||
|
|
ctx.stroke();
|
|||
|
|
// 内部:白色填充
|
|||
|
|
ctx.fillStyle = "#ffffff";
|
|||
|
|
ctx.shadowColor = "rgba(150, 180, 255, 0.8)";
|
|||
|
|
ctx.shadowBlur = 4;
|
|||
|
|
ctx.fill();
|
|||
|
|
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 = 10000; // 总时长(ms)
|
|||
|
|
const FLAKE_COUNT = 160; // 雪花数量
|
|||
|
|
|
|||
|
|
// 初始化雪花,随机分布全屏(避免开始时全堆在顶部)
|
|||
|
|
const flakes = Array.from(
|
|||
|
|
{ length: FLAKE_COUNT },
|
|||
|
|
() => new Flake(w, h),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let animId = null;
|
|||
|
|
const startTime = performance.now();
|
|||
|
|
|
|||
|
|
function animate(now) {
|
|||
|
|
// 清除画布(透明,不遮挡聊天背景)
|
|||
|
|
ctx.clearRect(0, 0, w, h);
|
|||
|
|
|
|||
|
|
flakes.forEach((f) => {
|
|||
|
|
f.update();
|
|||
|
|
f.draw(ctx);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (now - startTime < DURATION) {
|
|||
|
|
animId = requestAnimationFrame(animate);
|
|||
|
|
} else {
|
|||
|
|
cancelAnimationFrame(animId);
|
|||
|
|
ctx.clearRect(0, 0, w, h);
|
|||
|
|
onEnd();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
animId = requestAnimationFrame(animate);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { start };
|
|||
|
|
})();
|