﻿//
// 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);
    				}
					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);
};



function AnimatableNode(domNode) {
    this.node = domNode;
}
AnimatableNode.prototype.getValue = function AnimatableNode_getValue(propertyName) {
    var node = this.node;
    var value = 0;
    switch (propertyName) {
        case "x":       { value = node.style.left; break; }
        case "y":       { value = node.style.top; break; }
        case "width":   { value = node.style.width; break; }
        case "height":  { value = node.style.height; break; }
        case "opacity": { 
                          value = (node.style.opacity * 100);  
                          break; 
                        }
        case "scrollPos": { value = node.parentElement.scrollTop; break;}
    }
    // If the properties are not set correctly in the stylesheet, this may produce NaN...
    return parseInt(value);
};

AnimatableNode.prototype.setValue = function AnimatableNode_setValue(propertyName, value) {
    var node = this.node;
    value = value + "px"; // animations in pixels
    switch (propertyName) {
        case "x":       { node.style.left = value; break; }
        case "y":       { node.style.top = value; break; }
        case "width":   { node.style.width = value; break; }
        case "height":  { node.style.height = value; break; }
        case "opacity": { 
                          node.style.opacity = (parseInt(value) / 100)
                          break;
                        }
        case "scrollPos": { node.parentElement.scrollTop = parseInt(value); break;}
    }
};


function GetScrollPos() {
    
    var ScrollTop = document.body.scrollTop;
    if (ScrollTop == 0)
    {
        if (window.pageYOffset)
            ScrollTop = window.pageYOffset;
        else
            ScrollTop = (document.body.parentElement) ? document.body.parentElement.scrollTop : 0;
    }
    
    return ScrollTop;
}
function SetScrollPos() {
    
    var mode = "scrollTop";
    
    var ScrollTop = document.body.scrollTop;
    if (ScrollTop == 0)
    {
        if (window.pageYOffset)
            ScrollTop = window.pageYOffset;
        else
            ScrollTop = (document.body.parentElement) ? document.body.parentElement.scrollTop : 0;
    }
    
    return ScrollTop;
}
