手把手编写一个VUE简易SVG动画组件

1,888 阅读3分钟

前言

前面介绍了动画的基本使用,这期结合VUE来实现一个简易的SVG路径动画组件。效果大致如下,在FlySvg组件中引入path,自动实现path的绘制动画效果。组件只支持一个path,多个path还是手动写js控制好一些。

  <FlySvg>
   <path d="M153 334
    C153 334 151 334 151 334
    C151 339 153 344 156 344
    C164 344 171 339 171 334
    C171 322 164 314 156 314
    C142 314 131 322 131 334
    C131 350 142 364 156 364
    C175 364 191 350 191 334
    C191 311 175 294 156 294
    C131 294 111 311 111 334
    C111 361 131 384 156 384
    C186 384 211 361 211 334
    C211 300 186 274 156 274"
    style="fill:white;stroke:red;stroke-width:2"/>
  </FlySvg>

circle.gif

工作中的svg动画

svg在做动画中还是挺常见的,相对于canvas而言,svg的适配效果会更好一些。来看一个之前做的大屏案例,页面中间有粒子移动效果,需要粒子按照指定的路线完成轨迹移动。这样的效果正好时候SVG登场,在这里案例中,需要4个svg的path路径在制作动画,业务性也比较强,不适合新手入门,本期要介绍的是单一的path路径实现的动画。 demo.gif

svg描边动画

之前已经介绍过了svg的描边动画了,今天在简单的回顾一下svg中stroke-dasharray属性的使用方式。

stroke-dasharray = '10'

image.png 可以看出来svg的stroke-dasharray可以根据给定的值实现对应的实线和虚线的效果。也就是每隔10画一段实线,每隔10画一条是空心的线,也就组成了图上的虚线。
而看strokeDashoffset则是偏移作用,当strokeDashoffset的大小等于path的长度,那么整个path就消失在失业中了。也就是让strokeDashoffset从path的最大长度逐步减少到0就实现了动画效果。

dash.gif

vue3 + svg + vite 实现组件

组件编写前,用伪代码来分析一下编写步骤:

  1. 需要一个svg容器组件, 容器宽高100% 100% 支持用户自定义viewbox
 <svg width="100%" height="100%" 
 :viewBox="viewBox"
  </svg>
  1. 支持用户使用自己的path
<svg>
    <slot></slot> //slot中添加path
</svg>
  1. svg容器组件要动态修改strokeDashoffset path的长度可以通过path.getTotalLength()来获取。
setup() {
 const strokeDasharray = ref(0)
 const strokeDashoffset = ref(0)
 onMounted(() => {
  const instance = getCurrentInstance()
  path = instance.ctx.$el.firstElementChild // 获取path的dom
  pathLength = path.getTotalLength()
  strokeDasharray.value = pathLength
  strokeDashoffset.value = pathLength
  
  // 然后开始动画
  render() {
      // 循环减少strokeDashoffset 直到0
  }
 })
}

初始化

npm init @vitejs/app

命令行中输入, 创建vite项目,选择vue模板

创建目录结构

新建lib目录,修改目录结构如下

image.png

开始编写代码,app.vue中引入组件

<template>
  <div class="stage">
    <FlySvg :viewWidth="580" :viewHeight="400" :loop="true">
      <path d="M153 334
        C153 334 151 334 151 334
        C151 339 153 344 156 344
        C164 344 171 339 171 334
        C171 322 164 314 156 314
        C142 314 131 322 131 334
        C131 350 142 364 156 364
        C175 364 191 350 191 334
        C191 311 175 294 156 294
        C131 294 111 311 111 334
        C111 361 131 384 156 384
        C186 384 211 361 211 334
        C211 300 186 274 156 274"
        style="fill:white;stroke:red;stroke-width:2"/>
    </FlySvg>
  </div>
</template>

<script>
import FlySvg from '../lib/main.js'
...

开始编写lib/index.vue - html部分

 <svg width="100%" height="100%" :
   viewBox="viewBox"
   class="fly-svg__wrapper"
   :style="{'stroke-dasharray': strokeDasharray, 'stroke-dashoffset': strokeDashoffset}">
    <slot></slot>
  </svg>

js部分

<script>
import {onMounted, ref, getCurrentInstance, computed } from 'vue'
import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame'
export default {
  name: 'FlySvg',
  props: {
    autoStart: {
      type: Boolean,
      default: true
    },
    loop: {
      type: Boolean,
      default: false
    },
    duration: {
      type: Number,
      default: 3000
    },
    useEasing: {
      type: Boolean,
      required: false,
      default: true
    },
    easingFn: {
      type: Function,
      default (currentTime, startValue, changeValue, duration) {
        currentTime /= duration;
        return changeValue * currentTime * currentTime + startValue
      }
    },
    viewWidth: {
      type: Number,
      required: false,
      default: 1024
    },
    viewHeight: {
      type: Number,
      required: false,
      default: 1024
    }
  },
  emits: ['down'],
  setup(props, context) {
    let path, rAF, pathLength, progress
    let startTime = 0
    const strokeDasharray = ref(0)
    const strokeDashoffset = ref(0)
    const render = (timestamp) => {
      if (!startTime) startTime = timestamp
      progress = timestamp - startTime
      let value
      if (props.useEasing) {
        value = pathLength - props.easingFn(progress, 0, pathLength ,props.duration)
        strokeDashoffset.value = value > 0 ? value : 0
      } else {
        value = (1 - (progress / props.duration)) * pathLength
        strokeDashoffset.value = value > 0 ? value : 0
      }
      if (progress < props.duration) {
        rAF = requestAnimationFrame(render)
      } else {
        context.emit('down')
        if (props.loop) {
          reset()
          rAF = requestAnimationFrame(render)
        }
      }
    }
    const reset = () => {
      startTime = 0
      cancelAnimationFrame(rAF)
      strokeDasharray.value = pathLength
      strokeDashoffset.value = pathLength
    }
    onMounted(() => {
      const instance = getCurrentInstance()
      path = instance.ctx.$el.firstElementChild
      pathLength = path.getTotalLength()
      strokeDasharray.value = pathLength
      strokeDashoffset.value = pathLength
      if (props.autoStart) {
        rAF = requestAnimationFrame(render)
      }
    })
    const viewBox = computed(() => {
      return `0 0 ${props.viewWidth} ${props.viewHeight}`
    })
     return {
      viewBox,
      strokeDasharray,
      strokeDashoffset
    }
  }
}
</script>

发布 && 使用组件

  1. 改造vite.config.js 把vite官网的库配置直接copy, 在external中添加vue,避免组件把vue源码都打包到组件的dist代码中。
const path = require('path')
import vue from '@vitejs/plugin-vue'
export default {
  plugins: [vue()],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'lib/main.js'),
      name: 'fly-svg'
    },
    rollupOptions: {
      // 请确保外部化那些你的库中不需要的依赖
      external: ['vue'],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
}
  1. npm run build
    打包生成dist文件
  2. npm login
    如果登录过就可以跳过这一步
  3. npm publish
    发布成功后可以在npm上搜索到fly-svg组件的包
  4. 发布后安装试用
    npm install fly-svg -S 尝试使用一下。
    网上随便找了个在线绘图工具画图svg.wxeditor.com/ 随手一画,灵魂之作即刻完成。

3.png 导出后把path粘贴进去,修改viewbox。 image.png

最终测试效果

最后看看效果吧,嗯嗯 感觉还可以的样子,嘿嘿,只需要放入path就自动实现动画啦,美滋滋。 final.gif

最后

本期采用vue3 + vite + svg 来实现了一个简易的svg动画组件,要注意的是写法上采用的是vue3,组件并不适合在vue2.x中使用,如果想在vue2中使用,需要手动改造噢,本期代码