本文做者@吳冠禧。javascript
今年Q1(2020年第一季度)參與了京喜事業部「京喜工廠」業務的前端開發。用戶能夠經過「京喜工廠」參與口罩、抽紙、大米等商品的「在線生產」,既能趣味造物,又能免費領獎品。目前能夠經過「京喜」小程序首頁訪問該活動。css
活動在上線一個月後,PV達到千萬的量級,引人注目。有很多前端同窗好奇裏面涉及到的動畫實現,文本應運而生。html
「京喜工廠」項目包括了微信小程序原生頁面和H5頁面這兩個平臺,項目中使用了大量的 CSS 動畫,在兩個平臺都可完美運行,還沒有發現明顯的兼容性問題。前端
本文就部分所涉及到的動畫效果進行復盤和總結。在真實的項目實戰中,手把手教你深刻學習 CSS 動畫的原理和實現細節。java
「京喜工廠」活動首頁的截圖效果以下:git
動畫是指由許多幀靜止的畫面,以必定的速度(如每秒16張)連續播放時,肉眼因 視覺暫留
產生錯覺。 要達成最基本的視覺暫留效果至少須要 10幀/秒 ,普通電影是 24幀/秒 , 普通顯示器刷新頻率是 60幀/秒。github
下面的兩個Gif都是用相同的6幀組成,可是播放速度不同,10幀/秒就有點動畫的效果,2幀/秒就會有卡頓的感受。web
10幀/秒: | 2幀/秒: |
---|---|
京喜工廠小人走路動畫(4倍速播放):canvas
整個動畫大致上就是小人按照從右側進入工廠,繞着工廠內一圈的方式最後從右側出去:小程序
走路過程當中會有走路的動做:
注意從右走到左時嗎,小人朝向右;從左走到右時,小人朝向左:
路徑過程當中會有幾個駐留點,每一個點駐留一小段時間,作工做中的動做:
咱們先來對比一下。
SVG 原生支持 SMIL(Synchronized Multimedia Integration Language), SMIL 容許你:
(1)變更一個元素的數字屬性(x、y……)<animate>
(2)變更變形屬性(translation或rotation)<animateTransform>
(3)變更顏色屬性 <animate>
|| <animateColor>(已廢棄)
(4)物件方向與運動路徑方向同步(路徑動畫) <animateMotion>
其實都算是常規的動畫能力,可是配合一些 SVG 專有的特性會產生一些奇妙和效果,例如 描邊動畫 就是利用 stroke-dasharray
和 stroke-dashoffset
實現的。另外,同爲路徑動畫 SMIL 的 <animateMotion>
就比 CSS 的 offset-path
兼容性好不少。
微信小程序:微信小程序不支持 SVG 及 SMIL 。
理論上, Javascript 能作任何動畫。 通常來講 Javascript 動畫能夠分爲 操縱 DOM 屬性的動畫
和 操縱 canvas api 的動畫
,這兩種都的原理都是經過 window.requestAnimationFrame()
或者 window.setTimeout()
這類時間控制函數實現每 16.7ms 顯示一幀畫面,從而達成 60幀/秒 的動畫。 另外,還有 Web Animations API
,將瀏覽器動畫引擎向開發者打開,並由JavaScript進行操做。它是將來對網絡上動畫化的支持最有效的方式之一,它使瀏覽器能夠進行本身的內部優化。可是兼容性較差。
微信小程序:微信小程序實現了本身的一套 WX Ainmation API
, 不兼容 web 標準。
CSS 動畫都是聲明式,使用 @keyframe
建立關鍵幀,瀏覽器就會自動計算出每 16.7ms 內的畫面變化,這些計算不是用 JS ,從而避免 GC 。CSS 動畫還有一個好處是能夠利用 translateZ
開啓 GPU 硬件加速,並且在 2020 年的當下,CSS 動畫的兼容性能夠說是很是好了。
有點遺憾的是 CSS 在路徑動畫 offset-path
上的兼容性仍是比較差。
微信小程序:微信小程序支持 CSS 動畫。
考慮到項目主要運行在 H5 和 微信小程序平臺上,綜合兼容性和本身的熟練度,最後仍是選擇用 CSS 動畫。
按照 animation-timing-function
(時間函數) 的不一樣,將 CSS 動畫分紅 「線性變化動畫」 和「非線性變化動」:
「線性變化動畫」 是指 animation-timing-function = linear(直線) || cubic-bezier-timing-function(貝塞爾曲線)
。
「非線性變化動畫」 是指 animation-timing-function = step-timing-function(分段)
。
推薦一個貝塞爾曲線的可視化網址:✿ cubic-bezier.com
非線性變化動畫,一般用來作 「幀動畫」。一般是設計師輸出一組序列幀圖片做背景圖,動畫時控制 background-position
屬性,並經過 step-timing-function
實現躍遷效果。
什麼意思?我來舉個小人走路的例子:
其實就兩張圖,分別是擡左腳和擡右腳(120 X 160),用工具將它們合成在一張圖裏(120 X 320)。
代碼以下:
<div class="anim linear"></div> <div class="anim steps"></div> 複製代碼
.anim{ width:120px; height:160px; background-image:url(./spirit.png); /* 合成圖 */ background-position:0 0; background-size:100% auto; } .liear{ animation:anim-walk 0.4s linear 0s infinite; } .steps{ animation:anim-walk 0.4s steps(1) 0s infinite; } @keyframes anim-walk{ 0% {background-position:0px 0px;} 50% {background-position:0px -160px;} 100% {background-position:0px 0px;} } 複製代碼
效果:
linear: | steps: |
---|---|
在 CSS 代碼裏,咱們定義了一個 叫 anim-walk
的一組關鍵幀,關鍵幀 0% 時 background-position-y
是 0, 50% 時 爲 -160 ,100% 時又回到 0。 從效果圖裏能夠看出,不一樣的 animation-timing-function
設置對動畫效果的影響。
linear
由於是線性變化,因此 0 ~ -160 ~ 0 之間的數據計算出來就是 0 ~ -40 ~ -80 ~ -120 ~ -160 ~ -120 ~ -80 ~ -40 ~ 0
steps
由於是非線性變化, 因此 0 ~ -160 ~ 0 之間的數據計算出來就是 0 ~ 0 ~ 0 ~ 0 ~ -160 ~ -160 ~ -160 ~ -160 ~ 0
在京喜工廠項目裏,小人是要在工廠的幾個點內移動。
最簡單的方法是用 offset-path
,用法在張鑫旭的這篇文章寫得很是詳細了:offset-path-css-animation。
缺點是兼容性較差,這裏就不詳細說明了。
在京喜工廠項目裏小人移動的路徑能夠從下面這個設定圖,標註的圓點都是要停留工做的。
能夠肯定的是,這些標註的圓點位置在 CSS 動畫裏確定是一個關鍵幀,而圓點與圓點之間的直線路徑還好辦,那曲線呢?
這裏我用到「CSS分層動畫」和「時間函數爲貝塞爾曲線的反作用」。 簡單來講,就是經過使用兩個或多個元素實現動畫效果(分層),咱們能夠更加細粒度地控制某個元素的路徑,沿着 X 軸運動使用一種 timing-function ,沿着 Y 軸運動使用另外一種 timing-function 。
假設有 A[0,0]、B[100,100]
兩點。從 A 移動到 B ,咱們能夠分拆成 X 軸的變化量,和 Y 軸的變化量。直線移動時,就是 X 軸與 Y 軸的累計變化量是同樣的:
<div class="anim-x"> <div class="anim-y"> </div> </div> 複製代碼
.anim-x{ animation: anim-x 1000ms 0s linear infinite; } .anim-y{ animation: anim-y 1000ms 0s linear infinite; } @keyframes anim-x { 0%{ transform:translate3d(0 , 0 , 0) } 100%{ transform:translate3d(100px , 0 , 0) } } @keyframes anim-y { 0%{ transform:translate3d(0 , 0 , 0) } 100%{ transform:translate3d(0 , 100px , 0) } } 複製代碼
反過來,若是 X軸 與 Y軸 的累計變化量不同,就會走出曲線:
<div class="anim-x"> <div class="anim-y"> </div> </div> 複製代碼
.anim-x{ animation: anim-x 1000ms 0s ease-in infinite; } .anim-y{ animation: anim-y 1000ms 0s ease-out infinite; } @keyframes anim-x { 0%{ transform:translate3d(0 , 0 , 0) } 100%{ transform:translate3d(100px , 0 , 0) } } @keyframes anim-y { 0%{ transform:translate3d(0 , 0 , 0) } 100%{ transform:translate3d(0 , 100px , 0) } } 複製代碼
這篇文章把原理寫得很詳細:CSS分層動畫可讓元素沿弧形路徑運動
路徑動畫的問題解決了,小人走路和工做的幀動畫也準備好,下面還有兩個小問題:
(1)小人走路和工做的幀動畫不能同時出現。
(2)路徑動畫從左走到右時小人的朝向,應該與從右走到左時相反。
這裏的解決方法也是「CSS分層動畫」和 「非線性動畫」。
再加多一層轉向動畫,一層控制 「小人走路幀動畫」 的動畫、一層控制 「小人工做幀動畫」 的動畫,這三個控制動畫都是「非線性動畫」。
大概的代碼能夠這樣寫:
<div class="anim-turn"> <div class="anim-walk"></div> <div class="anim-work"></div> </div> 複製代碼
.anim-turn{
animation: anim-turn ${allTime}ms 0s steps(1) infinite;
}
.anim-walk{
animation: anim-walk-opacity ${allTime}ms 0s steps(1) infinite,anim-walk 0.4s steps(1) 0s infinite;
}
.anim-work{
animation: anim-work-opacity ${allTime}ms 0s steps(1) infinite,anim-working 0.4s steps(1) 0s infinite;
}
@keyframes anim-turn { // 轉向動畫
0% {transform:scale(1,1)} // 正向
50% {transform:scale(-1,1)} // 反向
100% {transform:scale(1,1)} // 正向
}
@keyframes anim-walk-opacity { // 控制「小人走路幀動畫」的動畫
0% {opacity:1}
50% {opacity:0}
100% {opacity:1}
}
@keyframes anim-work-opacity { // 控制「小人工做幀動畫」的動畫
0% {opacity:0}
50% {opacity:1}
100% {opacity:0}
}
複製代碼
再加上 X軸 和 Y軸 的分層 CSS:
<div class="anim-x"> <div class="anim-y"> <div class="anim-turn"> <div class="anim-walk"></div> <div class="anim-work"></div> </div> </div> </div> 複製代碼
.anim-x{ animation: anim-x ${allTime}ms 0s linear infinite; } .anim-y{ animation: anim-y ${allTime}ms 0s linear infinite; } .anim-turn{ animation: anim-turn ${allTime}ms 0s steps(1) infinite; } .anim-walk{ animation: anim-walk-opacity ${allTime}ms 0s steps(1) infinite,anim-walk 0.4s steps(1) 0s infinite; } .anim-work{ animation: anim-work-opacity ${allTime}ms 0s steps(1) infinite,anim-working 0.4s steps(1) 0s infinite; } @keyframes ... 複製代碼
上面只是簡單的寫了這個動畫的簡單架構,而具體的動畫數據 @keyframes
纔是重點,並且這些關鍵幀確定不是手寫。
工欲善其事,必先利其器。
因此咱們來用 Vue 打造一個可視化工具[doge]。
大概長這樣子:
基本操做是「添加關鍵幀」、「調整每一個關鍵幀的屬性」、「生成測試動畫」、「輸出動畫CSS」。
「添加關鍵幀」:
「調整每一個關鍵幀的屬性」:
「生成測試動畫-輸出動畫CSS」:
工具的具體實現略過,這裏說一些關鍵細節:
(1)如何畫出動畫路徑?
(2)動畫時間怎麼算?
在路徑動畫裏,每兩個關鍵幀肯定了一段時間內元素的起點與終點,而時間函數着決定了這段時間內 X軸 與 Y軸 的變化量,咱們能夠將這段時間平均分爲 N 端,而後分別求出這 N 端時間終點時元素的位置,用直線連起來就能夠獲得一條近似的曲線。
舉個例子:
<div class="anim-x"> <div class="anim-y"> </div> </div> 複製代碼
@keyframes anim-x { 0%{ transform:translate3d(0 , 0 , 0); animation-timing-function:linear} 100%{ transform:translate3d(300px , 0 , 0) } } @keyframes anim-y { 0%{ transform:translate3d(0 , 0 , 0); animation-timing-function:cubic-bezier(0,.26,.74,1) } 100%{ transform:translate3d(0 , 300px , 0) } } .anim-x{ animation: anim-x 1000ms 0s; } .anim-y{ animation: anim-y 1000ms 0s; } 複製代碼
在這個例子裏,元素的 X軸 從 0 ~ 300 , animation-timing-function
是 linear
, Y軸 從 0 ~ 300, animation-timing-function
是 cubic-bezier(0,.26,.74,1)
,而後將時間長度定義爲 1,平均分爲 100 段,使用 for 循環求出不一樣進度時的 X 和 Y:
const moveTo = [0,0]; const step = 100; const dX = 300; const dY = 300; const timeFunX = "linear"; const timeFunY = "cubic-bezier(0,.26,.74,1)"; if (Math.abs(dX) > 0 || Math.abs(dY) > 0) { ctx.moveTo(moveTo[0],moveTo[1]); for(let i = 0;i <= step;i ++) { const x = getTimeFunctionValue(timeFunX,i/step) * dX + moveTo[0]; // 求出 timeFunX(linear) 對應時間進度下的 x const y = getTimeFunctionValue(timeFunY,i/step) * dY + moveTo[1]; // 求出 timeFunY(cubic-bezier(0,.26,.74,1)) 對應時間進度下的 y ctx.fillRect(x, y, 1, 1); if (i % 10 === 0) { ctx.font = "16px serif"; ctx.fillText(`(${x},${(y).toFixed(2)})`, x + 20, y + 20); } } } 複製代碼
效果以下:
剩下就是這個 getTimeFunctionValue(時間函數,時間進度[0,1])
應該怎麼寫?
首先要把 linear
和 其餘的貝塞爾曲線分開, linear
其實就是一條直線,時間進度輸入任何值,都返回相同的值。
function getTimeFunctionValue(timeFunctionName = "linear",x = 0){ ... if (timeFunctionName === "linear") return x; ... } 複製代碼
貝塞爾曲線呢?先來補習一下 CSS 動畫裏的貝塞爾曲線時間函數。
「貝塞爾曲線」是一種參數函數。計算機中應用比較普遍的是「三次貝塞爾曲線」。
P0、P一、P二、P3四個點在平面或在三維空間中定義了三次方貝塞爾曲線。曲線起始於P0走向P1,並從P2的方向來到P3。通常不會通過P1或P2;這兩個點只是在那裏提供方向信息。P0和P1之間的間距,決定了曲線在轉而趨進P2以前,走向P1方向的「長度有多長」。
曲線的參數形式爲:
CSS 動畫裏的貝塞爾曲線時間函數是一個簡化版的「三次貝塞爾曲線」,P0 固定爲 [0,0], P3 固定爲 [1,1]。
並且其直角座標系是區別於幾何座標(x,y),而是有其餘意義的,橫軸表明的是「時間進度(time)」,取值爲[0% ~ 100%]。豎軸表明的是屬性的「變化程度(progression)」,這個取值通常會在[0% ~ 100%],能夠小於0%,也能夠大於100%。
因此 這個簡化版的 CSS 貝塞爾曲線能夠用下面這兩個方程表示(代入 P0[0,0] P3[1,1]):
T(時間進度) = ...
P(變化百分比) = ...
cubic-bezier(0,.26,.74,1)
裏面的參數其實就是(P1_time,P1_progression,P2_time,P2_progression)。以下圖所示:
取 cubic-bezier(0,.26,.74,1)
裏面的參數(P1[0,0.26],P2[0.74,1]),代入上面的兩個公式,獲得下面的結果(方程):
T(時間進度) = ...
P(變化百分比) = ...
第一條方程中的 T 就是時間進度,是入參,解出這條 關於 t 的一元三次函數的根,代入第二條方程中,就能夠求得 P。P 就是 T 「時間進度」下的「變化程度」。
注意:三次函數有3個根,可是隻有實數並且在[0 ~ 1]之間的是正解。
上面咱們用積分的方法將動畫路徑近似的畫出來,就至關於咱們能夠求出動畫路徑的長度的近似值。長度 / 速度 = 動畫時間。其中速度能夠自定義。
京喜工廠還有一個傳送帶動畫,你們能夠看看下圖的最第一版本:
上面這張圖,能夠看到貨物從左往右沿着傳送帶移動。最初左邊看着還挺正常,可是到了右邊會出現後方貨物把前邊貨物蓋壓的現象。
緣由也很簡單,由於這幾個貨物是並排的元素,後面的元素層級總會比前面的高。就像下面這樣:
<div> <div>1</div> <!-- 顯示層級最低 --> <div>2</div> <div>3</div> <div>4</div> <div>5</div> <div>6</div> <!-- 顯示層級最高 --> </div> 複製代碼
但這個動畫想表現的層級是中間高,兩邊低。
有些同窗可能會想到用 z-index ,惋惜 z-index 在 CSS 動畫裏是不起做用的。
正確的解決方案是用 translateZ 將其轉換成 3D 顯示,從而實現中間高,兩邊低的層級:
@keyframes anim-z{ 0% {transform: perspective(500px) translateZ(0);} 50% {transform: perspective(500px) translateZ(50px);} 100% {transform: perspective(500px) translateZ(0);} } 複製代碼
增長後的效果:
幀動畫這裏還有一個抖動的問題,看上面的 gif 能夠發現小人有點抖動。這張 gif 是在 iPhone 6 Plus(手機屏幕像素是 414 px)上的顯示效果。
問題是出在單位轉換上:移動端的適配時,一般是用 rem ,小程序是用 rpx,他們在計算成 px 過程當中可能會出現取整的問題,從而形成幀動畫抖動。
逐幀動畫抖動的研究,看 「凹凸實驗室」 的這篇文章就夠了:《CSS技巧:逐幀動畫抖動解決方案》
這篇文章提出了三個方案 A、B、C ,其中方案C是「終極解決方案」。惋惜的是,這個方案用到的是 SVG,而小程序是不支持 SVG 的。
退而求其次,我選擇了方案 A ,就是用 CSS 的媒體查詢來寫斷點,斷點裏都用 px 作單位。
/* 300 ~ 349 之間用 iphone5(320)的數據 */ @media screen and (min-width: 300px) and (max-width: 349px) { .m_worker_employee { width:51px; height: 68px } @keyframes anim-working { 0% { background-position: 0px -204px } 50% { background-position: 0px -272px } 100% { background-position: 0px -204px } } } /* 350 ~ 399 之間用 iphone6(375)的數據 */ @media screen and (min-width: 350px) and (max-width: 399px) { .m_worker_employee { width:60px; height: 80px } @keyframes anim-working { 0% { background-position: 0px -240px } 50% { background-position: 0px -320px } 100% { background-position: 0px -240px } } } /* 400 ~ 449 之間用 iphone6P(414)的數據 */ @media screen and (min-width: 400px) and (max-width: 449px) { .m_worker_employee { width:66px; height: 88px } @keyframes anim-working { 0% { background-position: 0px -264px } 50% { background-position: 0px -352px } 100% { background-position: 0px -264px } } } 複製代碼
斷點應用後,幀動畫就不抖了。
最後總結一句:前端動畫方案有不少種,可是要根據使用的環境選擇最合適的;調試動畫很繁瑣,關鍵是要用合適的工具,沒有就本身造一個。
本文的公式是使用 「TeX」軟件,而後利用「MathJax」輸出爲 SVG ,在此推薦一下:www.mathjax.org/#demo
若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送: