- A+
最近,在开发过程中,遇到了一个圆形进度条的需求。 设计者希望进度条有渐变颜色,并且能有相应的动画。具体效果如图
由于是git镜像,看起来有点卡,但是实测帧率还是可以稳定在50到60之间。并且封装成Vue组件,只需要传入相应的参数就可以快速生成内容,如果你有类似的需求,可以参考以下链接 aboyl 的 github switch 分支到 svg-circle-progress 查看对应的源码和相关文档
我们先来解释一下总体思路:我们需要什么?
圆形进度条渐变动画
细节点圆形精度条的起点和终点均为矩形
我们先来实现一下循环进度条。实现思路很简单。 使用svg画两个圆圈,一个圆圈作为背景色,另一个圆圈作为进度条。 此时只需绘制圆弧即可。
如何画一个圆
参考svg文档,我们可以知道以下代码
疗效如图
我们得到一个黑色的圆环,其中 r 是直径 cx,cy 是 svg 中的坐标。 笔划是颜色 笔划宽度 是笔划的长度 填充是 none 表示不填充,否则我们会看到整个圆而不是圆环
然后我们需要画圆弧来画圆弧。 我们可以使用Stroke-dasharray。 他的初衷是用虚线和实线交替画一条线段。 我们将参数设置为“弦长,最大值”,这样显示效果比实线更好。 空的部分很长,所以我们不会看到第二段的虚线。 对于我们需要的圆领,我们可以将strike-linecap设置为round,最终效果如图
代码如下所示
<circle
:r="50"
:cx="100"
:cy="100"
:stroke="'red'"
:stroke-width="10"
fill="none"
/>
<circle
:r="50"
:cx="100"
:cy="100"
:stroke="'yellow'"
:stroke-dasharray="`100,100000`"
:stroke-width="10"
fill="none"
stroke-linecap="round"
/>
此时我们观察到起始方向是在右侧中间,所以我们旋转,给第二个圆加上旋转
transform="rotate(-90)"
transform-origin="center"
因为我们需要将其封装成一个组件,所以他应该接收
进度背景圆弧的颜色、内圆的直径、圆弧的长度以及svg的宽度和高度、外圆的直径和弦长
这种价值应该由我们来估计。 事实上,为了使用的方便,我们应该给出一些默认值。 组件代码如下
export default {
name: 'Progress',
props: {
progress: {
type: Number,
required: true,
},
progressOption: {
type: Object,
default: () => { },
},
},
data () {
return {
}
},
computed: {
arcLength () {
let circleLength = Math.floor(2 * Math.PI * this.option.radius)
let progressLength = this.progress * circleLength
return `${progressLength},100000000`
},
option () {
// 所有进度条的可配置项
let baseOption = {
radius: 100,
strokeWidth: 20,
backColor: 'red',
progressColor: 'yellow',
}
Object.assign(baseOption, this.progressOption)
// 中心位置自动生成
baseOption.cy = baseOption.cx = baseOption.radius + baseOption.strokeWidth
baseOption.size = (baseOption.radius + baseOption.strokeWidth) * 2
return baseOption
},
},
}
虽然现在可以改让人吐槽的配色了,但已经可以用了~
拿出来我们来实现渐变色
第一个想法其实就是寻找如何在svg中实现渐变颜色。 我一开始也搜索过,最后写下了下面的代码
<linearGradient id="gradient">
<stop
offset="0%"
style="stop-color: red;"
/>
<stop
offset="100%"
style="stop-color: yellow"
/>
<circle
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
:stroke="option.backColor"
:stroke-width="option.strokeWidth"
fill="none"
/>
<circle
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
:stroke="'url(#gradient)'"
:stroke-dasharray="arcLength"
:stroke-width="option.strokeWidth"
fill="none"
transform="rotate(-90)"
transform-origin="center"
stroke-linecap="round"
/>
疗效如图
为什么和我们预期的疗效不一样呢? 我们期望的是从顶部顺时针绘制一条圆弧,底部颜色为蓝色,末端为白色。 为什么会这样呢? 因为虽然我们的观点是错误的。 如果我们不做其他处理,那么简单地给一个圆加上一个渐变会是什么样子呢?
<linearGradient id="gradient">
<stop
offset="0%"
style="stop-color: green;"
/>
<stop
offset="100%"
style="stop-color: yellow"
/>
<circle
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
:stroke="'url(#gradient)'"
:stroke-width="option.strokeWidth"
/>
如图所示
可以看出,线性渐变是从左到左的。 刚才位置不对是因为轮换的激励
这样我们就可以推翻上面我们不靠谱的猜想,回去继续思考如何实现梯度了。 至于有没有办法利用svg渐变元素实现渐变交互svg画六边形,由于CSS可以做出很多带有渐变的游戏,所以我不能保证没有,但我觉得可能会比较麻烦实施想法,所以我改变想法去实施。
我们只需要自动估计梯度,也就是说,我们只需要实现算法来估计从颜色a到颜色b的渐变颜色,并使用不同弦长和不同颜色的圆进行重叠,这样我们就可以模拟渐变的实现需要注意的是,渐变的实现并不像有时认为的那样,是从000000累加到ffffff,而是需要先转成rgb再进行估计。 通过搜索引擎,我们可以找到一些实现良好的算法,具体原理就不详细描述了。 上面的文章也实现了rgb转16的补码的算法,但是核心梯度算法大致如下。 如有需要可以参考其他算法。
function gradientColor (startRGB, endRGB, step) {
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
var colorArr = []
for (var i = 0; i < step; i++) {
let color = 'rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'
colorArr.push(color)
}
return colorArr
}
可以看到rgb颜色的三位分别步进,达到了渐变的效果
接下来的问题是如何估计步数,但是根据我们上面的分析,渐变颜色应该对应于弧形。 经过一些测试,当步数为100时,肉眼不太能够辨别渐变颜色的存在。 (ps:本来在写这篇文章之前,我的想法是做一些估算,在进度高的情况下,步数会比进度低的少,但是讲到这里,我突然想到,如果太高的话,就会造成语无伦次……所以我可以回来修复一下计划。事实证明,如果你认真总结的话,你会发现更多的发现)
我们将步数设置为100,并将原始弦长分成100等分,得到一个链表。 我们根据上面生成的字段,使用v-for生成原始svg中的circl元素。 代码如下
export default {
name: 'Progress',
props: {
progress: {
type: Number,
required: true,
},
progressOption: {
type: Object,
default: () => { },
},
},
computed: {
arcArr () {
let circleLength = Math.floor(2 * Math.PI * this.option.radius)
let progressLength = this.progress * circleLength
const step = 100 // 设置到100则已经比较难看出来颜色断层
const gradientColor = (startRGB, endRGB, step) => {
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
let colorArr = []
for (let i = 0; i < step; i++) {
let color = `rgb(${sR * i + startR},${sG * i + startG},${sB * i + startB})`
colorArr.push(color)
}
return colorArr
}
let colorArr = gradientColor(this.option.startColor, this.option.endColor, step)
// 计算每个步进中的弧长
let arcLengthArr = colorArr.map((color, index) => ({
arcLength: `${index * (progressLength / 100)},100000000`,
color: color
}))
arcLengthArr.reverse()
return arcLengthArr
},
option () {
// 所有进度条的可配置项
let baseOption = {
radius: 100,
strokeWidth: 20,
backColor: '#E6E6E6',
startColor: [249, 221, 180],
endColor: [238, 171, 86], // 用于渐变色的开始
}
Object.assign(baseOption, this.progressOption)
// 中心位置自动生成
baseOption.cy = baseOption.cx = baseOption.radius + baseOption.strokeWidth
baseOption.size = (baseOption.radius + baseOption.strokeWidth) * 2
return baseOption
},
},
}
需要注意的是,我们最后把生成的字段颠倒了,不然弧线最长的弧线会挂在最后,导致我们看不到如图所示的渐变效果的效果
这里我稍微调整了一下颜色svg画六边形,使其符合我们的预期,最后给它添加了动画效果
这部分并不是一个很复杂的事情。 需要注意的是,我们需要去掉圆上的Stroke-DashArray,防止一开始渲染字符串长度然后立即消失进入动画的效果,即使影响不大,但还是需要注意,代码如下
<circle
v-for="(item, index) in arcArr"
:key="index"
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
:stroke="item.color"
:stroke-width="option.strokeWidth"
fill="none"
transform="rotate(-90)"
transform-origin="center"
stroke-linecap="round"
>
<animate
:to="item.arcLength"
begin="0s"
:dur="option.durtion"
from="0,1000000"
attributeName="stroke-dasharray"
fill="freeze"
/>
总结
确实,学了东西之后,需要做一些输出,才能达到真正的学习。 虽然一开始整个组件设计和现在差别很大,中间也走了很多弯路,从一开始就误解了svg的渐变。 是的,虽然当我改变主意的时候,我以为是svg渐变作用在圆弧上时顺时针画了一个圆,并在50%的时候从结束颜色切换到开始颜色,达到了循环渐变,直到写下这篇文章我才意识到自己的错误。 比如一开始加了渐变效果之后,步长的估计出现了问题,从而引发了精度的概念,我只能达到 0.01 的进度,而一旦切换到高精度,会导致动画特别卡。 后来,如果你改变主意,就会清楚得多。 需要补充以下内容
由于重点是梯度,所以对接受的参数没有太多要求。 查看附加的参考文章并更改它以使 startColor 和 endColor 接受正常颜色值。 这里只制作了两种渐变颜色。 如果需要多个梯度,我觉得修改算法也不会很难。 对于步数为100,我这里做了一些测试。 在我测试的颜色值下,我认为20的数量不会有太大区别,但性能上100的情况和20的情况似乎没有太大区别,所以没有进一步修正,但它变成了参数默认值的另一种实现方法:
果然,学无止境。 当我在网上搜索文章时,突然发现了一篇漏网之鱼的张大师的文章。 它只使用了两个圆形元素就达到了渐变的效果。 所以对于这个进度条我也借鉴了它的一些帮助。 进一步优化参考链接
张鑫旭的渐变进度条实现了,但是确定最终的尾部颜色是设定的结束颜色,不过这个也可以作为补充。 具体效果如图
可以看到一侧末端的颜色略带紫色。 具体实现方法可以看代码。 我个人认为需要注意的点实现方法本质上是两个圆的叠加,然后旋转得到colorA到colorB的中间值。 colorC,那么第一个是从colorA到colorC,第二个是从colorB到colorC,并从上到下旋转,这样叠加看起来就像从colorA到colorC再到colorB。 注意动画的实现,需要根据比例栏切割动画的时长,以免导致最后平滑动画在经过顶部时速度突然下降,注意注意动画的无缝衔接
具体参考代码如下
export default {
name: 'Progress2',
props: {
progress: {
type: Number,
required: true,
},
progressOption: {
type: Object,
default: () => { },
},
},
computed: {
arcOption () {
let arcConfig = {}
let circleLength = Math.floor(2 * Math.PI * this.option.radius)
// 如果此时小于0.5 则只需要显示最外层的圆弧 里面的圆弧不需要画了
// 时间计算 因为第二段的长度不见得等于第一段 所以不能平分时间 不然会导致第二端的速度出现骤降
// 因此需要按照比例进行时间计算
if (this.progress < 0.5) {
arcConfig.outArcLength = this.progress * circleLength
arcConfig.outDurtion = this.option.durtion // 为初始设置的动画值
arcConfig.innerArcLength = 0
arcConfig.innerInitArcLength = 0 // 为动画做准备
arcConfig.innerDurtion = 0
} else {
const time = this.option.durtion.split('s')[0]
arcConfig.outArcLength = 0.5 * circleLength
arcConfig.outDurtion = (0.5 / this.progress) * time + 's' //
arcConfig.innerArcLength = this.progress * circleLength
arcConfig.innerInitArcLength = 0.5 * circleLength // 为动画做准备 此时从中间开始
arcConfig.innerDurtion = ((this.progress - 0.5) / this.progress) * time + 's' // 为动画做准备 此时从中间开始
}
const tansfromColor = arr => `rgb(${arr[0]},${arr[1]},${arr[2]})`
arcConfig.outArcStartColor = tansfromColor(this.option.startColor)
arcConfig.outArcEndColor = tansfromColor(this.option.startColor.map((color, index) => color + (this.option.endColor[index] - color) / 2))
arcConfig.innerArcStartColor = tansfromColor(this.option.endColor)
arcConfig.innerArcEndColor = tansfromColor(this.option.startColor.map((color, index) => color + (this.option.endColor[index] - color) / 2))
return arcConfig
},
option () {
// 所有进度条的可配置项
let baseOption = {
radius: 100,
strokeWidth: 20,
backColor: '#E6E6E6',
startColor: [249, 221, 180],
endColor: [238, 171, 86],
durtion: '1s',
step: 100,
}
Object.assign(baseOption, this.progressOption)
// 中心位置自动生成
baseOption.cy = baseOption.cx = baseOption.radius + baseOption.strokeWidth
baseOption.size = (baseOption.radius + baseOption.strokeWidth) * 2
return baseOption
},
},
}
- 我的微信
- 这是我的微信扫一扫
-
- 我的微信公众号
- 我的微信公众号扫一扫
-