添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

SVG中的Transform详解---平移、旋转和缩放

想做一个svg的动画,但是却卡在了坐标变换,查了好久的资料,终于找到一篇讲得清楚的教程。

原文地址: css-tricks.com/transfor

下面文章是基于这篇教程,最后的案例也是来自于它。

前言

和普通的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 width="200" height="200">
   <!--未指定本次旋转的参考点,绕着左上角旋转-->
   <rect x="0" y="0" width="100" height="100" fill="yellow" transform="rotate(45)"/>
<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 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 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)'/>
  1. 根据矩形的x、y、width、height确定矩形的中心点(140,105)。
  2. translate(140 105) 将矩形向右移动140个单位长度Ux,向下移动105个单位长度Uy(最初Ux=Uy=1),同时变换参考点也作了同样的移动,变成了(140, 105)。
  3. 相对新的变换参考点, scale(2 1.5) 将矩形在X、Y轴分别放大2倍和1.5倍,同时X、Y轴的单位长度也分别放大2倍和1.5倍,此时 Ux=2,Uy=1.5
  4. translate(-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%); 
}



  1. 最初的变换参考点是元素的右下角
  2. rotate(90deg) 元素顺时针旋转90deg,同时本地坐标系统也顺时针旋转90deg,X轴正方向向下,Y轴正方向向左。
  3. translate(0, -100%) 向Y轴负方向移动矩形宽度的距离,也就是向右移动,同时变换参考点也作相同的移动。
  4. rotate(90deg) 元素再次顺时针旋转90deg,同时本地坐标系统也再次顺时针旋转90deg,X轴正方向向左,Y轴正方向向上。
  5. 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> 元素的左上角为原点,在这个平面上的图形可以将坐标定位在平面的任意位置。
而可见区域,是指只有这个区域内的图形才会被显示出来,超出区域的会被截断。默认情况下,是 <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>
   <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">
    <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;
            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>
    <svg viewBox='-512 -512 1024 1024' height="300" width="300">
          <polygon id='star' points='250,0 64,64 0,250 -64,64 -250,0 -64,-64 0,-250 64,-64'/>
        </defs>