svg渐变环形进度条组件

最近在开发的时候遇到了一个环形进度条的需求,设计师希望这个进度条是渐变色的,并且能有对应的动画。具体效果如图

image

因为git图的缘故所以看起来有点卡,但是实测帧数还是可以稳定到50到60之间的。
并且将其封装成了一个Vue组件,只需要传入对应的参数就可以快速的生成内容了,如果你有类似的要求可以参考以下链接
aboyl的github
切换分支到svg-circle-progress就可以查看对应的源代码以及相关文档

下面开始讲解整体的思路
我们需要什么?

  1. 一个环形进度条
  2. 渐变色
  3. 动画

细节点
环形精度条的首尾都是圆形

我们先来实现环形进度条。
实现思路很简单
使用svg画两个圆
一个圆作为底色,画满
另一个圆作为进度条,此时应该只画一段弧形

怎么画圆

参考svg的文档我们可以知道得出以下代码

  <svg :height="200" :width="200" x-mlns="http://www.w3.org/200/svg">
    <circle
      :r="50"
      :cx="100"
      :cy="100"
      :stroke="'red'"
      :stroke-width="10"
      fill="none"/>
  </svg>

效果如图


image

我们得到了一个红色的圆环
其中
r为半径
cx,cy为在svg中的坐标
stroke为颜色
stroke-width为画笔的宽度
fill为none表示不进行填充,不然我们看到的将是一整个圆而不是圆环

接下来我们需要画一段圆弧
画圆弧我们可以通过stoke-dasharray,他的本意是画实线跟虚线交替的线段,我们设置参数为 '弧长,极大值'
那么在显示上因为虚线空的部分很长,所以我们将看不到第二段的实线
对于我们需要的圆的头部 可以设置stroke-linecap为round
最终效果如图

image

代码如下

    <circle
      :r="50"
      :cx="100"
      :cy="100"
      :stroke="'red'"
      :stroke-width="10"
      fill="none" 
    />
    <circle
      :r="50"
      :cx="100"
      :cy="100"
      :stroke="'yellow'"
      :stroke-dasharray="`100,100000`"
      :stroke-width="10"
      fill="none"
      stroke-linecap="round"
    />
  </svg>

此时我们观测到起始方向是在左边的中间位置,因此我们进行旋转,在第二个圆上加上旋转

      transform="rotate(-90)"
      transform-origin="center"

因为我们需要封装成一个组件,那么他应该接收

  1. 进度
  2. 底色
  3. 弧度的颜色
  4. 内圆的半径
  5. 圆弧的宽度
    同时对于
  6. svg的宽高
  7. 外圆的半径
  8. 弧长

这些应该是我们进行计算得出来的值
当然为了使用上的方便 我们应该给出一些默认值
组件代码如下

<template>
  <svg
    :height="option.size"
    :width="option.size"
    x-mlns="http://www.w3.org/200/svg"
  >
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="option.backColor"
      :stroke-width="option.strokeWidth"
      fill="none"
    />
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="option.progressColor"
      :stroke-dasharray="arcLength"
      :stroke-width="option.strokeWidth"
      fill="none"
      transform="rotate(-90)"
      transform-origin="center"
      stroke-linecap="round"
    />
  </svg>
</template>
<script>
export default {
  name: 'Progress',
  props: {
    progress: {
      type: Number,
      required: true,
    },
    progressOption: {
      type: Object,
      default: () => { },
    },
  },
  data () {
    return {
    }
  },
  computed: {
    arcLength () {
      let circleLength = Math.floor(2 * Math.PI * this.option.radius)
      let progressLength = this.progress * circleLength
      return `${progressLength},100000000`
    },
    option () {
      // 所有进度条的可配置项
      let baseOption = {
        radius: 100,
        strokeWidth: 20,
        backColor: 'red',
        progressColor: 'yellow',
      }
      Object.assign(baseOption, this.progressOption)
      // 中心位置自动生成
      baseOption.cy = baseOption.cx = baseOption.radius + baseOption.strokeWidth
      baseOption.size = (baseOption.radius + baseOption.strokeWidth) * 2
      return baseOption
    },
  },
}
</script>

其实现在修改一下这让人吐槽的配色已经可以用来使用了~

接下来我们来实现渐变色

第一个思路当然是去搜索svg怎么实现渐变色了,我一开始也是进行了搜索,最终写出来了如下的代码

    <defs>
      <linearGradient id="gradient">
        <stop
          offset="0%"
          style="stop-color: red;"
        />
        <stop
          offset="100%"
          style="stop-color: yellow"
        />
      </linearGradient>
    </defs>
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="option.backColor"
      :stroke-width="option.strokeWidth"
      fill="none"
    />
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="'url(#gradient)'"
      :stroke-dasharray="arcLength"
      :stroke-width="option.strokeWidth"
      fill="none"
      transform="rotate(-90)"
      transform-origin="center"
      stroke-linecap="round"
    />
  </svg>

效果如图


image

为什么跟我们期望的效果不一样?
我们期望的是从最上方顺时针方向开始画圆弧,顶部颜色是红色,而到了结尾的时候是黄色的,为什么会这样呢?
因为其实我们的观点是错的。假如我们不做其他的处理,单纯的给一个圆加上渐变是什么样子的呢?

    <defs>
      <linearGradient id="gradient">
        <stop
          offset="0%"
          style="stop-color: green;"
        />
        <stop
          offset="100%"
          style="stop-color: yellow"
        />
      </linearGradient>
    </defs>
    <!-- <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="option.backColor"
      :stroke-width="option.strokeWidth"
      fill="none"
    /> -->
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="'url(#gradient)'"
      :stroke-width="option.strokeWidth"
    />
  </svg>

如图


image

可见

  1. 线性渐变是从左到右边
  2. 刚刚的位置不正确是因为设置了旋转的原因

那么我们可以推翻我们前面不靠谱的推测转回来继续思考怎么实现渐变的办法,对于有没有办法依靠svg的渐变元素这些来实现渐变的互动,鉴于CSS依靠渐变可以做出很多玩法,所以我不能打包票说没有,只不过我觉得可能实现思路会比较的麻烦,所以就换了一种思路来进行实现。

我们只需要手动计算渐变就ok了
也就是说只需要实现算法,计算出从颜色a到颜色b之间的各个渐变色是多少,依靠不同弧长不同颜色的圆进行重叠,那么我们就可以模拟渐变的实现
需要注意的是渐变的实现并不是有的时候相信的那样,从000000到ffffff进行累加,而是需要先转化成为rgb再进行计算
通过搜索引擎我们可以找到一些实现好的算法,对于具体的原理就没有再深究了,
参考文章
里面还实现了rgb转16进制的算法这些,不过核心的渐变算法大致如下,其他的算法如果有需要得话可以参考一下

      function gradientColor (startRGB, endRGB, step) {
        let startR = startRGB[0]
        let startG = startRGB[1]
        let startB = startRGB[2]
        let endR = endRGB[0]
        let endG = endRGB[1]
        let endB = endRGB[2]
        let sR = (endR - startR) / step // 总差值
        let sG = (endG - startG) / step
        let sB = (endB - startB) / step
        var colorArr = []
        for (var i = 0; i < step; i++) {
          let color = 'rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'
          colorArr.push(color)
        }
        return colorArr
      }

可以看到是对rgb颜色的三位分别进行步进以达到渐变的效果

接下来的问题便是如何计算步数,并且根据我们前面的分析,渐变色圆弧应该是对应的,经过一些测试,在步数为100的情况下,肉眼不是很能分辨出存在渐变色的空隙(ps:本来在写这篇文字之前自己的看法是进行一些计算,在进度高的情况下,步进次数会高于进度低的步进次数的说,不过写到这里的时候突然想到了过低会导致不连贯的问题。。。于是可以回去修方案了,事实证明确实进行总结确实会有更多的发现)

步数我们设定为100再对原来的弧长进行一百等分,就可以得到一个数组了,而原来的svg中circl元素我们则使用v-for根据前面生成的数组进行生成,代码如下

<template>
  <svg
    :height="option.size"
    :width="option.size"
    x-mlns="http://www.w3.org/200/svg"
  >
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="option.backColor"
      :stroke-width="option.strokeWidth"
      fill="none"
    />
    <circle
      v-for="(item, index) in arcArr"
      :key="index"
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="item.color"
      :stroke-dasharray="item.arcLength"
      :stroke-width="option.strokeWidth"
      fill="none"
      transform="rotate(-90)"
      transform-origin="center"
      stroke-linecap="round"
    />
  </svg>
</template>
<script>
export default {
  name: 'Progress',
  props: {
    progress: {
      type: Number,
      required: true,
    },
    progressOption: {
      type: Object,
      default: () => { },
    },
  },
  computed: {
    arcArr () {
      let circleLength = Math.floor(2 * Math.PI * this.option.radius)
      let progressLength = this.progress * circleLength
      const step = 100 // 设置到100则已经比较难看出来颜色断层
      const gradientColor = (startRGB, endRGB, step) => {
        let startR = startRGB[0]
        let startG = startRGB[1]
        let startB = startRGB[2]
        let endR = endRGB[0]
        let endG = endRGB[1]
        let endB = endRGB[2]
        let sR = (endR - startR) / step // 总差值
        let sG = (endG - startG) / step
        let sB = (endB - startB) / step
        let colorArr = []
        for (let i = 0; i < step; i++) {
          let color = `rgb(${sR * i + startR},${sG * i + startG},${sB * i + startB})`
          colorArr.push(color)
        }
        return colorArr
      }
      let colorArr = gradientColor(this.option.startColor, this.option.endColor, step)
      // 计算每个步进中的弧长
      let arcLengthArr = colorArr.map((color, index) => ({
        arcLength: `${index * (progressLength / 100)},100000000`,
        color: color
      }))
      arcLengthArr.reverse()
      return arcLengthArr
    },
    option () {
      // 所有进度条的可配置项
      let baseOption = {
        radius: 100,
        strokeWidth: 20,
        backColor: '#E6E6E6',
        startColor: [249, 221, 180],
        endColor: [238, 171, 86], // 用于渐变色的开始
      }
      Object.assign(baseOption, this.progressOption)
      // 中心位置自动生成
      baseOption.cy = baseOption.cx = baseOption.radius + baseOption.strokeWidth
      baseOption.size = (baseOption.radius + baseOption.strokeWidth) * 2
      return baseOption
    },
  },
}
</script>

需要注意的是我们最后的时候对生成的数组进行了一次颠倒,不然弧度最长的圆弧会挂在最后面,导致我们看不到渐变效果
效果如图

image

这里我稍微调整了一下颜色让他符合我们的预期

最后为其加上动画效果

这部分不是一个很复杂的事情,需要注意在circle上的stroke-dasharray我们需要去掉,避免出现弧长在一开始的时候就渲染了出来,然后又马上消失进入动画效果的情况,虽然影响不大,但是还是需要注意一下。
代码如下

    <circle
      v-for="(item, index) in arcArr"
      :key="index"
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="item.color"
      :stroke-width="option.strokeWidth"
      fill="none"
      transform="rotate(-90)"
      transform-origin="center"
      stroke-linecap="round"
    >
      <animate
        :to="item.arcLength"
        begin="0s"
        :dur="option.durtion"
        from="0,1000000"
        attributeName="stroke-dasharray"
        fill="freeze"
      />
    </circle>

总结

确实在学到东西以后需要做一些输出才能达到真正的学会,其实整个组件设计一开始的时候跟现在的区别还蛮大的,中间走了很多的弯路,关于svg的渐变从一开始就想错了,即使在我换思路的时候,我还以为是svg的渐变在作用于弧型的时候是顺时针画圆,并且在百分之五十的时候从结尾的颜色切换到开始的颜色达到循环渐变,直到自己写文章的时候才发现自己的错误,比如一开始的时候加入渐变效果后因为步长的计算有问题,导致出现了精度的概念,只能做到0.01的进度,而一旦切换到高精度就会导致动画非常卡顿,后来换了思路就清晰很多了
以下需要进行补充

  1. 因为重点在于渐变,所以对接受的参数就没有过多的需求了,附带的参考文章里面自己看一下改一改就能让startColor跟endColor接受正常的颜色值了
  2. 这里只做了两个渐变色,如果需要多个渐变感觉修正一下算法也不会很难
  3. 关于步进次数为100,这里自己也做过一些测试,在我试验的颜色值下面感觉在20这些数字差距也不会很大,不过性能看起在100的情况下跟20的情况下差距不大,就没有再进行修正了,而是成为了参数默认值默认值

另一种实现方式:

果然是学无止境,搜索网上的文章的时候突然发现了张大神的一篇漏网之鱼,居然只使用了两个circle元素就实现了渐变效果
于是我也从中吸取了一些帮助对这个进度条进行了进一步的优化
参考链接

张鑫旭渐变进度条实现
不过确定貌似是不能做到最后的尾部颜色是设置的结尾色,不过这样也可以作为一个补充,具体效果如图

image

可以看到右边的末端颜色略微淡色
具体实现方式看代码,个人认为需要注意的点

  1. 实现方式,本质上是两张circle的叠加,然后进行了旋转得到的,从颜色colorA到颜色colorB,算出中间值colorC,然后第一个从colorA到colorC,第二个从colorB到colorC,旋转到从上到下,那么进行叠加,看起来就是从colorA到colorC再到colorB了
  2. 要注意动画的实现,需要按照比例对动画时长进行切割,这样才不会导致在最后的时候原本流畅的动画过了最底部的时候速度突然骤降,注意动画的无缝衔接

具体参考代码如下

<template>
  <svg :height="option.size" :width="option.size" x-mlns="http://www.w3.org/200/svg">
    <defs>
      <linearGradient x1="1" y1="0" x2="0" y2="0" id="outGradient">
        <stop offset="0%" :stop-color="arcOption.outArcStartColor" />
        <stop offset="100%" :stop-color="arcOption.outArcEndColor" />
      </linearGradient>
      <linearGradient x1="1" y1="0" x2="0" y2="0" id="innerGradient">
        <stop offset="0%" :stop-color="arcOption.innerArcStartColor" />
        <stop offset="100%" :stop-color="arcOption.innerArcEndColor" />
      </linearGradient>
    </defs>
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      :stroke="option.backColor"
      :stroke-width="option.strokeWidth"
      fill="none"
    />
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      stroke="url('#innerGradient')"
      :stroke-width="option.strokeWidth"
      transform="rotate(-90)"
      transform-origin="center"
      fill="none"
      stroke-linecap="round"
      :stroke-dasharray="`0,1000000`"
    >
      <animate
        :to="`${arcOption.innerArcLength},1000000`"
        :begin="arcOption.outDurtion"
        :dur="arcOption.innerDurtion"
        :from="`${arcOption.innerInitArcLength},1000000`"
        attributeName="stroke-dasharray"
        fill="freeze"
      />
    </circle>
    <circle
      :r="option.radius"
      :cx="option.cx"
      :cy="option.cy"
      stroke="url('#outGradient')"
      :stroke-width="option.strokeWidth"
      :stroke-dasharray="`${arcOption.outArcLength},1000000`"
      fill="none"
      transform="rotate(-90)"
      transform-origin="center"
      stroke-linecap="round"
    >
      <animate
        :to="`${arcOption.outArcLength},1000000`"
        begin="0s"
        :dur="arcOption.outDurtion"
        from="0,1000000"
        attributeName="stroke-dasharray"
        fill="freeze"
      />
    </circle>
  </svg>
</template>
<script>
export default {
  name: 'Progress2',
  props: {
    progress: {
      type: Number,
      required: true,
    },
    progressOption: {
      type: Object,
      default: () => { },
    },
  },
  computed: {
    arcOption () {
      let arcConfig = {}
      let circleLength = Math.floor(2 * Math.PI * this.option.radius)
      // 如果此时小于0.5 则只需要显示最外层的圆弧 里面的圆弧不需要画了
      // 时间计算 因为第二段的长度不见得等于第一段 所以不能平分时间 不然会导致第二端的速度出现骤降
      // 因此需要按照比例进行时间计算
      if (this.progress < 0.5) {
        arcConfig.outArcLength = this.progress * circleLength
        arcConfig.outDurtion = this.option.durtion // 为初始设置的动画值
        arcConfig.innerArcLength = 0
        arcConfig.innerInitArcLength = 0 // 为动画做准备
        arcConfig.innerDurtion = 0
      } else {

        const time = this.option.durtion.split('s')[0]
        arcConfig.outArcLength = 0.5 * circleLength
        arcConfig.outDurtion = (0.5 / this.progress) * time + 's' // 
        arcConfig.innerArcLength = this.progress * circleLength
        arcConfig.innerInitArcLength = 0.5 * circleLength // 为动画做准备 此时从中间开始
        arcConfig.innerDurtion = ((this.progress - 0.5) / this.progress) * time + 's' // 为动画做准备 此时从中间开始
      }
      const tansfromColor = arr => `rgb(${arr[0]},${arr[1]},${arr[2]})`
      arcConfig.outArcStartColor = tansfromColor(this.option.startColor)
      arcConfig.outArcEndColor = tansfromColor(this.option.startColor.map((color, index) => color + (this.option.endColor[index] - color) / 2))
      arcConfig.innerArcStartColor = tansfromColor(this.option.endColor)
      arcConfig.innerArcEndColor = tansfromColor(this.option.startColor.map((color, index) => color + (this.option.endColor[index] - color) / 2))
      return arcConfig
    },
    option () {
      // 所有进度条的可配置项
      let baseOption = {
        radius: 100,
        strokeWidth: 20,
        backColor: '#E6E6E6',
        startColor: [249, 221, 180],
        endColor: [238, 171, 86],
        durtion: '1s',
        step: 100,
      }
      Object.assign(baseOption, this.progressOption)
      // 中心位置自动生成
      baseOption.cy = baseOption.cx = baseOption.radius + baseOption.strokeWidth
      baseOption.size = (baseOption.radius + baseOption.strokeWidth) * 2
      return baseOption
    },
  },
}
</script>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • @(HTML5)[canvas与SVG] [TOC] 十一 、SVG HTML体系中,最常用的绘制矢量图的技术是S...
    踏浪free阅读 4,475评论 0 2
  • 背景 当 JavaScript 被 Netscape 公司发明出来时,它被用来做一些琐细的事情,比如校验表单、计算...
    locky丶阅读 2,067评论 0 5
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。微博:厉圣杰微信公众号:牙锅子(基本不更)源码:CircleP...
    牙锅子阅读 16,590评论 24 86
  • 第一章 HTML5 (2014年10月29日发布)新特性: 10个 (1)新的语义标签 (2)增强型表单 (3)视...
    fastwe阅读 910评论 0 1
  • 是的,看标题能绕晕你。那就通俗点,说点人说的话吧。就是下面这货: 最像角度渐变对不对?好了,名字介绍到此结束,进入...
    泱泱悲秋阅读 9,041评论 1 15