用初中数学知识撸一个canvas环形进度条(代码片段)

几个我 几个我     2022-12-23     541

关键词:

周末好,今天给大家带来一款接地气的环形进度条组件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网页中的坐标系是这样的,从绝对定位的设置上其实就能看出来(topleft设置正负值会发生什么变化),而且原点(0, 0)是在盒子(比如说canvas)的左上角哦。

技术图片

对于角度而言,x轴正向,默认是顺时针方向旋转。

圆环的圆心就是canvas的中心,所以x, youterRadius的值就可以了。

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 / 2canvas.clientWidth / 2取得中点坐标,结合控制文字对齐的两个属性textAligntextBaseline,我们可以将文字绘制在画布中央。文字的值由label属性接收,字体大小由fontSize属性接收,颜色则取的fontColor

if (this.label) 
  ctx.font = `$this.fontSizepx 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,代表使用渐变色绘制进度弧,渐变方向我默认给的从上到下。如果希望使用普通颜色,withGradientfalse即可,并可以通过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这个组件而言,根据这个思路,我们也能迅速开发出适用于ReactAngular以及其他框架生态下的组件。工作三年有余,接触了不少框架和技术,经历了MVVMHybrid小程序跨平台大前端serverless的大火,也时常感慨“学不动了”,在这个快速演进的代码世界里常常感到失落。好在自己还没有丢掉分析问题的能力,而不仅仅是调用各种API和插件,这可能是程序员最宝贵的财富吧。前路坎坷,我辈当不忘初心,愿你出走半生,归来仍是少年!


首发链接


扫一扫下方小程序码或搜索Tusi博客,即刻阅读最新文章!

技术图片

使用canvas实现环形进度条

html代码:1<!DOCTYPEhtml>2<html>3<head>4<metacharset="utf-8"/>5<title></title>6</head>7<body>8<canvasid="pro"width="400"height="300"></canvas>9 查看详情

自定义圆环形进度条实现(代码片段)

...#xff0c;因为这个组件用的地方多,所以我就直接封装了一个通用组件。先看一下效果图:功能有:圆环的颜色和进度可以自定义;中间文字可以自定义;可以自定义圆环的宽度;可以设置底部文字(文字... 查看详情

h5canvas实现圆形时间倒计时进度条(代码片段)

...今天就来实现一下。一、效果展示实现效果要求:1.环形倒计时2.能够根据总时间和当前时间进行比例性的倒计时3.进度条环形能够有颜色渐变效果4.中间文字能够有颜色渐变效果二、准备文件在开发canvas程序时,我们通常... 查看详情

h5canvas实现圆形时间倒计时进度条(代码片段)

...今天就来实现一下。一、效果展示实现效果要求:1.环形倒计时2.能够根据总时间和当前时间进行比例性的倒计时3.进度条环形能够有颜色渐变效果4.中间文字能够有颜色渐变效果二、准备文件在开发canvas程序时,我们通常... 查看详情

自定义圆环形进度条实现(代码片段)

...#xff0c;因为这个组件用的地方多,所以我就直接封装了一个通用组件。先看一下效果图:功能有:圆环的颜色和进度可以自定义;中间文字可以自定义;可以自定义圆环的宽度;可以设置底部文字(文字... 查看详情

qt编写自定义控件14-环形进度条(代码片段)

...即当前进度90%,剩余的10%也需要设置成不同的颜色,还有一个重要的功能是,能够指定多个警戒值,一旦超过或者小于该值,则当前进度自动切换到预先设定的警戒值颜色,而不需要用户自己去判断警戒值去设置警戒颜色, 查看详情

怎样用div实现带百分百环形进度条

html5+javascript实现的圆形进度条,应该符合你要求源代码:http://blog.csdn.net/tangdou5682/article/details/52778766参考技术A1、html5直接就可用css3的属性去做圆形,border-radius即可实现圆角。2、图片形式,左右两侧做圆角图片,衔接好,即可... 查看详情

csssvg环形进度条-5(代码片段)

查看详情

htmlsvg环形进度条-7(代码片段)

查看详情

csssvg环形进度条-3。(代码片段)

查看详情

htmlsvg环形进度条-4(代码片段)

查看详情

htmlsvg环形进度条-2(代码片段)

查看详情

htmlsvg环形进度条-1(代码片段)

查看详情

一起talkandroid吧(第四百九十六回:自定义view实例二:环形进度条)

文章目录知识回顾实现思路实现方法示例代码各位看官们大家好,上一回中咱们说的例子是"如何使用Java版MQTT客户端",这一回中咱们说的例子是"自定义View实例二:环形进度条"。闲话休提,言归正转,让我们一起TalkAndroid吧!知... 查看详情

如何用svg写一个环形进度条以及动画(代码片段)

本次案例主要使用了svg的三个元素,分别为circle、text、path,关于svg的介绍大家可以看MDN上的相关教程,传送门由于svg可以写到HTML中,所以这里我们就可以很方便的做进度条加载动画啦,这次案例以vue来做数据交互svg的viewBoxviewB... 查看详情

实现环形进度条的几种方法(代码片段)

...,手机上的问题就多了,仅供参考。1、利用div的border画一个背景的圆环<divclass="demo1-bg1"></div>/*css*/.demo1-bg1width:100px;height:100px;display:-webkit-box;-webkit-box-pack:center;-webkit-box-align:center;margin:50pxauto;background:fff;border-radius:50%;box-sh... 查看详情

echarts实现环形进度条(代码片段)

效果图实现代码可直接复制运行:<!DOCTYPEhtml><html> <head> <metacharset="UTF-8"> <title>环形进度条</title> <scriptsrc="https://cdn.staticfile.org/echart 查看详情

环形进度条组件(代码片段)

<template><divclass="content"ref="box"><svg:id="idStr"style="transform:rotate(-90deg)":width="width":height="width 查看详情