1. 程式人生 > >CSS祕密花園: 沿著路徑的動畫

CSS祕密花園: 沿著路徑的動畫

CSS Secrets》是@Lea Verou最新著作,這本書講解了有關於CSS中一些小祕密。是一本CSSer值得一讀的一本書,經過一段時間的閱讀,我、@南北@彥子一起將在W3cplus釋出一系列相關的讀後感,與大家一起分享。

CSS Secrets

問題

幾年前,當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)。它們都是發生在0360deg之間(或01turn之間)。那麼我們要怎麼設定角度呢?答案比看起來要容易得多。我們只需要給動畫的反向設定360deg0deg,如下所示:

@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;
}

這似乎是我們今天做得最好的了。這可能是最短的程式碼。現在最少的重複和沒有多餘的HTML元素。

原文: http://www.w3cplus.com/css3/css-secrets/animation-along-a-circular-path.html © w3cplus.com