傳送門:從0到1,開發一個動畫庫(1)
上一節講到了最基礎的內容,為動畫構建“幀-值”對應的函式關係,完成“由幀到值”的計算過程。這一節將在上節程式碼的基礎上談談如何給一個完整的動畫新增各類事件。
在新增各類事件之前,我們先對_loop
迴圈函式進行一些改進:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
_loop() { const t = Date.now() - this.beginTime, d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; if (this.state === 'end' || t >= d) { this._end(); } else if (this.state === 'stop') { this._stop(t); } else if (this.state === 'init') { this._reset(); } else { this._renderFunction(t, d, func) window.requestAnimationFrame(this._loop.bind(this)); } } |
可以清晰地看到,我們在迴圈中增加了很多型別的判斷,根據state
當前不同的狀態執行相應的處理函式:我們新增了_end
、_stop
、_reset
方法分別去處理結束、暫停和重置這三種狀態,接下來我們依次講解這些狀態的處理。
End
我們在Core類下增加_end
、end
和renderEndState
方法,end
方法用於主動結束動畫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
end() { this.state === 'play' ? (this.state = 'end') : this._end(); } _end() { this.state = 'end'; this._renderEndState(); this.onEnd && this.onEnd(); } _renderEndState() { const d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; this._renderFunction(d, d, func); } |
通過執行end
方法,我們可以主動結束動畫:如果當前目標處於運動狀態,則將其設定為end
,因此下一個_loop
函式被執行的時候,程式就被流向了_end
處理函式;若為其他狀態,意味著迴圈沒有被開啟,我們就直接呼叫_end
方法,使其直接到終止狀態。
_end
函式的作用有三個:
- 將當前狀態設定為
end
(為何要重複設定一次狀態呢?這不是多餘的嗎?其實,倘若我們主動觸發end
去結束動畫,這的確是多餘的,但如果是動畫自己進行到了末尾,也就是t >= d
的時刻,則必須得在_end
中去設定狀態,以確保它處於結束狀態) - 通過
_renderEndState
方法,將目標變成結束狀態 - 若有回撥函式則執行回撥
Reset
重置動畫的方式也是大同小異,與上面一樣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
reset() { this.state === 'play' ? (this.state = 'init') : this._reset(); } _reset() { this.state = 'init'; this._renderInitState(); this.onReset && this.onReset(); } _renderInitState() { const d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; this._renderFunction(0, d, func); } |
Stop
讓動畫暫停也是與上述兩者一樣,但唯一的區別是,需要給_renderStopState
方法傳入當前時間進度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
stop() { if (this.state === 'play') { this.state = 'stop'; } else { // 使目標暫停,無需像end或reset那樣將目標變成結束/起始狀態,保持當前狀態即可 this.state = 'stop'; this.onStop && this.onStop(); } } _stop(t) { this.state = 'stop'; this._renderStopState(t); this.onStop && this.onStop(); } _renderStopState(t) { const d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; this._renderFunction(t, d, func); } |
play
我們要在動畫開始執行的時候觸發onPlay
事件,只需在_play
方法內增加一行程式碼即可:
1 2 3 4 5 6 7 8 9 10 |
_play() { this.state = 'play'; // 新增部分 this.onPlay && this.onPlay(); this.beginTime = Date.now(); const loop = this._loop.bind(this); window.requestAnimationFrame(loop); }``` |
完整程式碼如下:
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
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 (this.state === 'end' || t >= d) { this._end(); } else if (this.state === 'stop') { this._stop(t); } else if (this.state === 'init') { this._reset(); } 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); } _renderEndState() { const d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; this._renderFunction(d, d, func); } _renderInitState() { const d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; this._renderFunction(0, d, func); } _renderStopState(t) { const d = this.duration, func = Tween[this.timingFunction] || Tween['linear']; this._renderFunction(t, d, func); } _stop(t) { this.state = 'stop'; this._renderStopState(t); this.onStop && this.onStop(); } _play() { this.state = 'play'; this.onPlay && this.onPlay(); this.beginTime = Date.now(); const loop = this._loop.bind(this); window.requestAnimationFrame(loop); } _end() { this.state = 'end'; this._renderEndState(); this.onEnd && this.onEnd.call(this); } _reset() { this.state = 'init'; this._renderInitState(); this.onReset && this.onReset(); } play() { this._play(); } end() { this.state === 'play' ? (this.state = 'end') : this._end(); } reset() { this.state === 'play' ? (this.state = 'init') : this._reset(); } stop() { if (this.state === 'play') { this.state = 'stop'; } else { this.state = 'stop'; this.onStop && this.onStop(); } } } window.Timeline = Core; |
相應地,html的程式碼也更新如下,新增了各類按鈕,主動觸發目標的各類事件:
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 |
<!DOCTYPE html> <html> <head> <title></title> <style type="text/css"> #box { width: 100px; height: 100px; background: green; } </style> </head> <body> <div id="box"></div> <button id="start">START</button> <button id="end">END</button> <button id="stop">STOP</button> <button id="reset">RESET</button> <script type="text/javascript" src="timeline.min.js"></script> <script type="text/javascript"> const el = (name) => document.querySelector(name); const box = el('#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', onPlay: () => console.log('play'), onEnd: () => console.log('end'), onReset: () => console.log('reset'), onStop: () => console.log('stop') }) el('#start').onclick = () => timeline.play(); el('#end').onclick = () => timeline.end(); el('#stop').onclick = () => timeline.stop() el('#reset').onclick = () => timeline.reset() </script> </body> </html> |
看到這裡,我們第二節的內容就結束啦,下一節,我們將介紹:
- 支援自定義路徑動畫
- 動畫間的鏈式呼叫
下一節再見啦^_^