巧妙使用transform實現環形路徑平移動畫

最近在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的問題

前面說過,一個元素的多個變換函數能夠分散給多層元素。反過來,多層元素的變換函數,也能夠集中到單個元素。

這個思路是可行的,只不過,有一個必須解決的問題,就是transform-origin

在兩個元素的解決方案中,div.avatar設置了transform-origin爲另外一個點(環形路徑的圓心),而<img>transform-origin則取默認值,也就是圖片的中心(50%, 50%),這兩個變形原點是不同的:

多個transform-oirgin

在如今的css中,咱們並不能爲單個元素同時指定多個transform-origin(儘管在@keyframes的不一樣關鍵幀能夠設置不一樣的值),因此,咱們須要一點特別的技巧。

transform-origin的本質

咱們知道,一個元素最終的變形效果,與transformtransform-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)分別來自原來的兩個元素,它們的角度值是爲了互相抵消準備的,所以必須和爲360deg1turn = 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(),它們在fromto裏都是同樣的,所以,徹底能夠在動畫以外,一開始就把元素放在那個位置,從而消除這兩個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型路徑。參考環形路徑平移的方案,作一些調整,就能夠獲得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%的關鍵幀位置切換爲下面的半圓環路徑。因爲這個切換過程會讓元素小小地停滯一下,並非咱們想要的動畫,因此這裏用帶小數的關鍵幀位置來儘量縮短它的時長,使整個動畫更平滑。最終效果是:

S路徑平移

一點補充

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的理解。

相關文章
相關標籤/搜索