CSS秘密花園: 沿著路徑的動畫
《 CSS Secrets 》是 @Lea Verou 最新著作,這本書講解了有關于CSS中一些小秘密。是一本CSSer值得一讀的一本書,經過一段時間的閱讀,我、@南北和@彥子一起將在W3cplus發布一系列相關的讀后感,與大家一起分享。
問題
幾年前,當CSS動畫剛出來的時候是多么的令人興奮,那時 Chris Coyier 問我,有沒有什么方式使用CSS讓元素繞一個圓形的路徑運動。當時,它只是一個有趣的想法,但我在無意中發現有很多這方面的用例。例如,Google+添加新成員就使用了這樣一個動畫。如下圖所示:
一個與眾不同而又有趣的例子可以看看 俄羅斯科技網站 的404頁面,如下圖所示:
通常在404頁面上是一個很好的實踐地方,下如上圖所示,他提供網站主要幾個領域的導航菜單。這幾個菜單繞著一個圓形路徑運動。
然而,每一個菜單項類似行星繞著地球轉上一圈,而且上有的文字寫著“飛往其他星星的宇宙”。當然,如果只移動行星繞著循環的路徑轉而文字不旋轉,這將使這些文本幾乎無法閱讀。這只是眾多例子之中的一個。但我們怎樣才能使用CSS動畫達到這樣的效果呢?
我們來寫一個非常簡單的示例,一個頭動繞著圓形的路徑循環的旋轉,這個有點像前面提到的Google+效果的簡化版。其結構如下:
<div class="path">
<img src="lea.jpg" class="avatar" />
</div>
在還沒有開始制作動畫之前,我們先設置一些基本樣式(例如:大小、背景、外距等),看起來如下圖所示:
因為這些都是基本樣式,這里沒有寫出來,但如果你有什么困難,可以查看示例中的代碼。最主要的要記住,路徑直徑是 300px ,因此半徑是 150px 。
我們已經完成了基本樣式之后,就可以開始考慮動畫怎么寫。將頭像移到圓中,繞著橙色的路徑旋轉一圈。我們可能使用CSS動畫來實現,那這樣動畫怎么寫呢?當面對這個問題時,我們想到的是這樣:
@keyframes spin {
to { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = path radius */
}
雖然這朝著正確的方向邁進了一步,頭像在繞著圓形的路徑旋轉,如下圖所示:
正如上圖所示,頭像在繞圓形路徑旋轉時,你也發現頭像自身也顛倒了。如果是文本,文本也會被翻個底朝天,這對于閱讀來說是一個相當糟糕的事情。我們只希望頭像繞著圓路徑運動,同時自身保持同一個方向。
當時我和Chris都沒有想出一個合理的方式來解決這個問題。我們可以想出的最好方法是通過多個關鍵幀繪制近似一個圓形的路徑,顯然這不是一個好的主意,也沒有任何方式能定義出來這樣的圓形路徑。那么我們必須得想出一個更好的方法,對嗎?
使用兩個元素的解決方案
我根據Chris提供的參考意見,我終于想出了一個解決方案。這個解決方案背后的主要思想是來自于前面介紹的"平形四邊形"和"鉆石圖片":通過取消嵌套中的 transform 。然而,這是一個動畫,它發生在每一幀的動畫之中。需要特別說明的是,就像前面提到的內容,這里需要 兩個元素 。因此,我們需要在HTML中添加一個額外的HTML元素 <div> 來包裹頭像:
<div class="path">
<div class="avatar">
<img src="lea.jpg" />
</div>
</div>
讓我們把前面的動畫效果用到 .avatar 容器上。現在我們看到的效果和前面出現的效果是一樣的,這并不是我們需要的,因為它也旋轉元素自身。但是,如果我們給 .avatar 設置一個旋轉,并且給頭像 img 設置一個相反的旋轉,而且他們旋轉的值都是相同的,將會發生什么呢?如此一來,兩個旋轉將相互對沖,我們只會看到他們繞著旋轉的原點做圓周運動。
不過有一個問題:這里沒有表態的旋轉,他們都是經過一系列的角度旋轉。例如,如果角度是 60deg ,那么取消的旋轉角度應該是 -60deg (或 300deg ),如果旋轉的角度是 70deg ,那么取消的旋轉角度應該是 -70deg (或 290deg )。它們都是發生在 0 至 360deg 之間(或 0 至 1turn 之間)。那么我們要怎么設置角度呢?答案比看起來要容易得多。我們只需要給動畫的反向設置 360deg 至 0deg ,如下所示:
@keyframes spin {
to {
transform: rotate(1turn);
}
}
@keyframes spin-reverse {
from {
transform: rotate(1turn);
}
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = path radius */
}
.avatar > img {
animation: spin-reverse 3s infinite linear;
}
現在,在任何時間,當第一個動畫旋轉 x deg ,第二個旋轉 360 - x deg ,其中一個增加多少,另一個就要減少多少。這才是我們想要的,比如下圖所示的效果就是我們期望的效果。
代碼我們需要改進一些。首先,我們動畫的所有參數重復兩次。如果我們要調整時間,我們就需要調整兩次,這樣做較為麻煩。其實可以通過 inherit 屬性繼承父元素的 animation :
@keyframes spin {
to { transform: rotate(1turn); }
}
@keyframes spin-reverse {
from { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = path radius */
}
.avatar > img {
animation: inherit;
animation-name: spin-reverse;
}
然而,我們不需要一個全新的動畫,只需要最初的一個動畫。記得我們在介紹“閃爍”動畫一節中,有介紹過 animation-direction 屬性,其中有一個 alternate 值是非常有用的。在這里我們將使用 reverse 值,得到一個反向的原動畫,因此不需要創建第二個動畫:
@keyframes spin {
to { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = path radius */
}
.avatar > img {
animation: inherit;
animation-direction: reverse;
}
我們繼續吧!這可能不是最理想的解決方案,因為他需要添加額外的元素,但只使用了不到十行的CSS代碼實現了相當復雜的動畫效果。
使用單個元素的解決方案
在前一節中,我們解決了問題,但這不是最優的方案,因為它需要修改HTML。當初我給CSS工作組提了一個建議,建議可以在相同的元素指定多個轉換源。這應該能夠在一個元素上實現,也似乎是一個合理的要求。
在討論過程中,Aryeh Gregor給CSS的 transform 規范中提了這樣一個聲明,似乎令人困惑不解:
transform-origin 就是一個語法糖。你可以使用 translate() 替代。—— @Aryeh Gregor
有關于相關的討論可以點擊這里閱讀。
事實證明,每個 transform-origin 可以模擬兩次 translate() 。例如下面的兩個代碼段是等價的:
transform: rotate(30deg);
transform-origin: 200px 300px;
transform: translate(200px, 300px)
rotate(30deg)
translate(-200px, -300px);
transform-origin: 0 0;
這看起來很奇怪,讓我們對 transform 了解的更清楚, transform 函數不是獨立的。每個 transform 屬性不僅應用在元素上,而且整個坐標系統運用在同一個元素上,也將影響所有的 transform 。這也說明 為什么不同的 transfrom 順序很重要,不同順序的相同轉換可能前生的結果會完全不同 。如果你還不了解這一點,下圖可以幫助你更好的理解:
因此,我們可以利用這個方法來處理我們的動畫:
@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%, 150px) rotate(0turn) translate(-50%, -50%);
}
}
.avatar {
animation: spin 3s infinite linear;
}
.avatar > img {
animation: inherit;
animation-name: spin-reverse;
}
這樣看起來很笨拙,但不要擔心,接下來我們會改善。請注意,我們現在不再有不同的 transform-origin ,但我們要記住,我們要需要兩個元素和兩個動畫。現在所有都使用相同的 transform-origin ,可以在 .avatar 上結合這兩個動畫:
@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 3s infinite linear;
}
這代碼是得到了改善,但仍然讓人感到困惑。我們還能讓它變得更簡潔嗎?我們進一步來改進它:
我們連續做了多次 translate() 特別是 translate(-50%,-150px) 和 translate(50%,50%) 。不幸的是,百分比和絕對長度不能結合在一起(除非我們使用 calc() )。然而,水平的 translate 相互取消了,但還有兩個次 translateY ( 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(-150%) rotate(1turn) translateY(-150px) translateY(50%) rotate(0turn);
}
}
.avatar {
animation: spin 3s infinite linear;
}
代碼變得更少了,重復的也變得更少,但仍然不是很好。我們可以做到更好嗎?如果我們的頭像在圓的中心,如下圖所示:
我們可以繼續減少兩個 translate ,然后動畫就變成:
@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 3s infinite linear;
}
來自: http://www.w3cplus.com/css3/css-secrets/animation-along-a-circular-path.html