最近在CSS Secrets一书看到了这样一节:让一个元素沿环形路径平移。这是一个css动画的问题,但却没有看上去那么简单,其关键点是元素是平移的,也就是说,元素自身并不发生旋转,只是稳定地沿着一个环形的路径移动,像这样:css
在书中做者Lea Verou已经给出了解答(实际上,能够追溯到做者更早的这篇博文),不过,我认为再补充一点周边细节知识可能会更易于理解。所以,本文整理了一些东西,将尝试更详细地解答这个问题。html
最开始看到这个问题的时候,会很容易想到用transform-origin
定义圆心的位置,而后用rotate()
进行旋转。css代码大概是这样(半径为150px):css3
@keyframes spin { to { transform: rotate(1turn); } } .avatar{ animation: spin 10s infinite linear; transform-origin: 50% 150px; }
搭配的html很简单:git
<img class="avatar" src="edwardup_avatar.jpg" alt="" />
对应的效果是:github
能够看到,这是一个旋转动画,元素在沿着环形路径移动的同时,自身也会围绕圆心发生旋转。所以,这并非咱们想要的平移效果。ide
但另外一方面,元素沿环形路径移动这一点是符合咱们的目标的。因此,能够在这个基础上思考如何改进。函数
w3c的The Transform Function Lists里提到:动画
If a list of <transform-function> is provided, then the net effect is as if each transform function had been specified separately in the order provided.spa
意思是,当一个元素的transform
添加了多个变换函数时,其效果等同于按照这些变换函数的顺序依次分散添加在多层元素中。例如,如下元素:3d
<div style="transform:translate(-10px,-20px) scale(2) rotate(45deg) translate(5px,10px)"></div>
其变换结果等效于:
<div style="transform:translate(-10px,-20px)"> <div style="transform:scale(2)"> <div style="transform:rotate(45deg)"> <div style="transform:translate(5px,10px)"> </div> </div> </div> </div>
这是一条很是有用的规则。如今,假若有一个应用了旋转变换函数的元素是:
<div style="transform:rotate(45deg) rotate(-45deg)"></div>
显然,这个元素实际上是没有旋转的,由于两个旋转变换函数恰好抵消。这时候,咱们再用一下前面的规则,就知道它等同于:
<div style="transform:rotate(45deg)"> <div style="transform:rotate(-45deg)"></div> </div>
也就是说,内层元素能够经过变形来抵消外层的变形效果。
如今回到旋转动画,既然元素已是沿环形路径移动了,咱们要作的就是抵消掉元素自身的旋转。参考上面的原理,咱们能够增长一个容器元素:
<div class="avatar"> <img src="edwardup_avatar.jpg" alt="" /> </div>
而后为它们搭配不一样的动画:
@keyframes spin { to { transform: rotate(1turn); } } @keyframes spin-reverse { from { transform: rotate(1turn); } } .avatar { animation: spin 10s infinite linear; transform-origin: 50% 150px; } .avatar > img { animation: spin-reverse 10s infinite linear; }
这段代码把旋转动画搬到了div.avatar
这个容器元素上,而后为<img>
元素添加了一个恰好相反的旋转动画。
运行一下,会发现这就是咱们想要达到的效果(参见文章开头的图)。
在前面的解决方案中,为了让元素自身不发生旋转,增长了额外的容器元素。那么,若是只用单个元素,有办法实现吗?
前面说过,一个元素的多个变换函数能够分散给多层元素。反过来,多层元素的变换函数,也能够集中到单个元素。
这个思路是可行的,只不过,有一个必须解决的问题,就是transform-origin
。
在两个元素的解决方案中,div.avatar
设置了transform-origin
为另外一个点(环形路径的圆心),而<img>
的transform-origin
则取默认值,也就是图片的中心(50%, 50%
),这两个变形原点是不同的:
在如今的css中,咱们并不能为单个元素同时指定多个transform-origin
(尽管在@keyframes
的不一样关键帧能够设置不一样的值),因此,咱们须要一点特别的技巧。
咱们知道,一个元素最终的变形效果,与transform
及transform-origin
都有关。事实上,在w3c规范中,使用了transformation matrix一词来表明这个最终变形效果(从数学角度来讲,通常用一个矩阵来表示从一个坐标系到另外一个坐标系的变换效果)。
参考w3c的Transformation Matrix Computation,咱们能够知道transformation matrix是这样计算的:
transform-origin
的x、y、z坐标值,进行平移(translate)transform
里的变换函数执行乘法transform-origin
的x、y、z坐标值,进行反向平移注意transform-origin
在这里被表述为两次方向相反的平移,也就是说,transform-origin
并非什么特别的东西,它能够被translate()
替代。
在CSS Secrets一书中,做者Lea Verou也引用了css变形规范的当时的一位编辑Aryeh Gregor的这样一句话:
transform-origin 只是一个语法糖而已。实际上你老是能够用 translate() 来代替它。
举例来讲,这段代码:
.avatar{ transform: rotate(30deg); transform-origin: 200px 300px; }
等效于:
.avatar{ transform: translate(200px, 300px) rotate(30deg) translate(-200px, -300px); transform-origin: 0 0; }
了解到这一点,咱们就有办法继续了。
利用前面的原理,咱们把前面两个元素的transform-origin
的差别抹去(所有变为transform-origin: 0 0;
的等效),转移到transform
上:
@keyframes spin { from { transform: translate(50%, 150px) rotate(0turn) translate(-50%, -150px); } to { transform: translate(50%, 150px) rotate(1turn) translate(-50%, -150px); } } @keyframes spin-reverse { from { transform: translate(50%, 50%) rotate(1turn) translate(-50%, -50%); } to { transform: translate(50%, 50%) rotate(0turn) translate(-50%, -50%); } } .avatar { animation: spin 10s infinite linear; } .avatar > img { animation: spin-reverse 10s infinite linear; }
如今这段代码中,两个元素的transform-origin
已经一致了,而后咱们根据变换函数合并规则,将它们集中到一个元素上,此时html从新变为单个元素:
<img class="avatar" src="edwardup_avatar.jpg" alt="" />
对应的css:
@keyframes spin { from { transform: translate(50%, 150px) rotate(0turn) translate(-50%, -150px) translate(50%, 50%) rotate(1turn) translate(-50%, -50%); } to { transform: translate(50%, 150px) rotate(1turn) translate(-50%, -150px) translate(50%, 50%) rotate(0turn) translate(-50%, -50%); } } .avatar { animation: spin 10s infinite linear; }
上面的代码特地把transform
的值分红两行,分别表明原来的两个元素各自的变换函数。到此,这段代码就已经可让单个元素达成前文的两个元素的效果了。不过,这段代码还比较冗长,能够再作一点简化。
咱们很清楚transform
的变换函数的顺序很重要,不能随意交换,但相邻的同类变换函数能够考虑合并。
首先,能够找到位于中间的translate(-50%, -150px)
和translate(50%, 50%)
能够合并,获得translateY(-150px) translateY(50%)
(百分比和像素值则不能再合并)。
而后,以from
的部分为例,注意rotate(0turn)
和rotate(1turn)
分别来自原来的两个元素,它们的角度值是为了互相抵消准备的,所以必须和为360deg
(1turn
= 360deg
):其中一个的角度值为x,另外一个则为360 - x。
也就是说,元素在rotate(0turn)
以前(未发生旋转),和rotate(1turn)
以后(发生了两次旋转),元素的角度是一致的(合计恰好转了360deg
),此时发生的translate()
也能够合并。以此找到最前的translate(50%, 150px)
和最后的translate(-50%, -50%)
,它们能够合并,获得translateY(150px) translateY(-50%)
。
至此,代码变为:
@keyframes spin { from { transform: translateY(150px) translateY(-50%) rotate(0turn) translateY(-150px) translateY(50%) rotate(1turn); } to { transform: translateY(150px) translateY(-50%) rotate(1turn) translateY(-150px) translateY(50%) rotate(0turn); } } .avatar { animation: spin 10s infinite linear; }
代码虽然看起来没怎么变短,但变换函数更细致明确了。最后,注意最开始的两个translateY()
,它们在from
和to
里都是同样的,所以,彻底能够在动画以外,一开始就把元素放在那个位置,从而消除这两个translateY()
。
实际上,这两个translateY()
的位移作的事就是把这个元素放到环形路径的圆心。
这样,代码再变为:
@keyframes spin { from { transform: rotate(0turn) translateY(-150px) translateY(50%) rotate(1turn); } to { transform: rotate(1turn) translateY(-150px) translateY(50%) rotate(0turn); } } .avatar { animation: spin 10s infinite linear; }
这就是精简后的单元素环形路径平移的解决方案了。代码直观看上去,可能会以为比较难理解,毕竟它是咱们通过前面这样一大段的分析推理获得的。
尽管如此,也有一篇文章介绍了如何直接理解这段环形路径平移的代码,推荐有兴趣的你看看。
在环形平移路径的代码的基础上,改变起点或终点的圆环半径,能够获得螺旋路径:
@keyframes spin { from { transform: rotate(0turn) translateY(-150px) translateY(50%) rotate(2turn); } to { transform: rotate(2turn) translateY(-50px) translateY(50%) rotate(0turn); } }
对应的效果:
这里为了体现螺旋效果,把圈数增长到了2圈。
把两个环形各取一半拼在一块儿,就能够获得S型路径。参考环形路径平移的方案,作一些调整,就能够获得S型路径平移的写法:
@keyframes spin{ 0%{ transform: rotate(-90deg) translateX(50px) rotate(90deg);} 49.9%{ transform: rotate(-270deg) translateX(50px) rotate(270deg);} 50.0% { transform: translateY(100px) rotate(-90deg) translateX(50px) rotate(90deg);} 100% { transform: translateY(100px) rotate(90deg) translateX(50px) rotate(-90deg);} }
这里初始把元素放在了上面那个半圆环的圆心,而后在50.0%
的关键帧位置切换为下面的半圆环路径。因为这个切换过程会让元素小小地停滞一下,并非咱们想要的动画,因此这里用带小数的关键帧位置来尽量缩短它的时长,使整个动画更平滑。最终效果是:
matrix()
是transform
里一个特殊的变换函数,它能够经过矩阵乘法把rotate()
、translate()
等其余变换函数所有合并在一块儿。可是,matrix()
并不能简化本文的动画代码,由于css动画将没法确认如何生成关键帧之间的补间动画,若是关键帧里只有一个合并后的matrix()
,css动画只会按照平铺的方式去完成过渡。
以文章最开始的旋转动画为例,rotate(1turn)
转换后是matrix(1, 0, 0, 1, 0, 0)
,但若是直接写:
@keyframes spin { to { transform: matrix(1, 0, 0, 1, 0, 0); } }
结果就是,什么也不会发生。
只经过一个transform
加上一段神秘代码,就能够作这样特别的动画,我以为是颇有意思的。但愿本文的这样一番解读,能够帮助你加深对css的transform
的理解。