本
文
摘
要
想做一个svg的动画,但是却卡在了坐标变换,查了好久的资料,终于找到一篇讲得清楚的教程。
原文地址: https://css-tricks.com/transforms-on-svg-elements/下面文章是基于这篇教程,最后的案例也是来自于它。
前言
和普通的HTML元素一样,SVG元素可以通过transform进行形状变换,但其中的很多细节,却和普通的HTML元素变换差异较大。
SVG元素,原文中SVG elements,指的是<svg>包含的各种形状元素,<rect>、<circle>、<line>、<polygon>、<path>等,为了方便,此处以及下文都将其翻译“SVG元素”,而文中的“<svg>元素”特指<svg>标签代表的元素。太绕了!!大家懂我的意思就好--对于SVG元素,transform 有两种用法,一个是在 SVG元素标签 里写的 transform 属性、另一种是在 CSS 样式里写的 transform。
//SVG元素标签里写的 transform 属性 <rect transform="rotate(45) scale(1.2)" width="100" height="100" fill="yellow" /> //CSS样式里写的 transform rect{ transform: rotate(20deg) scale(1.2); }两者的不同之处,首先,CSS样式里写的 transform 在版本较老的IE里面不支持,如果要兼容这部分浏览器,可以选择 SVG元素标签里写的 transform 属性;其次,如果是SVG元素标签里写的 transform 属性,变换函数的参数都只能是数字,不能带单位。而rotate和skew中角度的参数默认以deg为单位。
普通的HTML元素和SVG元素在Transform形状变换方面最主要的区别,在于本地坐标系统的不同。 本地坐标系统的原点决定了变换的参考点。
对于普通的HTML元素,本地坐标系统的原点,默认值是元素自身在x、y方向的中心位置50% 50%(这里仅考虑二维平面),也就是元素的旋转、移位、缩放等操作都是以元素自身在x、y方向的中心位置进行的。
而SVG元素,本地坐标系统的原点,在该SVG元素没有进行任何形状变换的情况下,是在SVG画布的0 0的位置(默认是<svg>元素的左上角)。
transform变换是怎么发生的?
translate--平移
平移,指的是该元素上所有的点相对于变换参考点沿着相同的方向,移动相同的距离。
上图是元素作transform: translate(295px,115px)平移变换,左边是普通的HTML元素,右边是SVG元素。
虽然变换参考点不同,但结果一样。实际上,平移的结果跟XY轴的方向、单位长度有关。
普通的HTML元素和SVG元素的坐标系统都是:向右为X正方向,向下为Y正方向。translate(295px,115px)要执行的操作是:向右移295px,向下移115px。变换参考点是平面上的任何一点,所得到的结果都一样。但假如坐标系统向右为X正方向,向上为Y正方向,那么translate(295px,115px)就应该:向右移295px,向上移115px。单位长度的影响会在后面讨论。
我们前面提到,对于SVG元素,transform 可以写在 CSS 样式里,也可以写在SVG元素标签的属性上。
//CSS样式支持的语法 //tx,ty分别是X、Y方向的平移量,带单位,如transform: translate(20px) transform: translateX(tx) transform: translateY(ty) transform: translate(tx[, ty]) //只写一个值时,默认是X方向的平移量,Y方向默认是0 //如果只在Y方向偏移,要写成:transform: translate(0, 20px) //SVG元素属性支持的语法 <rect ... transform="translate(tx[ ty])" /> <rect ... transform="translate(tx[, ty])" /> /*** 与css里的写法区别: 1、不支持translateX、translateY 2、tx、ty只能是数字,不能带单位 3、tx、ty之间可以是空格或者逗号 transform="translate(10,20)" 或者 transform="translate(10 20)" ***/rotate--旋转
一个图形在平面上的旋转有两个决定因素:参考点和旋转角度(角度的正负决定旋转方向)。
参考点的理解可以类比这些图钉:每一张卡片可以围绕固定它的图钉旋转,图钉就是卡片的旋转变换参考点。
这种情况下参考点是图形中的某一点,也有可能是图形之外的某一点。比如下面这种情况:
一个图形可以选择平面中任意一点作为旋转变换参考点。而如果两次旋转,初始位置相同,旋转角度相同,但参考点不同,最终位置就会不同。
上图是普通的HTML元素(左)和SVG元素(右)作transform:rotate(45deg)的结果。
如上所示,普通的HTML元素的变换参考点是元素自身在x、y方向的中心位置,而SVG元素的参考点是svg画布的原点。
//CSS样式支持的语法 transform: rotate(angle) //angle是旋转的角度,带单位(deg、rad、turn、grad)。 //angle的值也可以是由calc()计算而来,比如transform: rotate(calc(.25turn - 30deg)) //angle的值若为正,则是顺时针旋转,若为负,则是逆时针旋转。 //元素transform属性支持的语法 <rect ... transform="rotate(angle[ x y])" /> /*** 与css里的写法区别: 1、rotate的参数不只是angle,多了一对可选参数x,y 2、angle,x,y只能是数字,不能带单位,angle是以deg来衡量。 3、angle,x,y之间可以是空格或者逗号 ***/SVG元素transform属性多出来的可选参数x,y,用于指定本次旋转的参考点。x,y必须成对出现,如果只写一个,则是无效语法。
既然能够指定旋转的参考点,那么SVG元素围绕自身中心进行旋转该怎么设置呢?
<svg width="200" height="200"> <rect x="0" y="0" width="100" height="100" fill="yellow"/> </svg> <svg width="200" height="200"> <!--未指定本次旋转的参考点,绕着左上角旋转--> <rect x="0" y="0" width="100" height="100" fill="yellow" transform="rotate(45)"/> </svg> <svg width="200" height="200"> <!--指定本次旋转的参考点为rect的中心坐标(50,50),绕着这一点旋转--> <rect x="0" y="0" width="100" height="100" fill="yellow" transform="rotate(45,50,50)" /> </svg>我们终于GET了第一种指定变换参考点的方法,可惜它是一次性的,它只能指定本次旋转的参考点。
当我们连续进行两次旋转,一次顺时针,一次逆时针,角度相同。
<svg width="200" height="200"> <rect x="0" y="0" width="100" height="100" fill="yellow" transform="rotate(45) rotate(-45)"/> </svg> <svg width="200" height="200"> <rect x="0" y="0" width="100" height="100" fill="yellow" transform="rotate(45,50,50) rotate(-45)" /> </svg>可以看到第三个矩形,并未回归原来位置,因为第一次旋转的参考点为rect的中心坐标,第二次未指定,旋转参考点又变成了默认的左上角。
如果想每一次的旋转都围绕元素中心点,又不想每一次都指定(x,y),可以借助transform-origin。
rect{ transform-origin: 50px 50px; //矩形的中心坐标 } <svg width="200" height="200"> <rect x="0" y="0" width="100" height="100" fill="yellow"/> </svg> <svg width="200" height="200"> <rect x="0" y="0" width="100" height="100" fill="yellow" transform="rotate(45)"/> </svg>transform-origin的取值有很多,参考 MDN,但是对于SVG元素,transform-origin: right top 、transform-origin: 50% 50%这些写法都是针对<svg>元素(上面的蓝色边框区域),比如transform-origin: 50% 50%,其实是蓝色边框区域的中心点。而要定位到黄色矩形的中心点,需要写transform-origin: 50px 50px这种绝对数值。
scale -- 缩放
缩放,改变的是,图形上所有的点,与变换参考点之间的距离。
推导过程(以上图右边为例):选取图形上任意一点B,与变换参考点A连接成一条线AB(用向量AB描述更准确),向量AB在XY轴的分量分别为(x,y)。如果图形作缩放变换transform: scale(sx, xy),向量AB在XY轴的分量就会变为(sx.x,sy.y),这时原来的B点,就移到了新的newB点。缩放变换就是图形上所有的点都作上述移动的结果。
上述transform: scale(sx, sy),我们假设了sx>1,sy>1。如果sx,sy中谁小于1(准确来说是绝对值小于1),就会导致图形在那个方向压缩,而如果sx,sy中谁小于0,就会导致图形相对变换参考点在那个方向进行反转。推导过程和上面一样,大家可以推导一遍加深理解。
由缩放的原理可以知道,缩放的结果是受变换参考点影响的。上图是普通的HTML元素(左)和SVG元素(右)作transform: scale(sx, sy)的结果。
//CSS样式支持的语法 transform: scaleX(sx) transform: scaleY(sy) transform: scale(sx[, sy]) //sx,sy表示缩放的倍数,不带单位 //scaleX(sx)、scaleY(sy)分别指定X、Y方向的放缩,scaleX(sx)等同于scale(sx,1),scaleY(sy)等同于scale(1,sy) //scale(sx[, sy])第二个参数可选,如果省略,表示sy和sx相同,等比例缩放。 //SVG元素属性支持的语法 <rect ... transform="scale(sx[ sy])" /> /*** 与css里的写法区别: 1、不支持scaleX、scaleY 2、tx、ty之间可以是空格或者逗号 ***/对于SVG元素,如果要以元素自身中心点来缩放,我们可以像上面rotate变换那样在css样式中指定transform-origin。
而另一种更通用灵活的方法就是链式变换。
链式变换
我们知道在变换中很重要的因素就是本地坐标系统,它的XY轴方向、原点位置、单位长度,直接影响着最终的结果。
那么一个元素的本地坐标系统是否是一直不变的呢(具体来说就是XY轴方向、原点和单位长度)?
实际上,translate、scale、rotate都会改变该元素的本地坐标系统:translate会改变本地坐标系统的原点位置,rotate会改变本地坐标系统的XY轴方向,scale会改变本地坐标系统XY轴方向的单位长度。
而我们要记住的是:每次变换参考点永远是本地坐标系统的原点(在没有通过transform-origin指定的情况下),涉及到(x,y)坐标值的变化永远是在XY轴方向的度量(而非水平或垂直方向),距离的计算是基于单位长度的计算。按照这个规则,我们来分析一下下面的链式变换。
<rect x=65 y=65 width=150 height=80 transform=translate(140 105) scale(2 1.5) translate(-140 -105)/>根据矩形的x、y、width、height确定矩形的中心点(140,105)。translate(140 105)将矩形向右移动140个单位长度Ux,向下移动105个单位长度Uy(最初Ux=Uy=1),同时变换参考点也作了同样的移动,变成了(140, 105)。相对新的变换参考点,scale(2 1.5)将矩形在X、Y轴分别放大2倍和1.5倍,同时X、Y轴的单位长度也分别放大2倍和1.5倍,此时 Ux=2,Uy=1.5translate(-140 -105)将矩形向左移动140个单位长度,向上移动105个单位长度,此时单位长度已经变化,所以移动的距离其实是(140 * 2,105 * 1.5)。这也为什么第一个translate(140 105)和第二个translate(-140 -105)移动距离不相等的原因。translate、scale、rotate都会改变该元素的本地坐标系统,这一点对于普通的HTML元素和SVG元素都是一样,毕竟,他们的区别只在于本地坐标系统原点的不同,变换的原理是相同的。
以普通的HTML元素为例:
div { transform-origin: right bottom; transform: rotate(90deg) translate(0, -100%) rotate(90deg) translate(0, 100%); }最初的变换参考点是元素的右下角rotate(90deg)元素顺时针旋转90deg,同时本地坐标系统也顺时针旋转90deg,X轴正方向向下,Y轴正方向向左。translate(0, -100%)向Y轴负方向移动矩形宽度的距离,也就是向右移动,同时变换参考点也作相同的移动。rotate(90deg)元素再次顺时针旋转90deg,同时本地坐标系统也再次顺时针旋转90deg,X轴正方向向左,Y轴正方向向上。translate(0, 100%)向Y轴正方向移动矩形宽度的距离,也就是向上移动。SVG元素以自身中心点进行变换的解决方案
到目前位置,我们知道的SVG元素以自身中心点进行变换的解决方案有:
1、transform="rotate(angle[ x y])"指定本次旋转的参考点。
<rect x="0" y="0" width="100" height="100" transform="rotate(45,50,50)" />2、通过transform-origin指定变换参考点。
rect{ transform-origin: 50px 50px; //矩形的中心坐标 }3、链式变换
<rect x="0" y="0" width="100" height="100" transform="translate(50 50) rotate(45) translate(-50 -50)"/>还有一种更为简洁的方式,就是合理设置svg标签的viewBox属性。
我们知道,SVG元素本地坐标系统的原点,也就是变换参考点,在该SVG元素没有进行任何形状变换的情况下,是在SVG画布的0 0的位置,默认是<svg>元素的左上角。
我们可以把svg的绘制和显示分成两个部分:画布和可见区域。
画布可以看成是一个大大的二维平面(甚至可以是无限大的),以元素的左上角为原点,在这个平面上的图形可以将坐标定位在平面的任意位置。
而可见区域,是指只有这个区域内的图形才会被显示出来,超出区域的会被截断。默认情况下,是<svg>元素宽高(height和width属性)限制的区域。理解svg的绘制和显示对于理解变换坐标系统至关重要,更多详细介绍可以参考 svg从入门到图标绘制和组件封装
既然变换参考点是SVG画布的0 0的位置,那么如果图形的中心点与SVG画布0 0的位置重叠,那岂不就是图形以自身中心点进行变换。
对于一个长150,宽80的矩形,将其左上角坐标设为(-75,-40),那么它的中心点就与SVG画布的原点重合。
<rect x=-75 y=-40 width=150 height=80>但此时,坐标为负的那部分图形被裁剪,不会出现在可见区域。
<svg> <rect x=-75 y=-40 width=150 height=80 fill="orange"></rect> </svg> <svg> <rect x=0 y=0 width=150 height=80 fill="orange"></rect> </svg>这时,就需要设置svg标签的viewBox属性,来设置“新的可见区域”。原理在 svg从入门到图标绘制和组件封装 有非常详细的说明。
<svg viewBox=-140 -105 280 210 height="200" width="300"> <rect x=-75 y=-40 width=150 height=80 fill="orange"></rect> </svg>设置左上角坐标(-140,-105),往右走280,往下走210的这块区域为“新的可见区域”,绘制在这个区域里的图形将完全显示,而矩形在这个区域中。并且,此时可见区域的中心点恰好也在SVG画布的原点,所以此时矩形完全居中。
下面是一个旋转的案例:
<svg viewBox=-140 -105 650 350> <rect x=-75 y=-40 width=150 height=80 transform=rotate(45)/> </svg>综合案例
下面这个demo就将通过设置svg标签的viewBox属性,将SVG画布的原点移到可见区域的中心,里面所有的SVG元素都将绕中心作变换。
最终效果:
源码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> body{ text-align: center; } svg{ background: #333; } @keyframes ani-1 { 0% { transform: scale(0); } 25% { transform: scale(1); } 50% { transform: rotate(120deg); } 75%, 100% { transform: rotate(120deg) translate(13em) scale(.2); } } @keyframes ani-2 { 0% { transform: scale(0); } 25% { transform: scale(1); } 50% { transform: rotate(240deg); } 75%, 100% { transform: rotate(240deg) translate(13em) scale(.2); } } @keyframes ani-3 { 0% { transform: scale(0); } 25% { transform: scale(1); } 50% { transform: rotate(360deg); } 75%, 100% { transform: rotate(360deg) translate(13em) scale(.2); } } use:nth-of-type(1){ fill: hsl(120, 100%, 80%); animation: ani-1 4s linear infinite; } use:nth-of-type(2){ fill: hsl(240, 100%, 80%); animation: ani-2 4s linear infinite; } use:nth-of-type(3){ fill: hsl(360, 100%, 80%); animation: ani-3 4s linear infinite; } </style> </head> <body> <svg viewBox=-512 -512 1024 1024 height="300" width="300"> <defs> <polygon id=star points=250,0 64,64 0,250 -64,64 -250,0 -64,-64 0,-250 64,-64/> </defs> <use href=#star/> <use href=#star/> <use href=#star/> </svg> </body> </html>