API Docs for:
Show:

File: ../src/resources/animation.js

///@INFO: ANIMATION

//Interpolation methods
LS.NONE = 0;
LS.LINEAR = 1;
LS.TRIGONOMETRIC = 2;
LS.BEZIER = 3;
LS.SPLINE = 4;

/**
* An Animation is a resource that contains samples of properties over time, similar to animation curves
* Values could be associated to an specific node.
* Data is contained in tracks
*
* @class Animation
* @namespace LS
* @constructor
*/

function Animation(o)
{
	this.name = "";
	this.takes = {}; //packs of tracks
	if(o)
		this.configure(o);
}

Animation.EXTENSION = "wbin";
Animation.DEFAULT_SCENE_NAME = "@scene";
Animation.DEFAULT_DURATION = 10;

/**
* Create a new take inside this animation (a take contains all the tracks)
* @method createTake
* @param {String} name the name
* @param {Number} duration
* @return {LS.Animation.Take} the take
*/
Animation.prototype.createTake = function( name, duration )
{
	if(!name)
		throw("Animation Take name missing");

	var take = new Animation.Take();
	take.name = name;
	take.duration = duration;
	if(duration === undefined)
		take.duration = Animation.DEFAULT_DURATION;
	this.addTake( take );
	return take;
}

/**
* adds an existing take
* @method addTake
* @param {LS.Animation.Take} name the name
*/
Animation.prototype.addTake = function(take)
{
	this.takes[ take.name ] = take;
	return take;
}

/**
* returns the take with a given name
* @method getTake
* @param {String} name
* @return {LS.Animation.Take} the take
*/
Animation.prototype.getTake = function( name )
{
	return this.takes[ name ];
}

/**
* renames a take name
* @method renameTake
* @param {String} old_name
* @param {String} new_name
*/
Animation.prototype.renameTake = function( old_name, new_name )
{
	var take = this.takes[ old_name ];
	if(!take)
		return;
	delete this.takes[ old_name ];
	take.name = new_name;
	this.takes[ new_name ] = take;
	LEvent.trigger( this, "take_renamed", [old_name, new_name] );
}

/**
* removes a take
* @method removeTake
* @param {String} name
*/
Animation.prototype.removeTake = function( name )
{
	var take = this.takes[ name ];
	if(!take)
		return;
	delete this.takes[ name ];
	LEvent.trigger( this, "take_removed", name );
}

/**
* returns the number of takes
* @method getNumTakes
* @return {Number} the number of takes
*/
Animation.prototype.getNumTakes = function()
{
	var num = 0;
	for(var i in this.takes)
		num++;
	return num;
}

Animation.prototype.addTrackToTake = function(takename, track)
{
	var take = this.takes[ takename ];
	if(!take)
		take = this.createTake( takename );
	take.addTrack( track );
}


Animation.prototype.configure = function(data)
{
	if(data.name)
		this.name = data.name;

	if(data.takes)
	{
		var num_takes = 0;
		this.takes = {};
		for(var i in data.takes)
		{
			var take = new LS.Animation.Take( data.takes[i] );
			if(!take.name)
				console.warn("Take without name");
			else
			{
				this.addTake( take );
				take.loadResources(); //load associated resources
			}
			num_takes++;
		}
		if(!num_takes)
			this.createTake("default", LS.Animation.DEFAULT_DURATION );
	}
}

Animation.prototype.serialize = function()
{
	return LS.cloneObject(this, null, true);
}

Animation.fromBinary = function( data )
{
	if(data.constructor == ArrayBuffer)
		data = WBin.load(data, true);

	var o = data["@json"];
	if(!o) //sometimes the data already comes extractedin the object itself
		o = data;

	for(var i in o.takes)
	{
		var take = o.takes[i];
		for(var j in take.tracks)
		{
			var track = take.tracks[j];
			var name = "@take_" + i + "_track_" + j;
			if( data[name] )
				track.data = data[name];
		}
	}

	return new LS.Animation( o );
}

Animation.prototype.toBinary = function()
{
	var o = {};
	var tracks_data = [];

	//we need to remove the bin data to generate the JSON
	for(var i in this.takes)
	{
		var take = this.takes[i];
		for(var j in take.tracks)
		{
			var track = take.tracks[j];
			track.packData(); //reduce storage space and speeds up loading

			if(track.packed_data)
			{
				var bindata = track.data;
				var name = "@take_" + i + "_track_" + j;
				o[name] = bindata;
				track.data = null;
				tracks_data.push( bindata );
			}
		}
	}

	//create the binary
	o["@json"] = LS.cloneObject(this, null, true);
	var bin = WBin.create(o, "Animation");

	//restore the bin data state in this instance
	for(var i in this.takes)
	{
		var take = this.takes[i];
		for(var j in take.tracks)
		{
			var track = take.tracks[j];
			var name = "@take_" + i + "_track_" + j;
			if(o[name])
				track.data = o[name];
		}
	}

	return bin;
}

//Used when the animation tracks use names instead of node ids
//to convert the track locator to node names, so they affect to only one node
Animation.prototype.convertNamesToIDs = function( use_basename, root )
{
	var num = 0;
	for(var i in this.takes)
	{
		var take = this.takes[i];
		num += take.convertNamesToIDs( use_basename, root );
	}
	return num;
}

//Used when the animation tracks use UIDs instead of node names
//to convert the track locator to node names, so they can be reused between nodes in the same scene
Animation.prototype.convertIDsToNames = function( use_basename, root )
{
	var num = 0;
	for(var i in this.takes)
	{
		var take = this.takes[i];
		num += take.convertIDsToNames( use_basename, root );
	}
	return num;
}

/**
* changes the packing mode of the tracks inside all takes
* @method setTracksPacking
* @param {boolean} pack if true the tracks will be packed (used a typed array)
* @return {Number} te number of modifyed tracks
*/
Animation.prototype.setTracksPacking = function(v)
{
	var num = 0;
	for(var i in this.takes)
	{
		var take = this.takes[i];
		num += take.setTracksPacking(v);
	}
	return num;
}

/**
* optimize all the tracks in all the takes, so they take less space and are faster to compute (when possible)
* @method optimizeTracks
* @return {Number} the number of takes
*/
Animation.prototype.optimizeTracks = function()
{
	var num = 0;
	for(var i in this.takes)
	{
		var take = this.takes[i];
		num += take.optimizeTracks();
	}
	return num;
}

/**
* It creates a PlayAnimation component to the node (or reuse and old existing one). Used when a resource is assigned to a node
* @method assignToNode
* @param {LS.SceneNode} node node where to assign this animation
*/
Animation.prototype.assignToNode = function(node)
{
	var component = node.getComponent( LS.Components.PlayAnimation );
	if(!component)
		component = node.addComponent( LS.Components.PlayAnimation );
	component.animation = this.fullpath || this.filename;
}



LS.Classes["Animation"] = LS.Animation = Animation;

/**  
* Represents a set of animations
*
* @class Take
* @namespace LS.Animation
* @constructor
*/
function Take(o)
{
	/** 
	* @property name {String}
	**/
	this.name = null;
	/** 
	* @property tracks {Array}
	**/
	this.tracks = [];
	/** 
	* @property duration {Number} in seconds
	**/
	this.duration = LS.Animation.DEFAULT_DURATION;
	
	if(o)
		this.configure(o);

}

Take.prototype.configure = function( o )
{
	if( o.name )
		this.name = o.name;
	if( o.tracks ) 
	{
		this.tracks = []; //clear
		for(var i in o.tracks)
		{
			var track = new LS.Animation.Track( o.tracks[i] );
			this.addTrack( track );
		}
	}
	if( o.duration )
		this.duration = o.duration;
}

Take.prototype.serialize = Take.prototype.toJSON = function()
{
	return LS.cloneObject(this, null, true);
}

/**
* creates a new track from a given data
* @method createTrack
* @param {Object} data in serialized format
* @return {LS.Animation.Track} the track
*/
Take.prototype.createTrack = function( data )
{
	if(!data)
		throw("Data missing when creating track");

	var track = this.getTrack( data.property );
	if( track )
		return track;

	var track = new LS.Animation.Track( data );
	this.addTrack( track );
	return track;
}

/**
* For every track, gets the interpolated value between keyframes and applies the value to the property associated with the track locator
* Locators are in the form of "{NODE_UID}/{COMPONENT_UID}/{property_name}"
*
* @method applyTracks
* @param {number} current_time the time of the anim to sample
* @param {number} last_time this is used for events, we need to know where you were before
* @param {boolean} ignore_interpolation in case you want to sample the nearest one
* @param {SceneNode} weight [Optional] allows to blend animations with current value (default is 1)
* @param {Number} root [Optional] if you want to limit the locator to search inside a node
* @param {Function} on_pre_apply [Optional] a callback called per track to see if this track should be applyed, if it returns false it is skipped. callback receives (track, current_time, root_node, weight)
* @param {Function} on_apply_sample [Optional] a callback called before applying a keyframe, if the callback returns false the keyframe will be skipped. callback parameters ( track, sample, root_node, weight )
* @return {Component} the target where the action was performed
*/
Take.prototype.applyTracks = function( current_time, last_time, ignore_interpolation, root_node, scene, weight, on_pre_apply, on_apply_sample )
{
	scene = scene || LS.GlobalScene;
	if(weight === 0)
		return;

	weight = weight || 1;

	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if( track.enabled === false || !track.data )
			continue;

		if(on_pre_apply && on_pre_apply( track, current_time, root_node, weight ) === false)
			continue;

		//events are an special kind of tracks, they execute actions
		if( track._type_index == Track.EVENT )
		{
			var keyframe = track.getKeyframeByTime( current_time );
			if( !keyframe || keyframe[0] < last_time || keyframe[0] > current_time )
				return;

			//need info to search for node
			var info = scene.getPropertyInfoFromPath( track._property_path );
			if(!info)
				return;

			var value = keyframe[1];

			if(value[2] == 1) //on function call events the thirth value [2] is 1
			{
				//functions
				if(info.node && info.target && info.target[ value[0] ] )
					info.target[ value[0] ].call( info.target, value[1] );
			}
			else
			{
				//events
				if(info.target) //components
					LEvent.trigger( info.target, keyframe[1], keyframe[1][1] );
				else if(info.node) //nodes
					LEvent.trigger( info.node, keyframe[1][0], keyframe[1][1] );
			}
		}
		else //regular tracks
		{
			//read from the animation track the value
			var sample = track.getSample( current_time, !ignore_interpolation );

			//to blend between animations...
			if(weight !== 1)
			{
				var current_value = scene.getPropertyValueFromPath( track._property_path, sample, root_node, 0 );
				sample = LS.Animation.interpolateLinear( sample, current_value, weight, null, track._type, track.value_size, track );
			}

			//apply the value to the property specified by the locator
			if( sample !== undefined ) 
			{
				if( on_apply_sample && on_apply_sample( track, sample, root_node, weight ) === false)
					continue; //skip
				track._target = scene.setPropertyValueFromPath( track._property_path, sample, root_node, 0 );
			}
		}
	}
}



Take.prototype.addTrack = function( track )
{
	this.tracks.push( track );
}

Take.prototype.getTrack = function( property )
{
	for(var i = 0; i < this.tracks.length; ++i)
		if(this.tracks[i].property == property)
			return this.tracks[i];
	return null;
}

Take.prototype.removeTrack = function( track )
{
	for(var i = 0; i < this.tracks.length; ++i)
		if(this.tracks[i] == track)
		{
			this.tracks.splice( i, 1 );
			return;
		}
}


Take.prototype.getPropertiesSample = function(time, result)
{
	result = result || [];
	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		var value = track.getSample( time );
		result.push([track.property, value]);
	}
	return result;
}

Take.prototype.actionPerSample = function(time, callback, options)
{
	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		var value = track.getSample(time, true);
		if( options.disabled_tracks && options.disabled_tracks[ track.property ] )
			continue;
		callback(track.property, value, options);
	}
}

//Ensures all the resources associated to keyframes are loaded in memory
Take.prototype.loadResources = function()
{
	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if(track._type == "texture")
		{
			var l = track.getNumberOfKeyframes();
			for(var j = 0; j < l; ++j)
			{
				var keyframe = track.getKeyframe(j);
				if(keyframe && keyframe[1] && keyframe[1][0] != ":")
					LS.ResourcesManager.load( keyframe[1] );
			}
		}
	}
}

//convert track locators from using UIDs to use node names (this way the same animation can be used in several parts of the scene)
Take.prototype.convertNamesToIDs = function( use_basename, root )
{
	var num = 0;
	for(var j = 0; j < this.tracks.length; ++j)
	{
		var track = this.tracks[j];
		num += track.convertNameToID( use_basename, root )
	}
	return num;
}

//convert track locators from using UIDs to use node names (this way the same animation can be used in several parts of the scene)
Take.prototype.convertIDsToNames = function( use_basename, root )
{
	var num = 0;
	for(var j = 0; j < this.tracks.length; ++j)
	{
		var track = this.tracks[j];
		num += track.convertIDtoName( use_basename, root )
	}
	return num;
}

Take.prototype.setTracksPacking = function(v)
{
	var num = 0;
	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if( track.packed_data == v)
			continue;
		if(v)
			track.packData();
		else
			track.unpackData();
		num += 1;
	}
	return num;
}

/**
* Optimizes the tracks by changing the Matrix tracks to Trans10 tracks which are way faster and use less space
* @method optimizeTracks
*/
Take.prototype.optimizeTracks = function()
{
	var num = 0;
	var temp = new Float32Array(10);

	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if( track.convertToTrans10() )
			num += 1;
	}
	return num;
}

//assigns the same translation to all nodes?
Take.prototype.matchTranslation = function( root )
{
	var num = 0;

	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];

		if(track._type != "trans10" && track._type != "mat4")
			continue;

		if( !track._property_path || !track._property_path.length )
			continue;

		var node = LSQ.get( track._property_path[0], root );
		if(!node)
			continue;
		
		var position = node.transform.position;
		var offset = track.value_size + 1;

		var data = track.data;
		var num_samples = data.length / offset;
		if(track._type == "trans10")
		{
			for(var j = 0; j < num_samples; ++j)
				data.set( position, j*offset + 1 );
		}
		else if(track._type == "mat4")
		{
			for(var j = 0; j < num_samples; ++j)
				data.set( position, j*offset + 1 + 12 ); //12,13,14 contain translation
		}

		num += 1;
	}

	return num;
}

/**
* If this is a transform track it removes translation and scale leaving only rotations
* @method onlyRotations
*/
Take.prototype.onlyRotations = function()
{
	var num = 0;

	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if( track.onlyRotations() )
			num += 1;
	}
	return num;
}

/**
* removes scaling in transform tracks
* @method removeScaling
*/
Take.prototype.removeScaling = function()
{
	var num = 0;

	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if( track.removeScaling() )
			num += 1;
	}
	return num;
}


Take.prototype.setInterpolationToAllTracks = function( interpolation )
{
	var num = 0;
	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		if(track.interpolation == interpolation)
			continue;
		track.interpolation = interpolation;
		num += 1;
	}

	return num;
}

Take.prototype.trimTracks = function( start, end )
{
	var num = 0;
	for(var i = 0; i < this.tracks.length; ++i)
	{
		var track = this.tracks[i];
		num += track.trim( start, end );
	}

	this.duration = end - start;

	return num;
}

Take.prototype.stretchTracks = function( duration )
{
	if(duration <= 0 || this.duration == 0)
		return 0;
	var scale = duration / this.duration;
	this.duration *= scale;
	for(var i = 0; i < this.tracks.length; ++i)
		this.tracks[i].stretch( scale );
	return this.tracks.length;
}


Animation.Take = Take;


/**
* Represents one track with data over time about one property
* Data could be stored in two forms, or an array containing arrays of [time,data] (unpacked data) or in a single typed array (packed data), depends on the attribute typed_mode
*
* @class Animation.Track
* @namespace LS
* @constructor
*/

function Track(o)
{
	/** 
	* @property enabled {Boolean} if it must be applied
	**/
	this.enabled = true;
	/** 
	* @property name {String} title to show in the editor
	**/
	this.name = ""; //title
	/** 
	* @property type {String} if the data is number, vec2, color, etc
	**/
	this._type = null; //type of data (number, vec2, color, texture, etc)
	this._type_index = null; //type in number format (to optimize)
	/** 
	* @property interpolation {Number} type of interpolation LS.NONE, LS.LINEAR, LS.TRIGONOMETRIC, LS.BEZIER, LS.SPLICE
	**/
	this.interpolation = LS.NONE;
	/** 
	* @property looped {Boolean} if the last and the first keyframe should be connected
	**/
	this.looped = false; //interpolate last keyframe with first

	//data
	this.packed_data = false; //this means the data is stored in one continuous datatype, faster to load but not editable
	this.value_size = 0; //how many numbers contains every sample of this property, 0 means basic type (string, boolean)
	/** 
	* @property data {*} contains all the keyframes, could be an array or a typed array
	**/
	this.data = null; //array or typed array where you have the time value followed by this.value_size bytes of data
	this.data_table = null; //used to index data when storing it

	//to speed up sampling
	Object.defineProperty( this, '_property', {
		value: "",
		enumerable: false,
		writable: true
	});

	Object.defineProperty( this, '_property_path', {
		value: [],
		enumerable: false,
		writable: true
	});

	if(o)
		this.configure(o);
}

Track.FRAMERATE = 30;

//for optimization
Track.QUAT = LS.TYPES_INDEX["quat"];
Track.TRANS10 = LS.TYPES_INDEX["trans10"];
Track.EVENT = LS.TYPES_INDEX["event"];


/** 
* @property property {String} the locator to the property this track should modify ( "node/component_uid/property" )
**/
Object.defineProperty( Track.prototype, 'property', {
	set: function( property )
	{
		this._property = property.trim();
		this._property_path = this._property.split("/");
	},
	get: function(){
		return this._property;
	},
	enumerable: true
});

Object.defineProperty( Track.prototype, 'type', {
	set: function( t )
	{
		this._type = t;
		this._type_index = LS.TYPES_INDEX[t];
	},
	get: function(){
		return this._type;
	},
	enumerable: true
});

Track.prototype.configure = function( o )
{
	if(!o.property)
		console.warn("Track with property name");

	if(o.enabled !== undefined) this.enabled = o.enabled;
	if(o.name) this.name = o.name;
	if(o.property) this.property = o.property;
	if(o.type) this.type = o.type;
	if(o.looped) this.looped = o.looped;
	if(o.interpolation !== undefined)
		this.interpolation = o.interpolation;
	else
		this.interpolation = LS.LINEAR;

	if(o.data_table) this.data_table = o.data_table;

	if(o.value_size) this.value_size = o.value_size;

	//data
	if(o.data)
	{
		this.data = o.data;

		//this is ugly but makes it easy to work with the collada importer
		if(o.packed_data === undefined && this.data.constructor !== Array)
			this.packed_data = true;
		else
			this.packed_data = !!o.packed_data;

		if( o.data.constructor == Array )
		{
			if( this.packed_data )
				this.data = new Float32Array( o.data );
		}
		//else
		//	this.unpackData();
	}

	if(o.interpolation && !this.value_size)
		o.interpolation = LS.NONE;
}

Track.prototype.serialize = function()
{
	var o = {
		enabled: this.enabled,
		name: this.name,
		property: this.property, 
		type: this._type,
		interpolation: this.interpolation,
		looped: this.looped,
		value_size: this.value_size,
		packed_data: this.packed_data,
		data_table: this.data_table
	}

	if(this.data)
	{
		if(this.value_size <= 1)
		{
			if(this.data.concat)
				o.data = this.data.concat(); //regular array, clone it
			else
				o.data = new this.data.constructor( o.data ); //clone for typed arrays (weird, this should never happen but it does)
		}
		else //pack data
		{
			this.packData();
			o.data = new Float32Array( this.data ); //clone it
			o.packed_data = this.packed_data;
		}
	}

	return o;
}

Track.prototype.toJSON = Track.prototype.serialize;

Track.prototype.clear = function()
{
	this.data = [];
	this.packed_data = false;
}

/**
* used to change every track so instead of using UIDs for properties it uses node names
* this is used when you want to apply the same animation to different nodes in the scene
* @method getIDasName
* @param {boolean} use_basename if you want to just use the node name, othewise it uses the fullname (name with path)
* @param {LS.SceneNode} root
* @return {String} the result name
*/
Track.prototype.getIDasName = function( use_basename, root )
{
	if( !this._property_path || !this._property_path.length )
		return null;

	if(this._property_path[0][0] !== LS._uid_prefix)
		return null; //is already using names

	var node = LSQ.get( this._property_path[0], root );
	if(!node)
	{
		console.warn("getIDasName: node not found in LS.GlobalScene: " + this._property_path[0] );
		return false;
	}

	if(!node.name)
	{
		console.warn("getIDasName: node without name?");
		return false;
	}

	var result = this._property_path.concat();
	if(use_basename)
		result[0] = node.name;
	else
		result[0] = node.fullname;
	return result.join("/");
}

//used to change every track so instead of using node names for properties it uses node uids
//this is used when you want to apply an animation to an specific node
Track.prototype.convertNameToID = function( root )
{
	if(this._property_path[0][0] === LS._uid_prefix)
		return false; //is already using UIDs

	var node = LSQ.get( this._property_path[0], root );
	if(!node)
		return false;

	this._property_path[0] = node.uid;
	this._property = this._property_path[0].join("/");
	return true;
}

//used to change every track so instead of using UIDs for properties it uses node names
//this is used when you want to apply the same animation to different nodes in the scene
Track.prototype.convertIDtoName = function( use_basename, root )
{
	var name = this.getIDasName( use_basename, root );
	if(!name)
		return false;
	this._property = name;
	this._property_path = this._property.split("/");
	return true;
}

/**
* Adds a new keyframe to this track
* @method addKeyframe
* @param {Number} time time stamp in seconds
* @param {*} value anything you want to store
* @param {Boolean} skip_replace if you want to replace existing keyframes at same time stamp or add it next to that
* @return {Number} index of keyframe
*/
Track.prototype.addKeyframe = function( time, value, skip_replace )
{
	if(this.value_size > 1)
		value = new Float32Array( value ); //clone

	if(this.packed_data)
		this.unpackData();

	if(!this.data)
		this.data = [];

	for(var i = 0; i < this.data.length; ++i)
	{
		if(this.data[i][0] < time )
			continue;
		if(this.data[i][0] == time && !skip_replace )
			this.data[i][1] = value;
		else
			this.data.splice(i,0, [time,value]);
		return i;
	}

	this.data.push( [time,value] );
	return this.data.length - 1;
}

/**
* returns a keyframe given an index
* @method getKeyframe
* @param {Number} index
* @return {Array} the keyframe in [time,data] format
*/
Track.prototype.getKeyframe = function( index )
{
	if(index < 0 || index >= this.data.length)
	{
		console.warn("keyframe index out of bounds");
		return null;
	}

	if(this.packed_data)
	{
		var pos = index * (1 + this.value_size );
		if(pos > (this.data.length - this.value_size) )
			return null;
		return [ this.data[pos], this.data.subarray(pos+1, pos+this.value_size+1) ];
		//return this.data.subarray(pos, pos+this.value_size+1) ];
	}

	return this.data[ index ];
}

/**
* returns the first keyframe that matches this time
* @method getKeyframeByTime
* @param {Number} time
* @return {Array} keyframe in [time,value]
*/
Track.prototype.getKeyframeByTime = function( time )
{
	var index = this.findTimeIndex( time );
	if(index == -1)
		return;
	return this.getKeyframe( index );
}

/**
* changes a keyframe time and rearranges it
* @method moveKeyframe
* @param {Number} index
* @param {Number} new_time
* @return {Number} new index
*/
Track.prototype.moveKeyframe = function(index, new_time)
{
	if(this.packed_data)
	{
		//TODO
		console.warn("Cannot move keyframes if packed");
		return -1;
	}

	if(index < 0 || index >= this.data.length)
	{
		console.warn("keyframe index out of bounds");
		return -1;
	}

	var new_index = this.findTimeIndex( new_time );
	var keyframe = this.data[ index ];
	var old_time = keyframe[0];
	if(old_time == new_time)
		return index;
	keyframe[0] = new_time; //set time
	if(old_time > new_time)
		new_index += 1;
	if(index == new_index)
	{
		//console.warn("same index");
		return index;
	}

	//extract
	this.data.splice(index, 1);
	//reinsert
	index = this.addKeyframe( keyframe[0], keyframe[1], true );

	this.sortKeyframes();
	return index;
}

/**
* Sometimes when moving keyframes they could end up not sorted by timestamp, which will cause problems when sampling, to avoid it, we can force to sort all keyframes
* @method sortKeyframes
*/
Track.prototype.sortKeyframes = function()
{
	if(this.packed_data)
	{
		this.unpackData();
		this.sortKeyframes();
		this.packData();
	}
	this.data.sort( function(a,b){ return a[0] - b[0];  });
}

/**
* removes one keyframe
* @method removeKeyframe
* @param {Number} index
*/
Track.prototype.removeKeyframe = function(index)
{
	if(this.packed_data)
		this.unpackData();

	if(index < 0 || index >= this.data.length)
	{
		console.warn("keyframe index out of bounds");
		return;
	}

	this.data.splice(index, 1);
}

/**
* returns the number of keyframes
* @method getNumberOfKeyframes
*/

Track.prototype.getNumberOfKeyframes = function()
{
	if(!this.data || this.data.length == 0)
		return 0;

	if(this.packed_data)
		return this.data.length / (1 + this.value_size );
	return this.data.length;
}

//check for the last sample time
Track.prototype.computeDuration = function()
{
	if(!this.data || this.data.length == 0)
		return 0;

	if(this.packed_data)
	{
		var time = this.data[ this.data.length - 2 - this.value_size ];
		this.duration = time;
		return time;
	}

	//not typed
	var last = this.data[ this.data.length - 1 ];
	if(last)
		return last[0];
	return 0;
}

Track.prototype.isInterpolable = function()
{
	if( this.value_size > 0 || LS.Interpolators[ this._type ] )
		return true;
	return false;
}

/**
* takes all the keyframes and stores them inside a typed-array so they are faster to store in server or work with
* @method packData
*/
Track.prototype.packData = function()
{
	if(!this.data || this.data.length == 0)
		return 0;

	if(this.packed_data)
		return;

	if(this.value_size == 0)
		return; //cannot be packed (bools and strings cannot be packed)

	var offset = this.value_size + 1;
	var data = this.data;
	var typed_data = new Float32Array( data.length * offset );

	for(var i = 0; i < data.length; ++i)
	{
		typed_data[i*offset] = data[i][0];
		if( this.value_size == 1 )
			typed_data[i*offset+1] = data[i][1];
		else
			typed_data.set( data[i][1], i*offset+1 );
	}

	this.data = typed_data;
	this.packed_data = true;
}

/**
* takes all the keyframes and unpacks them so they are in a simple array, easier to work with
* @method unpackData
*/
Track.prototype.unpackData = function()
{
	if(!this.data || this.data.length == 0)
		return 0;

	if(!this.packed_data)
		return;

	var offset = this.value_size + 1;
	var typed_data = this.data;
	var data = Array( typed_data.length / offset );

	for(var i = 0; i < typed_data.length; i += offset )
		data[i/offset] = [ typed_data[i], typed_data.subarray( i+1, i+offset ) ];

	this.data = data;
	this.packed_data = false;
}

/**
* Returns nearest index of keyframe with time equal or less to specified time (Dichotimic search)
* @method findTimeIndex
* @param {number} time
* @return {number} the nearest index (lower-bound)
*/
Track.prototype.findTimeIndex = function(time)
{
	var data = this.data;
	if(!data || data.length == 0)
		return -1;

	if(this.packed_data)
	{
		var offset = this.value_size + 1; //data size plus timestamp
		var l = data.length;
		var n = l / offset; //num samples
		var imin = 0;
		var imid = 0;
		var imax = n;

		if(n == 0)
			return -1;
		if(n == 1)
			return 0;

		//time out of duration
		if( data[ (imax - 1) * offset ] < time )
			return (imax - 1);

		//dichotimic search
		// continue searching while [imin,imax] are continuous
		while (imax >= imin)
		{
			// calculate the midpoint for roughly equal partition
			imid = ((imax + imin)*0.5)|0;
			var t = data[ imid * offset ]; //get time
			if( t == time )
				return imid; 
			//when there are no more elements to search
			if( imin == (imax - 1) )
				return imin;
			// determine which subarray to search
			if (t < time)
				// change min index to search upper subarray
				imin = imid;
			else         
				// change max index to search lower subarray
				imax = imid;
		}
		return imid;
	}

	//unpacked data
	var n = data.length; //num samples
	var imin = 0;
	var imid = 0;
	var imax = n;

	if(n == 0)
		return -1;
	if(n == 1)
		return 0;

	//time out of duration
	if( data[ (imax - 1) ][0] < time )
		return (imax - 1);

	while (imax >= imin)
	{
		// calculate the midpoint for roughly equal partition
		imid = ((imax + imin)*0.5)|0;
		var t = data[ imid ][0]; //get time
		if( t == time )
			return imid; 
		//when there are no more elements to search
		if( imin == (imax - 1) )
			return imin;
		// determine which subarray to search
		if (t < time)
			// change min index to search upper subarray
			imin = imid;
		else         
			// change max index to search lower subarray
			imax = imid;
	}

	return imid;
}

/**
* Samples the data in one time, taking into account interpolation.
* Warning: if no result container is provided the same container is reused between samples to avoid garbage, be careful.
* @method getSample
* @param {number} time
* @param {number} interpolation [optional] the interpolation method could be LS.NONE, LS.LINEAR, LS.BEZIER
* @param {*} result [optional] the container where to store the data (in case is an array). IF NOT CONTAINER IS PROVIDED THE SAME ONE IS RETURNED EVERY TIME!
* @return {*} data
*/
Track.prototype.getSample = function( time, interpolate, result )
{
	if(!this.data || this.data.length === 0)
		return undefined;

	if(this.packed_data)
		return this.getSamplePacked( time, interpolate, result );
	return this.getSampleUnpacked( time, interpolate, result );
}

//used when sampling from a unpacked track (where data is an array of arrays)
Track.prototype.getSampleUnpacked = function( time, interpolate, result )
{
	time = Math.clamp( time, 0, this.duration );

	var index = this.findTimeIndex( time );
	if(index === -1)
		index = 0;

	var index_a = index;
	var index_b = index + 1;
	var data = this.data;
	var value_size = this.value_size;

	interpolate = interpolate && this.interpolation && (this.value_size > 0 || LS.Interpolators[ this._type ] );

	if(!interpolate || (data.length == 1) || index_b == data.length || (index_a == 0 && this.data[0][0] > time)) //(index_b == this.data.length && !this.looped)
		return this.data[ index ][1];

	var a = data[ index_a ];
	var b = data[ index_b ];

	var t = (b[0] - time) / (b[0] - a[0]);

	//multiple data
	if( value_size > 1 )
	{
		result = result || this._result;
		if( !result || result.length != value_size )
			result = this._result = new Float32Array( value_size );
	}

	if(this.interpolation === LS.LINEAR)
	{
		if( value_size == 1 )
			return a[1] * t + b[1] * (1-t);

		return LS.Animation.interpolateLinear( a[1], b[1], t, result, this._type, value_size, this );
	}
	else if(this.interpolation === LS.BEZIER)
	{
		//bezier not implemented for interpolators
		if(value_size === 0 && LS.Interpolators[ this._type ] )
		{
			var func = LS.Interpolators[ this._type ];
			var r = func( a[1], b[1], t, this._last_value );
			this._last_value = r;
			return r;
		}

		var pre_a = index > 0 ? data[ index - 1 ] : a;
		var post_b = index < data.length - 2 ? data[ index + 2 ] : b;

		if(value_size === 1)
			return Animation.EvaluateHermiteSpline(a[1],b[1],pre_a[1],post_b[1], 1 - t );

		result = Animation.EvaluateHermiteSplineVector( a[1], b[1], pre_a[1], post_b[1], 1 - t, result );

		if(this._type_index == Track.QUAT)
		{
			quat.slerp( result, b[1], a[1], t ); //force quats without bezier interpolation
			quat.normalize( result, result );
		}
		else if(this._type_index == Track.TRANS10)
		{
			var rotR = result.subarray(3,7);
			var rotA = a[1].subarray(3,7);
			var rotB = b[1].subarray(3,7);
			quat.slerp( rotR, rotB, rotA, t );
			quat.normalize( rotR, rotR );
		}

		return result;
	}

	return null;
}

//used when sampling from a packed track (where data is a typed-array)
Track.prototype.getSamplePacked = function( time, interpolate, result )
{
	time = Math.clamp( time, 0, this.duration );

	var index = this.findTimeIndex( time );
	if(index == -1)
		index = 0;

	var value_size = this.value_size;
	var offset = (value_size+1);
	var index_a = index;
	var index_b = index + 1;
	var data = this.data;
	var num_keyframes = data.length / offset;

	interpolate = interpolate && this.interpolation && (value_size > 0 || LS.Interpolators[ this._type ] );

	if( !interpolate || num_keyframes == 1 || index_b == num_keyframes || (index_a == 0 && this.data[0] > time)) //(index_b == this.data.length && !this.looped)
		return this.getKeyframe( index )[1];

	//multiple data
	if( value_size > 1 )
	{
		result = result || this._result;
		if( !result || result.length != value_size )
			result = this._result = new Float32Array( value_size );
	}

	var a = data.subarray( index_a * offset, (index_a + 1) * offset );
	var b = data.subarray( index_b * offset, (index_b + 1) * offset );

	var t = (b[0] - time) / (b[0] - a[0]);

	if(this.interpolation === LS.LINEAR)
	{
		if( value_size == 1 ) //simple case
			return a[1] * t + b[1] * (1-t);

		var a_data = a.subarray(1, value_size + 1 );
		var b_data = b.subarray(1, value_size + 1 );
		return LS.Animation.interpolateLinear( a_data, b_data, t, result, this._type, value_size, this );
	}
	else if(this.interpolation === LS.BEZIER)
	{
		if( value_size === 0 ) //bezier not supported in interpolators
			return a[1];

		var pre_a = index > 0 ? data.subarray( (index-1) * offset, (index) * offset ) : a;
		var post_b = index_b < (num_keyframes - 1) ? data.subarray( (index_b+1) * offset, (index_b+2) * offset ) : b;

		if( value_size === 1 )
			return Animation.EvaluateHermiteSpline( a[1], b[1], pre_a[1], post_b[1], 1 - t );

		var a_value = a.subarray(1,offset);
		var b_value = b.subarray(1,offset);

		result = Animation.EvaluateHermiteSplineVector( a_value, b_value, pre_a.subarray(1,offset), post_b.subarray(1,offset), 1 - t, result );

		if(this._type_index == Track.QUAT )
		{
			quat.slerp( result, b_value, a_value, t );
			quat.normalize( result, result ); //is necesary?
		}
		else if(this._type_index == Track.TRANS10 )
		{
			var rotR = result.subarray(3,7);
			var rotA = a_value.subarray(3,7);
			var rotB = b_value.subarray(3,7);
			quat.slerp( rotR, rotB, rotA, t );
			quat.normalize( rotR, rotR ); //is necesary?
		}

		return result;
	}

	return null;
}

/**
* returns information about the object being affected by this track based on its locator
* the object contains a reference to the object, the property name, the type of the data
* @method getPropertyInfo
* @param {LS.Scene} scene [optional]
* @return {Object} an object with the info { target, name, type, value }
*/
Track.prototype.getPropertyInfo = function( scene )
{
	scene = scene || LS.GlobalScene;

	return scene.getPropertyInfo( this.property );
}

/**
* returns an array containing N samples for this property over time using the interpolation of the track
* @method getSampledData
* @param {Number} start_time when to start sampling
* @param {Number} end_time when to finish sampling
* @param {Number} num_samples the number of samples
* @return {Array} an array containing all the samples
*/
Track.prototype.getSampledData = function( start_time, end_time, num_samples )
{
	var delta = (end_time - start_time) / num_samples;
	if(delta <= 0)
		return null;

	var samples = [];
	for(var i = 0; i < num_samples; ++i)
	{
		var t = i * delta + start_time;
		var sample = this.getSample( t, true );
		if(this.value_size > 1)
			sample = new sample.constructor( sample );
		samples.push(sample);
	}

	return samples;
}

/**
* removes keyframes that are before or after the time range
* @method trim
* @param {number} start time
* @param {number} end time
*/
Track.prototype.trim = function( start, end )
{
	if(this.packed_data)
		this.unpackData();

	var size = this.data.length;

	var result = [];
	for(var i = 0; i < size; ++i)
	{
		var d = this.data[i];
		if(d[0] < start || d[0] > end)
			continue;
		d[0] -= start;
		result.push(d);
	}
	this.data = result;

	//changes has been made?
	if(this.data.length != size)
		return 1;
	return 0;
}

/**
* Scales the time in every keyframe
* @method stretch
* @param {number} scale the sacle to apply to all times
*/
Track.prototype.stretch = function( scale )
{
	if(this.packed_data)
		this.unpackData();
	var size = this.data.length;
	for(var i = 0; i < size; ++i)
		this.data[i][0] *= scale; //scale time
	return 1;
}

/**
* If the track used matrices, it transform them to position,quaternion and scale (10 floats, also known as trans10)
* this makes working with animations faster
* @method convertToTrans10
*/
Track.prototype.convertToTrans10 = function()
{
	if( this.value_size != 16 )
		return false;

	//convert samples
	if(!this.packed_data)
		this.packData();

	//convert locator
	var path = this.property.split("/");
	if( path[ path.length - 1 ] != "matrix") //from "nodename/matrix" to "nodename/transform/data"
		return false;

	path[ path.length - 1 ] = "Transform/data";
	this.property = path.join("/");
	this.type = "trans10";
	this.value_size = 10;
	var temp = new Float32Array(10);

	var data = this.data;
	var num_samples = data.length / 17;
	for(var k = 0; k < num_samples; ++k)
	{
		var sample = data.subarray(k*17+1,(k*17)+17);
		LS.Transform.fromMatrix4ToTransformData( sample, temp );
		data[k*11] = data[k*17]; //timestamp
		data.set(temp,k*11+1); //overwrite inplace (because the output is less big that the input)
	}
	this.data = new Float32Array( data.subarray(0,num_samples*11) );

	return true;
}

/**
* If this track changes the scale, it forces it to be 1,1,1
* @method removeScaling
*/
Track.prototype.removeScaling = function()
{
	var modified = false;

	if(this.type == "matrix")
	{
		this.convertToTrans10();
		modified = true;
	}

	if( this.type != "trans10" )
	{
		if(modified)
			return true;
	}

	var num_keyframes = this.getNumberOfKeyframes();
	for( var j = 0; j < num_keyframes; ++j )
	{
		var k = this.getKeyframe(j);
		k[1][7] = k[1][8] = k[1][9] = 1; //set scale equal to 1
	}
	return true;
}


Track.prototype.onlyRotations = (function()
{
	var temp = new Float32Array(10);
	var temp_quat = new Float32Array(4);

	return function(){

		//convert locator
		var path = this.property.split("/");
		var last_path = path[ path.length - 1 ];
		var old_size = this.value_size;
		if( this.type != "mat4" && this.type != "trans10" )
			return false;

		if(last_path == "matrix")
			path[ path.length - 1 ] = "Transform/rotation";
		else if (last_path == "data")
			path[ path.length - 1 ] = "rotation";

		//convert samples
		if(!this.packed_data)
			this.packData();

		this.property = path.join("/");
		var old_type = this._type;
		this.type = "quat";
		this.value_size = 4;

		var data = this.data;
		var num_samples = data.length / (old_size+1);

		if( old_type == "mat4" )
		{
			for(var k = 0; k < num_samples; ++k)
			{
				var sample = data.subarray(k*17+1,(k*17)+17);
				var new_data = LS.Transform.fromMatrix4ToTransformData( sample, temp );
				temp_quat.set( temp.subarray(3,7) );
				data[k*5] = data[k*17]; //timestamp
				data.set( temp_quat, k*5+1); //overwrite inplace (because the output is less big that the input)
			}
		}
		else if( old_type == "trans10" )
		{
			for(var k = 0; k < num_samples; ++k)
			{
				var sample = data.subarray(k*11+4,(k*11)+8);
				data[k*5] = data[k*11]; //timestamp
				data.set( sample, k*5+1); //overwrite inplace (because the output is less big that the input)
			}
		}
		
		this.data = new Float32Array( data.subarray(0,num_samples*5) );
		return true;
	};
})();


Animation.Track = Track;

Animation.interpolateLinear = function( a, b, t, result, type, value_size, track )
{
	if(value_size == 1)
		return a * t + b * (1-t);

	if( LS.Interpolators[ type ] )
	{
		var func = LS.Interpolators[ type ];
		var r = func( a, b, t, track._last_v );
		track._last_v = r;
		return r;
	}

	result = result || track._result;

	if(!result || result.length != value_size)
		result = track._result = new Float32Array( value_size );

	var type_index = LS.TYPES_INDEX[ type ];

	switch( type_index )
	{
		case Track.QUAT:
			quat.slerp( result, b, a, t );
			quat.normalize( result, result );
			break;
		case Track.TRANS10: 
			for(var i = 0; i < 3; i++) //this.value_size should be 10
				result[i] = a[i] * t + b[i] * (1-t);
			for(var i = 7; i < 10; i++) //this.value_size should be 10
				result[i] = a[i] * t + b[i] * (1-t);
			var rotA = a.subarray(3,7);
			var rotB = b.subarray(3,7);
			var rotR = result.subarray(3,7);
			quat.slerp( rotR, rotB, rotA, t );
			quat.normalize( rotR, rotR );
			break;
		default:
			for(var i = 0; i < value_size; i++)
				result[i] = a[i] * t + b[i] * (1-t);
	}
	return result;
}

Animation.EvaluateHermiteSpline = function( p0, p1, pre_p0, post_p1, s )
{
	var s2 = s * s;
	var s3 = s2 * s;
	var h1 =  2*s3 - 3*s2 + 1;          // calculate basis function 1
	var h2 = -2*s3 + 3*s2;              // calculate basis function 2
	var h3 =   s3 - 2*s2 + s;         // calculate basis function 3
	var h4 =   s3 -  s2;              // calculate basis function 4
	
	var t0 = p1 - pre_p0;
	var t1 = post_p1 - p0;

	return h1 * p0 + h2 * p1 + h3 * t0 + h4 * t1;
}

Animation.EvaluateHermiteSplineVector = function( p0, p1, pre_p0, post_p1, s, result )
{
	result = result || new Float32Array( result.length );

	var s2 = s * s;
	var s3 = s2 * s;
	var h1 =  2*s3 - 3*s2 + 1;          // calculate basis function 1
	var h2 = -2*s3 + 3*s2;              // calculate basis function 2
	var h3 =   s3 - 2*s2 + s;         // calculate basis function 3
	var h4 =   s3 -  s2;              // calculate basis function 4

	for(var i = 0, l = result.length; i < l; ++i)
	{
		var t0 = p1[i] - pre_p0[i];
		var t1 = post_p1[i] - p0[i];
		result[i] = h1 * p0[i] + h2 * p1[i] + h3 * t0 + h4 * t1;
	}

	return result;
}

LS.registerResourceClass( Animation );

//extra interpolators ***********************************
LS.Interpolators = {};

LS.Interpolators["texture"] = function( a, b, t, last )
{
	var texture_a = a ? LS.getTexture( a ) : null;
	var texture_b = b ? LS.getTexture( b ) : null;

	if(a && !texture_a && a[0] != ":" )
		LS.ResourcesManager.load(a);
	if(b && !texture_b && b[0] != ":" )
		LS.ResourcesManager.load(b);

	var texture = texture_a || texture_b;

	var black = gl.textures[":black"];
	if(!black)
		black = gl.textures[":black"] = new GL.Texture(1,1, { format: gl.RGB, pixel_data: [0,0,0], filter: gl.NEAREST });

	if(!texture)
		return black;

	var w = texture ? texture.width : 256;
	var h = texture ? texture.height : 256;

	if(!texture_a)
		texture_a = black;
	if(!texture_b)
		texture_b = black;

	if(!last || last.width != w || last.height != h || last.format != texture.format )
		last = new GL.Texture( w, h, { format: texture.format, type: texture.type, filter: gl.LINEAR } );

	var shader = gl.shaders[":interpolate_texture"];
	if(!shader)
		shader = gl.shaders[":interpolate_texture"] = GL.Shader.createFX("color = mix( texture2D( u_texture_b, uv ), color , u_factor );", "uniform sampler2D u_texture_b; uniform float u_factor;" );

	gl.disable( gl.DEPTH_TEST );
	last.drawTo( function() {
		gl.clearColor(0,0,0,0);
		gl.clear( gl.COLOR_BUFFER_BIT );
		texture_b.bind(1);
		texture_a.toViewport( shader, { u_texture_b: 1, u_factor: t } );
	});

	return last;
}