CSS分層動畫可以讓元素沿弧形路徑運動
譯者注:部分代碼示例在原文中可以看效果(作者寫在博文里面了…),我偷懶把它做成Gif圖了。
CSS 的 animations (動畫) 和 transitions(變換)擅于實現從點 A 到點 B 的直線運動,運動軌跡是直線路徑。給一個元素添加了 animation 或者 transition 以后,無論你如何調整 貝塞爾曲線 ,都無法讓它沿著弧形路徑運動。你可以通過自定義 timing function 屬性,做出彈動的效果,但是它沿著 X 和 Y 軸相對移動的值永遠是相同的。
與其使用 JavaScript 實現外觀自然的運動,不如嘗試用這種簡單的方式:分層動畫,繞過已有的限制。通過使用兩個或多個元素實現動畫效果,我們可以更加細粒度地控制某個元素的路徑,沿著 X 軸運動使用一種 timing function ,沿著 Y 軸運動使用另一種 timing function 。
問題所在
當我們深入探討解決方案之前,看看到底問題在哪。CSS animations 和 transitions 限制我們只能沿直線路徑運動。元素總是沿著點 A 到點 B 的最短路徑運動,如果我們另辟蹊徑,告訴 CSS 沿著“更好的路徑”,而不是“最短路徑”運動呢?
用 CSS (開啟硬件加速)實現兩點之間的運動,最直截了當的方式是使用 transform 的 translate 在一定時間內移動某個元素。這就產生了直線運動。在 @keyframes 中,我們打算在 (0,0) 和 (100,-100) 間來回運動,見上圖例子:
@keyframes straightLine { 50% { transform: translate3D(100px, -100px, 0); } } .dot { animation: straightLine 2.5s infinite linear; }
這些看起來并不難懂,但我們稍等片刻,思考一下我們需要的解決方案,拆分開來的動畫,視覺上長什么樣子呢。
0% 時,元素從 (0,0) 出發, 50% 時,我們用了 translate3D(100px, -100px, 0) 把它移動到 (100,-100),然后原路返回。換句話說,我們把元素向右移動了 100px ,向上移動了 100px ,兩個方向聯合作用使元素沿著一個角度運動。
解決方案:每個軸執行自己的動畫函數
那么,原先展示的例子中我們如何實現的弧形路徑呢?為了讓創建的路徑不是直線, 我們想讓元素沿 X 軸和 Y 軸的運動速度不同步 。
先前例子中都用到了 linear 線性運動函數,如果我們給運動的元素包裹一個容器,我們可以為 X 軸應用一種動畫函數,Y 軸應用另一種動畫函數。以下例子,我們在 X 軸使用 ease-in ,Y 軸使用 ease-out 。
每個軸元素的具體實現
不幸的是,我們不能只把 transform 動畫簡單疊加:因為只有最后聲明的動畫會執行。那么我們如何把兩個動畫效果聯合起來呢?可以把一個元素放入另一個元素內部,給容器元素加一種動畫,給里面的子元素添加另一種動畫。
在以上例子中,你已經看到一個點沿著弧形路徑運動,看到兩個獨立的元素一起做動畫,只不過容器元素是完全透明的。為了清晰地看到兩個元素沿著弧形路徑是如何相互作用的,我們給容器元素加個邊框看看唄:
那個點藏在帶邊框的盒子內部,跟隨盒子一起沿 X 軸遠動,同時它自己又在 Y 軸方向上下運動。去掉容器盒子的邊框,我們就得到了弧形路徑。與其在 HTML 中用兩個元素,還不如用偽元素實現嘞。如果 HTML 是這樣:
<div class="dot"></div>
我們可以添加偽元素:
.dot { /* 容器:沿 X 軸運動 */ } .dot::after { /* 黑點兒,沿 Y 軸運動 */ }
然后,我們需要兩塊獨立的動畫代碼:X 軸,Y 軸各一塊。注意一處用了 ease-in ,另一處用了 ease-out :
.dot { /*省略 一些布局代碼...*/ animation: xAxis 2.5s infinite ease-in; } .dot::after { /* 渲染小黑點兒*/ animation: yAxis 2.5s infinite ease-out; } @keyframes xAxis { 50% { animation-timing-function: ease-in; transform: translateX(100px); } } @keyframes yAxis { 50% { animation-timing-function: ease-out; transform: translateY(-100px); } }
加上 WebKit 前綴,用一些自定義的貝塞爾曲線代替 ease-in 和 ease-out ,我們就可以實現文章最開頭展示的效果:
.demo-dot { -webkit-animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); } .demo-dot::after { content: ''; display: block; width: 20px; height: 20px; border-radius: 20px; background-color: #fff; -webkit-animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); } @-webkit-keyframes yAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @keyframes yAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @-webkit-keyframes xAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes xAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); -webkit-transform: translateX(100px); transform: translateX(100px); } }
以下是文章起始處的例子:
你可能注意到我們在所有例子中都用了 @keyframes ,這純粹是因為我們想展示黑點兒往返的兩種狀態。如果只想實現點 A 至點 B 的運動,使用 transition 屬性做分層動畫同樣好用。
如果有個絕對定位的元素,通過給 left 和 bottom 屬性加特效,就可以實現弧形路徑運動,單個元素就可以,不需要容器元素。為什么不這么做呢:它性能稍差一些,動畫的每一幀都會引起重繪。使用帶偽元素的分層動畫, translate 屬性又開了硬件加速,動畫效果更好,性能也更高。
譯者自己搞了個絕對定位的例子:
來自: http://jinlong.github.io/2016/01/14/moving-along-a-curved-path-in-css-with-layered-animation/