週末好,今天給你們帶來一款接地氣的環形進度條組件vue-awesome-progress
。近日被設計小姐姐要求實現這麼一個環形進度條效果,大致由四部分組成,分別是底色圓環,進度弧,環內文字,進度圓點。設計稿截圖以下:javascript
個人第一反應仍是找現成的組件,市面上不少組件都實現了前3點,獨獨沒找到能畫進度圓點的組件,否則稍加定製也能複用。既然沒有現成的組件,只有本身用vue + canvas
擼一個了。html
先放個效果圖,而後再說下具體實現過程,各位看官且聽我慢慢道來。前端
源碼地址,歡迎star
和提issue
。vue
npm install --save vue-awesome-progress
import Vue from 'vue' import VueAwesomeProgress from "vue-awesome-progress" Vue.use(VueAwesomeProgress)
import VueAwesomeProgress from "vue-awesome-progress" export default { components: { VueAwesomeProgress }, // 其餘代碼 }
因爲當前版本發佈時,未進行babel
編譯,所以使用時須要自行將vue-awesome-progress
歸入babel-loader
的解析範圍。示例以下:java
// resolve函數是鏈接路徑的,方法體是path.join(__dirname, "..", dir) { test: /\.js$/, loader: "babel-loader", include: [ resolve("src"), resolve("node_modules/vue-awesome-progress") ] }
任何事都不是一蹴而就的,咱們首先來實現一個靜態的效果,而後再實現動畫效果,甚至是複雜的控制邏輯。node
第一步是肯定畫布大小。從設計稿咱們能夠直觀地看到,整個環形進度條的最外圍是由進度圓點肯定的,而進度圓點的圓心在圓環圓周上。webpack
所以咱們得出僞代碼以下:git
// canvasSize: canvas寬度/高度 // outerRadius: 外圍半徑 // pointRadius: 圓點半徑 // pointRadius: 圓環半徑 canvasSize = 2 * outerRadius = 2 * (pointRadius + circleRadius)
據此咱們能夠定義以下組件屬性:程序員
props: { circleRadius: { type: Number, default: 40 }, pointRadius: { type: Number, default: 6 } }, computed: { // 外圍半徑 outerRadius() { return this.circleRadius + this.pointRadius }, // canvas寬/高 canvasSize() { return 2 * this.outerRadius + 'px' } }
那麼canvas
大小也能夠先進行綁定了github
<template> <canvas ref="canvasDemo" :width="canvasSize" :height="canvasSize" /> </template>
getContext('2d')
方法返回一個用於在canvas
上繪圖的環境,支持一系列2d
繪圖API
。
mounted() { // 在$nextTick初始化畫布,否則dom還未渲染好 this.$nextTick(() => { this.initCanvas() }) }, methods: { initCanvas() { var canvas = this.$refs.canvasDemo; var ctx = canvas.getContext('2d'); } }
完成了上述步驟後,咱們就能夠着手畫各個元素了。咱們先畫圓環,這時咱們還要定義兩個屬性,分別是圓環線寬circleWidth
和圓環顏色circleColor
。
circleWidth: { type: Number, default: 2 }, circleColor: { type: String, default: '#3B77E3' }
canvas
提供的畫圓弧的方法是ctx.arc()
,須要提供圓心座標,半徑,起止弧度,是否逆時針等參數。
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
咱們知道,Web
網頁中的座標系是這樣的,從絕對定位的設置上其實就能看出來(top
,left
設置正負值會發生什麼變化),並且原點(0, 0)
是在盒子(好比說canvas
)的左上角哦。
對於角度而言,0°
是x
軸正向,默認是順時針方向旋轉。
圓環的圓心就是canvas
的中心,因此x
, y
取outerRadius
的值就能夠了。
ctx.strokeStyle = this.circleColor; ctx.lineWidth = this.circleWidth; ctx.beginPath(); ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360)); ctx.stroke();
注意arc
傳的是弧度參數,而不是咱們常理解的360°
這種概念,所以咱們須要將咱們理解的360°
轉爲弧度。
// deg轉弧度 deg2Arc(deg) { return deg / 180 * Math.PI }
調用fillText
繪製文字,利用canvas.clientWidth / 2
和canvas.clientWidth / 2
取得中點座標,結合控制文字對齊的兩個屬性textAlign
和textBaseline
,咱們能夠將文字繪製在畫布中央。文字的值由label
屬性接收,字體大小由fontSize
屬性接收,顏色則取的fontColor
。
if (this.label) { ctx.font = `${this.fontSize}px Arial,"Microsoft YaHei"` ctx.fillStyle = this.fontColor; ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText(this.label, canvas.clientWidth / 2, canvas.clientWidth / 2); }
支持普通顏色和漸變色,withGradient
默認爲true
,表明使用漸變色繪製進度弧,漸變方向我默認給的從上到下。若是但願使用普通顏色,withGradient
傳false
便可,並能夠經過lineColor
自定義顏色。
if (this.withGradient) { this.gradient = ctx.createLinearGradient(this.circleRadius, 0, this.circleRadius, this.circleRadius * 2); this.lineColorStops.forEach(item => { this.gradient.addColorStop(item.percent, item.color); }); }
其中lineColorStops
是漸變色的顏色偏移斷點,由父組件傳入,可傳入任意個顏色斷點,格式以下:
colorStops2: [ { percent: 0, color: '#FF9933' }, { percent: 1, color: '#FF4949' } ]
畫一條從上到下的進度弧,即270°
到90°
ctx.strokeStyle = this.withGradient ? this.gradient : this.lineColor; ctx.lineWidth = this.lineWidth; ctx.beginPath(); ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, this.deg2Arc(270), this.deg2Arc(90)); ctx.stroke();
其中lineWidth
是弧線的寬度,由父組件傳入
lineWidth: { type: Number, default: 8 }
最後咱們須要把進度圓點補上,咱們先寫死一個角度90°
,顯而易見,圓點座標爲(this.outerRadius, this.outerRadius + this.circleRadius)
畫圓點的代碼以下:
ctx.fillStyle = this.pointColor; ctx.beginPath(); ctx.arc(this.outerRadius, this.outerRadius + this.circleRadius, this.pointRadius, 0, this.deg2Arc(360)); ctx.fill();
其中pointRadius
是圓點的半徑,由父組件傳入:
pointRadius: { type: Number, default: 6 }
固然,進度條的角度是靈活定義的,包括開始角度,結束角度,都應該由調用者隨意給出。所以咱們再定義一個屬性angleRange
,用於接收起止角度。
angleRange: { type: Array, default: function() { return [270, 90] } }
有了這個屬性,咱們就能夠隨意地畫進度弧和圓點了,哈哈哈哈。
老哥,這種圓點座標怎麼求?
噗......看來高興過早了,最重要的是根據不一樣角度求得圓點的圓心座標,這讓我頓時犯了難。
通過冷靜思考,我腦子裏閃過了一個利用正餘弦公式求座標的思路,但前提是座標系原點若是在圓環外接矩形的左上角纔好算。仔細想一想,冇問題啦,我先給座標系平移一下,最後求出來結果,再補個平移差值不就好了嘛。
👆畫圖工具不是很熟練,這裏圖沒畫好,線歪了,請忽略細節。
好的,咱們先給座標系向右下方平移pointRadius
,最後求得結果再加上pointRadius
就行了。僞代碼以下:
// realx:真實的x座標 // realy:真實的y座標 // resultx:平移後求取的x座標 // resultx:平移後求取的y座標 // pointRadius 圓點半徑 realx = resultx + pointRadius realy = resulty = pointRadius
求解座標的思路大概以下,分四個範圍判斷,得出求解公式,應該還能夠化簡,不過我數學太菜了,先這樣吧。
getPositionsByDeg(deg) { let x = 0; let y = 0; if (deg >= 0 && deg <= 90) { // 0~90度 x = this.circleRadius * (1 + Math.cos(this.deg2Arc(deg))) y = this.circleRadius * (1 + Math.sin(this.deg2Arc(deg))) } else if (deg > 90 && deg <= 180) { // 90~180度 x = this.circleRadius * (1 - Math.cos(this.deg2Arc(180 - deg))) y = this.circleRadius * (1 + Math.sin(this.deg2Arc(180 - deg))) } else if (deg > 180 && deg <= 270) { // 180~270度 x = this.circleRadius * (1 - Math.sin(this.deg2Arc(270 - deg))) y = this.circleRadius * (1 - Math.cos(this.deg2Arc(270 - deg))) } else { // 270~360度 x = this.circleRadius * (1 + Math.cos(this.deg2Arc(360 - deg))) y = this.circleRadius * (1 - Math.sin(this.deg2Arc(360 - deg))) } return { x, y } }
最後再補上偏移值便可。
const pointPosition = this.getPositionsByDeg(nextDeg); ctx.arc(pointPosition.x + this.pointRadius, pointPosition.y + this.pointRadius, this.pointRadius, 0, this.deg2Arc(360));
這樣,一個基本的canvas
環形進度條就成型了。
靜態的東西逼格天然是不夠的,所以咱們須要再搞點動畫效果裝裝逼。
咱們先簡單實現一個線性的動畫效果。基本思路是把開始角度和結束角度的差值分爲N
段,利用window.requestAnimationFrame
依次執行動畫。
好比從30°
到90°
,我給它分爲6段,每次畫10°
。要注意canvas
畫這種動畫過程通常是要重複地清空畫布並重繪的,因此第一次我畫的弧線範圍就是30°~40°
,第二次我畫的弧線範圍就是30°~50°
,以此類推......
基本的代碼結構以下,具體代碼請參考vue-awesome-progress v1.1.0
版本,若是順手幫忙點個star
也是極好的。
animateDrawArc(canvas, ctx, startDeg, endDeg, nextDeg, step) { window.requestAnimationFrame(() => { // 清空畫布 ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight); // 求下一個目標角度 nextDeg = this.getTargetDeg(nextDeg || startDeg, endDeg, step); // 畫圓環 // 畫文字 // 畫進度弧線 // 畫進度圓點 if (nextDeg !== endDeg) { // 知足條件繼續調用動畫,不然結束動畫 this.animateDrawArc(canvas, ctx, startDeg, endDeg, nextDeg, step) } } }
線性動畫顯得有點單調,可操做性不大,所以我考慮引入貝塞爾緩動函數easing
,而且支持傳入動畫執行時間週期duration
,加強了可定製性,使用體驗更好。這裏不列出實現代碼了,請前往vue-awesome-progress查看。
<vue-awesome-progress label="188人" :duration="10" easing="0,0,1,1" /> <vue-awesome-progress label="36℃" circle-color="#FF4949" :line-color-stops="colorStops" :angle-range="[60, 180]" :duration="5" /> // 省略部分... <vue-awesome-progress label="188人" easing="1,0.28,0.17,0.53" :duration="10" /> <vue-awesome-progress label="36℃" circle-color="#FF4949" :line-color-stops="colorStops" :angle-range="[60, 180]" :duration="5" easing="0.17,0.67,0.83,0.67" />
能夠看到,當傳入不一樣的動畫週期duration
和緩動參數easing
時,動畫效果各異,徹底取決於使用者本身。
固然根據組件支持的屬性,咱們也能夠定製出其餘效果,好比不顯示文字,不顯示圓點,弧線線寬與圓環線寬同樣,不使用漸變色,不須要動畫,等等。咱們後續也會考慮支持更多能力,好比控制進度,數字動態增加等!具體使用方法,請參考vue-awesome-progress。
寫完這個組件有讓我感受到,程序員最終不是輸給了代碼和技術的快速迭代,而是輸給了本身的邏輯思惟能力和數學功底。就vue-awesome-progress這個組件而言,根據這個思路,咱們也能迅速開發出適用於React
,Angular
以及其餘框架生態下的組件。工做三年有餘,接觸了很多框架和技術,經歷了MVVM
,Hybrid
,小程序
,跨平臺
,大前端
,serverless
的大火,也時常感慨「學不動了」,在這個快速演進的代碼世界裏經常感到失落。好在本身尚未丟掉分析問題的能力,而不只僅是調用各類API
和插件,這多是程序員最寶貴的財富吧。前路坎坷,我輩當不忘初心,願你出走半生,歸來還是少年!
掃一掃下方小程序碼或搜索Tusi博客
,即刻閱讀最新文章!