用初中数学知识撸一个canvas环形进度条
source link: https://segmentfault.com/a/1190000020953625
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
周末好,今天给大家带来一款接地气的环形进度条组件 vue-awesome-progress
。近日被设计小姐姐要求实现这么一个环形进度条效果,大体由四部分组成,分别是底色圆环,进度弧,环内文字,进度圆点。设计稿截图如下:
我的第一反应还是找现成的组件,市面上很多组件都实现了前3点,独独没找到能画进度圆点的组件,不然稍加定制也能复用。既然没有现成的组件,只有自己用 vue + canvas
撸一个了。
效果图
先放个效果图,然后再说下具体实现过程,各位看官且听我慢慢道来。
安装与使用
源码地址 ,欢迎 star
和提 issue
。
安装
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 }, // 其他代码 }
webpack配置
由于当前版本发布时,未进行 babel
编译,因此使用时需要自行将 vue-awesome-progress
纳入 babel-loader
的解析范围。示例如下:
// resolve函数是连接路径的,方法体是path.join(__dirname, "..", dir) { test: /\.js$/, loader: "babel-loader", include: [ resolve("src"), resolve("node_modules/vue-awesome-progress") ] }
静态展示
任何事都不是一蹴而就的,我们首先来实现一个静态的效果,然后再实现动画效果,甚至是复杂的控制逻辑。
确定画布大小
第一步是确定画布大小。从设计稿我们可以直观地看到,整个环形进度条的最外围是由进度圆点确定的,而进度圆点的圆心在圆环圆周上。
因此我们得出伪代码如下:
// 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
大小也可以先进行绑定了
<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] } }
有了这个属性,我们就可以随意地画进度弧和圆点了,哈哈哈哈。
老哥,这种圆点坐标怎么求?
噗......看来高兴过早了,最重要的是根据不同角度求得圆点的圆心坐标,这让我顿时犯了难。
经过冷静思考,我脑子里闪过了一个利用正余弦公式求坐标的思路,但前提是坐标系原点如果在圆环外接矩形的左上角才好算。仔细想想,冇问题啦,我先给坐标系平移一下,最后求出来结果,再补个平移差值不就行了嘛。
:point_up_2:画图工具不是很熟练,这里图没画好,线歪了,请忽略细节。
好的,我们先给坐标系向右下方平移 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博客
,即刻阅读最新文章!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK