之前在 b 站上看到一个关于下雨效果的视频,觉得很有意思,于是就想着自己动手实现一下。

那个视频中是使用 css 实现的,我就想着试试另一种方案,使用 js 实现下雨效果。

效果预览

我们先来看看最终的效果

大体思路

使用 js 实现的话就得用 canvas 了

我们先考虑一个雨滴的效果怎么实现的

首先我们可以观察得到,雨滴是由一个矩形形成的,下落到地面的时候,高度会逐渐变小,高度变小后宽度会逐渐变大,然后消失。

那么思路是不是很明确了,先画一个矩形,然后逐渐增大它的 Y 轴坐标,到达一定的值(地面)的时候就停止 Y 轴变化,然后逐渐减小高度到一定的值,高度到达一定的值后再增大宽度,最后消失。

代码实现

我这里是采用面向对象的方式实现的

由于实现起来相对比较简单,这里不再对代码进行过多解释,具体可以参考代码中的注释

将下面代码随便粘贴到一个 js 文件或者一个 script 标签中即可运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// 立即执行函数
(function () {
// 雨滴类
class RainDrop {
constructor(params) {
// 获取参数并进行设置
const {
width,
height,
ctx,
canvas,
speed,
rangeY,
targetY,
color,
rainDropWidth,
rainDropHeight,
rainDropXScale,
rainDropYScale,
rainDropXExpandOffset,
rainDropYExpendOffset,
} = params;
this.params = params;
this.x = Math.random() * width;
this.y = rangeY[0] + Math.random() * (rangeY[1] - rangeY[0]);
this.complete = false;
this.width =
rainDropWidth[0] +
Math.random() * (rainDropWidth[1] - rainDropWidth[0]);
this.height =
rainDropHeight[0] +
Math.random() * (rainDropHeight[1] - rainDropHeight[0]);
this.targetWidth = this.width * rainDropXScale;
this.targetHeight = this.height / rainDropYScale;
this.speed = speed[0] + Math.random() * (speed[1] - speed[0]);
this.targetY = targetY[0] + Math.random() * (targetY[1] - targetY[0]);
this.color = color;
}

// 雨滴扩展函数
expand() {
if (this.height > this.targetHeight) {
return (this.height -= this.params.rainDropYExpendOffset);
}
if (this.width < this.targetWidth) {
return (this.width += this.params.rainDropXExpandOffset);
}
// 雨滴已完成
this.complete = true;
}

animate() {
// 如果到达了目标高度,则开始扩展雨滴,否则继续下落
if (this.y >= this.targetY) {
this.expand();
} else {
this.y += this.speed;
}
}
}

// 整个雨滴效果类
class Rain {
constructor(params) {
this.drops = []; // 雨滴数组
if (!params.canvas) {
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d");
} else {
this.canvas = params.canvas;
this.ctx = this.canvas.getContext("2d");
}
// 参数对象,默认参数与传入的参数进行合并
this.params = {
width: window.innerWidth,
height: window.innerHeight,
rainDropWidth: [1, 5],
rainDropHeight: [2, 12],
rainDropYScale: 5,
rainDropXScale: 5,
rainDropXExpandOffset: 1.5,
rainDropYExpendOffset: 1.5,
speed: [4, 12],
targetY: [window.innerHeight * 0.7, window.innerHeight * 0.9],
rangeY: [0, 100],
color: "#05a2eb",
...params, // 合并传入的参数
};
this.canvas.width = this.params.width;
this.canvas.height = this.params.height;
this.canvas.style.zIndex = -9999;
this.canvas.style.pointerEvents = "none";
this.canvas.style.backgroundColor = "rgba(0, 0, 0, 1)";
document.body.appendChild(this.canvas);
}

// 雨滴绘制函数
drawDrop(drop) {
this.ctx.beginPath();
this.ctx.fillStyle = drop.color;
// 雨滴的坐标是以矩形中心为原点的
// 所以这里需要将矩形的中心坐标调整到雨滴的坐标上
this.ctx.fillRect(
drop.x - drop.width / 2,
drop.y - drop.height,
drop.width,
drop.height
);
this.ctx.closePath();
}

// 整个效果的动画函数
animate() {
// 清除画布
this.ctx.clearRect(0, 0, this.params.width, this.params.height);
// 绘制所有雨滴,并执行雨滴的动画函数
this.drops.forEach((drop) => {
this.drawDrop(drop);
drop.animate();
});
// 过滤掉已完成的雨滴
this.drops = this.drops.filter((drop) => !drop.complete);
}

init() {
setInterval(() => {
this.drops.push(new RainDrop(this.params));
}, 20);
}
}

// 构建下雨效果对象
const rain = new Rain({
width: window.innerWidth, // 整个效果的宽度
height: window.innerHeight, // 整个效果的高度
rainDropWidth: [1, 5], // 雨滴的宽度范围
rainDropHeight: [2, 12], // 雨滴的高度范围
rainDropYScale: 5, // 雨滴的高度缩放比例
rainDropXScale: 5, // 雨滴的宽度缩放比例
rainDropXExpandOffset: 1.5, // 雨滴的宽度扩展比例
rainDropYExpendOffset: 1.5, // 雨滴的高度扩展比例
speed: [4, 12], // 雨滴的速度范围
targetY: [window.innerHeight * 0.7, window.innerHeight * 0.9], // 雨滴的初始Y轴坐标范围
rangeY: [0, 100], // 雨滴的初始高度范围
color: "#05a2eb", // 雨滴的颜色
canvas: null, // 传入的 canvas 元素,不传入的话内部会创建一个
});
// 初始化一下
rain.init();
// 渲染函数
const render = () => {
// 调用动画函数
rain.animate();
requestAnimationFrame(render);
};
// 调用渲染函数
render();
})();

代码执行流程

  1. 创建一个 Rain 实例,设置画布大小和样式。
  2. 初始化雨滴生成定时器,每隔 20 毫秒生成一个新的雨滴。
  3. 使用 requestAnimationFrame 不断调用 render 函数,更新和绘制雨滴。

类 Rain

这个类表示整个雨滴动画效果。

构造函数

  • 初始化雨滴数组 drops。
  • 创建或使用传入的 canvas 元素,并设置其大小和样式。
  • 初始化参数对象 params,包含画布的宽度、高度、雨滴的速度、颜色等属性。

方法

  • drawDrop(drop): 在画布上绘制一个雨滴。
  • animate(): 清除画布并重新绘制所有雨滴,更新雨滴的状态。
  • init(): 每隔 20 毫秒生成一个新的雨滴。

类 RainDrop

这个类表示一个雨滴对象。

构造函数

  • 接受一个参数对象 params,包含雨滴的各种属性。
  • 初始化雨滴的位置、大小、速度、目标位置和颜色等属性。

方法

  • expand(): 扩展雨滴的大小,直到达到目标大小。
  • animate(): 更新雨滴的位置,如果到达目标位置则调用 expand() 方法。

进阶

你还可以将雨滴设置成圆形或者换成其他图片,还可以随机设置雨滴的颜色等等来达到更好看的效果