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

boqk2414 8年前發布 | 18K 次閱讀 CSS 前端技術

最近在 CSS Secrets 一書看到了這樣一節:讓一個元素沿環形路徑平移。這是一個css動畫的問題,但卻沒有看上去那么簡單,其關鍵點是 元素是平移的 ,也就是說,元素自身并不發生旋轉,只是穩定地沿著一個環形的路徑移動,像這樣:

在書中作者Lea Verou已經給出了解答(實際上,可以追溯到作者更早的 這篇博文 ),不過,我認為再補充一點周邊細節知識可能會更易于理解。因此,本文整理了一些東西,將嘗試更詳細地解答這個問題。

從旋轉動畫開始

最開始看到這個問題的時候,會很容易想到用 transform-origin 定義圓心的位置,然后用 rotate() 進行旋轉。css代碼大概是這樣(半徑為150px):

@keyframes spin {
    to {
        transform: rotate(1turn);
    }
}

.avatar{
    animation: spin 10s infinite linear;
    transform-origin: 50% 150px;
}

搭配的html很簡單:

<img class="avatar" src="edwardup_avatar.jpg" alt="" />

對應的效果是:

可以看到,這是一個旋轉動畫,元素在沿著環形路徑移動的同時,自身也會圍繞圓心發生旋轉。因此,這并不是我們想要的平移效果。

但另一方面,元素沿環形路徑移動這一點是符合我們的目標的。所以,可以在這個基礎上思考如何改進。

利用多元素的變形相消

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.

意思是, 當一個元素的 transform 添加了多個變換函數時,其效果等同于按照這些變換函數的順序依次分散添加在多層元素中 。例如,以下元素:

<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% ),這兩個變形原點是不一樣的:

在現在的css中,我們并不能為單個元素同時指定多個 transform-origin (盡管在 @keyframes 的不同關鍵幀可以設置不同的值),所以,我們需要一點特別的技巧。

transform-origin的本質

我們知道,一個元素最終的變形效果,與 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型路徑。參考環形路徑平移的方案,做一些調整,就可以得到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/11/arc-path-movement

 

 本文由用戶 boqk2414 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!