threex
Version:
Game Extensions for three.js http://www.threejsgames.com/extensions/
204 lines (177 loc) • 6.09 kB
JavaScript
tQuery.registerStatic('createAnimation', function(opts){
return new tQuery.Animation(opts);
});
//////////////////////////////////////////////////////////////////////////////////
// Constructor //
//////////////////////////////////////////////////////////////////////////////////
/**
* handle an animation
*
* @name tQuery.Animation
* @class
*/
tQuery.registerStatic('Animation', function(opts){
opts = this._opts = tQuery.extend(opts, {
world : tQuery.world
})
this._keyframes = new Array;
this._totalTime = null;
this._onUpdate = null;
this._onCapture = function(position){};
this._initialPos = {};
this._propertyTweens = {};
});
/**
* Destructor
*/
tQuery.Animation.prototype.destroy = function(){
this.stop();
};
//////////////////////////////////////////////////////////////////////////////////
// setup //
//////////////////////////////////////////////////////////////////////////////////
/**
* @param {Number} duration the duration of this keyframe validity in seconds
* @param {Object} position list of properties involved in the animations
*/
tQuery.Animation.prototype.pushKeyframe = function(duration, position){
this._keyframes.push({
duration : duration,
position : position
});
return this; // for chained API
};
/**
* Set the Update callback
*
* @param {function} fn the update callback
*/
tQuery.Animation.prototype.onUpdate = function(fn){
this._onUpdate = fn
return this; // for chained API
}
/**
* Set the Capture callback
*
* @param {function} fn the update callback
*/
tQuery.Animation.prototype.onCapture = function(fn){
this._onCapture = fn
return this; // for chained API
}
/**
* Set propertyTweens
*
* @param {function} fn the update callback
*/
tQuery.Animation.prototype.propertyTweens = function(propertyTweens){
this._propertyTweens = propertyTweens;
return this; // for chained API
}
/**
* get the total animation duration
*
* @returns {Number} the duration of the whole animation
*/
tQuery.Animation.prototype.duration = function(){
if( this._keyframes.length === 0 ) return 0;
var lastKeyframe = this._keyframes[this._keyframes.length - 1];
return lastKeyframe._endTime;
};
//////////////////////////////////////////////////////////////////////////////////
// interpolation //
//////////////////////////////////////////////////////////////////////////////////
/**
* build a interpolated position
*
* @param {Number} age amount of seconds since the animation started
*/
tQuery.Animation.prototype._buildPosition = function(age){
// compute the deltatime
var delta = age % this.duration();
// find baseFrame based on delta
for(var frameIdx = 0; frameIdx < this._keyframes.length; frameIdx++){
var baseFrame = this._keyframes[frameIdx];
if( delta < baseFrame._startTime ) continue;
if( delta >= baseFrame._endTime ) continue;
break;
}
// sanity check - the baseFrame has to be known
console.assert( frameIdx !== this._keyframes.length );
// compute some variables
var timeOffset = delta - baseFrame._startTime;
var timePercent = timeOffset / baseFrame.duration;
var nextFrame = this._keyframes[ (frameIdx+1) % this._keyframes.length ];
//console.log("delta", delta)
//console.log("frameIdx", frameIdx)
//console.log("timeOffset", timeOffset)
//console.log("timePercent", timePercent)
var basePosition= baseFrame.position;
var nextPosition= nextFrame.position;
// zero this._initialPos if age > baseFrame.duration - it wont be usefull anymore
if( age > baseFrame.duration && this._initialPos ) this._initialPos= null;
// if frameIdx === 0 and there is a this._initialPos, use it as basePosition
if( frameIdx === 0 && this._initialPos ) basePosition = this._initialPos;
// compute the result based on the linear interpolation between the two frames based on time offset within the frame
var result = {};
for( var property in baseFrame.position ){
// check the property exists
console.assert( nextPosition[property] !== undefined );
console.assert( basePosition[property] !== undefined );
// linear interpolation between the values
var baseValue = basePosition[property];
var nextValue = nextPosition[property];
// define propertyTween for this property - default to linear interpolation
var propertyTween = this._propertyTweens[property] || function(baseValue, nextValue, timePercent){
return (1-timePercent) * baseValue + timePercent * nextValue;
}
// compute the actual result
result[property]= propertyTween(baseValue, nextValue, timePercent);
}
// return the result
return result;
};
//////////////////////////////////////////////////////////////////////////////////
// //
//////////////////////////////////////////////////////////////////////////////////
/**
* Start the animation
*/
tQuery.Animation.prototype.start = function(){
// update _startTime and _endTime
this._totalTime = 0;
this._keyframes.forEach(function(keyframe){
keyframe._startTime = this._totalTime;
this._totalTime += keyframe.duration;
keyframe._endTime = this._totalTime;
}.bind(this));
// get this._initialPos from this._onCapture()
// - the initial position is the position when the animation started.
// - it will be used as basePosition during the first keyframe of the animation
// - it is optional. the user may not define it
this._initialPos= tQuery.extend({}, this._keyframes[0].position)
this._onCapture(this._initialPos);
// init the loop callback
var startDate = Date.now()/1000;
var duration = this.duration();
this._$loopCb = this._opts.world.hook(function(){
var age = Date.now()/1000 - startDate;
var position = this._buildPosition(age)
this._onUpdate(position)
}.bind(this));
}
/**
* test if the animation is running or not
*
* @returns {boolean} return true if the animation is running, false otherwise
*/
tQuery.Animation.prototype.isRunning = function(){
return this._$loopCb ? true : false;
};
/**
* Stop the animation
*/
tQuery.Animation.prototype.stop = function(){
this._$loopCb && this._opts.world.unhook(this._$loopCb);
this._$loopCb = null;
}