童年回忆-类加速球的水球加载动画

逛博客的时候看到了这么一个水球效果,整体是一个球形容器,球顶不断有水滴滴落,随着水滴滴落,底部水位不断上升,水面上涨的同时伴随着波动的动画。

难道你就是…360加速小球???

总之就只是一个写起来好玩的东西 (有水文章之嫌疑)

(感谢豆包大人对代码变量名和代码注释的大力支持)

HTML 部分

1
2
3
4
5
6
7
<div>
<div class="wave_holder">
<div class="wave wave1"></div>
<div class="wave wave2"></div>
<div class="percentage">0%</div>
</div>
</div>

分别是整个球形容器 ,其中的波浪 wave1wave2 ,和加载进度

对于水球内水的波动立体效果 其实是用这里的两个波浪元素的差异实现的(看后面的 CSS 部分)

CSS 部分

水球本体部分

1
2
3
4
5
6
7
8
9
.wave_holder  {
position: relative;
width: 200px;
height: 200px;
border-radius: 50%;
background: rgba(255,  255,  255,  0.1);
overflow: hidden;
box-shadow: 00010pxrgba (255,  255,  255,  0.05),     0020pxrgba(108,  92,  231,  0.3);
}

简单构造了一个圆

这里用 box-shadow 实现了一个 外发光和内阴影 效果 能让整个水球显得更加有质感一点()

波浪部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.wave {
position: absolute;
width: 200%;
height: 200%;
top: 50%;
left: -50%;
transform-origin: 50% 50%;
animation: wave 6s linear infinite;
}

.wave1 {
background: rgba(108, 92, 231, 0.8);
border-radius: 45%;
animation-duration: 6s;
}

.wave2 {
background: rgba(0, 206, 255, 0.6);
border-radius: 40%;
animation-duration: 8s;
top: 60%;
}

这里通过设置两层波浪颜色的不同和 border-radius 的不同来让这两层波浪有了差异从而实现立体感(也许吧..)

现在就只需要添加波浪的滚动效果了 这里直接给波浪添加旋转动画 通过设置不同的动画速度来实现层次感

1
2
3
4
5
6
7
8
@keyframes wave {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

加载进度部分

1
2
3
4
5
6
7
8
9
10
11
12
.percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: black;
font-family: sans-serif;
font-size: 2rem;
font-weight: bold;
z-index: 10;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}

通过设置 z-index 来保证进度始终在最上方

通过设置 text-shadow 来让进度看起来发光()

为了让整个进度加载得更加有趣 决定在这里添加一个水滴元素 通过水滴的滴落来增加加载进度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.drop  {
position: absolute;
width: 8px;
height: 12px;
transform-origin: center;
opacity: 0;
background: linear-gradient(
180deg,
rgba(0, 206, 255, 0.9),
rgba(108, 92, 231, 0.9)
);
border-radius: 50% 50% 50% 50%/60% 60% 40% 40%;
box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
}

水滴设置的效果跟之前的设置都差不多

JavaScript 部分

一些基础的逻辑部分这里就省略了(详细可以看最后面的全代码) 这里只写一些在这里学到的东西

随机生成水滴

这里需要处理的一个是水滴出现的时机 和 水滴每次随机出现的位置

水滴出现的时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let progress = 0;
const totalTime = 15000; // 15秒加载时间
const interval = 50; // 更新间隔
const increment = 100 / (totalTime / interval);

// 水滴参数
const dropIntervalMin = 200; // 最小滴落间隔(ms)
const dropIntervalMax = 300; // 最大滴落间隔(ms)
let nextDropTime = 0; // 下次滴落时间

if (Date.now() > nextDropTime && progress < 100) {
createDrop();
nextDropTime =
Date.now() +
dropIntervalMin +
Math.random() * (dropIntervalMax - dropIntervalMin);
}

水滴每次出现的位置

1
2
3
4
// 随机位置
const x = Math.random() * 80 + 10; // 10% - 90%
drop.style.left = `${x}%`;
drop.style.top = "-15px";

这里用随机数来设定水滴的水平位置

同时把水滴的竖直位置固定在容器的上方 这样来模拟水滴从最上面掉下来的状态

模拟水滴下落

这里其实就是模拟的重力作用下的掉落(跟我寒假大作业的时候写的子弹下降大差不差 hhh)

1
2
3
4
5
6
7
8
9
10
11
// 下落动画
const height = waveLoader.offsetHeight;
const gravity = 0.25;
const drag = 0.98;
let velocity = 0;
let position = -15;

velocity += gravity;
velocity *= drag;
position += velocity;
drop.style.top = `${position}px`;

到达水面后的处理

这也是一开始自己没有注意到的一点 即当水滴到达了水面后 应该将其移除掉从而体现出融入水面的感觉

这部分也是跟着源码学习到的

水滴的移除通过设置 opacity 来实现

水滴的移除与否通过比较其位置和水平面的关系来决定

1
2
3
4
5
6
7
8
9
10
11
12
13
// 计算水面位置
const waterLevel = 100 - progress;
const waterY = (waterLevel / 100) * height;

// 到达水面后
if (position >= waterY - 5) {
clearInterval(dropInterval);
// 隐藏并移除水滴
drop.style.transition = "opacity 0.2s";
drop.style.opacity = "0";
setTimeout(() => waveLoader.removeChild(drop), 300);
}
}, 16);

注意这里设置 position >= waterY - 5 是提前触发 这样能更加自然

动画循环

这也是自己完全没有想到的循环这个过程 这部分都是在源码中学习的

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
// 动画循环
const loadingInterval = setInterval(() => {
progress += increment;
progress = Math.min(progress, 100);
percentageEl.textContent = `${Math.round(progress)}%`;

// 调整波浪位置
const height = 100 - progress;
wave1.style.top = `${height}%`;
wave2.style.top = `${height + 10}%`;

// 随机生成水滴
if (Date.now() > nextDropTime && progress < 100) {
createDrop();
nextDropTime =
Date.now() +
dropIntervalMin +
Math.random() * (dropIntervalMax - dropIntervalMin);
}

// 重置进度
if (progress >= 100) {
setTimeout(() => {
// 重置进度条
setTimeout(() => {
progress = 0;
}, 1500);
}, 1000);
}
}, interval);

源码部分

这里像源码一样设置了

loader-container:固定定位的全屏容器,用于覆盖整个页面,z-index: 9999确保层级最高

不得不说确实美观了不少

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>水球进度加载</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}

.loader-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #1a1e2d;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

.wave_holder {
position: relative;
width: 200px;
height: 200px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
overflow: hidden;
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0.05), 0 0 20px rgba(108, 92, 231, 0.3);
}

.wave {
position: absolute;
width: 200%;
height: 200%;
top: 50%;
left: -50%;
transform-origin: 50% 50%;
animation: wave 6s linear infinite;
}

.wave1 {
background: rgba(108, 92, 231, 0.8);
border-radius: 45%;
animation-duration: 6s;
}

.wave2 {
background: rgba(0, 206, 255, 0.6);
border-radius: 40%;
animation-duration: 8s;
top: 60%;
}

.percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-family: sans-serif;
font-size: 2rem;
font-weight: bold;
z-index: 10;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}

@keyframes wave {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.drop {
position: absolute;
width: 8px;
height: 12px;
transform-origin: center;
opacity: 0;
background: linear-gradient(
180deg,
rgba(0, 206, 255, 0.9),
rgba(108, 92, 231, 0.9)
);
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
}

.ripple {
position: absolute;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform-origin: center;
opacity: 0;
}
</style>
</head>
<body>
<div class="loader-container">
<div class="wave_holder">
<div class="wave wave1"></div>
<div class="wave wave2"></div>
<div class="percentage">0%</div>
</div>
</div>

<script>
const percentageEl = document.querySelector(".percentage");
const wave1 = document.querySelector(".wave1");
const wave2 = document.querySelector(".wave2");
const waveLoader = document.querySelector(".wave_holder");

let progress = 0;
const totalTime = 15000; // 15秒加载时间
const interval = 50; // 更新间隔
const increment = 100 / (totalTime / interval);

// 水滴参数
const dropIntervalMin = 200; // 最小滴落间隔(ms)
const dropIntervalMax = 300; // 最大滴落间隔(ms)
let nextDropTime = 0; // 下次滴落时间

// 创建水滴元素
function createDrop() {
const drop = document.createElement("div");
drop.classList.add("drop");
waveLoader.appendChild(drop);

// 随机位置
const x = Math.random() * 80 + 10; // 10% - 90%
drop.style.left = `${x}%`;
drop.style.top = "-15px";

// 下落动画
const height = waveLoader.offsetHeight;
const gravity = 0.25;
const drag = 0.98;
let velocity = 0;
let position = -15;

// 显示水滴
drop.style.opacity = "1";

// 计算水面位置
const waterLevel = 100 - progress;
const waterY = (waterLevel / 100) * height;

const dropInterval = setInterval(() => {
velocity += gravity;
velocity *= drag;
position += velocity;
drop.style.top = `${position}px`;

// 到达水面后
if (position >= waterY - 5) {
clearInterval(dropInterval);
// 隐藏并移除水滴
drop.style.transition = "opacity 0.2s";
drop.style.opacity = "0";
setTimeout(() => waveLoader.removeChild(drop), 300);
}
}, 16);
}

// 动画循环
const loadingInterval = setInterval(() => {
progress += increment;
progress = Math.min(progress, 100);
percentageEl.textContent = `${Math.round(progress)}%`;

// 调整波浪位置
const height = 100 - progress;
wave1.style.top = `${height}%`;
wave2.style.top = `${height + 10}%`;

// 随机生成水滴
if (Date.now() > nextDropTime && progress < 100) {
createDrop();
nextDropTime =
Date.now() +
dropIntervalMin +
Math.random() * (dropIntervalMax - dropIntervalMin);
}

// 重置进度
if (progress >= 100) {
setTimeout(() => {
// 重置进度条
setTimeout(() => {
progress = 0;
}, 1500);
}, 1000);
}
}, interval);
</script>
</body>
</html>