前言

最近,我对网站白天的雪花动画做了个优化。原来的雪花效果过于单一,没有什么新鲜感,所以我决定进行一次改造,稍微丰富一下雪花的样式和特效。下面是我的优化过程和具体的代码,希望给各位朋友们提供一下参考和灵感。

初始版本

最初的雪花动画相对简单,雪花形状只是简单的绘制了一个圆形,通过垂直速度和水平速度来实现雪花的飘落过程。代码如下所示:

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
const h = () => {
n.clearRect(0, 0, i.width, i.height);
const r = e.minDist;
for (let t = 0; t < o; t++) {
let o = s[t];
const h = a,
w = d,
m = o.x,
c = o.y,
p = Math.sqrt((h - m) * (h - m) + (w - c) * (w - c));
if (p < r) {
const e = (h - m) / p,
t = (w - c) / p,
i = r / (p * p) / 2;
o.velX -= i * e,
o.velY -= i * t
} else
o.velX *= .98,
o.velY < o.speed && o.speed - o.velY > .01 && (o.velY += .01 * (o.speed - o.velY)),
o.velX += Math.cos(o.step += .05) * o.stepSize;
n.fillStyle = "rgba(" + e.color + ", " + o.opacity + ")",
o.y += o.velY,
o.x += o.velX,
(o.y >= i.height || o.y <= 0) && l(o),
(o.x >= i.width || o.x <= 0) && l(o),
n.beginPath(),
n.arc(o.x, o.y, o.size, 0, 2 * Math.PI), // 绘制一个圆,半径为o.size, 2 * Math.PI表示绘制一个完整的圆,即360°
n.fill()
}
t(h)
}

第一步:丰富雪花形态

为了让雪花看起来不再那么单调,我就设计了几款六边形状的雪花,加入了一点点的细节,具体如下所示。

第一种:

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
// 绘制六边形雪花
const drawFirstSnowflake = (ctx, x, y, size, color, opacity, rotation) => {
const branches = 6; // 六边形对称
const angleStep = (Math.PI * 2) / branches;

// 绘制雪花主分支
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation); // 根据当前旋转角度旋转画布
ctx.strokeStyle = "rgba(" + color + ", " + opacity + ")"; // 设置颜色和透明度
ctx.lineWidth = size * 0.05; // 可以调整线宽来提高精度

for (let i = 0; i < branches; i++) {
ctx.rotate(angleStep);
drawFirstBranch(ctx, size); // 绘制每个主分支
}
ctx.restore();
};

// 绘制雪花分支,添加细节
const drawFirstBranch = (ctx, size) => {
const branchLength = size; // 主干长度
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -branchLength); // 主干
ctx.stroke();

// 在主干上添加更多的小分支
const smallBranchLength = branchLength * 0.4; // 小分支长度
const angle = Math.PI / 6; // 分支的基础角度

// 添加多层分支
const branchLayers = 8; // 增加小分支层数(提高密度和精度)
for (let i = 1; i <= branchLayers; i++) {
const offset = (branchLength / (branchLayers + 1)) * i; // 分支位置
const angleOffset = Math.PI / 16; // 每层小分支的固定角度偏移

// 左侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
-smallBranchLength * Math.sin(angle + angleOffset * i), // 动态调整角度偏移
-offset - smallBranchLength * Math.cos(angle + angleOffset * i)
);
ctx.stroke();

// 右侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
smallBranchLength * Math.sin(angle - angleOffset * i), // 动态调整角度偏移
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.stroke();

// 在小分支上添加末端装饰
if (i > branchLayers / 2) { // 仅在靠近末端部分增加装饰
ctx.beginPath();
ctx.moveTo(
smallBranchLength * Math.sin(angle - angleOffset * i),
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.lineTo(
smallBranchLength * 1.2 * Math.sin(angle - angleOffset * i + Math.PI / 12), // 动态调整角度偏移
-offset - smallBranchLength * 1.2 * Math.cos(angle - angleOffset * i + Math.PI / 12)
);
ctx.stroke();
}
}
};

第一种雪花截图:
主要是通过正弦函数sin()和余弦函数cos()来控制线条的偏移角度,为了清晰展示雪花形状,下面这张图中雪花的半径为50,实际展示效果参考本站即可。
img.png

第二种:

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
// 绘制六边形雪花
const drawSecondSnowflake = (ctx, x, y, size, color, opacity, rotation) => {
const branches = 6; // 六边形对称
const angleStep = (Math.PI * 2) / branches;

// 绘制雪花主分支
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation); // 根据当前旋转角度旋转画布
ctx.strokeStyle = "rgba(" + color + ", " + opacity + ")"; // 设置颜色和透明度
ctx.lineWidth = size * 0.05; // 可以调整线宽来提高精度

for (let i = 0; i < branches; i++) {
ctx.rotate(angleStep);
drawSecondBranch(ctx, size); // 绘制每个主分支
}
ctx.restore();
};

// 绘制新的雪花分支
const drawSecondBranch = (ctx, size) => {
const branchLength = size * 1.5; // 主干长度
const smallBranchLength = branchLength * 0.4; // 小分支长度
const angle = Math.PI / 6; // 分支的基础角度

// 主干
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -branchLength); // 主干
ctx.stroke();

// 在主干上添加更多的小分支
const branchLayers = 6; // 增加小分支层数
for (let i = 1; i <= branchLayers; i++) {
const offset = (branchLength / (branchLayers + 1)) * i; // 分支位置
const angleOffset = Math.PI / 16; // 每层小分支的固定角度偏移

// 左侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
-smallBranchLength * Math.sin(angle + angleOffset * i),
-offset - smallBranchLength * Math.cos(angle + angleOffset * i)
);
ctx.stroke();

// 右侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
smallBranchLength * Math.sin(angle - angleOffset * i),
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.stroke();

// 在小分支上添加末端装饰
if (i > branchLayers / 2) { // 仅在靠近末端部分增加装饰
ctx.beginPath();
ctx.moveTo(
smallBranchLength * Math.sin(angle - angleOffset * i),
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.lineTo(
smallBranchLength * 1.2 * Math.sin(angle - angleOffset * i + Math.PI / 12), // 动态调整角度偏移
-offset - smallBranchLength * 1.2 * Math.cos(angle - angleOffset * i + Math.PI / 12)
);
ctx.stroke();
}
}

// 增加更小的分支
const miniBranchLength = branchLength * 0.2; // 更小的分支
const miniBranchLayers = 4;
for (let j = 1; j <= miniBranchLayers; j++) {
const miniOffset = (branchLength / (miniBranchLayers + 1)) * j;
const miniAngleOffset = Math.PI / 20;

// 一侧分支
ctx.beginPath();
ctx.moveTo(0, -miniOffset);
ctx.lineTo(
miniBranchLength * Math.sin(angle + miniAngleOffset * j),
-miniOffset - miniBranchLength * Math.cos(angle + miniAngleOffset * j)
);
ctx.stroke();

// 另一侧的分支
ctx.beginPath();
ctx.moveTo(0, -miniOffset);
ctx.lineTo(
-miniBranchLength * Math.sin(angle - miniAngleOffset * j),
-miniOffset - miniBranchLength * Math.cos(angle - miniAngleOffset * j)
);
ctx.stroke();
}
};

第二种雪花截图:
和第一种实现的效果差不多,主要的改动是在第一种的基础上在对应线条的右侧添加了的一部分装饰线条、调整了一下偏移角度等等。当然这个雪花的半径也是50,实际大小要小很多,效果也比这个好,具体参考本站。
img_1.png

第二步:给下落的雪花添加新的动画

1、为了让雪花下落看起来更加自然,我给雪花下落时添加了一定的旋转角度和旋转速度,旋转方向根据角度的正负来决定向左还是向右。下面我们需要在雪花的初始化参数中添加旋转角度和旋转速度这两个参数,具体代码如下所示:
雪花参数初始化:

1
2
3
4
5
6
7
8
9
10
11
12
const e = {
flakeCount: 50, // 雪花数目
minDist: 150, // 最小距离
color: "255, 255, 255", // 雪花颜色
size: 6, // 雪花大小
speed: 0.5, // 雪花速度
opacity: 0.6, // 雪花透明度
stepsize: 0.5, // 步距
rotation: Math.PI * 2 ,// 旋转角度,在后续的初始化中设置范围
rotationSpeed: 0.02, // 旋转速度
snowflakeMethod: drawFirstSnowflake // 初始化生成雪花的方法
};

在上面的代码中,我们可以随机的设置雪花的基本参数以及选择雪花形状的初始化方法,其中参数大小要根据实际情况设置,参数太大或者太小都会导致实际的效果不太好。

2、在添加完初始化参数后,我们需要在雪花对应的初始化代码中添加ctx.rotate(rotation); 这行代码,具体在上方的雪花初始化中都有描述。接下来需要在雪花运行的主函数中更新角度偏移o.rotation += o.rotationSpeed,具体如下所示:
雪花运动的主函数:

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
const h = () => {
n.clearRect(0, 0, i.width, i.height);
const r = e.minDist;
for (let t = 0; t < o; t++) {
let o = s[t];
const h = a,
w = d,
m = o.x,
c = o.y,
p = Math.sqrt((h - m) * (h - m) + (w - c) * (w - c));
if (p < r) {
const e = (h - m) / p,
t = (w - c) / p,
i = r / (p * p) / 2;
o.velX -= i * e;
o.velY -= i * t;
} else {
o.velX *= 0.98;
if (o.velY < o.speed && o.speed - o.velY > 0.01)
o.velY += 0.01 * (o.speed - o.velY);
o.velX += Math.cos((o.step += 0.05)) * o.stepSize;
}
o.rotation += o.rotationSpeed; // 更新旋转角度
o.y += o.velY;
o.x += o.velX;

// 雪花超出边界时重置位置
if (o.y >= i.height || o.y <= 0) {
o.opacity -= 0.02; // 慢慢淡出
if (o.opacity <= 0) resetFlake(o); // 完全透明后重置雪花
}
if (o.x >= i.width || o.x <= 0) {
o.opacity -= 0.02; // 慢慢淡出
if (o.opacity <= 0) resetFlake(o); // 完全透明后重置雪花
}
// 调用绘制雪花的代码
o.snowflakeMethod(n, o.x, o.y, o.size, e.color, o.opacity, o.rotation);
}
t(h);
};

第三步:雪花参数重置

第二步主要是完成了雪花参数的初始化以及角度偏移操作,后面主要是实现雪花位置的重置。当雪花下落到页面底部时,我们就需要对雪花的基本参数以及初始化方法进行重置,让他可以重新从某个随机的位置下落。基本参数可以参考上方的初始化参数。同时雪花的形状的初始化方法也可以进行重置,我们可以在多个雪花的初始化方法中随机取一个,这样就实现页面上雪花形状随机的效果了。
雪花重置:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 重置雪花位置、属性以及调用方法
const resetFlake = (flake) => {
flake.x = Math.floor(Math.random() * i.width);
flake.y = 0;
flake.size = 4 * Math.random() + e.size;
flake.speed = Math.random() + e.speed;
flake.velY = flake.speed;
flake.velX = 0;
flake.opacity = 0.6 * Math.random() + e.opacity;
flake.rotation = Math.random() * e.rotation;
flake.rotationSpeed = (Math.random() - 0.5) * e.rotationSpeed;
flake.snowflakeMethod = drawRandomSnowflake();
};

随机调用雪花初始化方法:

1
2
3
4
5
6
7
8
// 随机选择一个雪花绘制方法
const drawRandomSnowflake = () => {
const snowflakeMethods = [drawFirstSnowflake, drawSecondSnowflake];

// 随机选择一个方法
const randomIndex = Math.floor(Math.random() * snowflakeMethods.length);
return snowflakeMethods[randomIndex];
};

雪花形状的初始化方法你可以参考以上的两种方法,也可以自定义自己实现的方法,只需要添加到snowflakeMethods数组中即可。

第四步:初始化调用

在以上操作完成后,就需要初始化调用一下整个流程的代码了以及在调用的页面初始化一个Canvas,这个没什么好说明的,具体代码如下所示:
雪花代码调用初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 初始化雪花
(() => {
for (let t = 0; t < o; t++) {
s.push({
speed: Math.random() + e.speed,
velX: 0,
velY: Math.random() + e.speed,
x: Math.floor(Math.random() * i.width),
y: Math.floor(Math.random() * i.height),
size: 4 * Math.random() + e.size,
stepSize: (Math.random() / 30) * e.stepsize,
step: 0,
opacity: 0.6 * Math.random() + e.opacity,
rotation: Math.random() * e.rotation,
rotationSpeed: (Math.random() - 0.5) * e.rotationSpeed,
snowflakeMethod: e.snowflakeMethod
});
}
h();
})();

在这个初始化代码的循环中o是雪花的数量,我们需要根据第二步中雪花的初始化参数对每个雪花的参数进行初始化。函数h()是雪花运动的主函数。

初始化Canvas:

1
<canvas id="snow"></canvas>

完整代码

下面是实现雪花动画的完整js代码

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
function snowflake(){
if (
!(
navigator.userAgent.match(
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
)
)
) {
// 只在非移动设备上执行雪花动画
window &&
(() => {
// 绘制六边形雪花
const drawFirstSnowflake = (ctx, x, y, size, color, opacity, rotation) => {
const branches = 6; // 六边形对称
const angleStep = (Math.PI * 2) / branches;

// 绘制雪花主分支
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation); // 根据当前旋转角度旋转画布
ctx.strokeStyle = "rgba(" + color + ", " + opacity + ")"; // 设置颜色和透明度
ctx.lineWidth = size * 0.05; // 可以调整线宽来提高精度

for (let i = 0; i < branches; i++) {
ctx.rotate(angleStep);
drawBranch(ctx, size); // 绘制每个主分支
}
ctx.restore();
};

// 绘制雪花分支,增加更多的细节
const drawBranch = (ctx, size) => {
const branchLength = size; // 主干长度
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -branchLength); // 主干
ctx.stroke();

// 在主干上添加更多的小分支
const smallBranchLength = branchLength * 0.4; // 小分支长度
const angle = Math.PI / 6; // 分支的基础角度

// 添加多层分支
const branchLayers = 8; // 增加小分支层数(提高密度和精度)
for (let i = 1; i <= branchLayers; i++) {
const offset = (branchLength / (branchLayers + 1)) * i; // 分支位置
const angleOffset = Math.PI / 16; // 每层小分支的固定角度偏移

// 左侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
-smallBranchLength * Math.sin(angle + angleOffset * i), // 动态调整角度偏移
-offset - smallBranchLength * Math.cos(angle + angleOffset * i)
);
ctx.stroke();

// 右侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
smallBranchLength * Math.sin(angle - angleOffset * i), // 动态调整角度偏移
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.stroke();

if (i > branchLayers / 2) {
ctx.beginPath();
ctx.moveTo(
smallBranchLength * Math.sin(angle - angleOffset * i),
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.lineTo(
smallBranchLength * 1.2 * Math.sin(angle - angleOffset * i + Math.PI / 12),
-offset - smallBranchLength * 1.2 * Math.cos(angle - angleOffset * i + Math.PI / 12)
);
ctx.stroke();
}
}
};
// 绘制新的六边形雪花
const drawSecondSnowflake = (ctx, x, y, size, color, opacity, rotation) => {
const branches = 6; // 六边形对称
const angleStep = (Math.PI * 2) / branches;

// 绘制雪花主分支
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation); // 根据当前旋转角度旋转画布
ctx.strokeStyle = "rgba(" + color + ", " + opacity + ")"; // 设置颜色和透明度
ctx.lineWidth = size * 0.05; // 可以调整线宽来提高精度

for (let i = 0; i < branches; i++) {
ctx.rotate(angleStep);
drawNewBranch(ctx, size); // 绘制每个主分支
}
ctx.restore();
};

// 绘制新的雪花分支
const drawNewBranch = (ctx, size) => {
const branchLength = size * 1.3; // 主干长度
const smallBranchLength = branchLength * 0.4; // 小分支长度
const angle = Math.PI / 6; // 分支的基础角度

// 主干
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -branchLength); // 主干
ctx.stroke();

// 在主干上添加更多的小分支
const branchLayers = 6; // 增加小分支层数
for (let i = 1; i <= branchLayers; i++) {
const offset = (branchLength / (branchLayers + 1)) * i; // 分支位置
const angleOffset = Math.PI / 16; // 每层小分支的固定角度偏移

// 左侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
-smallBranchLength * Math.sin(angle + angleOffset * i),
-offset - smallBranchLength * Math.cos(angle + angleOffset * i)
);
ctx.stroke();

// 右侧分支
ctx.beginPath();
ctx.moveTo(0, -offset);
ctx.lineTo(
smallBranchLength * Math.sin(angle - angleOffset * i),
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.stroke();

if (i > branchLayers / 2) {
ctx.beginPath();
ctx.moveTo(
smallBranchLength * Math.sin(angle - angleOffset * i),
-offset - smallBranchLength * Math.cos(angle - angleOffset * i)
);
ctx.lineTo(
smallBranchLength * 1.2 * Math.sin(angle - angleOffset * i + Math.PI / 12),
-offset - smallBranchLength * 1.2 * Math.cos(angle - angleOffset * i + Math.PI / 12)
);
ctx.stroke();
}
}

const miniBranchLength = branchLength * 0.2;
const miniBranchLayers = 4;
for (let j = 1; j <= miniBranchLayers; j++) {
const miniOffset = (branchLength / (miniBranchLayers + 1)) * j;
const miniAngleOffset = Math.PI / 20;

ctx.beginPath();
ctx.moveTo(0, -miniOffset);
ctx.lineTo(
miniBranchLength * Math.sin(angle + miniAngleOffset * j),
-miniOffset - miniBranchLength * Math.cos(angle + miniAngleOffset * j)
);
ctx.stroke();

ctx.beginPath();
ctx.moveTo(0, -miniOffset);
ctx.lineTo(
-miniBranchLength * Math.sin(angle - miniAngleOffset * j),
-miniOffset - miniBranchLength * Math.cos(angle - miniAngleOffset * j)
);
ctx.stroke();
}
};

// 随机选择一个雪花绘制方法
const drawRandomSnowflake = () => {
const snowflakeMethods = [drawFirstSnowflake, drawSecondSnowflake ];

// 随机选择一个方法
const randomIndex = Math.floor(Math.random() * snowflakeMethods.length);
return snowflakeMethods[randomIndex];
};

// 使用节流函数来控制事件触发频率
const throttle = (fn, wait) => {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
fn(...args);
}
};
}
const e = {
flakeCount: 50, // 雪花数目
minDist: 150, // 最小距离
color: "255, 255, 255", // 雪花颜色
size: 6, // 雪花大小
speed: 0.5, // 雪花速度
opacity: 0.6, // 雪花透明度
stepsize: 0.5, // 步距
rotation: Math.PI * 2 ,// 旋转角度
rotationSpeed: 0.02, // 旋转速度
snowflakeMethod: drawFirstSnowflake // 初始化生成雪花的方法
};

const t =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (e) {
window.setTimeout(e, 1e3 / 60);
};
window.requestAnimationFrame = t;

const i = document.getElementById("snow"),
n = i.getContext("2d"),
o = e.flakeCount;
let a = -100,
d = -100,
s = [];

// 设置canvas宽高
i.width = window.innerWidth;
i.height = window.innerHeight;

// 雪花运动的主函数
const h = () => {
n.clearRect(0, 0, i.width, i.height);
const r = e.minDist;
for (let t = 0; t < o; t++) {
let o = s[t];
const h = a,
w = d,
m = o.x,
c = o.y,
p = Math.sqrt((h - m) * (h - m) + (w - c) * (w - c));
if (p < r) {
const e = (h - m) / p,
t = (w - c) / p,
i = r / (p * p) / 2;
o.velX -= i * e;
o.velY -= i * t;
} else {
o.velX *= 0.98;
if (o.velY < o.speed && o.speed - o.velY > 0.01)
o.velY += 0.01 * (o.speed - o.velY);
o.velX += Math.cos((o.step += 0.05)) * o.stepSize;
}
o.rotation += o.rotationSpeed; // 更新旋转角度
o.y += o.velY;
o.x += o.velX;

// 雪花超出边界时重置位置
if (o.y >= i.height || o.y <= 0) {
o.opacity -= 0.02; // 慢慢淡出
if (o.opacity <= 0) resetFlake(o); // 完全透明后重置雪花
}
if (o.x >= i.width || o.x <= 0) {
o.opacity -= 0.02; // 慢慢淡出
if (o.opacity <= 0) resetFlake(o); // 完全透明后重置雪花
}
// 随机绘制雪花
o.snowflakeMethod(n, o.x, o.y, o.size, e.color, o.opacity, o.rotation);
}
t(h);
};

// 重置雪花位置、属性以及调用方法
const resetFlake = (flake) => {
flake.x = Math.floor(Math.random() * i.width);
flake.y = 0;
flake.size = 4 * Math.random() + e.size;
flake.speed = Math.random() + e.speed;
flake.velY = flake.speed;
flake.velX = 0;
flake.opacity = 0.6 * Math.random() + e.opacity;
flake.rotation = Math.random() * e.rotation;
flake.rotationSpeed = (Math.random() - 0.5) * e.rotationSpeed;
flake.snowflakeMethod = drawRandomSnowflake();
};

document.addEventListener("mousemove", e => {
a = e.clientX;
d = e.clientY;
});

// 监听窗口大小变化
window.addEventListener("resize",throttle(() => {
i.width = window.innerWidth;
i.height = window.innerHeight;
}, 20));

// 初始化雪花
(() => {
for (let t = 0; t < o; t++) {
s.push({
speed: Math.random() + e.speed,
velX: 0,
velY: Math.random() + e.speed,
x: Math.floor(Math.random() * i.width),
y: Math.floor(Math.random() * i.height),
size: 4 * Math.random() + e.size,
stepSize: (Math.random() / 30) * e.stepsize,
step: 0,
opacity: 0.6 * Math.random() + e.opacity,
rotation: Math.random() * e.rotation,
rotationSpeed: (Math.random() - 0.5) * e.rotationSpeed,
snowflakeMethod: e.snowflakeMethod
});
}
h();
})();
})();
}
}

总结

由于技术有限,以上方法目前来说还存在许多要优化的地方,比如最主要的性能问题、雪花在出现和消失时的淡入淡出不是很流畅等等。总的来说嘛,这个功能的实现可以说是有了一定基础,但在细节上还需要调整调整。