最近在開發(fā)的時候遇到了一個環(huán)形進度條的需求,設(shè)計師希望這個進度條是漸變色的,并且能有對應(yīng)的動畫郑气。具體效果如圖
因為git圖的緣故所以看起來有點卡怠堪,但是實測幀數(shù)還是可以穩(wěn)定到50到60之間的。
并且將其封裝成了一個Vue組件雷逆,只需要傳入對應(yīng)的參數(shù)就可以快速的生成內(nèi)容了弦讽,如果你有類似的要求可以參考以下鏈接
aboyl的github
切換分支到svg-circle-progress就可以查看對應(yīng)的源代碼以及相關(guān)文檔
下面開始講解整體的思路
我們需要什么?
- 一個環(huán)形進度條
- 漸變色
- 動畫
細(xì)節(jié)點
環(huán)形精度條的首尾都是圓形
我們先來實現(xiàn)環(huán)形進度條。
實現(xiàn)思路很簡單
使用svg畫兩個圓
一個圓作為底色膀哲,畫滿
另一個圓作為進度條往产,此時應(yīng)該只畫一段弧形
怎么畫圓
參考svg的文檔我們可以知道得出以下代碼
<svg :height="200" :width="200" x-mlns="http://www.w3.org/200/svg">
<circle
:r="50"
:cx="100"
:cy="100"
:stroke="'red'"
:stroke-width="10"
fill="none"/>
</svg>
效果如圖
我們得到了一個紅色的圓環(huán)
其中
r為半徑
cx,cy為在svg中的坐標(biāo)
stroke為顏色
stroke-width為畫筆的寬度
fill為none表示不進行填充,不然我們看到的將是一整個圓而不是圓環(huán)
接下來我們需要畫一段圓弧
畫圓弧我們可以通過stoke-dasharray,他的本意是畫實線跟虛線交替的線段某宪,我們設(shè)置參數(shù)為 '弧長,極大值'
那么在顯示上因為虛線空的部分很長仿村,所以我們將看不到第二段的實線
對于我們需要的圓的頭部 可以設(shè)置stroke-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"
/>
</svg>
此時我們觀測到起始方向是在左邊的中間位置,因此我們進行旋轉(zhuǎn),在第二個圓上加上旋轉(zhuǎn)
transform="rotate(-90)"
transform-origin="center"
因為我們需要封裝成一個組件兴喂,那么他應(yīng)該接收
- 進度
- 底色
- 弧度的顏色
- 內(nèi)圓的半徑
- 圓弧的寬度
同時對于 - svg的寬高
- 外圓的半徑
- 弧長
這些應(yīng)該是我們進行計算得出來的值
當(dāng)然為了使用上的方便 我們應(yīng)該給出一些默認(rèn)值
組件代碼如下
<template>
<svg
:height="option.size"
:width="option.size"
x-mlns="http://www.w3.org/200/svg"
>
<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="option.progressColor"
:stroke-dasharray="arcLength"
:stroke-width="option.strokeWidth"
fill="none"
transform="rotate(-90)"
transform-origin="center"
stroke-linecap="round"
/>
</svg>
</template>
<script>
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
},
},
}
</script>
其實現(xiàn)在修改一下這讓人吐槽的配色已經(jīng)可以用來使用了~
接下來我們來實現(xiàn)漸變色
第一個思路當(dāng)然是去搜索svg怎么實現(xiàn)漸變色了蔼囊,我一開始也是進行了搜索,最終寫出來了如下的代碼
<defs>
<linearGradient id="gradient">
<stop
offset="0%"
style="stop-color: red;"
/>
<stop
offset="100%"
style="stop-color: yellow"
/>
</linearGradient>
</defs>
<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"
/>
</svg>
效果如圖
為什么跟我們期望的效果不一樣?
我們期望的是從最上方順時針方向開始畫圓弧,頂部顏色是紅色,而到了結(jié)尾的時候是黃色的衣迷,為什么會這樣呢?
因為其實我們的觀點是錯的畏鼓。假如我們不做其他的處理,單純的給一個圓加上漸變是什么樣子的呢?
<defs>
<linearGradient id="gradient">
<stop
offset="0%"
style="stop-color: green;"
/>
<stop
offset="100%"
style="stop-color: yellow"
/>
</linearGradient>
</defs>
<!-- <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-width="option.strokeWidth"
/>
</svg>
如圖
可見
- 線性漸變是從左到右邊
- 剛剛的位置不正確是因為設(shè)置了旋轉(zhuǎn)的原因
那么我們可以推翻我們前面不靠譜的推測轉(zhuǎn)回來繼續(xù)思考怎么實現(xiàn)漸變的辦法,對于有沒有辦法依靠svg的漸變元素這些來實現(xiàn)漸變的互動壶谒,鑒于CSS依靠漸變可以做出很多玩法云矫,所以我不能打包票說沒有,只不過我覺得可能實現(xiàn)思路會比較的麻煩汗菜,所以就換了一種思路來進行實現(xiàn)让禀。
我們只需要手動計算漸變就ok了
也就是說只需要實現(xiàn)算法,計算出從顏色a到顏色b之間的各個漸變色是多少陨界,依靠不同弧長不同顏色的圓進行重疊堆缘,那么我們就可以模擬漸變的實現(xiàn)
需要注意的是漸變的實現(xiàn)并不是有的時候相信的那樣,從000000到ffffff進行累加,而是需要先轉(zhuǎn)化成為rgb再進行計算
通過搜索引擎我們可以找到一些實現(xiàn)好的算法,對于具體的原理就沒有再深究了,
參考文章
里面還實現(xiàn)了rgb轉(zhuǎn)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顏色的三位分別進行步進以達到漸變的效果
接下來的問題便是如何計算步數(shù)吼肥,并且根據(jù)我們前面的分析,漸變色跟圓弧應(yīng)該是對應(yīng)的,經(jīng)過一些測試,在步數(shù)為100的情況下麻车,肉眼不是很能分辨出存在漸變色的空隙(ps:本來在寫這篇文字之前自己的看法是進行一些計算缀皱,在進度高的情況下,步進次數(shù)會高于進度低的步進次數(shù)的說动猬,不過寫到這里的時候突然想到了過低會導(dǎo)致不連貫的問題啤斗。。赁咙。于是可以回去修方案了,事實證明確實進行總結(jié)確實會有更多的發(fā)現(xiàn))
步數(shù)我們設(shè)定為100再對原來的弧長進行一百等分钮莲,就可以得到一個數(shù)組了,而原來的svg中circl元素我們則使用v-for根據(jù)前面生成的數(shù)組進行生成免钻,代碼如下
<template>
<svg
:height="option.size"
:width="option.size"
x-mlns="http://www.w3.org/200/svg"
>
<circle
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
:stroke="option.backColor"
:stroke-width="option.strokeWidth"
fill="none"
/>
<circle
v-for="(item, index) in arcArr"
:key="index"
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
:stroke="item.color"
:stroke-dasharray="item.arcLength"
:stroke-width="option.strokeWidth"
fill="none"
transform="rotate(-90)"
transform-origin="center"
stroke-linecap="round"
/>
</svg>
</template>
<script>
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 // 設(shè)置到100則已經(jīng)比較難看出來顏色斷層
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
},
},
}
</script>
需要注意的是我們最后的時候?qū)ι傻臄?shù)組進行了一次顛倒,不然弧度最長的圓弧會掛在最后面,導(dǎo)致我們看不到漸變效果
效果如圖
這里我稍微調(diào)整了一下顏色讓他符合我們的預(yù)期
最后為其加上動畫效果
這部分不是一個很復(fù)雜的事情,需要注意在circle上的stroke-dasharray我們需要去掉,避免出現(xiàn)弧長在一開始的時候就渲染了出來,然后又馬上消失進入動畫效果的情況崔拥,雖然影響不大极舔,但是還是需要注意一下。
代碼如下
<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"
/>
</circle>
總結(jié)
確實在學(xué)到東西以后需要做一些輸出才能達到真正的學(xué)會,其實整個組件設(shè)計一開始的時候跟現(xiàn)在的區(qū)別還蠻大的链瓦,中間走了很多的彎路拆魏,關(guān)于svg的漸變從一開始就想錯了,即使在我換思路的時候慈俯,我還以為是svg的漸變在作用于弧型的時候是順時針畫圓渤刃,并且在百分之五十的時候從結(jié)尾的顏色切換到開始的顏色達到循環(huán)漸變,直到自己寫文章的時候才發(fā)現(xiàn)自己的錯誤贴膘,比如一開始的時候加入漸變效果后因為步長的計算有問題卖子,導(dǎo)致出現(xiàn)了精度的概念,只能做到0.01的進度刑峡,而一旦切換到高精度就會導(dǎo)致動畫非逞竺觯卡頓,后來換了思路就清晰很多了
以下需要進行補充
- 因為重點在于漸變氛琢,所以對接受的參數(shù)就沒有過多的需求了喊递,附帶的參考文章里面自己看一下改一改就能讓startColor跟endColor接受正常的顏色值了
- 這里只做了兩個漸變色随闪,如果需要多個漸變感覺修正一下算法也不會很難
- 關(guān)于步進次數(shù)為100阳似,這里自己也做過一些測試,在我試驗的顏色值下面感覺在20這些數(shù)字差距也不會很大铐伴,不過性能看起在100的情況下跟20的情況下差距不大撮奏,就沒有再進行修正了,而是成為了參數(shù)默認(rèn)值默認(rèn)值
另一種實現(xiàn)方式:
果然是學(xué)無止境,搜索網(wǎng)上的文章的時候突然發(fā)現(xiàn)了張大神的一篇漏網(wǎng)之魚,居然只使用了兩個circle元素就實現(xiàn)了漸變效果
于是我也從中吸取了一些幫助對這個進度條進行了進一步的優(yōu)化
參考鏈接
張鑫旭漸變進度條實現(xiàn)
不過確定貌似是不能做到最后的尾部顏色是設(shè)置的結(jié)尾色当宴,不過這樣也可以作為一個補充畜吊,具體效果如圖
可以看到右邊的末端顏色略微淡色
具體實現(xiàn)方式看代碼,個人認(rèn)為需要注意的點
- 實現(xiàn)方式户矢,本質(zhì)上是兩張circle的疊加,然后進行了旋轉(zhuǎn)得到的,從顏色colorA到顏色colorB玲献,算出中間值colorC,然后第一個從colorA到colorC,第二個從colorB到colorC,旋轉(zhuǎn)到從上到下梯浪,那么進行疊加捌年,看起來就是從colorA到colorC再到colorB了
- 要注意動畫的實現(xiàn),需要按照比例對動畫時長進行切割挂洛,這樣才不會導(dǎo)致在最后的時候原本流暢的動畫過了最底部的時候速度突然驟降礼预,注意動畫的無縫銜接
具體參考代碼如下
<template>
<svg :height="option.size" :width="option.size" x-mlns="http://www.w3.org/200/svg">
<defs>
<linearGradient x1="1" y1="0" x2="0" y2="0" id="outGradient">
<stop offset="0%" :stop-color="arcOption.outArcStartColor" />
<stop offset="100%" :stop-color="arcOption.outArcEndColor" />
</linearGradient>
<linearGradient x1="1" y1="0" x2="0" y2="0" id="innerGradient">
<stop offset="0%" :stop-color="arcOption.innerArcStartColor" />
<stop offset="100%" :stop-color="arcOption.innerArcEndColor" />
</linearGradient>
</defs>
<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('#innerGradient')"
:stroke-width="option.strokeWidth"
transform="rotate(-90)"
transform-origin="center"
fill="none"
stroke-linecap="round"
:stroke-dasharray="`0,1000000`"
>
<animate
:to="`${arcOption.innerArcLength},1000000`"
:begin="arcOption.outDurtion"
:dur="arcOption.innerDurtion"
:from="`${arcOption.innerInitArcLength},1000000`"
attributeName="stroke-dasharray"
fill="freeze"
/>
</circle>
<circle
:r="option.radius"
:cx="option.cx"
:cy="option.cy"
stroke="url('#outGradient')"
:stroke-width="option.strokeWidth"
:stroke-dasharray="`${arcOption.outArcLength},1000000`"
fill="none"
transform="rotate(-90)"
transform-origin="center"
stroke-linecap="round"
>
<animate
:to="`${arcOption.outArcLength},1000000`"
begin="0s"
:dur="arcOption.outDurtion"
from="0,1000000"
attributeName="stroke-dasharray"
fill="freeze"
/>
</circle>
</svg>
</template>
<script>
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 則只需要顯示最外層的圓弧 里面的圓弧不需要畫了
// 時間計算 因為第二段的長度不見得等于第一段 所以不能平分時間 不然會導(dǎo)致第二端的速度出現(xiàn)驟降
// 因此需要按照比例進行時間計算
if (this.progress < 0.5) {
arcConfig.outArcLength = this.progress * circleLength
arcConfig.outDurtion = this.option.durtion // 為初始設(shè)置的動畫值
arcConfig.innerArcLength = 0
arcConfig.innerInitArcLength = 0 // 為動畫做準(zhǔn)備
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 // 為動畫做準(zhǔn)備 此時從中間開始
arcConfig.innerDurtion = ((this.progress - 0.5) / this.progress) * time + 's' // 為動畫做準(zhǔn)備 此時從中間開始
}
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
},
},
}
</script>