找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4668

积分

0

好友

641

主题
发表于 3 天前 | 查看: 21| 回复: 0

在前端开发中,进度条是一个常见的视觉组件。无论是直角、圆角还是圆环形,相信大家都接触过。通常,我们可以使用 CSS、canvassvg 等多种技术来实现它们,并附带动画效果。今天,我们将聚焦于一种特定形态——半环形进度条,探讨如何分别使用 canvassvg 来实现它,并在此基础上进一步优化用户体验,例如应对数据更新和“升级”等交互场景。

为了方便演示,本文的代码示例均采用单文件的 Vue 组件形式。

夏凉津贴活动页面中的半环形进度条
已登录状态下的进度条效果

Canvas 实现步骤

一、准备工作

我们首先需要了解 canvas 绘图的基础,特别是画圆的 arc 方法。

Canvas中角度与圆的坐标关系示意图

ctx.arc() 方法的参数说明如下:

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

接着,在 Vue 组件的 data 中定义一些初始变量:

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.cHeight
this.canvas.width = this.cWidth
// 将绘图原点移到画布中央
ctx.translate(document.body.clientWidth / 2, document.body.clientWidth / 2)
ctx.fillStyle = '#FFF' // 初始填充颜⾊

二、绘制半圆环

借鉴绘制饼图的方法,我们先封装一个角度转弧度的工具函数。

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

为了便于后续动画调用,我们封装一个 renderRing 函数,它接受开始角度 startAngle 和结束角度 endAngle 两个参数。

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

执行以上代码,会得到一个半圆饼图。

Canvas绘制的半圆饼图

接下来绘制内环,通过逆时针绘制内圆来形成轨道。这里的 innerRadius 是前面定义的 radius - thickness

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

增加了内环的半圆环

现在得到的半圆环两侧是平的,而设计稿中进度条的两端是圆弧形的。因此,我们需要利用三角函数 Math.cosMath.sin 计算出起点和终点小圆的圆心坐标,然后绘制小圆。计算圆上点坐标的公式为:

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

由于我们是半圆,将 360 改为 180 即可。我们封装一个 calcRingPoint 函数来计算坐标,并在 renderRing 函数中绘制两端的小圆。

// 计算圆环上点的坐标
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()
}

绘制了端点小圆但顺序有误的半圆环

此时我们得到了一个有点问题的图形——起点处的小圆被内环路径覆盖了。我们需要调整绘制顺序:先画外环,再画内环,最后画起点处的小圆。调整后的 renderRing 函数核心逻辑如下(示意图):

调整绘制顺序后的正确半圆环

三、优化,提高还原度

目前绘制的半圆环看起来比设计稿短一些。为了更接近视觉稿,我们调整起始角度,让弧长超过180度,然后旋转画布将其摆正。

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 = startAngle
let total = 100 // 总进度
let percent = this.progress / total // 百分⽐
let twoEndAngle = percent * 220 + startAngle // 半圆原本是180,加长后是220
let step = (twoEndAngle - startAngle) / 100   // 设置步长速度
function animLoop() {
    if (tempAngle < twoEndAngle) {
        tempAngle += step
        renderRing(startAngle, tempAngle)
        window.requestAnimationFrame(animLoop)
    }
}
animLoop()

Canvas半环形进度条动画效果

大功告成!但你可能会发现图形有锯齿。这是因为 canvas 是位图,放大后曲线边缘容易不清晰。解决方法是先在高分辨率下绘制,再缩放到显示尺寸。

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

经过抗锯齿优化后,效果平滑了许多,高度还原了视觉稿。

抗锯齿优化后的Canvas进度条效果

SVG 实现步骤

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

svg 作为矢量图形,没有锯齿问题。实现的关键在于 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>

SVG整圆环进度条效果

根据圆周长公式 c = 2πr,半径140的圆周长约为 879。stroke-dasharray="260 879" 表示虚线长260,间隔879,因此我们看到了大约四分之一(260/879)的蓝色弧线。

二、改造 SVG 整圆

我们需要的是半圆环。半径为 140 的圆,半周长约为 440。通过设置 stroke-dasharray 为半周长值,并添加 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>

SVG倾斜半圆环

这仍然是一个倾斜的弧线。我们通过 CSS 旋转并调整弧长,使其达到设计稿的形态。

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

旋转后的SVG半圆环
最终调整后的SVG半圆环外框

此时,外环弧线的 stroke-dasharray 值调整为了 535。我们再添加一个内环作为进度填充部分,形成一个静态效果。

<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 实现动态轨道进度条

svg 的动态原理与 canvas 不同。进度值需要映射到弧线的实际长度上。对于超过半圆的弧长,我们需要重新计算其“有效周长”。

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-dasharray0 879 变为 267 879,实现了进度条从0滑动到50%的效果。至此,我们用 svg 实现了与 canvas 高度一致的视觉效果,并且它是矢量的!

SVG动态进度条动画效果

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

在实际活动场景中,用户完成任务后,进度条需要实时更新,有时还会伴随“升级”后进度重置再增长的效果。canvassvg 在这方面的默认表现截然不同。

Canvas 的表现:
由于 canvas 每次变化都是全量重绘,所以进度增加或减少时,它都会从0开始绘制到目标值。这在“进度减少”(如升级后等级提升,当前进度百分比降低)的场景下,会看到进度条先缩回再增长,体验稍显突兀。

Canvas进度增减动画示意

SVG 的表现:
svg 作为 DOM 元素,其变化依赖于 CSS 属性过渡。因此,进度变化时,它会平滑地从当前状态过渡到目标状态。这虽然连贯,但在“升级”场景下,直接从一个高百分比过渡到一个低百分比,会让用户感觉进度“倒退”了,逻辑上不合理。

SVG进度平滑过渡动画示意

好在 svg 元素本身可以被 JavaScript 操控。我们可以通过一些技巧来模拟“升级”效果:先让进度条快速充满,然后瞬间重置并隐藏,再显示并从0开始增长到新的进度值。

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)
}, 

这段代码的原理是:

  1. 先将进度条填充至100%(满值)。
  2. 延迟1秒后,隐藏内环,并将其进度值重置为0。
  3. 短暂延迟后,重新显示内环,并调用进度计算函数,让其从0动画增长到新的进度(例如25%)。

这样就营造出一种“升级后开启新一段进度”的视觉效果,用户体验更符合逻辑。

SVG模拟升级动画效果

总结

  • Canvas 实现过程较为复杂,但可控性极强,能够自由调整各种细节,适合应对复杂多变的需求。其进度值(0-100)与实际绘制的角度是直接对应的。
  • SVG 实现简单,代码量少,且作为矢量图形具有天然优势。但其进度计算依赖于路径长度,对于非标准圆弧需要额外换算。在复杂交互场景下,需要编写额外逻辑来优化体验。

没有绝对完美的方案,只有最适合具体场景的选择。Canvas 提供了像素级的控制力,而 SVG 则提供了简洁的声明式和基于DOM的交互能力。希望通过本文对两种实现方式的剖析与对比,能为你在前端可视化组件的技术选型与实现上带来一些启发。

欢迎在 云栈社区 与其他开发者交流更多关于 VueSVG 的前端实践经验。




上一篇:如何用CSS `:has`伪类实现表单验证、拖拽与日期选择?
下一篇:掌握CSS文字换行控制:精确解决中英文排版与业务场景布局难题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 17:25 , Processed in 0.571910 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表