loading 效果是一种常见的用户体验设计,它可以让用户在等待页面加载的过程中感受到动态和趣味。
大家好,我是渡一前端子辰老师
今天,我们就来学习一种简单而有趣的 loading 效果,它的原理是利用 CSS3 的 transform 和 animation 属性,让黑白两球不断地交替运动,形成一个旋转的圆环。
下面,我们就一步一步地来实现这个效果吧。
实现
首先,我们需要在 HTML 页面中创建一个 div 元素,作为 loading 效果的容器。
<div class="loading"></div>
然后,我们需要在这个容器中添加 36 个子元素,每个子元素都是一个 div 元素,类名为 dot。
这些子元素就是我们的黑白两球的容器。
<div class="loading">
<!-- dot 重复 36 次 -->
<div class="dot"></div>
</div>
然后我们用 scss 写一些样式,对 scss 不了解的话可以点击链接去看一下。
$ballSize: 10px; // 小球尺寸
$containerSize: 150px; // 容器尺寸
.loading {
width: $containerSize;
height: $containerSize;
margin: 50px auto;
position: relative;
border-radius: 50%;
outline: 1px solid #fff;
.dot {
position: absolute;
left: 50%;
top: 50%;
width: $ballSize;
height: $ballSize;
margin-left: -$ballSize / 2;
margin-top: -$ballSize / 2;
background: red;
}
}
我们给 loading 容器设置一些基本样式,让它居中显示,并且有一个圆形的轮廓。
接着,我们给每个 dot 容器设置一些样式,让它们绝对定位在 loading 容器的中心,并且有一个红色的背景色。
目前的效果就是这样子。
分布小球
显然,这还不是我们想要的效果。
我们需要把这些 dot 容器平均分布在圆周上,形成一个圆环。
例如,如果我们想让第一个 dot 容器移动到圆的顶部,我们可以这样写:
.dot:nth-child(1) {
transform: translateY(-$containerSize / 2);
}
然后要平均分布的话要旋转,所以我们要加上 rotate。
.dot:nth-child(1) {
transform: rotate(45deg) translateY(-$containerSize / 2);
}
例如:这里我们让它旋转 45 度。
那么我们就可以用这样的方式来处理每一个球,因为我们有 36 个球,圆有 360 度,所以两球之间的角度就是 360 / 36
,我们使用变量表示就是:
$n: 36; // 小球的数量
$pdeg: 360deg / $n; // 小球的间隔角度
所以我们就可以知道,第一个球的角度是 @pdeg
,第二个是 @pdeg * 2
,第三个是 @pdeg * 3
,依次类推。
那么移动的距离不变,旋转又是有规律的,所以我们就可以利用 scss 里边的循环来做这件事。
@for $i from 1 through $n {
.dot:nth-child(#{$i}) {
transform: rotate(#{$i * $pdeg}) translateY(-$containerSize / 2);
}
}
生成的代码就是这个样子,一直到 .dot:nth-child(36){...}
然后我们就得到了这样的效果。
生成黑白小球
小球的容器已经平均分布成一圈了,我们现在要做的就是在每一个容器里生成一个白色和一个黑色的小球。
我们这里利用伪元素 ::before
和 ::after
两个伪元素来实现两个小球。
.dot {
// etc...
&::before,
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
}
&::before {
background: #000;
}
&::after {
background: #fff;
}
}
可以看到现在白球和黑球重合在一起了,所以我们现在需要将白球和黑球分开,我们分别让一个球上移一个球下移。
.dot {
// etc...
&::before {
background: #000;
top: -100%;
}
&::after {
background: #fff;
top: 100%;
}
}
这样黑白两球就分开了,那么接下来就是实现动画了,让它们动起来。
实现动画
我们先来看一下两个球的运动方式。
如图所示黑白球交替环绕运动:
- 25% 的时候,黑球和白球运动到了中间,黑球覆盖在白球之上,黑球视觉拉近,白球视觉拉远。
- 50% 的时候,黑球和白球互换了位置。
- 75% 的时候,黑球和白球再次运动到了中间,白球覆盖在黑球之上,白球视觉拉近,黑球视觉拉远。
- 100% 的时候,各自回到 0% 的位置,然后往复循环。
上图中也可以看到,两个球的运动是 3D 的,这样才会有覆盖和视觉上大小的差别。
那么我们知道了运动的逻辑,现在去实现一下,我们先写黑球的动画。
$ani-duration: 2000ms; // 动画时间
.loading {
// etc...
.dot {
// etc...
&::before {
background: #000;
top: -100%;
animation: moveBlack $ani-duration infinite;
}
// etc...
}
}
@keyframes moveBlack {
25% {
transform: translate3d(0, 100%, $ballSize);
}
50% {
transform: translate3d(0, 200%, 0);
}
75% {
transform: translate3d(0, 100%, -$ballSize);
}
}
因为是 3D 的效果所以这里我们使用的是 translate3d:
- 25% 的时候,黑球移动到中间,x 轴不移动,y 轴移动 100%,z 轴拉近一个小球的身位。
- 50% 的时候,黑球移动对侧,x 轴不移动,y 轴移动 200%,z 轴恢复到初始位置。
- 75% 的时候,黑球移动到中间,x 轴不移动,y 轴移动 100%,z 轴拉远一个小球的身位。
- 100% 的时候,黑球回到 0% 的位置,然后往复循环。
然后我们将动画应用到黑球上,动画时间我们使用变量设置 2 秒,并且无限重复。
可以看到,现在黑球的动画就是这样的了。
但是这个动画看上去不太舒服,我们来调整一下每一段动画的时间曲线。
@keyframes moveBlack {
0% {
animation-timing-function: ease-in;
}
25% {
transform: translate3d(0, 100%, -$ballSize);
animation-timing-function: ease-out;
}
50% {
transform: translate3d(0, 200%, 0);
animation-timing-function: ease-in;
}
75% {
transform: translate3d(0, 100%, $ballSize);
animation-timing-function: ease-out;
}
}
经过子辰的多次尝试,以上的曲线是比较舒服的。
那么黑球的动画看上去就没问题了,而白球的动画的移动和黑球是正好相反的。
.loading {
// etc...
.dot {
// etc...
&::after {
background: #fff;
top: 100%;
animation: moveWhite $ani-duration infinite;
}
// etc...
}
}
@keyframes moveWhite {
0% {
animation-timing-function: ease-in;
}
25% {
transform: translate3d(0, -100%, -$ballSize);
animation-timing-function: ease-out;
}
50% {
transform: translate3d(0, -200%, 0);
animation-timing-function: ease-in;
}
75% {
transform: translate3d(0, -100%, $ballSize);
animation-timing-function: ease-out;
}
}
黑球和白球的动画都可以了,但是现在没有 3D 效果,是因为我们还缺少一个设置。
.loading {
// etc...
.dot {
// etc...
// 通过 perspective 属性设置景深
perspective: 70px;
// 开启 3D 效果
transform-style: preserve-3d;
}
}
这样就有放大缩小的感觉了。
那么我们就差做出两球的交替效果了,这无非就是动画的延迟。
对每一个 dot 里边的 before 和 after 设置不同的动画延迟。
@for $i from 1 through $n {
.dot:nth-child(#{$i}) {
transform: rotate(#{$i * $pdeg}) translateY(-$containerSize / 2);
&::before,
&::after {
animation-delay: $ani-duration / $n * $i;
}
}
}
我们知道动画的总时间是 $ani-duration
,小球的数量是 $n
,时间除以数量的话是不是每个小球平均的间隔时间,然后我们在乘以 $i
目前是第几个小球。
它就变成这样了,因为延迟时间有点太接近了,所以我们把延迟的时间增加一点。
@for $i from 1 through $n {
.dot:nth-child(#{$i}) {
transform: rotate(#{$i * $pdeg}) translateY(-$containerSize / 2);
&::before,
&::after {
animation-delay: $ani-duration / $n * 6 * $i;
}
}
}
这样效果到是出来了,但是一开始它延迟时间太久了,所以我们一开始给他设置为负数就可以了。
让它以之前那个时间点来计算它目前的位置。
@for $i from 1 through $n {
.dot:nth-child(#{$i}) {
transform: rotate(#{$i * $pdeg}) translateY(-$containerSize / 2);
&::before,
&::after {
animation-delay: -$ani-duration / $n * 6 * $i;
}
}
}
这样效果就完美了,最后我们去掉白线就可以了。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #66c7f4;
}
$ballSize: 10px; // 小球尺寸
$containerSize: 150px; // 容器尺寸
$n: 36; // 小球的数量
$pdeg: 360deg / $n; // 小球的间隔角度
$ani-duration: 2000ms; // 动画时间
.loading {
width: $containerSize;
height: $containerSize;
margin: 50px auto;
position: relative;
border-radius: 50%;
.dot {
position: absolute;
left: 50%;
top: 50%;
width: $ballSize;
height: $ballSize;
margin-left: -$ballSize / 2;
margin-top: -$ballSize / 2;
perspective: 70px;
transform-style: preserve-3d;
&::before,
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
}
&::before {
background: #000;
top: -100%;
animation: moveBlack 2s infinite;
}
&::after {
background: #fff;
top: 100%;
animation: moveWhite 2s infinite;
}
}
}
@keyframes moveBlack {
0% {
animation-timing-function: ease-in;
}
25% {
transform: translate3d(0, 100%, $ballSize);
animation-timing-function: ease-out;
}
50% {
transform: translate3d(0, 200%, 0);
animation-timing-function: ease-in;
}
75% {
transform: translate3d(0, 100%, -$ballSize);
animation-timing-function: ease-out;
}
}
@keyframes moveWhite {
0% {
animation-timing-function: ease-in;
}
25% {
transform: translate3d(0, -100%, -$ballSize);
animation-timing-function: ease-out;
}
50% {
transform: translate3d(0, -200%, 0);
animation-timing-function: ease-in;
}
75% {
transform: translate3d(0, -100%, $ballSize);
animation-timing-function: ease-out;
}
}
@for $i from 1 through $n {
.dot:nth-child(#{$i}) {
transform: rotate(#{$i * $pdeg}) translateY(-$containerSize / 2);
&::before,
&::after {
animation-delay: -$ani-duration / $n * 6 * $i;
}
}
}
总结
通过这篇文章,我们学习了如何用 CSS3 的 transform 和 animation 属性来实现一个简单而有趣的 loading 效果。
我们利用了伪元素来生成黑白两球,利用了 translate3d 函数来实现三维的移动效果,利用了 animation-delay 属性来实现交替的动画效果。
这个效果不仅可以增加用户的等待乐趣,也可以锻炼我们的 CSS3 技能。
希望你能喜欢这篇文章,并在自己的项目中尝试一下这个效果。
本文来源
本文来源自渡一官方公众号:Duing,欢迎关注,获取最新、最全、最深入的技术讲解
感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!