背景
138.2億年前,世界上沒有時間和空間,或許世界都不存在,在一個似有似無的點上,彙集了所有的物質,它孕育著無限的能量與可能性。
宇宙大爆炸
巨大的內力已無法被抑制,瞬間爆發,它爆炸了!世界上有了時間和空間,隨著歲月的變遷,時光的流逝,無數的星系、恆星、衛星、彗星形成。我們生活的地球,只是茫茫宇宙中的一個小小的天體,或許在遙遠的宇宙的另一邊,會有平行世界的存在,或許在那裡,我們可能是醫生、老師、公務員。科學家說我們的宇宙正在加速度的膨脹,暗能量在無限吞噬著暗物質,未來的世界將會變得虛無縹緲。
人類起源
宇宙的形成,帶來了無限可能性,人類釋放著慾望和剋制,對宇宙的渴望產生於公元前五世紀,古巴比倫人通過觀察天體的位置以及外觀變化,來預測人世間的各種事物。在遙遠的古羅馬,人們也舞弄著靈魂,把不羈的想象賦予肉體。Anim,來源於拉丁語,代表著靈魂與生命,代表著所有與生俱來。似乎世間萬物都存在聯絡,宇宙、自然都存在靈魂。
動畫的形成
兩萬五前年錢的石器時代,石洞中的野獸奔跑分析圖,這是人類開始試圖捕捉動作最早證據。文藝復興時期的達芬奇畫作上,用兩隻手臂兩條腿來標識上下襬動的動作,在一張畫作上做出不同時間的兩個動作。 直到1906年,世界上第一部動畫片《滑稽臉的幽默相》問世。
所以動畫是否就是將多個畫面連起來播放呢?
時間是連續的嗎?是可以無線分割的嗎?我也不太清楚,你看到的流星、人們的動作是連續的嗎?或許是吧,畢竟現實生活中還沒有像瞬間移動這種事情發生吧。
神經可能不是連續的,生物課學過,神經的傳遞是一個電訊號傳遞過程,並且是顆粒的(神經訊號),那麼我們看到的東西在我們腦海裡的成像一定不是連續的。
那麼我們為什麼能看到連續的動作呢?
視覺暫留(Persistence of vision),讓我們看到了連續的畫面,視神經反應速度大約為1/16秒,每個人不太一樣,有些人高一點,一些人低一點。上一次視神經傳遞的影象將會在大腦中存留,直到下一次神經訊號到達。維基百科上說,日常用的日光燈每秒鐘大約會熄滅100次,但是你並沒有感覺。
一般電影的在幀率在24FPS以上,一般30FPS以上大腦會認為是連貫的,我們玩的遊戲一般在30FPS,高幀率是60FPS。
小時候一定看過翻頁動畫吧,可以看一看翻頁動畫-地球進化史
前端動畫實現
Atwood 定律:Any application that can be written in JavaScript will eventually be written in JavaScript.
前端做動畫不是什麼新鮮事了,從jQuery時代,到當下,無不是前端動畫橫行的時代。
我們知道多張不同的影象連在一起就變成了動態的影象。
在前端的世界裡,瀏覽器在視覺暫留時間內,連續不斷的逐幀輸出影象。每一幀輸出一張影象。
提及動畫一定會討論到幀率(FPS, Frame Per Second),代表每秒輸出幀數,也就是瀏覽器每秒展示出多少張靜態的影象。
DOM動畫中的 CSS3
CSS3 動畫是當今盛行的 Web 端製作動畫的方式之一,對於移動裝置來說覆蓋率已經非常廣泛,在日常開發中可以使用。CSS3 動畫只能通過對 CSS 樣式的改變控制 DOM 進行動畫
DOM動畫中的 WebAnimation
WebAnimation 還在草案階段,在Chrome可以嘗試使用一下。移動裝置還是相當慘烈,iOS 並沒有開始支援。
CSS3 和 WebAnimation 都只能作用於DOM,那麼,如果我們想讓 Canvas 上的物件產生動畫,那我們該怎麼辦呢?
JavaScript
既然我們知道動畫的原理,其實就是讓使用者看到連續的圖片,並且每一張圖片是有變化的。
對於事物來講,我們可以通過改變某些數值來修改他的屬性,從來改變他的外在展示。比如正方形的邊長,顏色的RGB值,颱風的位置(世界座標),在每一幀去改變這些數值,根據這些數值將物件繪依次制到螢幕上,將會產生動畫。
通過上面的描述,我們知道,實現一個動畫,其實是數值隨時間變化,以幀為時間單位。
在很久很久以前,JavaScript 使用 setInterval
進行定時呼叫函式。所以可以使用setInterval來進行數值的改變。
為了更好的讓各位前端小哥哥小姐姐們做動畫,出現了requestAnimationFrame
,requestAnimationFrame
接收一個函式,這個函式將在下一幀渲染之前執行,也就是說,不需要太多次的計算,只要在下一幀渲染之前,我們將需要修改的數值修改掉即可。requestAnimationFrame
的幀率和硬體以及瀏覽器有關,一般是60FPS(16.66666666ms/幀)。
我們利用 Dom 進行動畫的演示~
元素移動
建立一個方塊
1 |
<div class=“box”></div> |
設定寬高和背景顏色
1 2 3 4 5 6 |
.box { width: 100px; height: 100px; background: red; } |
1 2 3 4 5 6 7 8 9 10 |
const box = document.querySelector('.box') // 獲取方塊元素 let value = 0 // 設定初始值 // 建立每一幀渲染之前要執行的方法 const add = () => { requestAnimationFrame(add) // 下一幀渲染之前繼續執行 add 方法 value += 5 // 每幀加數值增加5 box.style.transform = `translateX(${value}px)` // 將數值設定給 方塊 的 css 屬性 transform 屬性可以控制元素在水平方向上的位移 } requestAnimationFrame(add) // 下一幀渲染之前執行 add 方法 |
這樣,方塊每幀向右移動 5 畫素,每秒移動60*5=300
畫素,不是每秒跳動一下,而是一秒在300畫素內均勻移動哦。
補間動畫
上一個demo實現了小方塊從左到右的移動,但是貌似他會永無止境的移動下去,直到數值溢位,小時候學過flash的朋友都知道補間動畫,其實就是讓小方塊0px到300px平滑移動。其實就是固定的時間點,有固定的位置。
所以我們只需要根據運動的已過時間的百分比去計算數值。
保持之前的 HTML 和 CSS 不變
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * 執行補間動畫方法 * * @param {Number} start 開始數值 * @param {Number} end 結束數值 * @param {Number} time 補間時間 * @param {Function} callback 每幀的回撥函式 */ function animate(start, end, time, callback) { let startTime = performance.now() // 設定開始的時間戳 let differ = end - start // 拿到數值差值 // 建立每幀之前要執行的函式 function loop() { raf = requestAnimationFrame(loop) // 下一陣呼叫每幀之前要執行的函式 const passTime = performance.now() - startTime // 獲取當前時間和開始時間差 let per = passTime / time // 計算當前已過百分比 if (per >= 1) { // 判讀如果已經執行 per = 1 // 設定為最後的狀態 cancelAnimationFrame(raf) // 停掉動畫 } const pass = differ * per // 通過已過時間百分比*開始結束數值差得出當前的數值 callback(pass) // 呼叫回撥函式,把數值傳遞進去 } let raf = requestAnimationFrame(loop) // 下一陣呼叫每幀之前要執行的函式 } |
我們呼叫一下補間動畫,讓數值經過1秒勻速從0變成400。
1 2 3 4 5 |
let box = document.querySelector() animate(0, 400, 1000, value => { box.style.transform = `translateX(${value}px)` // 將數值設定給 方塊 的 css 屬性 transform 屬性可以控制元素在水平方向上的位移 }) |
一個簡單的勻速補間動畫就這麼被我們做好了。
非勻速動畫
那萬一,這個動畫不是非勻速的,比如抖一抖啊,彈一彈,那該怎麼辦呢?
當然也是一樣,根據已過時間的百分比去計算數值
時間是勻速的,但是數值不是,如果數值變化是有規律的,那麼我們就可以使用時間來表示數值,建立一個接收時間比例(當前時間百分比),返回當前位置比例(當前位置百分比)的方法。
我們稱這個方法叫做緩動方法。
如果速度從慢到快,我們可以把時間和數值的影象模擬成以下的樣子。
公式為 rate = time ^ 2
對應的函式應該是
1 2 3 4 |
function easeIn(time) { // 接收一個當前的時間佔總時間的百分比比 return time ** 2 } |
這個實現加速後抖動結束的效果,在Time小於0.6時是一個公式,time大於0.6是另外一個公式。
Time < 0.6 時: Rate = Time / 0.6 ^ 2
Time > 0.6 時: Rate = Math.sin((Time-0.6) ((3 Math.PI) / 0.4)) * 0.2 + 1
最終實現的函式是
1 2 3 4 5 6 7 |
function shake(time) { if (time < 0.6) { return (time / 0.6) ** 2 } else { return Math.sin((time-0.6) * ((3 * Math.PI) / 0.4)) * 0.2 + 1 } } |
我們改造一下之前的 animate
函式,接收一個 easing 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/** * 執行補間動畫方法 * * @param {Number} start 開始數值 * @param {Number} end 結束數值 * @param {Number} time 補間時間 * @param {Function} callback 每幀回撥 * @param {Function} easing 緩動方法,預設勻速 */ function animate(start, end, time, callback, easing = t => t) { let startTime = performance.now() // 設定開始的時間戳 let differ = end - start // 拿到數值差值 // 建立每幀之前要執行的函式 function loop() { raf = requestAnimationFrame(loop) // 下一陣呼叫每幀之前要執行的函式 const passTime = performance.now() - startTime // 獲取當前時間和開始時間差 let per = passTime / time // 計算當前已過百分比 if (per >= 1) { // 判讀如果已經執行 per = 1 // 設定為最後的狀態 cancelAnimationFrame(raf) // 停掉動畫 } const pass = differ * easing(per) // 通過已過時間百分比*開始結束數值差得出當前的數值 callback(pass) } let raf = requestAnimationFrame(loop) // 下一陣呼叫每幀之前要執行的函式 } |
測試一下,將我們剛剛建立的 easing 方法傳進來
加速運動
1 2 3 4 5 |
let box = document.querySelector('.box') animate(0, 500, 400, value => { box.style.transform = `translateX(${value}px)` // 將數值設定給 方塊 的 css 屬性 transform 屬性可以控制元素在水平方向上的位移 }, easeIn) |
加速後抖一抖
1 2 3 4 5 |
let box = document.querySelector('.box') animate(0, 500, 400, value => { box.style.transform = `translateX(${value}px)` // 將數值設定給 方塊 的 css 屬性 transform 屬性可以控制元素在水平方向上的位移 }, shake) |
總結
這些只是 JavaScript 動畫基礎中的基礎,理解動畫的原理後,再做動畫就更得心應手了。
市面上有很多JS動畫庫,大家可以開箱即用。有一些是針對DOM操作的,也有一些是針對 JavaScript 物件。實現原理你都已經懂了。
上述程式碼已釋出到Github: https://github.com/fanmingfei/animation-base