傳送門:從0到1,開發一個動畫庫(2)
如今市面上關於動畫的開源庫多得數不勝數,有關於CSS、js甚至是canvas渲染的,百花齊放,效果炫酷。但你是否曾想過,自己親手去實現(封裝)一個簡單的動畫庫?
本文將從零開始,講授如何搭建一個簡單的動畫庫,它將具備以下幾個特徵:
- 從實際動畫中抽象出來,根據給定的動畫速度曲線,完成“由幀到值”的計算過程,而實際渲染則交給開發者決定,更具擴充性
- 支援基本的事件監聽,如
onPlay
、onStop
、onReset
、onEnd
,及相應的回撥函式 - 支援手動式觸發動畫的各種狀態,如
play
、stop
、reset
、end
- 支援自定義路徑動畫
- 支援多組動畫的鏈式觸發
完整的專案在這裡:點贊行為高尚!,歡迎各種吐槽和指正^_^
OK,話不多說,現在正式開始。
作為開篇,本節將介紹的是最基本、最核心的步驟——構建“幀-值”對應的函式關係,完成“由幀到值”的計算過程。
目錄結構
首先介紹下我們的專案目錄結構:
1 2 3 4 |
/timeline /index..js /core.js /tween.js |
/timeline
是本專案的根目錄,各檔案的作用分別如下:
index.js
專案入口檔案core.js
動畫核心檔案easing.js
存放基本緩動函式
引入緩動函式
所謂動畫,簡單來說,就是在一段時間內不斷改變目標某些狀態的結果。這些狀態值在運動過程中,隨著時間不斷髮生變化,狀態值與時間存在一一對應的關係,這就是所謂的“幀-值”對應關係,常說的動畫緩動函式也是相同的道理。
有了這種函式關係,給定任意一個時間點,我們都能計算出對應的狀態值。OK,那如何在動畫中引入緩動函式呢?不說廢話,直接上程式碼:
首先我們在core.js中建立了一個Core
類:
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 28 29 30 31 32 |
class Core { constructor(opt) { // 初始化,並將例項當前狀態設定為'init' this._init(opt); this.state = 'init'; } _init(opt) { this._initValue(opt.value); // 儲存動畫總時長、緩動函式以及渲染函式 this.duration = opt.duration || 1000; this.timingFunction = opt.timingFunction || 'linear'; this.renderFunction = opt.render || this._defaultFunc; // 未來會用到的事件函式 this.onPlay = opt.onPlay; this.onEnd = opt.onEnd; this.onStop = opt.onStop; this.onReset = opt.onReset; } _initValue(value) { // 初始化運動值 this.value = []; value.forEach(item => { this.value.push({ start: parseFloat(item[0]), end: parseFloat(item[1]), }); }); } } |
我們在建構函式中對例項呼叫_init
函式,對其初始化:將傳入的引數儲存在例項屬性中。
當你看到_initValue
的時候可能不大明白:外界傳入的value
到底是啥?其實value
是一個陣列,它的每一個元素都儲存著獨立動畫的起始與結束兩種狀態。這樣說好像有點亂,舉個例子好了:假設我們要建立一個動畫,讓頁面上的div同時往右、左分別平移300px、500px,此外還同時把自己放大1.5倍。在這個看似複雜的動畫過程中,其實可以拆解成三個獨立的動畫,每一動畫都有自己的起始與終止值:
- 對於往右平移,就是把css屬性的
translateX
的0px變成了300px - 同理,往下平移,就是把
tranlateY
的0px變成500px - 放大1.5倍,也就是把
`scale
從1變成1.5
因此傳入的value應該長成這樣:[[0, 300], [0, 500], [1, 1.5]]
。我們將陣列的每一個元素依次儲存在例項的value屬性中。
此外,renderFunction
是由外界提供的渲染函式,即opt.render
,它的作用是:
動畫運動的每一幀,都會呼叫一次該函式,並把計算好的當前狀態值以引數形式傳入,有了當前狀態值,我們就可以自由地選擇渲染動畫的方式啦。
接下來我們給Core類新增一個迴圈函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
_loop() { const t = Date.now() - this.beginTime, d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; if (t >= d) { this.state = 'end'; this._renderFunction(d, d, func); } else { this._renderFunction(t, d, func); window.requestAnimationFrame(this._loop.bind(this)); } } _renderFunction(t, d, func) { const values = this.value.map(value => func(t, value.start, value.end - value.start, d)); this.renderFunction.apply(this, values); } |
_loop
的作用是:倘若當前時間進度t
還未到終點,則根據當前時間進度計算出目標現在的狀態值,並以引數的形式傳給即將呼叫的渲染函式,即renderFunction
,並繼續迴圈。如果大於duration
,則將目標的運動終止值傳給renderFunction
,運動結束,將狀態設為end
。
程式碼中的Tween
是從tween.js檔案引入的緩動函式,tween.js的程式碼如下(網上搜搜基本都差不多= =):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * t: current time(當前時間); * b: beginning value(初始值); * c: change in value(變化量); * d: duration(持續時間)。 * Get effect on 'http://easings.net/zh-cn' */ const Tween = { linear: function (t, b, c, d) { return c * t / d + b; }, // Quad easeIn: function (t, b, c, d) { return c * (t /= d) * t + b; }, easeOut: function (t, b, c, d) { return -c * (t /= d) * (t - 2) + b; }, easeInOut: function (t, b, c, d) { if ((t /= d / 2) |
最後,給Core
類增加play
方法:
1 2 3 4 5 6 7 8 9 10 11 |
_play() { this.state = 'play'; this.beginTime = Date.now(); // 執行動畫迴圈 const loop = this._loop.bind(this); window.requestAnimationFrame(loop); } play() { this._play(); } |
core.js的完整程式碼如下:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
import Tween from './tween'; class Core { constructor(opt) { this._init(opt); this.state = 'init'; } _init(opt) { this._initValue(opt.value); this.duration = opt.duration || 1000; this.timingFunction = opt.timingFunction || 'linear'; this.renderFunction = opt.render || this._defaultFunc; /* Events */ this.onPlay = opt.onPlay; this.onEnd = opt.onEnd; this.onStop = opt.onStop; this.onReset = opt.onReset; } _initValue(value) { this.value = []; value.forEach(item => { this.value.push({ start: parseFloat(item[0]), end: parseFloat(item[1]), }); }) } _loop() { const t = Date.now() - this.beginTime, d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; if (t >= d) { this.state = 'end'; this._renderFunction(d, d, func); } else { this._renderFunction(t, d, func); window.requestAnimationFrame(this._loop.bind(this)); } } _renderFunction(t, d, func) { const values = this.value.map(value => func(t, value.start, value.end - value.start, d)); this.renderFunction.apply(this, values); } _play() { this.state = 'play'; this.beginTime = Date.now(); const loop = this._loop.bind(this); window.requestAnimationFrame(loop); } play() { this._play(); } } window.Timeline = Core; |
在html中引入它後就可以愉快地呼叫啦^ _ ^
PS:該專案是用webpack打包並以timeline.min.js作為輸出檔案,由於暫時沒用到index.js檔案,因此暫時以core.js作為打包入口啦~
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 28 29 |
<!DOCTYPE html> <html> <head> <title></title> <style type="text/css"> #box { width: 100px; height: 100px; background: green; } </style> </head> <body> <div id="box"></div> <script type="text/javascript" src="timeline.js"></script> <script type="text/javascript"> const box = document.querySelector('#box'); const timeline = new Timeline({ duration: 3000, value: [[0, 400], [0, 600]], render: function(value1, value2) { box.style.transform = `translate(${ value1 }px, ${ value2 }px)`; }, timingFunction: 'easeOut', }) timeline.play(); </script> </body> </html> |
看到這裡,本文就差不多結束了,下節將介紹如何在專案中加入各類事件監聽及觸發方式。
本系列文章將會繼續不定期更新,歡迎各位大大指正^_^