cover_image

半环形进度条在起点活动中的应用

阅文前端团队 阅文前端团队
2023年01月10日 08:42

前言

相信看到此文的小伙伴们,平时工作中多多少少应该会接触过进度条方面的需求,比如直角进度条、圆角进度条、圆环进度条等,大家可能也有多种方法比如利用 CSS canvas svg去实现这些需求并予以加上动画。而本次要和大家分享的就是半环形进度条如何用 canvas 和 svg 去实现,以及如何在实现的基础上去增强用户的体验。

先来看视觉稿和最终实现

为了方便演示,本文中的案例均使用单文件的 vue +html 来做代码实例演示

图片

图片

Canvas 实现步骤

一、准备工作

我们首先要做一些 canvas 的基本准备工作。

图片

先来了解一下 canvas 的画圆 arc 方法的用法和绘制角度原理。

参数描述
x圆的中心的 x 坐标。
y圆的中心的 y 坐标。
r圆的半径。
sAngle起始角,以弧度计(弧的圆形的三点钟位置是 0 度)
eAngle结束角,以弧度计。
counterclockwise可选。逆时针还是顺时针。false = 顺时针,true = 逆时针

然后我们定义一些初始变量

data() {    return {        canvas: null,  // canvas 实例对象        cWidth: 750, // 预设宽度        cHeight: 750, // 预设高度        progress: 50, // 假设从接口获取的进度目前是 50    }},

接着我们在 methods 中添加一个名为 initCircleProgress 的方法,定义一些绘制所需的变量以及角度等等单位

// initCircleProgress 方法体中的代码let radius = 124 // 外环半径let thickness = 12 // 圆环厚度let innerRadius = radius - thickness // 内环半径let startAngle = -180 // 开始⾓度let endAngle = 0 // 结束⾓度let x = 0 // 圆⼼x坐标let y = 0 // 圆⼼y坐标

对 canvas 做好初始化的准备

// html 结构<canvas id="circleProgress"></canvas>
// initCircleProgress 方法体中的代码this.canvas = document.getElementById('circleProgress')let ctx = this.canvas.getContext('2d')this.canvas.style.width = this.cWidth + 'px'this.canvas.style.height = this.cHeight + 'px'this.canvas.height = this.cHeightthis.canvas.width = this.cWidth// 将绘图原点移到画布中央ctx.translate(document.body.clientWidth / 2, document.body.clientWidth / 2)ctx.fillStyle = '#FFF' // 初始填充颜⾊

二、绘制半圆环

我们借鉴了 canvas 绘制饼图的方法,先封装一个角度转弧度的小函数,以便之后调用

// ⾓度转弧度function angle2Radian(angle) {    return (angle * Math.PI) / 180}

接下来我们先开始绘制外环,为了今后动画调用方便。我们再封装一个 renderRing 函数,它接受 2 个参数,分别是开始角度 startAngle 和结束角度 endAngle

renderRing(startAngle, endAngle)function renderRing(startAngle, endAngle) {  ctx.beginPath()  // 绘制外环  ctx.arc(0, 0, radius, angle2Radian(startAngle), angle2Radian(endAngle))}

以上代码就可以形成一个这样的半圆饼图

图片

/* 绘制内环 依然参考 canvas 饼图、环形图的一些技巧,通过逆时针绘制内圆形成进轨道 * 这里的 innerRadius 内圆半径在上面定义过,所以是 radius - thickness = 12 */ctx.arc(0, 0, innerRadius, angle2Radian(endAngle), angle2Radian(startAngle), true) // 从-180 到 0

图片

目前得到的半圆环,左侧和右侧是平的,而视觉稿进度条的起点和终点都是圆弧形的,所以接下来一点比较重要,需要你努力回忆起你初中时学过的三角函数知识,利用 cos 和 sin 算出 x,y 坐标后画小圆,关于 canvas 如何计算圆的坐标可以参考此文[1]

图片

x = Math.cos(Math.PI * 2 / 360 * 度数) * ry = Math.sin(Math.PI * 2 / 360 * 度数) * r

现在是半圆,所以只需把 360 改成 180 即可。我们再封一个 calcRingPoint 小函数,用来计算左右两侧小圆的坐标,随后在终点处画上一个小圆,左边的同理。

// 计算圆环上点的坐标function calcRingPoint(x, y, radius, angle) {    let res = {}    res.x = x + radius * Math.cos((angle * Math.PI) / 180)    res.y = y + radius * Math.sin((angle * Math.PI) / 180)    return res}// 接着绘制function renderRing(startAngle, endAngle) {  ...  // 计算外环与内环终点连接处的中⼼坐标  let oneCtrlPoint = calcRingPoint(      x,      y,      innerRadius + thickness / 2,      endAngle  )  // 绘制外环与内终点连接处的圆环  ctx.arc(      oneCtrlPoint.x,      oneCtrlPoint.y,      thickness / 2,      angle2Radian(-90),       angle2Radian(270) // 可任意调整,只要和原来平的轨道合并成一个圆即可  )  // 计算外环与内环起点连接处的中⼼坐标  let twoCtrlPoint = calcRingPoint(      x,      y,      innerRadius + thickness / 2,      startAngle  )  // 绘制外环与内环起点连接处的圆环  ctx.arc(      twoCtrlPoint.x,      twoCtrlPoint.y,      thickness / 2,      angle2Radian(-90),      angle2Radian(270)  )  ctx.fill()}

图片

经过以上步骤,我们就得到这样一个。。。有点问题的半圆环???,别急,我们需要调整一下,把绘制内环放到中间去执行,让起点处的绘制在最上层。

图片

这下就正常了~

图片

图片

三、优化,提高还原度

以上的结果似乎和视觉稿还差了一点,视觉稿要比半圆更长一点,所以我们需要整体扩大一下起始角度和结束角度,一调整后我们发现原本摆正的圆被旋转了,这时候就需要再用 rotate 方法将其进行视觉摆正,如下方右图。

let startAngle = -65 // 开始⾓度let endAngle = 155 // 结束⾓度ctx.rotate(angle2Radian(225)) // 将画布旋转225度

图片

图片

最后,我们要在轨道中不停的绘制新的小圆(这里其实是整个画布都在不停重绘),模拟进度条增涨的动画效果。

// 进度条颜⾊let progress = ctx.createLinearGradient(0, 0, 500, 0)progress.addColorStop(0, '#1075EB')progress.addColorStop(1, '#FFF')ctx.fillStyle = progress// 开始绘画let tempAngle = startAnglelet total = 100 // 总进度let percent = this.progress / total // 百分⽐let twoEndAngle = percent * 220 + startAngle // 半圆原本是180,加长后是220let step = (twoEndAngle - startAngle) / 100   // 设置步长速度function animLoop() {    if (tempAngle < twoEndAngle) {        tempAngle += step        renderRing(startAngle, tempAngle)        window.requestAnimationFrame(animLoop)    }}animLoop()

图片

大功告成!什么?有明显的锯齿?不慌,我们再来做一个小优化。

小知识点: canvas 毕竟只是绘图接口,他就像 photoshop 一样,越放大越容易出现锯齿,曲线则更为明显。知道这个原理就好办多了,我们首先需要将图像的宽高倍率放大。

let devicePixelRatio = 4 // 定义一个设备像素倍率变量this.canvas.height = this.cHeight * devicePixelRatiothis.canvas.width = this.cWidth * devicePixelRatio// 再缩放抗锯齿ctx.scale(devicePixelRatio, devicePixelRatio)

经过调整后,平滑和高清度改善许多,我们和视觉稿对比已经是高度还原了。

图片

svg 实现步骤

一、先取一个 svg 整圆,加上圆环

介于 svg 矢量图的特性,它不会存在像 canvas 那样出现有锯齿的情况,我们只需运用好 circle 标签的两个关键属性 stroke-dasharray 和 stroke-linecap

首先,我们可以先从网上借一个 svg 圆环图过来,做一些微调,得到下列的样子

<svg width="440" height="440" viewbox="0 0 440 440">    <circle cx="220" cy="220" r="140" stroke-width="16" stroke="#FFF" fill="none"></circle>    <circle cx="220" cy="220" r="140" stroke-width="16" stroke="#00A5E0" fill="none" stroke-dasharray="260 879"></circle></svg>

图片

我们再来回忆一下初中数学:c=2πr 圆的周长 = 圆周率 * 2 * 半径 , 以上圆就是 3.14 * 2* 140 = 879

在这里 stroke-dasharray 表示虚线长为 260,间距为 879,以此我们可以实现以上大约四分之一进度的圆环进度条,而由于间距879(此圆的周长),所以下一段虚线我们并看不见。

二、改造 svg 整圆

我们需要的是半圆,用以上 svg 开始改造。半径为 140 的圆,周长是 140 * 2 * 3.14 = 879,一半的圆周长就是 430 左右。两头的小圆则不需要像 canvas 那样复杂,只需加上属性 stroke-linecap="round" 即可。

<svg width="440" height="440" viewBox="0 -100 440 440" class="svg-out out">    <circle cx="180" cy="220" r="140"            stroke-width="16"            stroke="#FFF"            fill="none"            stroke-dasharray="430"            stroke-linecap="round"    ></circle></svg>

图片

这样就得到了一个倾斜半圆环,我们再把周长加长一些,旋转后找到一个平衡点,调整到想要的效果

.out {    width: 366px;    transform: rotate(-200deg);}

图片

图片

此时 svg 中的 stroke-dasharray 目前是 535。我们再把内圆调整成想要的颜色,调整好属性,先形成一个静态的 svg 效果

<svg width="440" height="440" viewBox="0 -100 440 440" class="svg-out out">    <!-- 外环 -->    <circle cx="180" cy="220" r="140"            stroke-width="16"            stroke="#FFF"            fill="none"            stroke-dasharray="535"            stroke-linecap="round"    ></circle>    <!-- 内环 -->    <circle class="inner" cx="180" cy="220" r="140" stroke-width="16" stroke="#1075EB" fill="none" stroke-dasharray="0 879" stroke-linecap="round"></circle></svg>

图片

三、svg 实现动态轨道进度条

由于 svg 实现方法和 canvas 完全不一样,从 0-100 的进度值,是按照圆的周长来计算的,所以我们要把实际的进度条数值做减半处理,来显示 svg 目前所在圆环内的当前进度值。

mounted() {    this.calcSvgProgress(this.progress) // progress: 50}, methods: {  calcSvgProgress(progress, delay = 500) {    // 整圆c=2πr,半圆则是c=πr r=180是半圆,我们目前比半圆多一点点,所以取 170    let percent = progress / 100, perimeter = Math.PI * 170    setTimeout(() => {        document.querySelector('.inner').setAttribute('stroke-dasharray', perimeter * percent + " 879");    }, delay)  }

别忘了用 css 加上动画过渡

.inner {    transition: stroke-dasharray 1s;}

经过以上代码,我们动态的把 stroke-dasharray 从 0 879,变成了 267 879,这样就能让进度条从 0 滑动到一半,到此为止,我们用 svg 实现了与 canvas 一模一样的效果,并且,他还是矢量的!

图片

数据更新时的体验优化与差异

按照我们活动业务中的场景,用户可能在完成任务后随时能看到进度条的增涨变化或升级变化。

在这方面,如果你不做一些定制取优化体验的话,canvas 和 svg 的表现就完全不同了。

首先来看 canvas

图片

由于canvas 每次变化都是重绘,所以他每次都会从 0 开始绘制到最终值。

再来看 svg

图片

由于 svg 本身就是矢量图形,所以在它发生变化时,只是进行了变形或者位移,动画也比较连贯。

假设用户升级后,发现看到的进度条由多变少,这就显得比较奇怪了。

不过,好在 svg 以及 svg 内部的标签也都属于 dom 元素,所以我们是可以用 js 对它们进行一些操控的。我们用简单的一段代码就能模拟出升级的效果

svgLevelUp() {    let circle = document.querySelector('.inner');    let perimeter = Math.PI * 170    circle.setAttribute('stroke-dasharray', perimeter + "  879");    setTimeout(() => {        circle.style.display = 'none'        circle.setAttribute('stroke-dasharray', '0 879')        setTimeout(() => {            circle.style.display = 'block'            this.calcSvgProgress(25, 100)        }, 10)    }, 1000)}, 

原理很简单:先把 stroke-dasharray 改成满值,做个延时 → 内环元素暂时 display:none ,同时改回空(0,879)再延时 → 重新让内环 display:block ,此时再拉接口获取最新 progress 进度值,来达到一个让用户看到升级后,进度又涨到 25% 进度的效果。

图片

总结

canvas 实现过程较为复杂,但可以自由调整细节,比较容易应付多变的需求,整个进度条 0-100 值和实际数值相匹配。

svg 实现过程简单,代码量也比较少,按照 stroke-dasharray 的特性,若需求较为复杂时,可能在视觉还原度上会有一些难度,并有一定的局限性,需要自行再添加方法优化体验,整个进度条 0-100 的值是按照整圆来计算的,若半圆则需要减半做换算处理。

没有最好的方案,只有最适合的方案。以上就是本次分享所带来的所有内容了,希望能给屏幕前的小伙伴们开拓一些思路,感谢阅读。

References

[1] canvas通过三角函数中获得圆上某点的坐标https://developer.aliyun.com/article/922877


继续滑动看下一个
阅文前端团队
向上滑动看下一个