最近项目中使用到一个Loading效果,其实是一个很简单的效果,主要是因为这个Loading出现在不同的场景之中,而且大小也不一致。对于这样的效果,往往都会想通过组件的方式来处理,其出发点就是更易维护,易扩展。当然,这对于前端的同学而言并没有什么复杂性,也没有多少技术含量。不过我还是希望把这个过程记录下来。
咱们先来看一个截图:
从上图可以看出来,其效果是一样的,不同之处是使用场景不同,大小不同而以。那么接下来,就来聊聊这样的一个效果怎么通过不同的方式来完成。
实现原理
实现上面的这样的一个效果,我们需要有一点点数学相关的知识,这样更有易于后续效果制作。比如说,示例中有14
个圆点,那么这14
个圆点具有自己相关的特性参数:
- 圆点的半径,比如说
30px
- 圆点的颜色,比如说
#f36
- 圆点的位置,按一定的比例分布在一个容器上,并且围成一个圆形
比如图所示:
注意,上较绘制的不是很标准,只是为了阐述问题。这里将会使用到一些数学公式,因为我们需要知道每个圆点的圆心的位置。
继续简化一下,就如下图这样:
这里会运用到一些角度和弧度相关的知识,其实这部分知识点,在学习Canvas的时候有所涉猎。在CSS中,咱们做旋转一般使用的是deg
(角度)为单位,但在JavaScript绘制圆或圆弧却常用弧度rad
为单位。
一个完整的圆的弧度是2π
,所以2π rad = 360°
,1 π rad = 180°
,1°=π/180 rad
,1 rad = 180°/π
(约57.29577951°
)。以度数表示的角度,把数字乘以π/180
便转换成弧度;以弧度表示的角度,乘以180/π
便转换成度数。
rad = (π / 180) * deg
同样的:
deg = (rad * 180) / π
使用JavaScript来实现角度和弧度之间的换算。一个π
大约是3.141592653589793
,在JavaScript中对应的是Math.PI
。那么角度和弧度之间的换算:
rad = (Math.PI * deg) / 180
同样的:
deg = (rad * 180) / Math.PI
下图展示了常见的角度和弧度之间的换算:
接下来回到我们的示例中来,示例有14
个圆点,那么其每个圆点对应的位置可以通过下面的公式计算出来。首先计算出每个点对应的rad
值。
rad = Math.PI * deg / 180
根据上面的公式,我们需要知道deg
。众所周知,一个圆是360deg
,我们在这个圆上平均布了14
个点,那么每个圆对应的deg
值是
deg = 360 / 14 * i
其中i
是一个从0 ~ 13
的索引值。套到对应的公式中:
rad = Math.PI * 360 / 14 * i / 180
在JavaScript中,使用一个for
循环,可以打印出其值:
for (let i = 0, len = 14; i < len; i++) {
let rad = Math.PI * 360 / 14 * i / 180
console.log(`第${i+1}个圆点对应的rad值:${rad}`)
}
根据上面的计算得到每个圆点对应的rad
值,接下来就需要利用三角函数相关的知识,来计算每个圆点圆心的(x,y)
值。
换成JavaScript中的数学公式:
dotX = Math.cos(rad) * r
dotY = Math.sin(rad) * r
假设外圆的容器半径r = 100
。继续将上面的值放到for
循环中:
for (let i = 0, len = 14; i < len; i++) {
let rad = Math.PI * 360 / 14 * i / 180
console.log(`第${i+1}个圆点对应的rad值:${rad}`)
let dotX = Math.cos(rad) * 100
let dotY = Math.sin(rad) * 100
console.log(`第${i+1}个圆点对应坐标值:(${dotX},${dotY})`)
}
最后通过CSS的transform
中translate()
可以将14
个圆点围成一个圆圈。此时,如果你的效果中不是14
个圆点,而是五个圆点的时候,你只需要修改相关的参数即可。
圆点排列,通过上面的公式计算,已经搞定。现在是需要一个动画效果,让你的圆点变得更为有趣。也就是说,只有配上了动效,才是我们最终需要的Loadingu效果。接下来,来看一下,圆点的动效。
比如,我们有一个简单的动效,这个动效是通过keyframes
来完成的。具体效果可以根据自己的需要来做,这里只是一个简单的效果:
@keyframes ball-spin {
0%,
100% {
opacity: 1;
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
transform: scale(0);
}
}
不用我解释,大家一看就明白。假设我们的动画的持续时间是1s
。效果如下:
如果你要的是这样的一个效果,那到这里,理论上是完成了。可我们要的效果是这样的:
要实现这样的一个效果,我们需要对每个圆点的animation-delay
做一些处理。前面提到过,整个动画的animation-duration
是1s
,那么每个圆点的animation-delay
依此延迟1/14 s
。如此一来,咱们可以计算出时间:
-1 * (1 + (i + 1) * 1 / 14)
注意:此处的-1
控制的是方向,让你能感觉到loading效果是顺时针还是逆时针转。系数为-1
表时逆时针,就是上图看到的效果。
整个运用到的原理就如上所述。接下来分别看看几种不同方式的实现。
纯CSS实现方式
Loading效果实现方式有很多种,比如@css_live整理这些Loading动效的Demo,就涵盖了多种实现方式,有纯CSS的、有Canvas的,也有SVG的。而且在CodePen上也有很多Loading的Demo或这里的Demo演示。
这里我们先用纯CSS的方式来完成。我们需要一个完成动效的HTML结构:
<div class="loading">
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
<div><span></span></div>
</div>
div.loading
下的div + span
对应的个数就是你所需要的圆点数。这里使用了14
个。为什么要使用div+span
样的嵌套标签,简单的解释一下。因为圆点有两个效果:
- 位置排列,使用的是
transfrom
中的translate
- 动效使用的是
transform
中的scale()
如果我们只用一个div
,那么圆点的排列和动效中同时使用到的transform
较为难控制。所以为了让实现方式更简单,牺牲了结构,嵌套了一个span
标签。也就是排列用在div
上,动效用在span
上。
众所周知,纯CSS是不具备动态计算的能力,如果我们要实现上面的效果,就要人肉的去计算。不过值得庆幸的是,有很多优秀的CSS处理器,比如说Sass、LESS之类的都具备动态计算,也具备上面所说的数学函数和for
循环的特性。这样一来,事情就变得简单多了。
详细代码就不全贴了,整几个关键的代码片段。首先根Demo效果所需要的参数,声明几个变量:
$loadingSize: 200px; // Loading容器的大小
$dotRadius: 24px; // 圆点半径
$dotNums: 14; // 圆点个数(需要和div.loading中子元素div个数对应起来)
$dotColor: #f36; // 圆点颜色
另外for
循环以及圆点位置相关的代码如下:
.loading {
width: $loadingSize;
height: $loadingSize;
color: $dotColor;
transform-origin: center;
div {
color: $dotColor;
width: $dotRadius;
height: $dotRadius;
margin-top: $dotRadius / 2;
margin-left: $dotRadius / 2;
span {
width: $dotRadius;
height: $dotRadius;
animation: ball-spin 1s infinite ease-in-out;
background-color: currentColor;
border: 0 solid currentColor;
}
@for $i from 1 through 14 {
&:nth-child(#{$i}) {
transform: translate(cos(($i - 1) * 360deg / $dotNums) * $loadingSize / 2, sin(($i - 1) * 360deg / $dotNums) * $loadingSize / 2);
& > span {
animation-delay: -(1 + $i * 1 / $dotNums) * 1s
}
}
}
}
}
对应一些三角函数的代码这里就不列出来了。如果你感兴趣的话可以使用sass-math。你也可以将示例中相应的函数像SassMagic一样放到一个Sass的仓库中,方便之后使用。
最终的效果如下:
虽然效果出来了,但其可扩展性相对而言较为麻烦一点,比如说我要一个小一点的,个数少一点的,颜色不一样的。那么需要重新修改前面声明的变量,然后再编译出相应的代码,修改对应的HTML结构。感兴趣的同学可以试试。
CSS自定义属性
CSS自定义属性已经是很成熟的CSS属性了。在小站上也有多篇文章介绍了有关于CSS自定义属性。为什么会想到CSS自定义属性呢?因为能使用CSS处理器变量的,就可以使用CSS自定义属性。但在这里有一个蛋疼的地方,就是CSS自定义属性调用通过var()
函数来完成,然后计算要依赖于calc()
函数。那么要把其结合Sass这样的处理器来做,就基本上是无解(至少我现在没找到可解的方案)。但CSS自定义属性有一个特性setProperty()
,可以使用它来完成CSS自定义属性的动态变化。
因此,使用CSS自定义属性方案来实现Loading的动画效果的时候,我借用了原生JavaScript的手段来完成圆点的排列和动画的计算。为了能更好的展示不同的Loading效果,接下来的Demo提供了一些可配置参数。这些可配置参数,让你来动态修改CSS自定义属性的值。先上效果吧:
你可以像下面这样,修改Demo右侧的一些参数,实现不一样的Loading效果,如下图所示:
为了节省篇幅,也将只贴一些关键的代码:
:root {
--loadingRadius: 168px;
--dotRadius: 24px;
--dotColor: #d8d8d8;
}
.loading{
width: var(--loadingRadius);
height: var(--loadingRadius);
color: var(--dotColor);
transform-origin: center;
div {
width: var(--dotRadius);
height: var(--dotRadius);
color: var(--dotColor);
margin-top: calc((var(--dotRadius) / 2) * -1);
margin-left: calc((var(--dotRadius) / 2) * -1);
}
span {
animation: ball-spin 1s infinite ease-in-out;
width: var(--dotRadius);
height: var(--dotRadius);
}
}
对应的JavaScript代码如下:
const style = document.documentElement.style;
var rangs = {
dotNums: document.getElementById('dotNums'),
loadingRadiusVal: document.getElementById('loadingRadius'),
dotRadiusVal: document.getElementById('dotRadius'),
dotColorVal: document.getElementById('dotColor')
}
// 创建修改CSS自定义属性的函数
function valueChange(id, value) {
style.setProperty('--' + id, value);
}
// 动态创建div.loading下的子元素div+span
function insertHtml() {
let loadingWrap = document.getElementById('loading');
var dots = rangs.dotNums.value;
for(let i = 0; i < dots; i++) {
let divEle = document.createElement('div');
let spanEle = document.createElement('span');
divEle.appendChild(spanEle);
loadingWrap.appendChild(divEle)
}
}
// 右侧参数面板的控制
rangs.loadingRadiusVal.addEventListener('input', function(e){
valueChange(e.currentTarget.id, e.currentTarget.value + 'px');
})
rangs.dotRadiusVal.addEventListener('input', function(e){
valueChange(e.currentTarget.id, e.currentTarget.value + 'px');
})
rangs.dotColorVal.addEventListener('input', function(e){
valueChange(e.currentTarget.id, e.currentTarget.value);
})
function transformDot() {
let dotNums = document.querySelectorAll('.loading > div');
let loadingRadiusVal = rangs.loadingRadiusVal.value;
let dotRadiusVal = rangs.dotRadiusVal.value;
let dotColorVal = rangs.dotColorVal.value;
// 计算圆点的位置和动效
for (let i = 0, len = dotNums.length; i < len; i++) {
let rad = 2 * Math.PI / dotNums.length * i;
let dotX = Math.cos(rad) * loadingRadiusVal / 2;
let dotY = Math.sin(rad) * loadingRadiusVal / 2;
dotNums[i].style.transform = `translate(${dotX}px,${dotY}px)`;
dotNums[i].firstElementChild.style.animationDelay = -1 * (1 + (i + 1) * 1 / dotNums.length) + 's';
}
}
insertHtml();
transformDot();
// 修改参数后修改动效
rangs.dotNums.addEventListener('input', function(e){
let loadingWrap = document.getElementById('loading');
loadingWrap.innerHTML = '';
insertHtml();
transformDot();
})
对应的HTML模板,这里就不展示了。
其实我还在想,这个效果,我们应该也可以使用CSS Houdini来完成。如果你对CSS Houdini有一定的了解,不仿尝试写写。如果你对CSS Houdini一点都不了解,建议点击这里先了解一下,我想你会从此喜欢上她的。
Vue写Loading组件
除了上面的方法之外,为了更好的使用,还可以将其写成一个组件。比如一个Vue组件,当然喜欢使用其他JavaScript框架的同学也可以使用别的,比如React。这里用的是Vue。
首先创建一个Vue组件,你可以使用单的一个文件,也可以使用组件模板。因为Demo是在Codepen上展示的,我就使用了一个组件模板:
<template id="loading">
<div class="loading">
<div v-for="(dotNum, index) in dotNums" :key="index" :style="dotTransform(index, dotNums)">
<span :style="dotAimation(index, dotNums)"></span>
</div>
</div>
</template>
Vue.component('loading',{
template: '#loading',
props: {
loadingRadiusVal: {
type: Number,
required: true,
default: 168
},
dotRadiusVal: {
type: Number,
required: true,
default: 24
},
dotColorVal: {
type: String,
required: true,
default: '#d8d8d8'
},
dotNums: {
type: Number,
required: true,
default: 10
}
},
methods: {
dotTransform: function(index, dotNums) {
let rad = 2 * Math.PI / dotNums * index;
let dotX = Math.cos(rad) * this.loadingRadiusVal / 2;
let dotY = Math.sin(rad) * this.loadingRadiusVal / 2;
return {
transform: `translate(${dotX}px,${dotY}px)`
};
},
dotAimation: function(index, dotNums) {
let delayTime = `${-1 * (1 + (index + 1) * 1 / dotNums) }s`
return {
animationDelay: delayTime
}
}
}
})
有了这个组件的时候,咱们可以这样调用:
<loading :dot-color-val="dotColor" :dot-nums="dotNums" :loading-radius-val="loadingRadius" :dot-radius-val="dotRadius" :style="changeStyle"></loading>
let app = new Vue({
el: '#app',
data () {
return {
loadingRadius: 168,
dotRadius: 20,
dotColor: '#ff3366',
dotNums: 12
}
},
computed: {
changeStyle: function() {
let rootEle = document.documentElement;
rootEle.style.setProperty('--loadingRadius', `${this.loadingRadius}px`)
rootEle.style.setProperty('--dotRadius', `${this.dotRadius}px`)
rootEle.style.setProperty('--dotColor', this.dotColor)
}
}
})
和前面的示例一样,也提供了一个参数控制面板,不过在Vue中参数控制面板控制CSS自定义属性就容易的多了,采用v-model
的双向绑定即可。最终效果如下:
因为是Vue的初学者,如果写得不好,还请各咱大神多多指正。如果你也是Vue的爱好者或初学者,可以和我一起来学习Vue相关的知识。当然,Vue写的各式各样的Loading组件非常的多,比如这里就收集了很多。
总结
这篇文章主要介绍了如何使用不同的方式来实现一个Loading的动效。说实在的,前端就是这样,实现一个效果有很多种方式方法。不同层次的同学可以使用不同的方式。就好比这篇文章中介绍的。不懂JavaScript的可以使用纯CSS(最好你对CSS处理器有所了解),懂JavaScript的话,你可以使用JS,配合一些优秀的CSS特性。当然,为了更易于维护和扩展,也可以借助Vue这样的框架,将整个效果封装成一个组件。
那么整个效果的实现方式不同,但其原理是一样的。对于初学者而言,最关键的是要掌握原理。只有掌握了原理部分,你才能根据自己的环境选择实现方案。
如果上面有不对之处,或者你有更好的方案,欢迎在下面的评论中与我一起共享。如果这篇文章对你有所帮助,可以赏杯咖啡,鼓励我继续创作。(^_^)!!!
如需转载,烦请注明出处:https://www.w3cplus.com/animation/loading-animation-component.html