dojo中动画部分分为两部分:dojo/_base/fx, dojo/fx。dojo/_base/fx部分是dojo动画的基石,里面有两个底层API:animateProperty、anim和两个常用动画:fadeIn、fadeOut(类似jQuery中的show、hide)。dojo/fx中有两个复合动画:chain(类似jQuery中的动画队列)、combine和三个动画函数:wipeIn、wipeOut、slideTo。
dojo中动画的原理与jquery类似,都是根据setInterval计算当前时间与初试时间差值与总时间的半分比来确定元素动画属性的当前计算值:
percent = (Date.now() - startTime) / duration;
value = (end - start) * percent + start;
接下来我们会一步步的演化,慢慢接近dojo API
首先我们实现一个动画函数,他接受以下参数:
- node: 动画元素
- prop: 动画属性
- start: 动画起始值
- end: 动画结束值
- duration: 动画执行时间
- interval:动画间隔
function Animate(node, prop, start, end, duration, interval) {
var startTime = Date.now();
var timer = setInterval(function(){
var percent = (Date.now() - startTime) / duration;
percent = percent < 0 ? 0 : percent;
percent = percent > 1 ? 1 : percent;
var v = (end - start) * percent + start;
node.style[prop] = v;
if (percent >= 1) {
clearInterval(timer);
}
}, interval);
}
示例:
dojo中所有的动画函数都返回一个Animation的实例:Animation拥有一系列的属性和动画控制方法,下面我们简单的实现play和stop方法:
function Animate(node, prop, start, end, duration, interval/*, delay*/) {
var timer = null;
var startTime =0;
function startTimer() {
timer = setInterval(function(){
var percent = (Date.now() - startTime) / duration;
percent = percent < 0 ? 0 : percent;
percent = percent > 1 ? 1 : percent;
var v = (end - start) * percent + start;
node.style[prop] = isFinite(v) ? v /*+ 'px'*/ : v;
if (percent >= 1) {
clearInterval(timer);
timer = null;
}
}, interval);
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
return {
play: function() {
if (startTime === 0) {
startTime = Date.now();
}
startTimer();
},
stop: function() {
stopTimer();
}
}
}
这里将上文中Animate函数放入startTimer函数中,增加stopTimer用来移除定时器。
示例:
下面我们要支持延时和暂停功能,实现延迟的方法在于使用setTimeout设置延时启动时间,对于暂停功能,我们需要记录所有的暂停时间,在计算当前百分比时减去所有的暂停时间。
function Animate(node, prop, start, end, duration, interval, delay) {
var timer = null;
var startTime =0;
var delayTimer = null;
var paused = false;
var pauseStartTime = null;
var pausedTime = 0;//记录所有的暂停时间
function startTimer() {
timer = setInterval(function(){
var percent = (Date.now() - startTime - pausedTime) / duration;减去暂停消耗的时间
percent = percent < 0 ? 0 : percent;
percent = percent > 1 ? 1 : percent;
var v = (end - start) * percent + start;
node.style[prop] = isFinite(v) ? v /*+ 'px'*/ : v;
if (percent >= 1) {
stopTimer();
}
}, interval);
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function clearDelayTimer() {
clearTimeout(delayTimer);
delayTimer = null;
}
return {
play: function() {
if (startTime === 0) {
startTime = Date.now();
}
if (paused) {
pausedTime += Date.now() - pauseStartTime;计算暂停时间
startTimer();
paused = false;
} else if (isFinite(delay)) {
delayTimer = setTimeout(function() {
clearDelayTimer();
startTime = Date.now();
startTimer();
}, delay); //delay延迟启动
} else {
startTimer();
}
},
pause: function() {
paused = true;
if (delayTimer) {
clearDelayTimer();
} else {
stopTimer();
pauseStartTime = Date.now();记录本次暂停起始时间
}
},
stop: function() {
stopTimer();
}
}
}
示例:
dojo/fx.animateProperty中可以设置多个动画属性,实现方式不难,只需要在每次动画计算时依次计算各个动画属性即可。
function Animate(node, props, duration, interval, delay) {
var timer = null;
var startTime =0;
var delayTimer = null;
var paused = false;
var pauseStartTime = null;
var pausedTime = 0;
function startTimer() {
timer = setInterval(function(){
var percent = (Date.now() - startTime - pausedTime) / duration;
percent = percent < 0 ? 0 : percent;
percent = percent > 1 ? 1 : percent;
for (var p in props) {
var prop = props[p];
node.style[p] = ((prop.end - prop.start) * percent + prop.start) + (prop.units ? prop.units : '');
}
if (percent >= 1) {
stopTimer();
}
}, interval);
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function clearDelayTimer() {
clearTimeout(delayTimer);
delayTimer = null;
}
return {
play: function() {
if (startTime === 0) {
startTime = Date.now();
}
if (paused) {
pausedTime += Date.now() - pauseStartTime;
startTimer();
paused = false;
} else if (isFinite(delay)) {
delayTimer = setTimeout(function() {
clearDelayTimer();
startTime = Date.now();
startTimer();
}, delay);
} else {
startTimer();
}
},
pause: function() {
paused = true;
if (delayTimer) {
clearDelayTimer();
} else {
stopTimer();
pauseStartTime = Date.now();
}
},
stop: function() {
stopTimer();
}
}
}
var btnPlay = document.getElementById('btnPlay');
var n = document.getElementById('anim');
var anim = Animate(n, {
opacity: {start: 0.3, end: 1},
width: {start:50, end: 500, units: 'px'}
}, 5000, 25, 1000);
btnPlay.onclick = function() {
anim.play();
}
btnPause = document.getElementById('btnPause');
btnPause.onclick = function() {
anim.pause();
}
翻看dojo代码(dojo/_base/fx line:567)我们会发现,在进行动画之前dojo对一些动画属性做了预处理:
- 针对width/height动画时,元素本身inline状态的处理
- 对于Opacity的处理,IE8以下在style中设置滤镜
- 对于颜色动画的处理
下面我们进行的是事件点的添加,在dojo的实际源码中,回调事件的实现是通过实例化一个dojo/Evented对象来实现的,dojo/Evented是dojo整个事件驱动编程的基石,凡是拥有回调事件的对象都是它的实例。dojo/Evented的核心是dojo/on和dojo/aspect, 这两部分的解释可以看一下我的这几篇文章:
Javascript aop(面向切面编程)之around(环绕)
这里我们将事件回调挂载到实例上
function Animate(node, props, duration, interval, delay, callbacks) {
var timer = null;
var startTime =0;
var delayTimer = null;
var percent = null;
var stopped = false;
var ended = false;
var paused = false;
var pauseStartTime = null;
var pausedTime = 0;
function startTimer() {
timer = setInterval(function(){
if (!percent) {
callbacks.onBegin ? callbacks.onBegin() : null;
}
percent = (Date.now() - startTime - pausedTime) / duration;
percent = percent < 0 ? 0 : percent;
percent = percent > 1 ? 1 : percent;
for (var p in props) {
var prop = props[p];
node.style[p] = ((prop.end - prop.start) * percent + prop.start) + (prop.units ? prop.units : '');
}
callbacks.onAnimate ? callbacks.onAnimate() : null;
if (percent >= 1) {
stopTimer();
ended = true;
callbacks.onEnd ? callbacks.onEnd() : null;
}
}, interval);
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function clearDelayTimer() {
clearTimeout(delayTimer);
delayTimer = null;
}
return {
play: function() {
if (ended) {
return;
}
if (startTime === 0) {
startTime = Date.now();
callbacks.beforeBegin ? callbacks.beforeBegin() : null;
}
if (paused) {
pausedTime += Date.now() - pauseStartTime;
startTimer();
paused = false;
} else if (isFinite(delay)) {
delayTimer = setTimeout(function() {
clearDelayTimer();
startTime = Date.now();
startTimer();
}, delay);
} else {
startTimer();
}
callbacks.onPlay ? callbacks.onPlay() : null;
},
pause: function() {
paused = true;
if (delayTimer) {
clearDelayTimer();
} else {
stopTimer();
pauseStartTime = Date.now();
}
callbacks.onPause ? callbacks.onPause() : null;
},
stop: function() {
stopTimer();
stopped = true;
callbacks.onStop ? callbacks.onStop() : null;
}
}
}
dojo/fx中最重要的两个函数就是chain和combine,chain函数允许我们一次执行一系列动画与jQuery中动画队列的功能类似。由于每个Animation实例都拥有onEnd事件,所以chain函数的实现原理就是在每个动画结束后,调用下一个动画的play函数。要模仿这个功能关键是如果在onEnd函数执行后绑定play函数。dojo中使用aspect.after方法,这里我们简单实现:为Function.prototype添加after方法:
Function.prototype.after = function(fn) {
var self = this;
return function() {
var results = self.apply(this, arguments);
fn.apply(this, [results]);
}
}
还有一个问题就是,上文中利用闭包的实现方式,所有对象的play方法都共享一套变量,在多个实例时有很大问题,所以从现在开始我们使用对象方式构造Animate类。
下一步就是combine,combine允许多个动画联动。combine的实现原理比较简单,依次调用Animation数组中的各对象的方法即可。
Function.prototype.after = function(fn) {
var self = this;
return function() {
var results = self.apply(this, arguments);
fn.apply(this, [results]);
}
}
function Animate(node, props, duration, interval, delay) {
this.node = node;
this.props = props;
this.duration = duration;
this.interval = interval;
this.delay = delay;
this.timer = null;
this.startTime = 0;
this.delayTimer = null;
this.percent = null;
this.stopped = false;
this.ended = false;
this.paused = false;
this.pauseStartTime = null;
this.pausedTime = 0;
}
Animate.prototype._startTimer = function() {
var self = this;
this.timer = setInterval(function() {
if (!self.percent) {
self.onBegin ? self.onBegin() : null;
}
var percent = (Date.now() - self.startTime - self.pausedTime) / self.duration;
percent = percent < 0 ? 0 : percent;
percent = percent > 1 ? 1 : percent;
self.percent = percent;
for (var p in self.props) {
var prop = self.props[p];
self.node.style[p] = ((prop.end - prop.start) * percent + prop.start) + (prop.units ? prop.units : '');
}
self.onAnimate ? self.onAnimate() : null;
if (self.percent >= 1) {
self._stopTimer();
self.ended = true;
self.onEnd ? self.onEnd() : null;
}
}, this.interval);
};
Animate.prototype._stopTimer = function() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
};
Animate.prototype._clearDelayTimer = function() {
clearTimeout(this._delayTimer);
this._delayTimer = null;
};
Animate.prototype.play = function() {
if (this.ended) {
return;
}
if (this.startTime === 0) {
this.startTime = Date.now();
this.beforeBegin ? this.beforeBegin() : null;
}
if (this.paused) {
this.pausedTime += Date.now() - this.pauseStartTime;
this._startTimer();
this.paused = false;
} else if (isFinite(this.delay)) {
var self = this;
this._delayTimer = setTimeout(function() {
self._clearDelayTimer();
self.startTime = Date.now();
self._startTimer();
}, this.delay);
} else {
this._startTimer();
}
this.onPlay ? this.onPlay() : null;
};
Animate.prototype.pause = function() {
this.paused = true;
if (this._delayTimer) {
this._clearDelayTimer();
} else {
this._stopTimer();
this.pauseStartTime = Date.now();
}
this.onPause ? this.onPause() : null;
};
Animate.prototype.stop = function() {
this._stopTimer();
this.stopped = true;
this.onStop ? this.onStop() : null;
}
var btnPlay = document.getElementById('btnPlay');
var n = document.getElementById('anim');
var anim1 = new Animate(n, {
opacity: {start: 0, end: 1},
width: {start:50, end: 500, units: 'px'}
}, 5000, 25, 1000);
var anim2 = new Animate(n, {
// opacity: {start: 1, end: 0.3},
height: {start:50, end: 500, units: 'px'}
}, 5000, 25, 1000);
var anim3 = new Animate(n, {
opacity: {start: 1, end: 0.3},
height: {start:500, end: 50, units: 'px'}
}, 5000, 25, 1000);
var anim = combine([anim1, anim2]);
// anim = chain([anim, anim3]);
btnPlay.onclick = function() {
anim.play();
}
btnPause = document.getElementById('btnPause');
btnPause.onclick = function() {
anim.pause();
}
function combine(anims) {
var anim = {
play: function() {
for (var i = 0, len = anims.length; i < len; i++) {
anims[i].play();
}
},
pause: function() {
for (var i = 0, len = anims.length; i < len; i++) {
anims[i].pause();
}
},
stop: function() {
for (var i = 0, len = anims.length; i < len; i++) {
anims[i].stop();
}
}
};
return anim;
}
function chain(anims) {
var index = 0;
for (var i = 0, len = anims.length; i < len; i++) {
var a1 = anims[i];
var a2 = anims[i + 1];
if (a2) {
a1.onEnd = a1.onEnd ? a1.onEnd.after(function() {
index++;
anims[index].play();
}) : (function() {}).after(function() {
index++;
anims[index].play();
});
}
}
var anim = {
play: function() {
anims[index].play();
},
pause: function() {
anims[index].pause();
},
stop: function() {
anims[index].stop();
}
};
return anim;
}
dojo中chain、combine两个函数返回的对象跟Animation拥有同样的方法和属性,这也意味着利用这两个函数我们可以构造出更复杂的动画:
var anim1 = new Animate(n, {
opacity: {start: 0, end: 1},
width: {start:50, end: 500, units: 'px'}
}, 5000, 25, 1000);
var anim2 = new Animate(n, {
// opacity: {start: 1, end: 0.3},
height: {start:50, end: 500, units: 'px'}
}, 5000, 25, 1000);
var anim3 = new Animate(n, {
opacity: {start: 1, end: 0.3},
height: {start:500, end: 50, units: 'px'}
}, 5000, 25, 1000);
var anim = combine([anim1, anim2]);
anim = chain([anim, anim3]);
在此有兴趣的读者可以自行实现。