Aitter's Blog

SVG线条动画应用

本节主要讲解如何实现以下几种动画

  1. 图表展示-圆环
  2. 操作成功,操作失败动画
  3. Logo 线条绘制动画
  4. Loading 加载动画

通过普通的div实现方式与svg的实现做对比,体验使用SVG做动画的好处,以及SVG在实际项目中应用

并使用Vue及React封装一个基于SVG的圆环图表展示组件

CSS3 版本的实现

效果来源 sweetAlert 插件
CSS3实现鸡蛋饼饼状图loading

使用多层遮罩+css动画

成功的动画

结构

<!-- 深色环 -->
<div class="ui-icon-success">
<!-- 对号短线 -->
<span class="ui-icon-success__line ui-icon-success__line-tip"></span>
<!-- 对号长线 -->
<span class="ui-icon-success__line ui-icon-success__line-long"></span>
<!-- 浅色环 -->
<span class="ui-icon-success__placeholder"></span>
<!-- 圆环的修复 -->
<span class="ui-icon-success__fix"></span>
</div>

圆环动画:半圆遮罩旋转,漏出下面深色圆环,旋转一周之后,回位,重新遮盖住,显示浅色圆环
对号动画,线段的位移

CSS

.ui-icon-success{
&.active{
&:after{
animation: rotatePlaceholder 4.25s ease-in;
}
.ui-icon-success__line-tip{
animation: animateSuccessTip 0.75s;
}
.ui-icon-success__line-long{
animation: animateSuccessLong 0.75s;
}
}
}
@keyframes rotatePlaceholder {
0% { transform: rotate(-45deg); }
5% { transform: rotate(-45deg); }
12% { transform: rotate(-405deg); }
100% { transform: rotate(-405deg); }
}
@keyframes animateSuccessLong {
0% { width: 0; right: 46px; top: 54px; }
65% { width: 0; right: 46px; top: 54px; }
84% { width: 55px; right: 0px; top: 35px; }
100% { width: 47px; right: 8px; top: 38px; }
}
@keyframes animateSuccessTip {
0% { width: 0; left: 1px; top: 19px; }
54% { width: 0; left: 1px; top: 19px; }
70% { width: 50px; left: -8px; top: 37px; }
84% { width: 17px; left: 21px; top: 48px; }
100% { width: 25px; left: 14px; top: 45px; }
}

loading动画

使用三个半圆 + 深色圆环 + 浅色圆环

结构

<div class="ui-icon-circle">
<span class="ui-icon-circle__placeholder"></span>
<span class="ui-icon-circle__half"></span>
</div>
  • 浅色圆环 ui-icon-circle
  • 右边半圆遮罩 ui-icon-circle:after 旋转180后隐藏
  • 左边圆遮罩 ui-icon-circle:before 旋转180
  • 右边半圆带深色环遮罩 ui-icon-circle __half 用于修复被遮罩的半环

css

.ui-icon-circle{
&.active{
&:after{
animation: circle-right-rotate 1s linear both;
}
&:before{
animation: circle-left-rotate .5s .5s linear both;
}
}
}
@keyframes circle-right-rotate {
0% {transform:rotateZ(0); opacity: 1;}
50% {transform:rotateZ(180deg); opacity: 1;}
51%,100% { transform:rotateZ(180deg); opacity: 0;}
}
@keyframes circle-left-rotate {
0% {transform:rotateZ(0);}
100% { transform:rotateZ(180deg);}
}

线条动画原理回顾

两个关键的属性

stroke-dasharray: 逗号或空格分隔的数值列表。表示各个虚线端的长度。可以是固定的长度值,也可以是百分比值
stroke-dashoffset: 表示虚线的起始偏移

两种实现方式

  • 使用stroke-dasharray, 改变 线段的长度
  • 使用stroke-dashoffset, 改变起始偏移的距离

SVG微交互的实际应用

loading、占位图、圆环进度、各种图表、H5活动

示例动画1
屏幕适配
页面转场

获取Path的总长度

el.getTotalLength() 只能获取Path的长度
其它形状的获取,可以从AI或sketch的属性面板中获取或通过公式计算得出

常用SVG标签参数解析

Circle 圆

<circle cx="100" cy="100" r="50" fill="#fff"></circle>
  • r 半径
  • cx 圆心x位置, 默认为 0
  • cy 圆心y位置, 默认为 0

Path 路径

这里只列举用到的圆弧的表示方法

两点之间的弧形可能有四种情总,两种小弧形,两种大弧形

a 45 45, 0, 0, 0, 125 125
参数说明:起点坐标、large-arc-flag(大小弧标记,0小,1大)、sweep-flag(弧线方向,0逆时针,1顺时针)、终点

<svg width="325px" height="325px" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M80 80
A 45 45, 0, 0, 0, 125 125
L 125 80 Z" fill="green"/>
<path d="M230 80
A 45 45, 0, 1, 0, 275 125
L 275 80 Z" fill="red"/>
<path d="M80 230
A 45 45, 0, 0, 1, 125 275
L 125 230 Z" fill="purple"/>
<path d="M230 230
A 45 45, 0, 1, 1, 275 275
L 275 230 Z" fill="blue"/>
</svg>

更多关于Path的参数说明,请查看 SVG PATH MDN

Line 直线

<line x1="10" x2="50" y1="110" y2="150"/>
  • x1 起点的x位置
  • y1 起点的y位置
  • x2 终点的x位置
  • y2 终点的y位置

polyline 折线

<polyline points="60 110, 65 120, 70 115, 75 130, 80 125, 85 140, 90 135, 95 150, 100 145"/>

points 点集数列,每个数字用空白、逗号、终止命令符或者换行符分隔开,每个点必须包含2个数字,一个是x坐标,一个是y坐标 如0 0, 1 1, 2 2”

SVG成功动画

结构

一个背景环,一个动画环,一条折线

<g id="Group-3" transform="translate(2.000000, 2.000000)">
<circle id="Oval-2" stroke="rgba(165, 220, 134, 0.2)" stroke-width="4" cx="41.5" cy="41.5" r="41.5"></circle>
<circle class="ui-success-circle" id="Oval-2" stroke="#A5DC86" stroke-width="4" cx="41.5" cy="41.5" r="41.5"></circle>
<polyline class="ui-success-path" id="Path-2" stroke="#A5DC86" stroke-width="4" points="19 38.8036813 31.1020744 54.8046875 63.299221 28"></polyline>
</g>

动画

先发生环的填充和收缩动画,再延时执行对号的成功动画

@keyframes ani-success-circle {
to{stroke-dashoffset: 782.2565707438586px;}
}
@keyframes ani-success-path {
0% {stroke-dashoffset: 62px;}
65% {stroke-dashoffset: -5px;}
84%{stroke-dashoffset: 4px;}
100%{stroke-dashoffset: -2px;}
}

SVG失败动画

结构

背景环、动画环、X号的两条线

<g id="Group-2" transform="translate(2.000000, 2.000000)">
<circle id="Oval-2" stroke="rgba(252, 191, 191, .5)" stroke-width="4" cx="41.5" cy="41.5" r="41.5"></circle>
<circle class="ui-error-circle" stroke="#F74444" stroke-width="4" cx="41.5" cy="41.5" r="41.5"></circle>
<path class="ui-error-line1" d="M22.244224,22 L60.4279902,60.1837662" id="Line" stroke="#F74444" stroke-width="3" stroke-linecap="square"></path>
<path class="ui-error-line2" d="M60.755776,21 L23.244224,59.8443492" id="Line" stroke="#F74444" stroke-width="3" stroke-linecap="square"></path>
</g>

动画

先环进行动画,然后 X号的右边线开始描边动画,然后左边线开始描边动画

@keyframes ani-error-line{
to { stroke-dashoffset: 0; }
}
@keyframes ani-error-circle {
0% {
stroke-dasharray: 0, 260.75219024795285px;
stroke-dashoffset: 0;
}
35% {
stroke-dasharray: 120px, 120px;
stroke-dashoffset: -120px;
}
70% {
stroke-dasharray: 0, 260.75219024795285px;
stroke-dashoffset: -260.75219024795285px;
}
100% {
stroke-dasharray: 260.75219024795285px, 0;
stroke-dashoffset: -260.75219024795285px;
}
}

SVG加载动画

结构

一个圆环

<div class="ui-loading">
<div class="ui-loading-inner" :style="innerStyle">
<svg viewBox="0 0 100 100">
<circle class="ui-loading-path" cx="50" cy="50" r="40" fill="none" :style="strokeStyle"></circle>
</svg>
</div>
<span class="ui-loading-text"><slot><slot></span>
</div>

动画

不停的旋转,并增长线段的长度

@keyframes ui-loading-spin {
0%{transform:rotate(0); stroke-dasharray:0, 251.327px;}
100%{transform:rotate(360deg); stroke-dasharray:251.327px, 251.327px;}
}

SVG Logo动画

原理: 多个文字路径叠加,不同的描边长度,同时动画

结构

<svg version="1.1" viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
<symbol id="text">
<text x="0" y="35%" class="text">AITTER</text>
</symbol>
<g>
<use xlink:href="#text" class="use-text"></use>
<use xlink:href="#text" class="use-text"></use>
<use xlink:href="#text" class="use-text"></use>
<use xlink:href="#text" class="use-text"></use>
<use xlink:href="#text" class="use-text"></use>
</g>
</svg>

动画

.use-text:nth-child(1) {
stroke: #360745;
animation: animation1 8s infinite ease-in-out forwards;
}
.use-text:nth-child(2) {
stroke: #D61C59;
animation: animation2 8s infinite ease-in-out forwards;
}
.use-text:nth-child(3) {
stroke: #E7D84B;
animation: animation3 8s infinite ease-in-out forwards;
}
.use-text:nth-child(4) {
stroke: #EFEAC5;
animation: animation4 8s infinite ease-in-out forwards;
}
.use-text:nth-child(5) {
stroke: #1B8798;
animation: animation5 8s infinite ease-in-out forwards;
}
@keyframes animation1 {
50%,70%{ stroke-dasharray: 7% 28%; stroke-dashoffset: 7%; }
}
@keyframes animation2 {
50%,70%{ stroke-dasharray: 7% 28%; stroke-dashoffset: 14%; }
}
@keyframes animation3 {
50%,70%{ stroke-dasharray: 7% 28%; stroke-dashoffset: 21%; }
}
@keyframes animation4 {
50%, 70%{ stroke-dasharray: 7% 28%; stroke-dashoffset: 28%; }
}
@keyframes animation5 {
50%,70%{ stroke-dasharray: 7% 28%; stroke-dashoffset: 35%; }
}

封装成组件

将圆环进度封装成Vue及React组件

React组件 - Circle

查看Demo

使用方法

React.createClass({
getInitialState(){
return {
percent1: 20,
}
},
render(){
const precent1= this.state.percent1
return (
<Circle
percent={ percent1 }
strokeWidth={8}
trailWidth={8}
strokeColor="#34C0E3">
{ percent1 }
</Circle>
)
}

配置参数

{
strokeWidth: 1, // 自定义描边宽度,这个宽度是相对于svg容器的
strokeColor: '#37c7fa', //自定义描边颜色
trailWidth: 1, // 自定义背景宽度
trailColor: '#d9d9d9', // 自定义背景颜色
percent: 0, //当前的进度
strokeLinecap: 'round', //端点的样式
}

实现细节

  • 使用 Math.PI * 2 * r 来计算出圆的周长
  • 使用 Path 来绘制两段弧形组成圆形
  • 使用 stroke-dashoffset 来实现描边动画
  • 组件的大小由来层包装的容器决定
var Circle = React.createClass({
getInitialState(){
const radius = 50 - this.props.strokeWidth / 2;
return {
radius: radius, // 半径
len: Math.PI * 2 * radius, // 周长
pathString: `M 50,50 m 0,-${radius}
a ${radius},${radius} 0 1 1 0,${2 * radius}
a ${radius},${radius} 0 1 1 0,-${2 * radius}`,
}
},
render(){
let { pathString, len, radius } = this.state;
let {
strokeWidth, strokeColor, trailWidth, trailColor, percent,
strokeLinecap, className, children, ...others
} = this.props;
const pathStyle = {
'strokeDasharray': `${len}px ${len}px`,
'strokeDashoffset': `${((100 - percent) / 100 * len)}px`,
'transition': 'stroke-dashoffset 0.6s ease 0s, stroke 0.6s ease'
}
const cls = classNames('mt-circle', {
[className]: className
})
return(
<div className={ cls }>
<svg viewBox="0 0 100 100">
<path
d={ pathString }
stroke={ trailColor }
strokeWidth={ trailWidth }
fillOpacity="0"/>
<path
d={ pathString }
strokeLinecap={ strokeLinecap }
stroke={ strokeColor }
strokeWidth={ strokeWidth }
fillOpacity="0"
style={ pathStyle }/>
</svg>
<div className="mt-circle-content">{ children }</div>
</div>
)
}
})

Vue组件 - ui-progress-circle

基本原理与设置同上

使用方法

<progress-circle
:percent="percent"
:strokeColor="strokeColor"
:strokeWidth="strokeWidth"
:size="size"
>
new Vue({
el: '#app',
template: '#main-page',
data: {
percent:10,
strokeColor: '#108EE9',
strokeWidth: 5,
size: 140,
pathLen: 0,
}
}

配置参数

'size': {
type: Number,
default: 100,
},
'percent':{
type:Number,
default: 0
},
'strokeWidth':{
type:Number,
default: 8
},
'strokeColor':{
type:String,
default: '#108EE9'
},
'trailColor':{
type:String,
default: '#F7F7F7'
},
'trailWidth':{
type:Number,
default: 8
}

实现细节

<div class="progress-circle">
<div class="progress-circle-inner" :style="innerStyle">
<svg viewBox="0 0 100 100">
<circle class="progress-circle-trail" cx="50" cy="50" r="40" fill="none" :style="trailStyle"></circle>
<circle class="progress-circle-path" cx="50" cy="50" r="40" fill="none" :style="strokeStyle"></circle>
</svg>
</div>
<span class="progress-circle-text"><slot><slot></span>
</div>
Vue.component('progress-circle', {
data:function(){
return {
len: Math.PI*2*40
}
},
props: {
'size': {
type: Number,
default: 100,
},
'percent':{
type:Number,
default: 0
},
'strokeWidth':{
type:Number,
default: 8
},
'strokeColor':{
type:String,
default: '#108EE9'
},
'trailColor':{
type:String,
default: '#F7F7F7'
},
'trailWidth':{
type:Number,
default: 8
}
},
template: '#progress-circle',
computed: {
innerStyle: function(){
return { width:`${this.size}px`, height: `${this.size}px`}
},
strokeStyle: function(){
const currPercent = this.percent/100 * this.len;
return {
'stroke':this.strokeColor,
'stroke-dasharray':`${currPercent}px, ${this.len}px`,
'stroke-width': `${this.strokeWidth}px`,
}
},
trailStyle: function(){
return {
'stroke':this.trailColor,
'stroke-dasharray':`${this.len}px, ${this.len}px`,
'stroke-width': `${this.trailWidth}px`,
}
},
}
})