最近在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裏提到:post
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.動畫
意思是,當一個元素的transform
添加了多個變換函數時,其效果等同於按照這些變換函數的順序依次分散添加在多層元素中。例如,如下元素:spa
<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是這樣計算的:
[1] 從一個單位矩陣(identity matrix)開始
[2] 根據transform-origin
的x、y、z座標值,進行平移(translate)
[3] 從左向右依次對transform
裏的變換函數執行乘法
[4] 根據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
的理解。
(從新編輯自個人博客,原文地址:http://acgtofe.com/posts/2016...)