//
// AnimationLibrary
//
// A simple library for animating the properties of JavaScript objects.
//
// See: http://www.cybergrain.com/tech/anim
//
// Copyright Jon Meyer, 2004
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
// 
// See: http://www.gnu.org/licenses/gpl.txt for mre
//

function AnimationLibrary() { }

//
// Setting this to false disables all animations - after that, any call to
// animate() is instant.
//
AnimationLibrary.enabled = true;

//
// Indicates how often the animation library's tick function is called (in millis).
// The tick function uses the system clock to determine animation values, so
// chaning this alters the frame rate (how jerky/smooth things look) but not the
// amount of time an animation takes.
//
AnimationLibrary.tickInterval = 50;

// List of ongoing animations
//
AnimationLibrary.animations = new Array();

// Create an animation - basic argument form is:
//
// AnimationLibrary.animate(object, durationInMillis,
//       propertyName1, value1,
//       propertyName2, value2,
//       ...
//       propertyNameN, valueN);
//
// See documentation for further discussion of special property names.
//
AnimationLibrary.animate = function() {
	AnimationLibrary.init();
	
	var control = arguments[0];
	if (control == undefined) {
		return;
	}

	var dur = arguments[1];

	var b = 0.5, g = 0.5, onComplete = undefined;
	
	var fromVals = new Object(), toVals = new Object();
	for (var i = 2; i< arguments.length; i += 2) {
		var pname = arguments[i], val = arguments[i+1];
		switch (pname) {
		    case "bias": { b = val; break; }
		    case "gain": { g = val; break; }
		    case "onComplete": { onComplete = val; break; }
		    default:		       
			    if (!AnimationLibrary.enabled) {
				    control.setValue(pname, val);
			    } else {
				    fromVals[pname] = control.getValue(pname);
				    toVals[pname] = val;
			    }	
		}
	}
	
	var a = AnimationLibrary.findAnimation(control, fromVals, true);

	if (a != null) {
		a.toVals = toVals;
		a.nextAnim = null;
		a.prepare();
		return a;
	}

	a = new AnimationObject(control, fromVals, toVals, dur, b, g, onComplete);
	if (AnimationLibrary.enabled) {AnimationLibrary.animations.push(a);}
	return a;
};

//
// Stops any animations. This has two uses:
//
// By anim object
//    var anim = AnimationLibrary.animate(myControl, 1000, "x", 100);
//    AnimationLibrary.stop(anim);
//
// By control:
//
//     AnimationLibrary.stop(myControl); // clears all animations on myControl
//
//
AnimationLibrary.stop = function AnimationLibrary_stop(controlOrAnimObj) {
	if (controlOrAnimObj == null) return;

	var done = undefined;
	var animations = AnimationLibrary.animations;
	
	for (var i = 0; i < animations.length; i++) {
		var anim = animations[i];
		if (anim == controlOrAnimObj || anim.control == controlOrAnimObj) {
			if (done == undefined) {
				done = new Array();
			}
			done.push(anim);
		}
	}

	if (done != undefined) {
		for (var i = 0; i<done.length; i++) {
			var index = AnimationLibrary.indexOfAnimation(done[i]);
			animations.splice(index, 1);
		}
	}
};

AnimationLibrary.indexOfAnimation = function AnimationLibrary_indexOfAnimation(controlOrAnimObj) {
	if (controlOrAnimObj == null) return -1;
	var animations = AnimationLibrary.animations;
	
	for (var i = 0; i< animations.length; i++) {
		var a = animations[i];
		if (a == controlOrAnimObj || a.control == controlOrAnimObj) {
			return i;
		}
	}
	return -1;
};

//
// Given a property name that is being animated on a control, return the associated 
// animation object. This only matches animation objects on which the property names are
// the same. If cancel is true, then the animation is stopped if it ongoing.
//
AnimationLibrary.findAnimation = function(control, pnames, cancel) {
	if (control == null) return null;
	var animations = AnimationLibrary.animations;
	for (var i = animations.length - 1; i >= 0; i--) {
		var a = animations[i];
		if (a.control == control) {
			if (pnames != undefined) {
				var match = true;
				if (a.fromVals.length != pnames.length) match = false;
				for (var key in pnames) {
					if (cancel) {
					    // This prevents the animation from 
					    // doing anything more with the property name. Note that
					    // other properties may be part of this animation, so we 
					    // leave those alone.
					    a.toVals[key] = null; 
					}
					if (a.fromVals[key] == undefined) { match = false; break; } 
				}
				if (!match) continue;
			}
			return a;
		}
	}
	return null;
};

//
// Gets an int milliseconds that is a representation of the time elapsed since 
// the page loaded.
//
AnimationLibrary.getTimeMillis = function AnimationLibrary_getTimeMillis() {
    var start = AnimationLibrary.startTime;
    var now = new Date();
    return now.getTime() - start;
};

// Used for debugging. Requires an element like this to be added to the page...
// <textarea rows=20 cols=80 id="spew">
//
AnimationLibrary.debugMsg = function debugMsg(keyword, animObj) 
{
    if (document.getElementById("spew") != null) 
    {
        var str = keyword + ": " + animObj.toString() + "\n";
        document.getElementById("spew").value += str;
    }
}

//
// Does one tick's worth of animation
//
AnimationLibrary.tick = function AnimationLibrary_tick() {
	var animations = AnimationLibrary.animations;
	var time = AnimationLibrary.getTimeMillis();
	var toRemove = undefined;
	var toAdd = undefined;
	
	for (var i = 0; i < animations.length; i++) 
	{
		var anim = animations[i];
		var control = anim.control;
		var t = (time-anim.t)/(anim.d);
		var fromVals = anim.fromVals;
		var toVals = anim.toVals;
		
		if (t > 1) 
		{
			if (toRemove == undefined) 
			{
				toRemove = new Array();
			}
			
			for (var key in toVals) 
			{
				if (fromVals[key] != undefined) 
				{
					var val = toVals[key];
					var type = typeof(val);
					if (type == 'function') 
					{ 
					    val = val(1.0); 
					    type = typeof(val); 
					}
					
					if (type == 'number') 
					{
    					anim.control.setValue(key, val);
	    				}
				}
			}
			
			toRemove.push(anim);
			anim.fireComplete();
			
			if (anim.nextAnim != undefined) 
			{
				if (toAdd == undefined) 
				{
					toAdd = new Array();
				}
				anim.nextAnim.prepare();
				toAdd.push(anim.nextAnim);
			}
		}
		else 
		{
			if (anim.b != 0.5) {
				t = Math.bias(t, anim.b);
			}
			if (anim.g != 0.5) {
				t = Math.gain(t, anim.g);
			}
			
    	    		AnimationLibrary.debugMsg("Animate", anim);
			for (key in fromVals) 
			{
				var from = fromVals[key];
				var to = toVals[key];
				if (typeof(from) == 'number') 
				{
					var val = toVals[key];
					var type = typeof(val);
					
					if (type == 'function') 
					{ 
					    	val = val(t); 
					    	type = typeof(val); 
					} 
					else if (type == 'number') 
					{
    						val = Math.lerp(t, from, val);
					}
					
					if (type == 'number') 
					{
    						anim.control.setValue(key, val);
    					}
    					
					//BDS anim.control.setValue(key, val);
				}
			}
		}
	}
	
	if (toRemove != undefined) {
		for (var i = 0; i<toRemove.length; i++) {
			var anim = toRemove[i];
    	    AnimationLibrary.debugMsg("Remove", anim);
			var index = AnimationLibrary.indexOfAnimation(anim);
			animations.splice(index, 1);
		}
	}
	if (toAdd != undefined) {
		for (var i = 0; i < toAdd.length; i++) {
			animations.push(toAdd[i]);
		}
	}
    setTimeout("AnimationLibrary.tick();", AnimationLibrary.tickInterval);
};

// Initialization function
//
AnimationLibrary.inited = false;

AnimationLibrary.init = function AnimationLibrary_init() {
	if (!AnimationLibrary.inited) {
		AnimationLibrary.inited = true;
        var now = new Date();
        AnimationLibrary.startTime = now.getTime();

        // Start ticking
        AnimationLibrary.tick();
	}
};

//
// Animation Object - holds state for one ongoing animation
//
function AnimationObject(control, fromVals, toVals, duration, bias, gain, onComplete) {
	this.control = control;
	this.fromVals = fromVals;
	this.toVals = toVals;
	this.t = AnimationLibrary.getTimeMillis();
	this.d = duration;
	this.b = bias;
	this.g = gain;
	this.onComplete = onComplete;
	this.firstAnim = this;
};

AnimationObject.prototype.then = function AnimationObject_then()
{
	var control = this.control;
	var dur = arguments[0];
	var b = 0.5, g = 0.5, onComplete = undefined;

	var fromVals = new Object(), toVals = new Object();
	for (var i = 1; i< arguments.length; i += 2) {
		var pname = arguments[i], val = arguments[i+1];
        switch (pname) {
		    case "bias": { b = val; break; }
		    case "gain": { g = val; break; }
		    case "onComplete": { onComplete = val; break; }
		    default:		       
			    if (!AnimationLibrary.enabled) {
				    control.setValue(pname, val);
			    } else {
				    toVals[pname] = val;
			    }	
		}
	}

	this.nextAnim = new AnimationObject(control, fromVals, toVals, dur, b, g, onComplete);
	this.nextAnim.firstAnim = this.firstAnim;
	return this.nextAnim;
};

AnimationObject.prototype.forever = function AnimationObject_forever() {
    this.nextAnim = this.firstAnim;
};

AnimationObject.prototype.prepare = function AnimationObject_prepare() {
	this.t = AnimationLibrary.getTimeMillis();
	for (var key in this.toVals) {
		if (this.toVals[key] != undefined) {
			this.fromVals[key] = this.control.getValue(key);
		}
	}	
};

AnimationObject.prototype.fireComplete = function AnimationObject_fireComplete() {
	if (this.onComplete != undefined) this.onComplete(this);
};

AnimationObject.prototype.toString = function AnimationObject_toString() {
	var result = "{Anim: control " + this.control + " t " + this.t;  
	for (var key in this.fromVals) {
		result += " " + key + " " + this.fromVals[key] + "->" + this.toVals[key];
	}
	result += "}";
	return result;
};

//
// Math Helpers
// 
Math.lerp = function Math_lerp(t, a, b) { 
    return a+t*(b-a); 
};

Math.LOG_HALF = -0.6931471805599453; // Math.log(0.5) = -0.6931471805599453

// Perlin Bias Function	
Math.bias = function Math_bias(a, b) {
    if (b == 0.5) return a;
	if (a<0.001) {
		return 0;
	}
	if (a>0.999) {
		return 1;
	}
	if (b<0.001) {
		return 0;
	}
	if (b>0.999) {
		return 1;
	}
	return Math.pow(a, Math.log(b)/Math.LOG_HALF); 
};

// Perlin Gain Function	
Math.gain = function Math_gain(a, b) {
    if (b == 0.5) return a;
	if (a<0.001) {
		return 0;
	}
	if (a>0.999) {
		return 1;
	}
	b = Math.max(0.001, Math.min(.999, b));
	var p = Math.log(1-b)/ Math.LOG_HALF;
	return a < 0.5 ? (Math.pow(2 * a, p) / 2) : 1 - (Math.pow(2 * (1 - a), p) / 2);
};	

