SVG奇淫巧技(七):SVG Transform之万恶的左上角

1,511 阅读9分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第7篇文章。

transform作为CSS世界中最重要的属性之一,它的出现让原本枯燥单调的HTML变得生动了起来。

那么,为了图形展示而生的SVG当然不会屈居人后,要知道,SVG中的元素是自带transform 属性的,但SVG transformCSS transform 之间有着千丝万缕又截然不同的关系。

不过,那片瑰丽的前端世界,既然CSS transform可往,SVG transform亦可往。

SVG transform translate(位移)

96.png

如图所示,左侧CSStranslate是相对自己的中心点移动,而右侧SVG是相对于视窗的左上角进行移动,虽然两者的相对位置不一样,但是,对于单纯地位移来讲,无论你相对于哪个点位置,实际偏移的位置都是一样的,因此,从表现上讲,两者最终的位置看上去还是一样的。

但从本质上来讲,两者区别很大,SVG自带的transform进行translate变换时,仅支持translate(tx[ ty])这种用法(缺省使用0代替),当变换多个参数值的时候,可以使用,或者空格分隔,且不能包含单位,只能为纯数值,而且支持2D层面的属性,CSS中的translateX(tx)translateY(ty) 以及translate(tx[, ty])translateZ(tz) 并不支持。

另外,SVG中的translate位移和CSS相同,都是支持多声明累加的,例如:

transform="translate(30 20) translate(30 20)"

等价于

transform="translate(60 40)"

需要注意的是,两个translate中间不要混有其他的transform变换。否则,最终的位移就不是简单的相加了,至于为什么后面会提到。

SVG transform rotate(旋转)

上面的translate变换,从最终效果上似乎CSSSVG的区别不大。但是,从rotate旋转变换开始,区别就愈发明显了。

97.png

图示的45°旋转(左HTML元素,右SVG元素),之所以差别如此之大,还是源于相对的中心点不同。

另外,和CSS中旋转rotate(angle) 需要带单位不同(例如eg(度), turn(圈), grad(百分度)),SVG只能用数值来表示:

transform="rotate(45)"

具体语法为:rotate(angle[ x y]) ,大家注意到没有,这里有个[ x y][]表示这个可选参数。也就是说,SVG中的rotate旋转比CSSrotate多了一个可选参数,那这个可选参数[ x y]表示什么意思呢?

这个可选参数y是用来偏移transform变换中心点的,所以通过对变换中心点的偏移修正,我们也能让SVG元素围绕自身的中心点旋转了。

<rect x="30" y="30" width="120" height="90" rx="10" ry="10"
      fill="#a0b3d6" transform="rotate(45, 90 75)">
</rect>

98.png

translate一样,rotate旋转可以多个值并存:

transform="rotate(45) rotate(-45)"

不过,与CSS不同的是,SVG属性的transform声明的中心变换坐标是不能共享的,什么意思呢?

就是说第一次旋转我们设置了中心点后,第二次旋转时中心点再次回到了左上角,例如:

<rect x="30" y="30" width="120" height="90" rx="10" ry="10"
      fill="#a0b3d6" transform="rotate(45, 90 75) rotate(-45)">
</rect>

如果是CSS的话,转45°后再反向转-45°,就会回到原位才对,但是对SVG来说:

99.png

就变成了这样,究其原因还是因为“万恶”的左上角

虽然乍看上去,好像SVG的坐标系统有些怪怪的,但是,实际上,在有些需求场景下,SVG这种看似独立的偏移系统更容易实现一些功能。

比方说,我们希望某个SVG元素先以右下角为中心旋转90°,然后再以右上角为中心旋转90°,该怎么处理?

对于SVGtransform,我们直接面向需求写数值就可以了。假设我们的SVG元素的高宽是120*90, 左上角坐标是(30,30), 则,显然,右下角坐标是(150,120), 右上角坐标是(150,30),于是,我们的transform值就很简单:

transform="rotate(90, 150 120) rotate(90 150 30)"

100.png

但是,如果我们使用之前容易理解的CSS3来实现,反而就复杂了,因为CSS3中的transform的变换点只能一次性指定,如果要实现不同变换点的旋转效果,只能通过translate 再次偏移,例如,实现上面的右下角再右上角90°旋转,则要这样:

transform-origin: right bottom; /* 或者 100% 100% */
transform:
rotate(90deg)
translate(0, -100%) /* 从右下到右上 */
rotate(90deg)
translate(0, 100%);

101.gif

单纯从这个案例来看,显然CSS的方案要麻烦很多。可见,无论SVG的奇葩左上角还是CSS的便捷中心点,这两种坐标系统没有绝对的优劣,具体的如何抉择需要是需求而定。

SVG transform scale(缩放)

SVG中的scale缩放语法相比于前两个就太单纯了scale(sx[, sy])sx表示横坐标缩放比例,sy表示纵坐标缩放比例。其中sy是可缺省的,如果缺失,表示使用和sx一样的值,也就是等比例缩放。其中,sxsy两个参数可以用,空格分隔。这和CSS3中的使用有所不同,另外,SVG transform属性值缩放是不支持scaleXscaleY这些属性的。

老生常谈的相对位置依旧是最大的不同,左为HTML,右为SVG

102.png

可以看到,由于相对位置是左上角,导致SVG scale缩放后位置已经发生了偏移,所以如果我们想要实现居中缩放的效果需要再配合上translate才可以,例如:

103.gif

现在,大家应该知道CSSscale有多香了吧。

SVG transform skew(斜切)

SVGskew的语法相当有意思,在前面的一些变换中,例如位移、缩放之类是不支持translateXscaleX这种CSS常见用法的,但是这里的skew却有点让人哭笑不得:不支持skew(x[, y])这种语法,而只能是skewX或者skewY

所以,SVG斜切的语法只有:

transform="skewX(45)"

同样的,由于相对位置的差异,CSS实现的变换和SVG属性变换往往最后的位置是不一样的:

104.png

不仅如此,如果元素的中心点是SVG的左上角,则skewX(α1) skewX(α2)的最终位置和skewX(α1 + α2)是不一样的(注:上面说的位移、旋转和缩放不会这样子):

105.png

因为斜切的角度和元素偏移大小并不是线性的,比方说,从70°80°80°90°之间的位移大小,虽然都是相差10°,但是坐标偏移显然不是一个档次的。因此,分开多次连续斜切最终的坐标偏移要比一次性偏移来得小。

好的,SVG中的四种变换我们就已经都说完了,现在让我们来总结一下:

区别SVG transformCSS transform
相对位置左上角中心点
3D变换不支持支持
包含单位不支持支持
多参数空格分隔支持不支持
中心点共享不支持支持
translateX不支持支持
translateY不支持支持
scaleX不支持支持
scaleY不支持支持
rotateX不支持支持
rotateY不支持支持
skew(x, y)不支持支持

SVG transform变换处理的思路

通过上面的案例,我们已经了解了scale缩放、skew斜切这些SVG变换有多别扭,要是想如CSS transform-origin:50% 50%一样的中心点变换那般方便简洁,有没有什么好的办法呢?

当然有,具体有两个思路:

  1. 原始中心位置即SVG左上角:

106.gif

原先我们想实现一个围绕中心点的45°旋转,需要3步:

translate(140 105) rotate(45) translate(-140 -105)

现在,通过将元素初始中心点就设置在视窗的左上角,原本的3步就简化为了2步:

translate(140 105) rotate(45)
  1. 调整viewBox,其实这个思路与第1个思路的本质是一致的,只是实现方式不同而已:

107.png

我们可以通过调整viewBox(0,0)坐标设置为视窗中心,再让元素的中心点为(0,0),这样就只需要直接旋转就好了。

108.gif

是不是思路一下就清晰了很多,如果这两种思路依旧觉得很麻烦,到底有没有类似CSS transform那样方便易懂的实现方案呢?

有的,类似CSS transform的方案就是使用CSS transform它啊,还记得我们之前提到过,在SVG也算是HTML大家庭的一份子,所以SVG大部分属性都是和CSS互通有无的,现在咱们就来试试,走着。

CSS transform 操作SVG

109.jpg

<svg width="200" height="200" viewBox="0 0 100 100">
    <path d="M 50 1 99 50 50 99 1 50 Z" fill="none" stroke="black" />
    <circle cx="50" cy="50" r="49"  fill="none" stroke="black" />
</svg>

这是一个普普通通的SVG,现在我们想通过CSS transform来操作path标签来旋转45°

path {
    transform: rotate(45deg);
}

110.jpg

可以看到,path确实旋转了45°,但整个path大部分都已经超出了整个SVG的视区范围,难道又是万恶的左上角?

没关系,现在可是CSS的地盘,不会惯着SVG transform那套规则,直接改个transform-origin试试:

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

111.jpg

果然,SVG transform虽然霸道,但是在人家CSS的场子也不敢随便造次,现在相对位置乖乖的变成了中心点,旋转也不再只支持数值也可以包含单位了,再试试其他限制:

中心点是否共享

path {
    transform: rotate(30deg)  rotate(15deg);
    transform-origin: 50% 50%;
}

111.jpg

答案:支持

是否可以3D变换

path {
    transform: rotate3d(0, 1, 1, 45deg);
    transform-origin: 50% 50%;
}

112.jpg

答案:支持

是否支持 rotateX 或 rotateY

path {
    transform: rotateX(45deg);
    transform-origin: 50% 50%;
}

114.jpg

path {
    transform: rotateY(45deg);
    transform-origin: 50% 50%;
}

113.jpg

答案:支持

全都可以,虽然没有全部试验一番,但是管中规豹,目前除了左上角的魔咒尚未打破之外,其余规则全部继承CSS transform,现在我们可以得出一个结论。

使用CSS transform操作SVG元素与操作普通HTML元素,除了初始相对位置不同之外,其余并无二致。

CSS transform操作的区别SVGHTML
初始相对位置左上角中心点

SVG transform的应用

说了这么多理论,那具体有啥用呢?别急,这不就来了嘛:

translate(位移)

115.png

<svg width="200" height="200"viewBox="0 0 100 100">
    <rect x="5" y="5" width="40" height="40" fill="green" />
  
    <rect x="5" y="5" width="40" height="40" fill="blue"
          transform="translate(50)" />
  
    <rect x="5" y="5" width="40" height="40" fill="red"
          transform="translate(0 50)" />
  
    <rect x="5" y="5" width="40" height="40" fill="yellow"
           transform="translate(50,50)" />
</svg>

scale(缩放)

116.png

<svg width="200" height="200" viewBox="-50 -50 100 100">
    <!-- uniform scale -->
    <circle cx="0" cy="0" r="10" fill="red"
            transform="scale(4)" />
  
    <!-- vertical scale -->
    <circle cx="0" cy="0" r="10" fill="yellow"
            transform="scale(1,4)" />
  
    <!-- horizontal scale -->
    <circle cx="0" cy="0" r="10" fill="pink"
            transform="scale(4,1)" />
  
    <!-- No scale -->
    <circle cx="0" cy="0" r="10" fill="black" />
</svg>

rotate(旋转)

117.png

<svg width="200" height="200" viewBox="-100 -100 200 200">
    <g fill="none" stroke="red">
      <rect id="myRect" x="-50" y="-50" width="100" height="100" />
      <use href="#myRect" transform="rotate(5)" />
      <use href="#myRect" transform="rotate(10)"/>
      <use href="#myRect" transform="rotate(15)"/>
      <use href="#myRect" transform="rotate(20)"/>
      <use href="#myRect" transform="rotate(25)"/>
      <use href="#myRect" transform="rotate(30)"/>
      <use href="#myRect" transform="rotate(35)"/>
      <use href="#myRect" transform="rotate(40)"/>
      <use href="#myRect" transform="rotate(45)"/>
      <use href="#myRect" transform="rotate(50)"/>
      <use href="#myRect" transform="rotate(55)"/>
      <use href="#myRect" transform="rotate(60)"/>
      <use href="#myRect" transform="rotate(65)"/>
      <use href="#myRect" transform="rotate(70)"/>
      <use href="#myRect" transform="rotate(75)"/>
      <use href="#myRect" transform="rotate(80)"/>
      <use href="#myRect" transform="rotate(85)"/>
      <use href="#myRect" transform="rotate(90)"/>
    </g>
</svg>

skew(斜切)

118.png

<svg width="200" height="200" viewBox="-100 -100 200 200">
    <g fill="red">
      <rect x="-11.25" y="-50" width="25" height="100" transform="skewX(30)" />
      <rect x="-11.25" y="-50" width="25" height="100" transform="skewX(-30)" />
    </g>
</svg>

好了,以上这些都只是抛砖引玉,大家只要谨记SVG中万恶的左上角,无论SVG transform还是CSS transform,规矩再多不也是任君采撷嘛。

参考链接

理解SVG transform坐标变换