likes
comments
collection
share

你会用SVG绘制时间进度条嘛?

作者站长头像
站长
· 阅读数 32

前言

SVG想必大家都非常熟悉,即使没有写过,也多多少少听过。写一些简单的动画或者交互,是绕不开的一种语言,但是它最大的问题就是上手比较陡峭,API比较多,使用方式也比较多,很容易在中间迷失。今天带大家学习几种圆形倒计时的开发思路。

正文

文章导读

  • 成果在先
  • 基础认识
  • 环形倒计时
  • 扇形倒计时
  • 环形渐隐倒计时
  • 圆角矩形渐隐倒计时

成果在先

为了不浪费大家时间,成果在先,让我们先看一下,阅读完本篇文章,可以学会绘制哪些动效。

你会用SVG绘制时间进度条嘛?

你会用SVG绘制时间进度条嘛?

你会用SVG绘制时间进度条嘛?

你会用SVG绘制时间进度条嘛?

基础认识

为了照顾从未使用过SVG的小伙伴,我先在这里给SVG做一个自我介绍。

SVG 意为可缩放矢量图形,使用 XML 格式定义图像。SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失。可以绘制包括(不仅限于):矩形、圆形、椭圆、线条、多边形、路径、文本等形状。可以说只有你想不到的,没有你画不出来的。 我收集了几个地址,大家有兴趣可以进去学习一下SVG基础知识。

环形倒计时

你会用SVG绘制时间进度条嘛?

首先是第一个,也是最简单的,环形进度条。

实现思路:

  • 绘制一个圆
  • 内部填充透明色
  • 边框使用虚线,虚实线按倒计时比例绘制
  • 根据时间更改虚实线的长短

代码实现:

  <!-- html -->
  <svg class="countCircle" viewBox="0 0 20 20">
    <circle
      class="circleA"
      r="5"
      cx="10"
      cy="10"
      fill="transparent"
      stroke="rgba(255,255,255,.5)"
      stroke-linecap="round"
      stroke-width="0.8"
      :stroke-dasharray="strokeDasharray"
    />
  </svg>
// js
export default {
  data(){
    return {
      strokeDasharray:'',
      sum:10,  // 倒计时总数
      num:10,  // 当前倒计时
      timer:null,
    }
  },
  mounted(){
    this.startCountDown()
  },
  methods:{
    startCountDown(){
      const {sum,num} = this
      this.strokeDasharray = `0 calc(10 * ${Math.PI} * ${sum - num}/${sum}) calc(10 * ${Math.PI} * ${num}/${sum}) 0`
      this.timer && clearInterval(this.timer);
      this.timer = setInterval(() => {
        this.num -- 
        if(this.num<0){
          this.timer&&clearInterval(this.timer);
          return
        }
        const {sum,num} = this
        this.strokeDasharray = `0 calc(10 * ${Math.PI} * ${sum - num}/${sum}) calc(10 * ${Math.PI} * ${num}/${sum}) 0`
      }, 1000);
    }
  }
}

.countCircle{
  width: 200px;
  height: 200px;
  transform: rotate(-90deg);
}
.circleA{
  transition: stroke-dasharray 1s linear;
}

代码解析:

  • viewBox 用于表示当前SVG的实际比例,定义画布上可以显示的区域,比如上面的代码,元素的实际宽高是200*200(px),viewBox的值为0 0 20 20,表示从(0,0)开始渲染,到(20,20)结束。SVG中每一个单位代表200/20 = 10个像素。stroke-width="0.8"表示边框宽度为8px
  • circle 可以看到SVG中绘制了circle元素。这是SVG中的形状(圆形),可以设置该元素的宽、高、填充、边框等属性。cx cy属性代表圆的中心位置,r属性代表圆的半径。
  • stroke-linecap 定义不同类型线条的开始结束方式,对应类型如下。这里我使用了round,使倒计时看起来更加圆滑。 你会用SVG绘制时间进度条嘛?
  • stroke-dasharray 定义虚线的虚实比例组成,这也是最关键的属性。对应效果如下。
stroke-dasharray="" 
stroke-dasharray="4"
stroke-dasharray="4 1"
stroke-dasharray="4 1 2"
stroke-dasharray="4 1 2 3"

你会用SVG绘制时间进度条嘛?

可以看到如果只设置一个值`4`,它会根据四实四空的方式展示虚线,如果设置成`4 1`时,线段会根据`4 1 4 1 4 1 ...`(即四实一空)的方式展示整条线段;如果有三个值`4 1 2`,那它的虚实分布就是:`4 1 2 4 1 2 ...`。
根据该属性特性,我们只需要将倒计时已经过去的部分变为空即可。比如时间总长为6,倒计时过去了1,该属性应该为:`0 1 5 0`;倒计时过去了4,该属性应该为:`0 4 2 0`。直接根据周长等比划分:`0 calc(10 * ${Math.PI} * ${sum - num}/${sum}) calc(10 * ${Math.PI} * ${num}/${sum}) 0`

最后再给属性添加过渡属性:transition: stroke-dasharray 1s linear;,完美实现动画效果。

扇形倒计时

你会用SVG绘制时间进度条嘛?

第二个,进阶版。 比较聪明的读者就要说了:“这个和第一个很像哎,是不是可以复用代码呀,把边框的属性加宽到半径的宽度,不就变成这样了吗?” 说的不错,让我们来看一下效果: 你会用SVG绘制时间进度条嘛?

这?这转的是什么呀,怎么和我想象的结果不一样?

哈哈哈,大家发现了什么,属性stroke-linecap还是round呢。

代码实现:

// 保留第一版代码,修改两个属性。
stroke-linecap="buff"
stroke-width="10"

环形渐隐倒计时

你会用SVG绘制时间进度条嘛?

前期调研: 看到这个效果,我的第一感觉就是,这不就是一个渐变吗?简单,我只要给边框添加一个径向渐变颜色,我边框变化的时候就会跟着我的边框一起移动了。研究了一下API,实现代码如下:

添加linearGradient径向渐变元素,其中一边是透明色,使用offset控制开始渐变的位置。去掉stroke属性的颜色填充,填充该径向渐变元素。大功告成!

    <svg class="svgCCountCircle" viewBox="0 0 20 20">
        <linearGradient id="loopGradient">
          <stop offset="60%" style="stop-color: #a9a2fe" />
          <stop offset="90%" style="stop-color: rgba(130, 119, 255, 0)" />
        </linearGradient>
      <circle
        class="circle"
        r="5"
        cx="10"
        cy="10"
        fill="transparent"
        stroke="url(#loopGradient)"
        stroke-linecap="buff"
        stroke-width="0.5"
        :stroke-dasharray="strokeDasharray"
      />
    </svg>

然而,我还是把问题想简单了,最终还是出现了问题。

  1. 径向渐变是基于形状本身的,而不会基于边框,也就是说,渐变的方向是根据圆形的方向进行渲染的,不在乎你边框是否消失。而且边框并不是消失了, 只是利用dasharray属性,填充了虚线,看上去消失了而已,想想倒也合理。
  1. 因为我这个是圆形,当径向渐变渲染时,会把开始和结束部分的颜色都渲染成透明色。

难道这条路就是死胡同了吗?

并没有,经过我苦思冥想,终于想到了问题的解决办法。

问题1:我确实无法让渐变知道我边框消失的方向,但是我自己知道方向呀,山不转我转,我直接动态更改渐变的方向不就可以了吗? 问题2:既然他会同时渲染开始结束部分,那当它开始渲染时,我先用一个半圆挡住结束区域,转走之后我再显示。

赶紧给自己点个赞👍。

实现思路:

  • 添加径向渐变,作为边框颜色进行填充
  • 渐变方向根据时间动态变化
  • 添加1/4圆,倒计时前遮挡结束区域,知道旋转完成

代码实现:

    <svg class="svgCCountCircle" viewBox="0 0 20 20">
        <linearGradient id="loopGradient" :x1="initDirection.x1" :y1="initDirection.y1"
          :x2="initDirection.x2" :y2="initDirection.y2">
          <stop offset="60%" style="stop-color: #a9a2fe" />
          <!-- <stop offset="90%" style="stop-color: red" /> -->
          <stop offset="90%" style="stop-color: rgba(130, 119, 255, 0)" />
          <animate attributeName="x1" values="0%;0%;100%;100%;0%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
          <animate attributeName="y1" values="100%;0%;0%;100%;100%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
          <animate attributeName="x2" values="100%;0%;0%;100%;100%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
          <animate attributeName="y2" values="100%;100%;0%;0%;100%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
        </linearGradient>
      <circle
        class="circle"
        r="5"
        cx="10"
        cy="10"
        fill="transparent"
        stroke="url(#loopGradient)"
        stroke-linecap="buff"
        stroke-width="0.5"
        :stroke-dasharray="strokeDasharray"
      />
      <circle
      v-if="num>sum/2+2"
        r="5"
        cx="10"
        cy="10"
        fill="transparent"
        stroke="#a9a2fe"
        stroke-linecap="buff"
        stroke-width="0.5"
        :stroke-dasharray="`0 calc(10 * ${Math.PI} * 3/4) calc(10 * ${Math.PI} * 1/4) 0`"
      />
    </svg>
export default {
  data(){
    return {
      strokeDasharray:'',
      sum:30,
      num:28,
      timer:null,
      animateDuration: 0, // 旋转渐变所需动画时间
      keyTimes: '0;0;0;0;0', // 运动时间分割
      // 初始化渐变方向
      initDirection: {
        x1: '0%',
        y1: '100%',
        x2: '100%',
        y2: '100%',
      },
    }
  },
  mounted(){
    this.startCountdown()
  },
  methods:{
    startCountdown(){
      const {sum,num} = this
      this.animateDuration = sum
      this.strokeDasharray = `0 calc(10 * ${Math.PI} * ${sum - num}/${sum}) calc(10 * ${Math.PI} * ${num}/${sum}) 0`
      // 计算渐变的旋转角度
      this.parseDir()
      this.timer&&clearInterval(this.timer);
      this.timer =  setInterval(() => {
        this.num -- 
        const {sum,num} = this
        if(num<0){
          this.timer&&clearInterval(this.timer);
        }
        this.strokeDasharray = `0 calc(10 * ${Math.PI} * ${sum - num}/${sum}) calc(10 * ${Math.PI} * ${num}/${sum}) 0`
      }, 1000);
    },
    parseDir(){
      const {num,sum} = this
      const part = sum / 4
      const times = [0, 0.25, 0.5, 0.75, 1]
      this.initDirection = {
        x1: '0%',
        y1: '100%',
        x2: '100%',
        y2: '100%',
      }
      if (num !== sum && num > part * 3) {
        times[1] = (num - part * 3) / part * 0.25
        times[2] = times[1] + (1 - times[1]) / 3
        times[3] = times[2] * 2 - times[1]
      } else if (num <= part * 3 && num > part * 2) {
        times[1] = 0
        times[2] = (num - part * 2) / part * 0.25
        times[3] = times[2] + (1 - times[2]) / 2
        this.initDirection = {
          x1: '0%',
          y1: '0%',
          x2: '0%',
          y2: '100%',
        }
      } else if (num <= part * 2 && num > part) {
        times[1] = 0
        times[2] = 0
        times[3] = (num - part) / part * 0.5
        this.initDirection = {
          x1: '100%',
          y1: '0%',
          x2: '0%',
          y2: '0%',
        }
      } else if (num <= part) {
        times[1] = 0
        times[2] = 0
        times[3] = 0
        this.initDirection = {
          x1: '100%',
          y1: '100%',
          x2: '100%',
          y2: '0%',
        }
      }
      this.keyTimes = times.join(';')
    }
  }
}

代码解析:

  • 方向变化 linearGradient元素的方向变化是根据x1 x2 y1 y2四个属性控制的。 你会用SVG绘制时间进度条嘛? 如图所示,如果我想绘制从上到下的渐变,那么我的属性值应该是x1=100%、 y1=0%、x2=100%、y2=100%。 那么,我想要的径向渐变的轨迹其实就是: 你会用SVG绘制时间进度条嘛? 把图形变换成数据:

    x1y1x2y2
    100%0%100%100%
    100%100%0%100%
    0%100%0%0%
    0%0%100%0%

    这也就是上面代码的实现思路,根据时间控制这几个属性的变化。

圆角矩形渐隐倒计时

你会用SVG绘制时间进度条嘛?

接下来让我们来看最难的一个,类椭圆环形进度条。

前期调研 首先简单分析一下这个倒计时,很像一个椭圆但是弧度又不连贯,像一个矩形加了圆角,咦?那我们是不是可以绘制一个矩形,修改圆角,然后用上面旋转渐变的方式进行绘制呢? 说干就干!经过一些时间的编写,实现代码如下:

    <svg class="countRect" viewBox="0 0 20 40">
      <rect
        x="10"
        y="20"
        rx="5"
        ry="5"
        width="9"
        height="14"
        fill="transparent"
        stroke="#fff"
        stroke-linecap="buff"
        stroke-width="0.5"
      />
    </svg>

你会用SVG绘制时间进度条嘛?

嗯,效果还不错,给它加上渐变试试旋转效果。

......效果不好表现,辛苦读者脑补一下。😚 不对呀,怎么虚线渲染的速度和读秒的速度不一样呢。60秒的总时间,我读秒已经到20了,虚线还在30那里慢吞吞的走。

什么原因? 仔细研究了一波,找到了原因,我是根据周长去控制的虚线规律,而周长是根据原来的元素计算的,我给他加了圆角,导致图形畸变,矩形-->圆角矩形。得不到最终的周长了,导致无法清晰计算虚线的规律。

原来如此,看来rect方案不行了怎么解决呢? 啥也不说了,用path(一座大山)。

  • path 用于定义一个路径。可以根据计算绘制任何形状的图形。路径的属性还是比较多的,经过我长时间的研究和努力,终于把大部分属性都掌握了。大家时间宝贵,在这里就不详细展开了,如果感兴趣,我会再写一篇只针对SVGpath属性的讲解。 你会用SVG绘制时间进度条嘛?

我们对倒计时进行一个简单的拆解,可以看到只需要绘制六条路径,其中2条直线,4条弧线。那我们需要用到哪些属性呢?

你会用SVG绘制时间进度条嘛?

  • M move to 移动到,开始点 。参数(x y)

  • Z closepath 关闭路径,无参数

  • L line to 画直线。参数(x y)

  • Q quadratic Bézier curveto二次贝塞尔曲线到 参数(x1 y1,x y)

    你会用SVG绘制时间进度条嘛?

经过多次调试和计算,最终绘制出了这个类圆角矩形的倒计时路径。 path=M2 27 Q4 50, 27 52 L42 52 Q67 50, 68 27 Q67 4, 42 2 L27 2 Q4 4, 2 27 Z 使用之前的渐变思路,最终实现圆角矩形渐隐倒计时。 代码实现:

    <svg class="loop" viewBox="0 0 70 54">
        <linearGradient id="loopGradient" :x1="initDirection.x1" :y1="initDirection.y1"
          :x2="initDirection.x2" :y2="initDirection.y2">
          <stop offset="60%" style="stop-color: #a9a2fe" />
          <!-- <stop offset="90%" style="stop-color: red" /> -->
          <stop offset="90%" style="stop-color: rgba(130, 119, 255, 0)" />
          <animate attributeName="x1" values="100%;100%;0%;0%;100%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
          <animate attributeName="y1" values="0%;100%;100%;0%;0%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
          <animate attributeName="x2" values="0%;100%;100%;0%;0%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
          <animate attributeName="y2" values="0%;0%;100%;100%;0%" :keyTimes="keyTimes" :dur="animateDuration + 's'"
            fill="freeze" />
        </linearGradient>
        <path v-show="num > sum / 2 + 2"
          d="M2 27 Q4 50, 27 52 L35 52" fill="none" stroke="#a9a2fe" stroke-linecap="round" stroke-width="2"></path>
        <path ref="svgEllipse" class="loopPath"
          d="M2 27 Q4 50, 27 52 L42 52 Q67 50, 68 27 Q67 4, 42 2 L27 2 Q4 4, 2 27 Z" fill="none"
          stroke="url(#loopGradient)" stroke-linecap="round" stroke-width="2" :stroke-dasharray="ellipseDasharray">
        </path>
    </svg>
export default {
  data(){
    return {
      sum:10,
      num:5,
      timer:null,
      animateDuration: 0, // 旋转渐变所需动画时间
      keyTimes: '0;0;0;0;0', // 运动时间分割
      // 初始化渐变方向
      initDirection: {
        x1: '100%',
        y1: '0%',
        x2: '0%',
        y2: '0%'
      },
      ellipseDasharray:""
    }
  },
  mounted(){
    this.startCountdown()
  },
  computed:{
    // 椭圆周长
    ellipseLen () {
      return this.$refs.svgEllipse ? this.$refs.svgEllipse.getTotalLength() : 0
    },
  },
  methods:{
    startCountdown(){
      // 计算渐变的旋转角度
      this.parseDir()
      const {sum,num} = this
      this.animateDuration = num
      const ellipseSolid = this.ellipseLen * num / sum
      this.ellipseDasharray = `${ellipseSolid} ${this.ellipseLen - ellipseSolid}`
      this.timer&&clearInterval(this.timer);
      this.timer =  setInterval(() => {
        this.num -- 
        if(this.num<0){
          this.timer&&clearInterval(this.timer);
          return
        }
        const {sum,num} = this
        const ellipseSolid = this.ellipseLen * num / sum
        this.ellipseDasharray = `${ellipseSolid} ${this.ellipseLen - ellipseSolid}`
      }, 1000);
    },
    parseDir(){
      const {num,sum} = this
      // 计算渐变的旋转角度
      const part = sum / 4
      const times = [0, 0.25, 0.5, 0.75, 1]
      this.initDirection = {
        x1: '100%',
        y1: '0%',
        x2: '0%',
        y2: '0%',
      }
      if (num !== sum && num > part * 3) {
        times[1] = (num - part * 3) / part * 0.25
        times[2] = times[1] + (1 - times[1]) / 3
        times[3] = times[2] * 2 - times[1]
      } else if (num <= part * 3 && num > part * 2) {
        times[1] = 0
        times[2] = (num - part * 2) / part * 0.25
        times[3] = times[2] + (1 - times[2]) / 2
        this.initDirection = {
          x1: '100%',
          y1: '100%',
          x2: '100%',
          y2: '0%',
        }
      } else if (num <= part * 2 && num > part) {
        times[1] = 0
        times[2] = 0
        times[3] = (num - part) / part * 0.5

        this.initDirection = {
          x1: '0%',
          y1: '100%',
          x2: '100%',
          y2: '100%',
        }
      } else if (num <= part) {
        times[1] = 0
        times[2] = 0
        times[3] = 0
        this.initDirection = {
          x1: '0%',
          y1: '0%',
          x2: '0%',
          y2: '100%',
        }
      }
      this.keyTimes = times.join(';')
    }
  }
}

结语

好啦,文章到这里就结束了,感谢您观看到这里,您的观看就是对我的认可!如果文章对您有一点点帮助,也可以给笔者点个赞!♥️♥️♥️