本文作者:ivweb villainthr
TL;DR
本文主要是讲解关于 SVG 的一些高级动画特效,比如 SVG 动画标签,图形渐变,路径动画,线条动画,SVG 裁剪等。
例如:路径动画
图形渐变:
线条动画:
以及,相关的动画的矩阵知识,这个也是现在 CSS 动画里面最重要,同时也是最为欠缺的知识点:
文章会先从基本语法入手,然后,慢慢深入。介绍一些动画基本原理和对应的数学原理知识点。并且文章后面,还附有相关语法的介绍,当你在遇到不熟悉语法的时候可以参考参考。
前面一篇文章,主要介绍了一些 SVG 的基本概念和基本图形。接下来我们需要了解一下,SVG 处理矢量这个特性之外,还有啥内容吸引我们,能让 SVG 现在普及度这么高?
完整版可以关于我的公众号:前端小吉米。
分享吉米的前端路,后面也会定期推出当前热门的前端技术~ 比如,直播,VR
SVG Animation
在 SVG 中,如果我们想实现一个动画效果,可以使用 CSS,JS,或者直接使用 SVG 中自带的 animate
元素添加动画。
使用 CSS 的话,有两种选择一种是通过style
直接内联在里面,另外是直接使用相关的动画属性-- transform
。
<use id="star" class="starStyle" xlink:href="#starDef"
transform="translate(100, 100)"
style="fill: #008000; stroke: #008000"/>
而使用 SVG 中自定的 animate 主要还是 SVG 自己的东西,比较好用。如果想用 CSS 的动画,这都无所谓。
先看一个 SVG animate DEMO:
代码语言:javascript复制 <rect x="10" y="10" width="200" height="20" stroke="black" fill="none">
<animate
attributeName="width"
attributeType="XML"
from="200" to="20"
begin="0s" dur="5s"
fill="freeze" />
</rect>
通过将 animate 标签嵌套在指定的图形里面,即可实现变换的效果。另外,还有 animateTransform,它主要是用来做变形动画的。
代码语言:javascript复制 <rect x="-10" y="-10" width="20" height="20"
style="fill: #ff9; stroke: black;">
<animateTransform attributeType="XML"
attributeName="transform" type="scale"
from="1" to="4 2"
begin="0s" dur="4s" fill="freeze"/>
</rect>
简单来说:
- animate: 相当于 CSS 中的 transition
- animateTransform: 相当于 CSS 中的 transform
里面一些技术细节我们这里就不过多讲解了。这里,主要想介绍一下 animate 中的 morph 的效果。
animate morph
该效果主要做的就是图形内部的渐变。如图:
这种动画是怎么实现呢?
直接看代码吧:
代码语言:javascript复制 <path fill="#1EB287">
<animate
attributeName="d"
dur="1440ms"
repeatCount="indefinite"
keyTimes="0;
.0625;
.208333333;
.3125;
.395833333;
.645833333;
.833333333;
1"
calcMode="spline"
keySplines="0,0,1,1;
.42,0,.58,1;
.42,0,1,1;
0,0,.58,1;
.42,0,.58,1;
.42,0,.58,1;
.42,0,.58,1"
values="M 0,0
C 50,0 50,0 100,0
100,50 100,50 100,100
50,100 50,100 0,100
0,50 0,50 0,0
Z;
M 0,0
C 50,0 50,0 100,0
100,50 100,50 100,100
50,100 50,100 0,100
0,50 0,50 0,0
Z;
M 50,0
C 75,25 75,25 100,50
75,75 75,75 50,100
25,75 25,75 0,50
25,25 25,25 50,0
Z;
M 25,50
C 37.5,25 37.5,25 50,0
75,50 75,50 100,100
50,100 50,100 0,100
12.5,75 12.5,75 25,50
Z;
M 25,50
C 37.5,25 37.5,25 50,0
75,50 75,50 100,100
50,100 50,100 0,100
12.5,75 12.5,75 25,50
Z;
M 50,0
C 77.6,0 100,22.4 100,50
100,77.6 77.6,100 50,100
22.4,100, 0,77.6, 0,50
0,22.4, 22.4,0, 50,0
Z;
M 50,0
C 77.6,0 100,22.4 100,50
100,77.6 77.6,100 50,100
22.4,100, 0,77.6, 0,50
0,22.4, 22.4,0, 50,0
Z;
M 100,0
C 100,50 100,50 100,100
50,100 50,100 0,100
0,50 0,50 0,0
50,0 50,0 100,0
Z;"/>
</path>
这么多,是不是感觉有点懵逼。不过,我们细分来看一下其实很简单。里面主要是利用animate
中的 keyTimes
,calcMode
,keySplines
,以及 values
这几个属性。不急,我们一个一个来解释一下。
- keyTimes: 这其实和 CSS 中定义的
@keyframes
一样。通过 0-1 之间的值,定义每段动画完成的时间。格式为:value;value...
。例如0;.0625;.208333333;.3125;.395833333;.645833333;.833333333;1
。从第一个动画,到第二个动画经历的时间比例为 6.25%。并且,keyTimes 需要和 values 里面定义的帧数一致。 - calcMode: 用来定义动画具体的插值模型。取值有:
discrete | linear[default] | paced | spline
。具体可以参考 MDN。这里我们主要介绍一下 spline。该值表示每个动画间使用自定的贝塞尔变换曲线。如果没有特殊要求,使用 linear 其实已经足够了,这样就不用麻烦去定义下面的keySplines
属性。 - keySplines:该值用来具体定义动画执行时的 贝塞尔曲线。使用格式是通过
;
来分隔每一个值。即,cubic-bezier(.31,.57,.93,.46)
为一组。使用 keySplines 表达,则为:keySplines = ".31,.57,.93,.46;"
。当然,里面的贝塞尔曲线组数为整个动画帧数 - 1
。
而 values 就很简单了。它是直接结合attributeName
属性,来设置具体的值,每个值之间使用;
进行分隔。
像上面那样,可以在指定元素里面嵌套多个 animate,既实现了形状的改变,又实现了颜色的改变。Morph 比较常用于数字的更迭,比如,倒数 10s 的相关动画。到这里,Morpah 相关的知识点就结束了。
接着,让我们来看一下 SVG 中,另外一非常重要的标签 -- animateMotion
。
该标签可以让指定的元素,绕着指定的路径进行运动。所以这对于复杂的路径来说非常有用,因为我们很难使用 transform 去模拟复杂的变换路径。看一个 DEMO
animateMotion
animateMotion 大致的属性和 animate
差不多,不过,它还拥有自己特有的属性,比如 keyPoints
、rotate
、path
等。不过,calcMode 在 AM(animateMotion) 中的默认属性由,linear
变为 paced
。
这些属性,我们慢慢介绍,先从最简单的开始吧。首先,我们来看一个 DEMO:
代码语言:javascript复制<g>
<rect x="0" y="0" width="30" height="30" style="fill: #ccc;"/>
<circle cx="30" cy="30" r="15" style="fill: #cfc; stroke: green;"/>
<animateMotion from="0,0" to="60,30" dur="4s" fill="freeze"/>
</g>
- from,to:指定两点的位置,位置参数是以元素的坐标为原点的。
- dur:执行渲染时间
- fill:指定动画结束后停留的装填。有
freeze
和remove
效果。remove 表示回到动画开始的位置,freeze 表示停留在动画结束的位置。
如果,你想要更复杂的路径,可以直接使用path
属性来指定路径。用法和 path
标签中d
属性是一样的。
<rect x="0" y="0" width="30" height="30" style="fill: #ccc;">
<animateMotion
path="M50,125 C 100,25 150,225, 200, 125"
dur="6s" fill="freeze"/>
</rect>
或者使用mpath
标签,引用外部的 path
。
<path d="M10,110 A120,120 -45 0,1 110 10 A120,120 -45 0,1 10,110"
stroke="lightgrey" stroke-width="2"
fill="none" id="theMotionPath"/>
<circle cx="10" cy="110" r="3" fill="lightgrey" />
<circle cx="110" cy="10" r="3" fill="lightgrey" />
<!-- Red circle which will be moved along the motion path. -->
<circle cx="" cy="" r="5" fill="red">
<!-- Define the motion path animation -->
<animateMotion dur="6s" repeatCount="indefinite">
<mpath xlink:href="#theMotionPath"/>
</animateMotion>
</circle>
动画效果为:
所以,一般而言我们在定义 AM 的路径的时候,只用一种方式定义即可,否则会发生相应的覆盖:mpath
>path
>values
>from/to
。
在 AM 运动中,还有一个很重要的概念就是旋转角。默认情况下,运动物体的角度是按照它和坐标轴的初始角度确定的。例如:
这样看起来确实有些别扭,那能不能让物体垂直于路径进行运动呢?
有的,根据rotate
属性值,一共有 3 个值可供选择。
- auto:让物体垂直于路径的切线方向运动。不过,如果你的路径是闭合曲线的话,需要注意起始点的位置。
例如:
- auto-reverse:让物体垂直于路径的切线方向并 180°。也就是和 auto 运动关于切线对称。
- Number:让物体以固定的旋转角度运动。这个就相当于使用 transform:rotate(deg) 进行控制。
在动画设置标签中,还有一个更简单的--set
。
set
该标签也是用来模拟transition
效果的。它和 animate
的主要区别是,它仅仅需要 to 的指定属性,而不需要其他的参考属性,比如from
,by
等。那它有啥特别的存在意义吗?
有的,因为 set 针对于所有属性,甚至包括 style 里面的相关 CSS 属性。所以,可以靠它来很好描述一些非 number
的属性值。
<text text-anchor="middle" x="60" y="60" style="visibility: hidden;">
<set attributeName="visibility" attributeType="CSS"
to="visible" begin="4.5s" dur="1s" fill="freeze"/>
All gone!
</text>
矩阵动画
上面差不多简单阐述了关于 SVG 一些比较有特点的动画。当然,还有比较重要的线条动画,这个我们放到后面进行讲解。这里先来看一下所有动画中,非常重要的矩阵原理。线性代数应该是大学里面来说,最容易学的一门科目,MD。。。还记得,大学线代期末考试的时候,100 分的同学应该说是如韭菜地般,一抓一大片(对不起,我没能和他们同流合污。)
那矩阵是如何在动画中使用的呢?
简单的说,矩阵中的每个元素其实可以等价代换为每个因式里面的系数:
上面也叫作 三维矩阵。即,它涉及到 x,y,z 轴的计算。那对于我们平面 2D 变换来说,那么此时矩阵又是哪种形式呢?
很简单,只要将 z 轴永远置为一个常数就 OK。这里,惯例上是直接取 0 0 1 来设置。
不信的话,大家只要代进去乘以乘,应该就可以得到结果了。所以,在二维中,具体变换方式为:
后面,我们也会依据这个公式进行相关的变形操作。那矩阵变换是怎么运用到 CSS/SVG 当中呢?
在 CSS 中,是直接使用 transform
中的属性:
transform: matrix(a,b,c,d,e,f);
当然,在 SVG 中也是一样的:
代码语言:javascript复制<g transform="matrix(1,2,3,4,5,6)">
<line x1="10" y1="20" x2="30" y2="40" style="stroke-width: 10px; stroke: blue;"/>
</g>
所以,我们主要的重点就是讲解一下matrix
这个属性。它的格式为:
matrix(a,b,c,d,e,f);
对应于我们上面的公式有:
在接触transform
的时候,大家应该了解到 transform
里面有很多固定的动画属性:
- translate()
- rotate()
- scale()
- skew()
实际上,在底层还是使用 matrix 实现的变换。就拿 translate 举个例子吧。
translate 的格式为:
translate(dx,dy)
相当于参考当前原点,在 x/y 轴上移动 dx/dy 的距离。那么映射到矩阵,应该如何表示呢?
很简单,它等同于:
matrix(1 0 0 1 dx dy);
使用代数证明一下:
假设有matrix(1 0 0 1 20 30)
变为矩阵为:
根据,上面的表达式有:
代码语言:javascript复制 X = x'*1 y'*0 20 = x' 20
Y = x'*0 y'*0 30 = y' 30
所以,就是 X 在原有 X 轴坐标上向右移动 20 的距离,Y 相对于原有移动 30 的距离。
那么其他几个属性呢?也是怎么变化的吗?
恩,类似。只是里面取值不一样:
- scale(x,y): 放大 X/Y 轴,矩阵的表达为 matrix(x 0 0 y 0 0)。
- rotate(θ): 坐标旋转,矩阵的表达为 matrix(cosθ sinθ -sinθ cosθ 0 0)。
- skew(θx,θy): X/Y 轴拉伸,矩阵的表达为 matrix(1 tanθx tanθy 1 0 0)。
注意,上面三个都会改变原有物体的坐标系!!! 这点很重要,换句话说,后面每次变换都是基于前面一个的变换结果的。
详情看图:
详情可以参考:MDN matrix
不过,这并不是我们使用 matrix 的重点,也不是它的优势。它的优势在于可计算,即,能够将复杂的动画集合到一个表达式中,并且,后续的变换可以直接基于当前的 matrix。
我们先来了解一下,如果多个变换动画一起使用,matrix 应该如何表达呢?
只需要找到我们变换动画对应的矩阵,然后相乘即可。例如,先旋转 45°,然后放大 1.5 倍,则有变换动画为:
transform: rotate(45deg) scale(1.5,1.5);
注意,虽然,你定义动画是分开的,但此时的动画是同时进行的。为啥?因为,这两个动画实际上可以整合成为一个变换矩阵:
并且,位置是不可以调换的。比如,transform: scale(2,2) translate(20px,30px)
。即,你先放大两倍,然后移动 20,30 的距离。注意,这里移动的 20,30 相对的是已经放大过后的坐标,相对于原坐标而言就是 40,60 了。 如果,你调换位置,即transform: translate(20px,30px) scale(2,2)
。就变成现在原坐标移动 20,30,然后再放大两倍。
而上面强调的顺序关系,实际上就可以理解为矩阵不满足交换律的原则。因为一旦交换,结果很可能不一样。
矩阵高级用法
上面的内容只是简单的描述了关于矩阵的概念。在实际中,矩阵可以说是真正利器。
假设现在有一个动画,要求你将一个物体从一个点通过抛物线的方式移动到另外一个点,那么此时要求 JS/CSS 随你挑。此时,你会不会感觉,呼吸急促,头脑发热呢?