SVG元素上的transform

发布于 Gloria

同HTML元素一样,我们可以通过transform函数操作SVG元素。然而transform在SVG元素和HTML元素上的工作方式会有一些差别。

首先,IE不支持SVG元素的CSS transform属性,但是如果只是应用一些2D变换,为了适配IE,我们可以使用SVG的transform属性

SVG的transform属性中的所有函数的参数只能是纯数字,比如说,我们不能在translate函数中使用%单位(虽说在火狐浏览器中的CSS transform属性也不能使用—此处有链接—),rotateskew角度只能使用deg单位,我们能在CSS transform属性中可以使用的所有其它单位在这里都不能使用。

注:火狐浏览器现在已经支持transform-origin上应用带有%的值,不过与chorme不同的是,火狐的%是相对于svg画布而不是元素自身。

而且蛋疼的是,JavaScript的特征检测会有问题(通过JavaScript读取在外部CSS文件中设置的transform属性会返回与其等价的矩阵),所以我们需要另外一种检测IE的方法,或者直接在HTML上书写transform属性。

SVG元素和HTML元素工作方式的差异,主要是由元素坐标系的不同造成的。无论是HTML元素还是SVG元素,都有一个自己的坐标系。对于HTML元素,初始的坐标原点在元素的中心。对于SVG元素,其坐标系原点是在SVG画板的(0,0)处(假设在SVG标签内祖先元素和自身都不存在任何变换)。如果SVG元素的中心点不在画板的(0,0)点,像rotate,scale或者skew这些变换的结果,都会与HTML元素上应用的结果大不一样。

为了更好地理解这些差异,让我们来看看transform函数是如何工作的。

transform函数是如何工作的

我们要清楚在一个存在嵌套的元素上应用变换会有叠加效果。也就是说,在一个包含后代的元素上应用变换,后代元素也会根据自己的坐标系产生相应的变换。为了方便起见,我们后面的例子都是以祖先元素不存在变换,并且其中不包含任何子孙元素为前提的。

位移

位移会在相同的方向上以相同的距离移动元素上的所有点,并且会保留元素上除了位置信息的所有其它信息。这种位移可以被解释成移动一个元素的坐标原点,所以位置是相对于那个坐标原点的任何元素都会被移动。这种位移之后的效果并不依赖于坐标系的位置。

Figure #1: translate transform: HTML 元素 (左边) vs SVG 元素 (右边)

translate transform: HTML 元素 (左边) vs SVG 元素 (右边)

上面的图片展示了当translate分别应用到HTML元素和SVG元素上的区别。

正如我们看到的,它们的区别在于各自坐标系的位置。HTML元素的坐标原点在自身50% 50%处,SVG元素的坐标原点在SVG画布0 0处,不过无论坐标原点处在什么位置,它们最后呈现的效果都是一样的。

对于HTML和SVG元素,我们都可以在 CSS transform 中使用3种2D的位移函数:translateX(tx),translateY(ty) translate(tx[, ty])。前两个分别作用在 X方向 和 Y方向(相对于元素自身的坐标系)。需要注意的是,如果在translate之前存在另外的变换,X方向,Y方向就可能不再代表着水平方向,垂直方向。而第三个位移函数则同时在x,y方向上分别移动tx,ty个单位,ty是可选的,如果不明确指定,默认是0

SVG元素除了CSS tranform属性,还有SVG tranform属性。在这个例子中,我们在tranform属性中只定义了translate,SVG属性中可以使用逗号分隔,或者空格分隔,其中1代表着1px,下面的两种为SVG元素应用位移的方式是等价的:

使用CSS transform:

rec {
    /* doesn't work in IE */
    transform: translate(295px, 115px);
}

使用SVG transform:

<rect width='150' height='80' transform='translate(295 115)' />

注:SVG transform 和 CSS transform 将会被合并

连续的translate()将会被相加在一起,比如我们可以书写一个与translate(tx1 + tx2, ty1 + ty2)等价的链式写法translate(tx1, ty1) translate(tx2, ty2),注意,这种等价关系只有当两个translate()之间没有任何其它的转换的情况下成立。从translate(tx, ty)返回到初始状态,应用translate(-tx, -ty)即可。

旋转

2D旋转会基于一个固定点将元素及其子孙元素旋转一定的角度(固定点的位置在旋转前后不会被改变),旋转后的效果会因固定点位置的变化而不同。就像位移一样,旋转不会扭曲元素,元素自身的属性不会发生变化。rotate()同样具有可加性,在相反的方向应用相同角度的rotate()就能返回到初始状态。

Figure #2: basic rotate transform: HTML 元素 (左边) vs SVG 元素 (右边)

Figure #2: basic rotate transform: HTML 元素 (左边) vs SVG 元素 (右边)

上面展示了在两种不同元素上应用rotate()的区别。旋转一个元素,其自身的坐标系会基于原点旋转相同的角度,同时也会应用到后代元素。

一个元素上的所有点都会绕着自身的坐标原点旋转,HTML元素的坐标原点在50% 50%处,SVG元素的坐标原点则是在SVG画板的0 0处。

CSS transform中的2D旋转函数就是rotate(angle)angle可以是多种单位的值: degreesradiansturn,grad,我们也可以使用calc()(比如calc(.25turn - 30deg) ),这个属性现在只能在Chorme 38+/Opera 25+使用, 如果angle是正值,将会沿着顺时针方向旋转(反之,则会是逆时针)。

在SVG的transform属性中的旋转函数是这样的:rotate(angle[ x y])angle的值与CSS transform属性中的一样(必须是无单位的degree值,正值表示顺时针旋转,负值反之,可选的x y表示旋转时固定点的位置,其值默认是该元素坐标系的原点),如果只有x,则变换无效。

translate()一样,参数可以使用逗号或者空格分隔。

指定x y不代表该元素的坐标原点被移动到了x y上,就像元素一样,其坐标系也同时绕着x y点旋转。

也就是说,我们可以使用两种方法旋转一个SVG元素(效果可以在上面的图片中看到)。

使用CSS transform:

rect {
    /* doesn't work in IE */
    transform: rotate(45deg);
}

使用SVG transform属性:

<!-- works everywhere -->
<rect x='65' y='65' width='150' height='80' transform='rotate(45)' />

我们可以在CSS transform中指定transform-origin属性来模拟SVG中的x y 参数,长度单位是相对于元素坐标系而言的,百分比单位则是以元素自身为基准,完美!!不过也有一些需要注意的地方。

首先,transform-originrotate()中指定固定点,两者是不一样的,比如我们需要绕着元素的50% 50%点旋转SVG元素,下面是两种实现方式:

/*CSS*/

rect {
  transform: rotate(45deg);
  transform-origin: 50% 50%;
}

<!-- HTML -->
<rect x='65' y='65' width='150' height='80' 
      transform='rotate(45 140 105)' />
<!-- 140 = 65 + 150/2 -->
<!-- 105 = 65 +  80/2 -->

在Chorme中两者实现了相同的效果,如下图

Figure #3: 围绕一个固定点旋转一个 SVG 元素: 使用 CSS (左边) vs. 使用SVG transform(右边)

围绕一个固定点旋转一个 SVG 元素: 使用 CSS (左边) vs. 使用SVG transform(右边)

正如上图所展示的,第一个CSS transform例子会将元素的坐标系原点移动到自身的50% 50%位置,然后基于坐标系原点旋转,第二个SVG transform例子则是仅仅将元素中心点的位置作为旋转的固定点,自身的坐标系并没有因此改变,所以所有基于自身坐标系的变换都不会发生变化。

为了加深理解,我们再应用一个相同角度45°,相反方向的roatate()

/*CSS*/
rect {
  transform: rotate(45deg) rotate(-45deg);
  transform-origin: 50% 50%; /* Chrome, Firefox behaves differently */
}

<!-- HTML -->
<rect x='65' y='65' width='150' height='80' 
      transform='rotate(45 140 105) rotate(-45)' />
<!-- 140 = 65 + 150/2 -->
<!-- 105 = 65 +  80/2 -->

Figure #4: 在SVG元素上链式调用旋转: CSS transforms (左边) vs. SVG transform (右边)

在SVG元素上链式调用旋转: CSS transforms (左边) vs. SVG transform (右边)

如图所示,在CSS transform中,设置transform-origin50% 50%,两次的rotate()会相互抵消,因为两次的旋转都是围绕坐标系原点50% 50%,而在SVG transform中,第一次指定了旋转的固定点在元素中心,而第二次并没有指定,所以默认以元素坐标系原点为固定点。如果想实现预期的效果可以指定第二个为:rotate(-45 140 105)而不是rotate(-45)

我们能为SVG transform的每个roatate()指定不同的固定点,但是只能为CSS transform的每个rotate()指定一个transform-origin。如果想实现一个矩形先绕右下角的点旋转90°,再绕右上角旋转90°,对于SVG tranform来说很容易实现,为每个rotate()指定不同的固定点就可以了:

<rect x='0' y='80' width='150' height='80' 
      transform='rotate(90 150 160) rotate(90 150 80)'/>
<!--
bottom right:
  x = x-offset + width = 0 + 150 = 150
  y = y-offset + height = 80 + 80 = 160
top right:
  x = x-offset + width = 0 + 150 = 150
  y = y-offset = 80
-->

Figure #5: 链式调用不同固定点的rotate (SVG transform )

链式调用不同固定点的rotate (SVG transform )

我们如何做到在CSS transform中实现相同的效果呢?第一步很简单,我们可以指定transform-originright bottom,但是第二步呢?如果只是简单地链式书写在第一个的后面, 元素会以right bottom为固定点再次旋转90°

为了忽略transform-origin的位置,我们需要再添加3个变换,第 一个就是 translate(x, y) ,为了与第二次的旋转固定点对应,我们使用 translate(x, y) 使元素的坐标系原点和我们希望的固定点重合,第二个就是旋转,第三个是translate(-x, -y)(第一次变换的相反操作)。

下面是上面例子的代码:

/*CSS*/
rect {
  /* doesn't work as intended in Firefox 
   * % values are taken relative to the SVG, not the element
   * which actually seems to be correct */
  transform-origin: right bottom; /* or 100% 100%, same thing */
  transform:
    rotate(90deg)
    translate(0, -100%) /* go from bottom right to top right */
    rotate(90deg)
    translate(0, 100%);
}

下面这张图片展示了上面代码的步骤:

Figure #6: 链式CSS transform的工作流程

链式CSS transform的工作流程

然而在火狐浏览器中,transform-origin只能使用长度单位,translate()中的百分比单位也不能使用。

注:在火狐浏览器中transform-origin已经支持百分比单位了,但是它的行为与chorme不太一样,所以我们不建议您使用这个方法。

缩放

缩放会在相应的方向上应用缩放因子,然后改变元素所有点到其坐标系原点到的距离。除非各个方向的缩放因子都是相同的,不然该元素的形状必定被改变。

(-1,1)范围之内的缩放因子会缩小元素,范围之外的因子则会放大元素。一个负值的因子不仅会改变元素的大小,还会基于坐标系原点镜像处理所有的点。如果因子不等于1,那么在相应的方向上就会出现缩放的情况。

一个缩放函数的效果会依赖元素坐标系原点的位置,对于同一个元素,两个具有相同缩放因子的缩放函数,会因为不同的坐标系原点表现出不同的效果。

Figure #7: scale transform: HTML 元素 (左边) vs SVG 元素 (右边)

scale transform: HTML 元素 (左边) vs SVG 元素 (右边)

上图是分别在HTML元素和SVG元素上应用缩放的不同结果,两者拥有完全相同的缩放因子,不同的是,HTML元素的坐标系原点在元素50% 50%处,而SVG元素的坐标原点则是SVG画板的0 0处。

在CSS transform中有3种2D的缩放函数: scale(sx[, sy])scaleX(sx)scaleY(sy)。第一个函数,会同时在xy方向上应用sxsy缩放因子,sy是可选的,如果该函数只有一个参数,会默认是sx的值。其它两个函数是分别在两个方向进行缩放,scaleX(sx)scaleY(sy)分别等价于scale(sx,1)scale(1,sy),如果在此之前存在其它的变换,那么相应的xy方向将不再是水平和垂直的。

在SVG transform中,只有scale(sx[ sy])。同样,可以使用空格或者逗号分隔参数。

对于SVG元素来说,以下的两种缩放方法是等价的:

使用CSS transform:

/*CSS*/
rect {
   /* doesn't work in IE */
  transform: scale(2, 1.5);
}

使用SVG transform:

<!-- HTML -->
<!-- works everywhere -->
<rect x='65' y='65' width='150' height='80' transform='scale(2 1.5)' />

在同一个元素上应用上面两种方法会产生相同的效果,就像上一个图片中的右边的区域。如果我想实现像应用在HTML元素上一样的效果呢?来看一看我是如何让实现的。

我们可以使用CSS transform中的transform-origin改变元素的坐标原点,或者使用translate() scale() translate()这样的组合,在第二种方法中,我们先将元素坐标系原点移动到元素的50% 50%位置,然后应用缩放,最后做与第一个位移相反的操作。如果使用SVG transform属性,我们只能使用前面CSS transform中的第二种方式,类似的方法,下面是具体的代码。

使用CSS transform中的transform-origin

/*CSS*/
rect {
  transform: scale(2, 1.5);
  transform-origin: 50% 50%;
}

在CSS transform中链式调用:

rect {
  transform: translate(140px, 105px)
             scale(2 1.5)
             translate(-140px, -105px);
}

在SVG transform中链式调用:

<rect x='65' y='65' width='150' height='80' 
      transform='translate(140 105) scale(2 1.5) translate(-140 -105)'/>
<!-- works everywhere -->

下面的DEMO将会展示链式调用的工作过程(点击播放►按钮开始)

还有就是,两个连续的scale()scale(sx1, sy1) scale(sx2, sy2),可以写成scale(sx1*sx2, sy1*sy2)scale(1/sx1, 1/sy1)可以对scale(sx1,sy1)做相反的操作。如果所有的scale()参数的绝对值等于1,那么这个元素不产生任何的缩放。

倾斜

倾斜操作会将元素上所有点的坐标值在相应的方向上移动一段距离,这段距离取决于倾斜的角度而和每个点到倾斜轴的距离。也就是说,在一个方向上倾斜,另一个方向上的所有点相应的坐标值不会被改变。倾斜一个元素必定会扭曲这个元素,这点和旋转不太一样,倾斜一个矩形,这个矩形会变成不等边的平行四边形,倾斜一个圆,这个圆会变成椭圆。倾斜操作过后,角度(对于倾斜角α,矩形的直角将会变成90° ± α)或者长度都会发生变化,但是元素的原始区域会被保留。

另外,与位移,旋转不一样的是,倾斜不具有可加性,连续两次α1 α2的倾斜,不等于一次α1+α2的倾斜。

下面的DEMO展示了倾斜的工作方式,你可以调整倾斜轴,倾斜角度,瞧瞧它们是如何作用于初始正方形的:

倾斜的角度等于初始轴与终轴的夹角(非倾斜轴),如果应用[0°, 90°]夹角的倾斜,元素上所有点倾斜轴方向上的坐标会加上与其非倾斜轴方向坐标符号相等的值,如果是[-90°, 0°],则该值的符号相反。

如果我们应用一个在X轴方向上的倾斜,元素y轴方向的坐标保持不变,X轴方向的坐标会增加或者减少d,这个d取决于倾斜角度和该点y轴上的坐标,顶边和底边的长度不变,两侧的长度会随着倾斜角度的增加而增加,当角度为±90°时两侧将会是无限长,当角度慢慢靠近±180°的时候,两侧的长度将会逐渐减少。

注:(90°, 180°]之间的角α等同于α - 180°(落在区间(-90°, 0°]),同样,如果α(-180°, -90°] 之间,则等同于α + 180°(落在区间[0°, 90°))

同样,如果我们应用一个在y轴方向上的倾斜,元素X轴方向的坐标保持不变,y轴方向的坐标会增加或者减少d,这个d取决于倾斜角度和该点X轴上的坐标,两侧的长度不变,上下的长度会随着倾斜角度的增加而增加,当角度为±90°时上下将会是无限长,当角度慢慢靠近±180°的时候,上下的长度将会逐渐减少。

倾斜操作与旋转操作一样,元素的坐标原点都会影响最终的呈现效果,对同一个元素在相同的轴方向上做相同倾斜操作,会产生不同的结果:

Figure #8: skew transform: HTML元素 (左边) vs SVG元素 (右边)

skew transform: HTML元素 (左边) vs SVG元素 (右边)

以上是分别对HTML元素和SVG元素做相同的倾斜操作后的图片,相同的角度,相同的倾斜轴,不同的是,它们的坐标原点不一样。HTML元素在其50% 50%处,SVG元素在SVG画布的0 0处。

方便起见,在这我们只关注元素上的一个点:右上角。两者右上角在垂直方向上始终保持不变,在水平方向上,HTML元素中向左平移,SVG元素中向右平移,右下角都向右平移。为什么会这样?

就像前面提到的,X轴方向的倾斜,y轴方向的坐标保持不变,X轴方向的坐标会增加或者减少d,这个d取决于倾斜角度和该点y轴上的坐标。如果倾斜角在[0°, 90°]之间,则d的符号与y坐标相同,如果在[-90°, 0°]之间,则相反。

在这个例子中,倾斜角是60°,所以在这里右上角y轴方向的坐标是导致两者不同的关键,在HTML元素中,由于坐标系原点在元素中央,所以右上角的y坐标为负值,在SVG元素中,坐标系原点在SVG画板的0 0处,所以右上角y坐标为正值。像上面所说的,这样分别加在两者右上角d的符号也是相反的,也就造成了HTML元素的右上角向左移动,SVG元素的右上角向右移动。

无论是CSS transform还是SVG transform,都有两个倾斜函数:skewX(angle)skewY(angle),前者的倾斜轴是X轴,后者的倾斜轴是y轴。

在CSS transform中,angle的单位可以是deg rad turn grad ,还能使用calc()(需要牢记的是,只有Blink内核的浏览器支持在calc()中计算角度)

在SVG transform中,倾斜角度都是没有单位的deg值。

也就是说有两种方法书写上一张图片中右边的效果:

使用CSS transform:

rect {
    transform: skewX(60deg); /* doesn't work in IE */
}

使用SVG transform:

<!-- works everywhere -->
<rect x='65' y='65' width='150' height='80' transform='skewX(60)' />

如果我想实现像应用在HTML元素上一样的效果呢?就像旋转一样,存在3种方式:

使用CSS transform的transform-origin(不建议使用):

rect {
    transform: skewX(60deg);
    transform-origin: 50% 50%;
}

在CSS transform中链式调用:

rect {
    transform: translate(140px, 105px)
                skewX(60deg)
                translate(-140px, -105px);
}

在SVG transform中链式调用:

<!-- works everywhere -->
<rect x='65' y='65' width='150' height='80' 
      transform='translate(140 105) skewX(60) translate(-140 -105)' />

下面的DEMO将会展示链式调用的工作过程(点击播放►按钮开始)

简写调用链

我们可以通过链式调用,在SVG元素上实现在HTML元素上进行rotate scale skew操作一样的效果,使用SVG transform也能做到兼容IE浏览器。然而,你不觉得这样的写法很丑陋么?难道就没有更加简便的方式?

如果我们可以将元素的坐标系原点移动到元素自身的50% 50%处,那么调用链会大大缩短,像下面这样:

<rect x='-75' y='-40' width='150' height='80' 
      transform='translate(140 105) rotate(45)'/>
<!-- 75 = 150/2, 40 = 80/2 -->

如果使用SVG元素的viewBox属性,第一个translate也能被精简掉。viewBox属性中有4个值,前两个指定了SVG画布的左上角在显示区域的位置, 后两个则是SVG画布显示区域的widthheight,如果没有明确指定viewBox,画布的位置就是在显示区域的0 0处。

下面的两张图片能很好地展现viewBox='-140 -105 280 210'使用前和使用后的区别。

Figure #9: viewBox使用前 vs. viewBox使用后

viewBox使用前 vs. viewBox使用后

回到刚才的话题,如果想将SVG画布的坐标原点0 0移动到矩形的中心点,我们可以像下面这样设置viewBox:

<svg viewBox='-140 -105 650 350'>
  <rect x='-75' y='-40' width='150' height='80' transform='rotate(45)'/>
</svg>

实际使用

将SVG画布的原点移动到元素的中心点,可以大大简化SVG图形的变换操作。下面的demo展示了3个四角星(点击播放/暂停),最初都在中央,然后各自旋转,平移,倾斜,缩放,当中并没有使用transform-origin或者链式调用:

让我们看一看,这个demo是如何一步一步工作的。

非常轻松地画出四角星(其实就是一个包含8个点的多边形)

3个这样的星星,我不想连续3次重复定义这个多边形,所以使用了1个<defs>和3个<use>:

<svg viewBox='-512 -512 1024 1024'>
  <defs>
    <polygon id='star' points='250,0 64,64 0,250 -64,64 -250,0 -64,-64 0,-250 64,-64'/>
  </defs>

  <g>
    <use xlink:href='#star'/>
    <use xlink:href='#star'/>
    <use xlink:href='#star'/>
  </g>
</svg>

首先,星星需要从0放大到1:

use {
  animation: ani 4s linear infinite;
}

@keyframes ani {
  0% { transform: scale(0); }
  25%, 100% { transform: scale(1); }
}

上面的代码实现的效果:

下一步就是为我们的动画增加旋转效果,同时我希望每一个星星都有不同的旋转角度,下面是代码:

$n: 3;
$α: 360deg/$n;
$β: random($α/1deg)*1deg;

@for $i from 1 through $n {
  $γ: $β + ($i - 1)*$α;

  use:nth-of-type(#{$i}) {
    fill: hsl($γ, 100%, 80%);
    animation: ani-#{$i} 4s linear infinite;
  }

  @keyframes ani-#{$i} {
    0% { transform: scale(0); }
    25% { transform: scale(1); }
    50%, 100% { transform: rotate($γ); }
  }
}

实现的效果如下:

下一步,平移,旋转:

@keyframes ani-#{$i} {
  0% { transform: scale(0); }
  25% { transform: scale(1); }
  50% { transform: rotate($γ); }
  75%, 100% {
    transform: rotate($γ) translate(13em) scale(.2);
  }
}

好了,我们还需要与缩放配合,添加倾斜效果:

@keyframes ani-#{$i} {
  0% { transform: scale(0); }
  25% { transform: scale(1); }
  50% { transform: rotate($γ); }
  75% {
    transform: rotate($γ) translate(13em) scale(.2);
  }
  83% {
    transform: rotate($γ) translate(13em) scale(.2)
      skewY(30deg) scaleX(.866);
  }
  91% {
    transform: rotate($γ) translate(13em) scale(.2)
      skewY(60deg) scaleX(.5);
   }
  100% {
    transform: rotate($γ) translate(13em) scale(.2)
      skewY(90deg) scaleX(0);
  }
}

我在原来的基础上添加一些帧。当倾斜角线性变化的时候,其缩放因子会像余弦函数一样变化,如下图一样:

Figure #10: 三角函数 正弦 (蓝色) 余弦 (红色)

三角函数 正弦 (蓝色) 余弦 (红色)

在火狐浏览器中,这个纯CSS的DEMO有点蛋疼,然后因为IE不支持SVG元素上的CSS transform,所以这个demo在IE上也不能运行,使其兼容IE只能使用SVG transform,然后配合javascript动态地改变那些值,下面是这个兼容版本(点击开始):

本文根据@ANA TUDOR的《Transforms on SVG Elements》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://css-tricks.com/transforms-on-svg-elements

Gloria

在校大学生,软件工程专业,爱前端,爱篮球,爱挑战。Life is struggle , Be a cool shit . 邮箱:gloria_n@yeah.net.。

如需转载,烦请注明出处:https://www.fedev.cn/svg/transforms-on-svg-elements.htmlJordan Hydro 6 Sandals