传送门:从0到1,开发一个动画库(1)
上一节讲到了最基础的内容,为动画构建“帧-值”对应的函数关系,完成“由帧到值”的计算过程。这一节将在上节代码的基础上谈谈如何给一个完整的动画添加各类事件。
在添加各类事件之前,我们先对_loop
循环函数进行一些改进:
_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
方法用于主动结束动画:
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
重置动画的方式也是大同小异,与上面一样
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
方法传入当前时间进度:
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
方法内增加一行代码即可:
_play() {
this.state = ‘play‘;
// 新增部分
this.onPlay && this.onPlay();
this.beginTime = Date.now();
const loop = this._loop.bind(this);
window.requestAnimationFrame(loop);
}```
完整代码如下:
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的代码也更新如下,添加了各类按钮,主动触发目标的各类事件:
<!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>
看到这里,我们第二节的内容就结束啦,下一节,我们将介绍:
- 支持自定义路径动画
- 动画间的链式调用
下一节再见啦^_^