
/*
 * Fader - a JavaScript module for fading an HTML element from one color to
 * another, over a variable amount of time.
 *
 * Author:	Benjamin Black <benjamin.black@gmail.com>
 * Revision:	3.1.2
 * Link:	http://www.benjaminblack.net/js/Fader.js
 * License:	GPL (http://www.gnu.org/copyleft/gpl.html)
 * 
 * Copyright (C) 2006  Benjamin Black
 *
 * 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.
 *
 */


/**
 * Initiates a fade from the color specified by 'from', to the color specified by 'to.'
 * Only the first three arguments are required: the HTML element (containing a 'style' 
 * property pointing to valid CSS object) and the from/to colors.  The 'duration' 
 * argument defaults to 1000 (milliseconds), the 'id' argument defaults to 
 * "Fader_global", and the callback is entirely optional (may be null).
 *
 * The 'from' color is used only if there is not a currently active fade in the state
 * buffer with the supplied ID.  If an active fade for that ID exists, its instantaneous
 * color when this function is called is used in place.  This behavior can be overridden
 * with a call to fadeClear(); see the comment documentation for that function below.
 * 
 * Only one fade at a time can use the same ID, so if a fade with an already scheduled 
 * ID is scheduled, the previously scheduled fade will be cancelled, at whatever state 
 * it is currently in, by calling clearTimeout() on its timeoutID state variable, and the 
 * new fade will run in its place; as such, separate elements in the HTML document should 
 * be faded using unique IDs, such as the ID of the HTML element itself.
 *
 * An optional callback hook can be specified, which is run at the completion of the fade.  
 * This function is passed the State object of the just completed fade (see comment 
 * documentation for the __Fader_State class below).
 *
 *   element:	HTML element to be faded
 *   from:	A string specifying the color to start fading from.  May be in any of the
 *		allowable CSS color formats: #XXX, #XXXXXX, or rgb(X,X,X); or it may be a 
 *		named color; for a list of possible named colors, see the 'simple_colors' 
 * 		Array below.
 *   to:	A string specifying the color to finish fading to.  May be in any of the 
 *		allowable CSS color formats: #XXX, #XXXXXX, or rgb(X,X,X); or it may be a 
 *		named color; for a list of possible named colors, see the 'simple_colors' 
 * 		Array below.
 *   duration:	duration in milliseconds of the fade from beginning to end
 *   id:	fade ID for the state buffer, defaults to "FC_global" if not specified.
 *   		may be a string, a number, or an object; objects are converted to strings
 *   		by calling object.toString().
 *   callback:	a function hook that is called at the end of the fade
 *
 */
function fade(element, from, to, duration, id, callback)
{
	// Try/catch block to check the arguments.  All exceptions are caught in the 
	// catch block, displayed with an alert() call, and re-thrown.
	try {
		if (element == null || element.style == null)
			throw new Error("fader: 'element' must be an valid object and have a 'style' property");
		
		if (typeof from != "string")
			throw new Error("fader: 'from' must be a string");
	
		if (typeof to != "string")
			throw new Error("fader: 'to' must be a string");
	
		if (duration == null)
			duration = 1000;
			
		if (typeof duration != "number")
			throw new Error("fader: 'duration' must be a number");
		
		if (duration % 1 != 0)
			throw new Error("fader: 'duration' must be a whole number");
	
		if (duration < __Fader_fadeStep)
			throw new Error("fader: 'duration' too small, must be greater than fade step (" + __Fader_fadeStep + ")");

		if (id == null)
			id = "Fader_global";
		
		if (typeof id != "string" && typeof id != "number")
			throw new Error("fader: 'id' must be a string or a number!");
		
		id = "" + id; // convert number to string
		
		if (callback != null && typeof callback != "function")
			throw new Error("fader: 'callback' must be a function!");
		
		from = new RGBColor(from);
		to = new RGBColor(to);

		if (!from.ok)
			throw new Error("fader: 'from' is not a valid color!");
		if (!to.ok)
			throw new Error("fader: 'to' is not a valid color!");
	}
	catch(e) {
		alert(e);
		throw e;
	}
	
	var state;
	
	// check if there is an active fade for this ID, and cancel it if it exists
	state = __Fader_stateBuffer[id];
	if (state != null && state.timeoutID != null)
		clearTimeout(state.timeoutID);

	// if there is a currently active fade for this ID, use the current color from
	// it instead of the color specified by 'from'
	var redStart, greenStart, blueStart;
	if (state != null) {
		redStart = state.colorState.red;
		greenStart = state.colorState.green;
		blueStart = state.colorState.blue;
	}
	else {
		redStart = from.r;
		greenStart = from.g;
		blueStart = from.b;
	}
	
	// calculate the percent by which each RGB color should change with each update
	var percentChange = __Fader_fadeStep / duration;
	
	// calculate the values by which each RGB color should change with each update,
	// but ensure that they are at least -1 or 1.  Note that we use the RGB values
	// of 'from' even if the start values are "inherited" from an active fade.
	var redStep = ((redStep = (to.r - from.r) * percentChange) > 0) ? 
			Math.max(1, Math.round(redStep)) : 
			Math.min(-1, Math.round(redStep));
	var greenStep = ((greenStep = (to.g - from.g) * percentChange) > 0) ? 
			Math.max(1, Math.round(greenStep)) : 
			Math.min(-1, Math.round(greenStep));
	var blueStep = ((blueStep = (to.b - from.b) * percentChange) > 0) ? 
			Math.max(1, Math.round(blueStep)) : 
			Math.min(-1, Math.round(blueStep));

	// set the final values (always from 'to')
	var redFinal = to.r;
	var greenFinal = to.g;
	var blueFinal = to.b;
	
	// set the comparison functions for comparing the RGB current values to the final
	// values to Math.min or Math.max, depending on if the step is positive or negative
	// respectively
	var redComp = (redStep > 0) ? Math.min : Math.max;
	var greenComp = (greenStep > 0) ? Math.min : Math.max;
	var blueComp = (blueStep > 0) ? Math.min : Math.max;
	
	// initialize the ColorState with the start, step, and final values for the RGB
	// colors, and the comparison functions for the colors, for this fade
	var colorState = new __Fader_ColorState(redStart, greenStart, blueStart,
						redStep, greenStep, blueStep,
						redFinal, greenFinal, blueFinal,
						redComp, greenComp, blueComp);

	// initialize the State object with the state for this fade
	state = __Fader_stateBuffer[id] = new __Fader_State(id, element, colorState, callback);

	// start the fade
	__Fader_update(id);
}


/**
 * This function stops a currently active fade, specified by ID, by clearing the active 
 * timeout, if any, for the state for the ID, and removes the state from the buffer.
 *
 * This function is useful to override the behavior of the fade() function, which is to 
 * ignore the 'from' color if there is a currently active fade for the ID, and use the
 * instantaneous color of that active fade in its place.  Clearing the state for an ID
 * by calling this function prior to a fade() call effectively forces fade() to use the 
 * supplied 'from' color.
 *
 *  id:	identifier, index in the state buffer
 */
function fadeClear(id)
{
	var state = __Fader_stateBuffer[id];
	if (state != null) {

		// stop scheduled update, if it exists
		if (state.timeoutID != null) {
			clearTimeout(state.timeoutID);
		}
		
		// remove state from buffer
		__Fader_stateBuffer[id] = null;

	}
}


// The state buffer holds all State objects that are currently active (fading in or out).
var __Fader_stateBuffer = new Array();

// Specifies the time(ms) between updates while fading 
var __Fader_fadeStep = 50;



/**
 * Constructor for State objects.  A State object holds the state of a currently active 
 * fade.
 *
 * For internal use; not intended to be instantiated by users.
 *
 *   id:		the ID used to retrieve this state object from the state buffer
 *   fadeObject:	the HTML element that is being faded
 *   colorState:	an object representing the active color state for the fade
 *   callback:		a function hook that is called at the end of the fade
 */
function __Fader_State(id, fadeObject, colorState, callback)
{
	this.id = id;
	this.fadeObject = fadeObject;
	this.colorState = colorState;
	this.callback = callback;

	this.timeoutID = null;
}


/**
 * Constructor for ColorState objects.  A ColorState object represents the active color 
 * state of a currently active fade and stores all the necessary color information for 
 * the fade.
 *
 * This function is for internal use only.
 *
 *   red:		current RGB red color value
 *   green:		current RGB green color value
 *   blue:		current RGB blue color value
 *   redStep:		amount the RGB red color value changes with each fade update
 *   greenStep:		amount the RGB green color value changes with each fade update
 *   blueStep:		amount the RGB blue color value changes with each fade update
 *   redFinal:		final RGB red color value to fade to
 *   greenFinal:	final RGB green color value to fade to
 *   blueFinal:		final RGB blue color value to fade to
 *   redComp:		comparison function to compare current red to final red value;
 *			points to Math.min or Math.max depending on the sign of redStep
 *   greenComp:		comparison function to compare current green to final green value;
 *			points to Math.min or Math.max depending on the sign of greenStep
 *   blueComp:		comparison function to compare current blue to final blue value;
 *			points to Math.min or Math.max depending on the sign of blueStep
 */
function __Fader_ColorState(	red, green, blue, 
				redStep, greenStep, blueStep, 
				redFinal, greenFinal, blueFinal,
				redComp, greenComp, blueComp)
{
	this.red = red;
	this.green = green;
	this.blue = blue;
	this.redStep = redStep;
	this.greenStep = greenStep;
	this.blueStep = blueStep;
	this.redFinal = redFinal;
	this.greenFinal = greenFinal;
	this.blueFinal = blueFinal;
	this.redComp = redComp;
	this.greenComp = greenComp;
	this.blueComp = blueComp;
}


/**
 * This function is for internal use and is not intended to be called directly by
 * the user.  It is the function that does the work of fading, maintaining the state
 * of the fade and cleaning up when it's finished.  It is called by the fade() function
 * to begin the fade, and it calls itself repeatedly until the fade is finished.  It 
 * must be a global function for setTimeout() to be able to use it.
 * 
 * This function was intentionally designed to be as tight as possible, so many variables
 * which were directly calculated in earlier versions are now managed by the state objects
 * instead, and indirectly "passed" to this function.
 *
 * This function is for internal use only.
 *
 *   id:	the id of the fade to retrieve the state from the buffer
 */
function __Fader_update(id)
{
	var state = __Fader_stateBuffer[id];
	
	// clear the timeoutID (too late to interrupt this update)
	state.timeoutID = null;
	
	with (state.colorState) {
		// step the RGB values, capping at their final values
		red = redComp(red + redStep, redFinal);
		green = greenComp(green + greenStep, greenFinal);
		blue = blueComp(blue + blueStep, blueFinal);
		
		// update the color of the HTML element with the updated values
		state.fadeObject.style.color = "rgb(" + red + "," + green + "," + blue + ")";
		
		// if any of the RGB values are not at their final values, schedule another
		// update; otherwise finish
		if (red != redFinal || green != greenFinal || blue != blueFinal) {
			state.timeoutID = setTimeout("__Fader_update(\""+id+"\");", __Fader_fadeStep);
		}
		else {
			state.colorState = null;
			__Fader_stateBuffer[id] = null;
			if (state.callback != null)
				state.callback(state);
		}
	}
}


/**
 * A class to parse color values
 * @author Stoyan Stefanov <sstoo@gmail.com>
 * @link   http://www.phpied.com/rgb-color-parser-in-javascript/
 * @license Use it if you like it
 */
function RGBColor(color_string)
{
    this.ok = false;

    // strip any leading #
    if (color_string.charAt(0) == '#') { // remove # if any
        color_string = color_string.substr(1,6);
    }

    color_string = color_string.replace(/ /g,'');
    color_string = color_string.toLowerCase();

    // before getting into regexps, try simple matches
    // and overwrite the input
    for (var key in this.simple_colors) {
        if (color_string == key) {
            color_string = this.simple_colors[key];
        }
    }
    // emd of simple type-in colors

    // search through the definitions to find a match
    for (var i = 0; i < this.color_defs.length; i++) {
        var re = this.color_defs[i].re;
        var processor = this.color_defs[i].process;
        var bits = re.exec(color_string);
        if (bits) {
            channels = processor(bits);
            this.r = channels[0];
            this.g = channels[1];
            this.b = channels[2];
            this.ok = true;
        }

    }

    // validate/cleanup values
    this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
    this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
    this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);

}

    // array of color definition objects
RGBColor.prototype.color_defs = [
	{
		re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
		process: function (bits){
			return [
				parseInt(bits[1]),
				parseInt(bits[2]),
				parseInt(bits[3])
			];
		}
	},
	{
		re: /^(\w{2})(\w{2})(\w{2})$/,
		process: function (bits){
			return [
				parseInt(bits[1], 16),
				parseInt(bits[2], 16),
				parseInt(bits[3], 16)
			];
		}
	},
	{
		re: /^(\w{1})(\w{1})(\w{1})$/,
		process: function (bits){
			return [
				parseInt(bits[1] + bits[1], 16),
				parseInt(bits[2] + bits[2], 16),
				parseInt(bits[3] + bits[3], 16)
			];
		}
	}
];

RGBColor.prototype.simple_colors = {
	aliceblue: 'f0f8ff',
	antiquewhite: 'faebd7',
	aqua: '00ffff',
	aquamarine: '7fffd4',
	azure: 'f0ffff',
	beige: 'f5f5dc',
	bisque: 'ffe4c4',
	black: '000000',
	blanchedalmond: 'ffebcd',
	blue: '0000ff',
	blueviolet: '8a2be2',
	brown: 'a52a2a',
	burlywood: 'deb887',
	cadetblue: '5f9ea0',
	chartreuse: '7fff00',
	chocolate: 'd2691e',
	coral: 'ff7f50',
	cornflowerblue: '6495ed',
	cornsilk: 'fff8dc',
	crimson: 'dc143c',
	cyan: '00ffff',
	darkblue: '00008b',
	darkcyan: '008b8b',
	darkgoldenrod: 'b8860b',
	darkgray: 'a9a9a9',
	darkgreen: '006400',
	darkkhaki: 'bdb76b',
	darkmagenta: '8b008b',
	darkolivegreen: '556b2f',
	darkorange: 'ff8c00',
	darkorchid: '9932cc',
	darkred: '8b0000',
	darksalmon: 'e9967a',
	darkseagreen: '8fbc8f',
	darkslateblue: '483d8b',
	darkslategray: '2f4f4f',
	darkturquoise: '00ced1',
	darkviolet: '9400d3',
	deeppink: 'ff1493',
	deepskyblue: '00bfff',
	dimgray: '696969',
	dodgerblue: '1e90ff',
	feldspar: 'd19275',
	firebrick: 'b22222',
	floralwhite: 'fffaf0',
	forestgreen: '228b22',
	fuchsia: 'ff00ff',
	gainsboro: 'dcdcdc',
	ghostwhite: 'f8f8ff',
	gold: 'ffd700',
	goldenrod: 'daa520',
	gray: '808080',
	green: '008000',
	greenyellow: 'adff2f',
	honeydew: 'f0fff0',
	hotpink: 'ff69b4',
	indianred : 'cd5c5c',
	indigo : '4b0082',
	ivory: 'fffff0',
	khaki: 'f0e68c',
	lavender: 'e6e6fa',
	lavenderblush: 'fff0f5',
	lawngreen: '7cfc00',
	lemonchiffon: 'fffacd',
	lightblue: 'add8e6',
	lightcoral: 'f08080',
	lightcyan: 'e0ffff',
	lightgoldenrodyellow: 'fafad2',
	lightgrey: 'd3d3d3',
	lightgreen: '90ee90',
	lightpink: 'ffb6c1',
	lightsalmon: 'ffa07a',
	lightseagreen: '20b2aa',
	lightskyblue: '87cefa',
	lightslateblue: '8470ff',
	lightslategray: '778899',
	lightsteelblue: 'b0c4de',
	lightyellow: 'ffffe0',
	lime: '00ff00',
	limegreen: '32cd32',
	linen: 'faf0e6',
	magenta: 'ff00ff',
	maroon: '800000',
	mediumaquamarine: '66cdaa',
	mediumblue: '0000cd',
	mediumorchid: 'ba55d3',
	mediumpurple: '9370d8',
	mediumseagreen: '3cb371',
	mediumslateblue: '7b68ee',
	mediumspringgreen: '00fa9a',
	mediumturquoise: '48d1cc',
	mediumvioletred: 'c71585',
	midnightblue: '191970',
	mintcream: 'f5fffa',
	mistyrose: 'ffe4e1',
	moccasin: 'ffe4b5',
	navajowhite: 'ffdead',
	navy: '000080',
	oldlace: 'fdf5e6',
	olive: '808000',
	olivedrab: '6b8e23',
	orange: 'ffa500',
	orangered: 'ff4500',
	orchid: 'da70d6',
	palegoldenrod: 'eee8aa',
	palegreen: '98fb98',
	paleturquoise: 'afeeee',
	palevioletred: 'd87093',
	papayawhip: 'ffefd5',
	peachpuff: 'ffdab9',
	peru: 'cd853f',
	pink: 'ffc0cb',
	plum: 'dda0dd',
	powderblue: 'b0e0e6',
	purple: '800080',
	red: 'ff0000',
	rosybrown: 'bc8f8f',
	royalblue: '4169e1',
	saddlebrown: '8b4513',
	salmon: 'fa8072',
	sandybrown: 'f4a460',
	seagreen: '2e8b57',
	seashell: 'fff5ee',
	sienna: 'a0522d',
	silver: 'c0c0c0',
	skyblue: '87ceeb',
	slateblue: '6a5acd',
	slategray: '708090',
	snow: 'fffafa',
	springgreen: '00ff7f',
	steelblue: '4682b4',
	tan: 'd2b48c',
	teal: '008080',
	thistle: 'd8bfd8',
	tomato: 'ff6347',
	turquoise: '40e0d0',
	violet: 'ee82ee',
	violetred: 'd02090',
	wheat: 'f5deb3',
	white: 'ffffff',
	whitesmoke: 'f5f5f5',
	yellow: 'ffff00',
	yellowgreen: '9acd32'
};

// some getters
RGBColor.prototype.toRGB = function () {
	return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
}

RGBColor.prototype.toHex = function () {
	var r = this.r.toString(16);
	var g = this.g.toString(16);
	var b = this.b.toString(16);
	if (r.length == 1) r = '0' + r;
	if (g.length == 1) g = '0' + g;
	if (b.length == 1) b = '0' + b;
	return '#' + r + g + b;
}

