/*______________
|       ______  |   U I Z E     J A V A S C R I P T     A P I
|     /      /  |   -----------------------------------------
|    /    O /   |    MODULE : Uize.Fade Class (version 1.0.0)
|   /    / /    |    AUTHOR : Chris van Rensburg (http://www.tomkidding.com)
|  /    / /  /| |    ONLINE : http://www.tomkidding.com/uize/uize-js-api
| /____/ /__/_| | COPYRIGHT : (c)2005-2006 Chris van Rensburg
|          /___ |   LICENSE : Distributed under the terms of the GNU General Public License
|_______________|             http://www.gnu.org/licenses/gpl.txt
*/

/*
	TO DO
		- possibly implement arbitrarily complex value objects (this would cater to arrays, objects, and simple numbers)

		- consider changing the implementation of reverse, to instead swap the start and end values (or offer an additional property for this kind of fade reversing)

		- optimization ideas
			- possible optimization for fade where the start value is 0 and the end value is 1 (ie. no interpolation needs to be performed, just use the progress value)

			- provide feature to allow value increments to be specified (this can be used to drive optimization, so that fewer advances occur)

		- implement rate according to real time
			- at start of fade, record time

			- at each step...
				- remember time
				- perform update, where progress is based upon delta between current time and start time
				- set timeout, where timeout is calculated by remaining time divided by remaining progress steps, and adjusted for by average delta between intra-step time lapsed and timeout set (of course, timeout can't go less than 0)

			- things to record
				- start time
				- for calculating average extra intra-step burden
					- cumulative time "drift"
					- total number of steps performed

	NOTES
		- speed is: endValue - startValue / duration
			such that, startValue + ((endValue - startValue) / duration) * duration = endValue

		- when there is acceleration and deceleration, sustainSpeed is calculated by...
			(acceleration * sustainSpeed / 2) + ((1 - acceleration - deceleration) * sustainSpeed) + (deceleration * sustainSpeed / 2) == simpleSpeed
			therefore, sustainSpeed * (acceleration / 2 + (1 - acceleration - deceleration) + deceleration / 2) == simpleSpeed
			therefore, sustainSpeed == simpleSpeed / (acceleration / 2 + (1 - acceleration - deceleration) + deceleration / 2)
			therefore, sustainSpeed == simpleSpeed / ((acceleration + 2 * (1 - acceleration - deceleration) + deceleration) / 2)
			therefore, sustainSpeed == simpleSpeed / ((acceleration + 2 - 2 * acceleration - 2 * deceleration + deceleration) / 2)
			therefore, sustainSpeed == simpleSpeed / ((2 - acceleration - deceleration) / 2)
			therefore, sustainSpeed == 2 * simpleSpeed / (2 - acceleration - deceleration)

			where...
				simpleSpeed = speed when there are no acceleration and deceleration phases
				acceleration = fraction of duration spent accelerating (0 to 1)
				deceleration = fraction of duration spent decelerating (0 to 1)
				(1 - acceleration - deceleration) = fraction of duration spent at sustainSpeed

		- for convenience in this code, duration and displacement are both cast to 1 for the calculations. The final displacement is applied as a factor to the startValue-endValue delta in order to calculate running values.
*/

/*ScruncherSettings Mappings="=b" LineCompacting="TRUE"*/

/*?
	Introduction
		Summary
			A simple class that implements automation of a value (or set of values) between specified start and end values.

		Requires
			- Uize.js (base class)
*/

(function () {
	/*** Object Constructor ***/
		var
			_class = Uize.Fade = Uize.subclass (
				function () {
					var _this = this;

					/*** Private Instance Properties ***/
						_this._timeout = null;
						_this._timeoutHandler = function () {_this._advance ()};
				}
			),
			_classPrototype = _class.prototype
		;

	/*** Utility Functions ***/
		function _getTimeMs () {return (new Date ()).getTime ()}

	/*** Private Instance Methods ***/
		_classPrototype._advance = function () {
			var
				_this = this,
				_progress = Math.min ((_getTimeMs () - _this._startTimeMs) / _this._duration,1)
			;
			if (_this._reverse) _progress = 1 - _progress;
			_this.set ({_progress:_progress});
			if ((_this._reverse ? 0 - _progress : _progress - 1) >= 0) {
				_this.fireEvent ('Done');
					/*?
						Instance Events
							Done
								Fired each time the fade ends.

								NOTES
								- see also the =Start= instance event.
					*/
			} else {
				_this._timeout = setTimeout (_this._timeoutHandler,0);
			}
		};

	/*** Public Instance Methods ***/
		_classPrototype.stop = function () {
			var _this = this;
			if (_this._timeout) {
				clearTimeout (_this._timeout);
				_this._timeout = null;
			}
			/*?
				Instance Methods
					stop
						Stops the fade automation.

						NOTES
						- if the fade automation is not in progress, then this method has no effect
						- this method has no return value
						- see also the =start= instance method
			*/
		};

		_classPrototype.start = function () {
			var _this = this;
			_this.stop ();
			_this._startTimeMs = _getTimeMs ();
			if (_this._isNonLinear) {
				_this._sustain = 1 - _this._acceleration - _this._deceleration;
				_this._sustainSpeed = 2 / (2 - _this._acceleration - _this._deceleration);
				if (_this._hasAcceleration) _this._accelerationRate = _this._sustainSpeed / _this._acceleration;
				if (_this._hasDeceleration) _this._decelerationRate = - _this._sustainSpeed / _this._deceleration;
			}
			_this.fireEvent ('Start');
				/*?
					Instance Events
						Start
							Fired each time the fade starts.

							NOTES
							- see also the =Done= instance event.
				*/
			_this._value = _this._progress = null;
				/* NOTES:
					- this is to make sure there are always initial change events for these properties. However, maybe with the model of registering handlers on these properties, it shouldn't be necessary (or maybe it's not even appropriate) to coerce this change in this place. Maybe it's OK for the user of this class to only be informed when the value or progress actually changes.
				*/
			_this._advance ();
			/*?
				Instance Methods
					start
						Starts the fade automation.

						NOTES
						- Before the automation is started, it is stopped. So, if automation is in progress when this method is called, it will be reset and started over.
						- each time that automation is started using this method, the =Start= instance event is fired
						- this method has no return value
						- see also the =stop= instance method
			*/
		};

	/*** Public Static Methods ***/
		var _blendValues = _class.blendValues = function (_value1,_value2,_blend) {
			var _result;
			if (typeof _value1 == 'object' && _value1 && !(_value1 instanceof RegExp)) {
				if (_class.isArray (_value1)) {
					_result = [];
					_result.length = _value1.length;
				} else {
					_result = {};
				}
				for (var _property in _value1)
					_result [_property] = _blendValues (_value1 [_property],_value2 [_property],_blend)
				;
			} else {
				_result = _value1 + (_value2 - _value1) * _blend;
			}
			return _result;
			/*?
				Static Methods
					Uize.Fade.blendValues
						Returns a value, which is a blend between the two specified values.

						SYNTAX
						.................................................................................
						Uize.Fade.blendValues (value1INTorOBJorARRAY,value2INTorOBJorARRAY,blendFRACTION)
						.................................................................................

						In its simplest use, this method can be used to blend between two simple number type values, where the =blendFRACTION= parameter should be a floating point number in the range of =0= to =1=, where  a value of =0= will result in returning the value of the =value1INTorOBJorARRAY= parameter, the value of =1= will result in returning the value of the =value2INTorOBJorARRAY= parameter, and the value =.5= will result in returning the average of =value1INTorOBJorARRAY= and =value2INTorOBJorARRAY=.

						Beyond blending simple number values, this method can also be used to blend arbitrarily complex data structures, such as two arrays of numbers, or two RGB color objects whose properties are color channel values, or two arrays containing multiple objects whose properties are numbers, and even more complex structures.

						EXAMPLE
						...............................................................................
						Uize.Fade.blendValues ({red:255,green:255,blue:255},{red:0,green:0,blue:0},.5);
						...............................................................................

						To illustrate the ability to blend complex values, the above statement of code would return the object ={red:127.5,green:127.5,blue:127.5}=, corresponding to mid gray.
			*/
		};

	/*** Setup Properties ***/
		function _updateOptimizationProperties () {
			var _this = this;
			_this._hasAcceleration = _this._acceleration != 0;
			_this._hasDeceleration = _this._deceleration != 0;
			_this._isNonLinear = _this._hasAcceleration || _this._hasDeceleration;
		}

		_class.registerProperties ({
			_acceleration:{
				name:'acceleration',
				onChange:_updateOptimizationProperties,
				value:0
				/*?
					Set-get Properties
						acceleration
							A floating point value in the range of 0 to 1, specifying which fraction of the fade should be an acceleration phase.

							NOTES
							- the initial value is =0=
							- the values of the =acceleration= and =deceleration= set-get properties must add up to =1=
							- see also the =deceleration= set-get property
				*/
			},
			_deceleration:{
				name:'deceleration',
				onChange:_updateOptimizationProperties,
				value:0
				/*?
					Set-get Properties
						deceleration
							A floating point value in the range of 0 to 1, specifying which fraction of the fade should be a deceleration phase.

							NOTES
							- the initial value is =0=
							- the values of the =acceleration= and =deceleration= set-get properties must add up to =1=
							- see also the =deceleration= set-get property
				*/
			},
			_duration:{
				name:'duration',
				value:2000
				/*?
					Set-get Properties
						duration
							A value, in milliseconds, specifying how long the fade process should take.

							NOTES
							- the initial value is =2000= (ie. two seconds)
							- the actual duration of the fade process is not affected by the values of the =acceleration= and =deceleration= set-get properties
				*/
			},
			_endValue:{
				name:'endValue',
				onChange:_updateOptimizationProperties,
				value:100
				/*?
					Set-get Properties
						endValue
							The value (or set of values) that should be reached at the end of the fade process.

							NOTES
							- the initial value is =100=
							- see also the =startValue= set-get property
				*/
			},
			_progress:{
				name:'progress',
				onChange:function () {
					var
						_this = this,
						_blend = _this._progress
					;
					if (_this._isNonLinear) {
						function _constrain (_value,_min,_max) {return Math.max (Math.min (_value,_max),_min)}
						_blend = 0;
						if (_this._hasAcceleration) {
							var _accelerationElapsed = _constrain (_this._progress,0,_this._acceleration);
							_blend += (_accelerationElapsed * _this._accelerationRate / 2) * _accelerationElapsed;
						}
						if (_this._sustain)
							_blend += _this._sustainSpeed * _constrain (_this._progress - _this._acceleration,0,_this._sustain)
						;
						if (_this._hasDeceleration) {
							var _decelerationElapsed = _constrain (_this._progress - _this._acceleration - _this._sustain,0,_this._deceleration);
							_blend += (_this._sustainSpeed + _decelerationElapsed * _this._decelerationRate / 2) * _decelerationElapsed;
						}
						_blend = Math.min (_blend,1);
					}
					_this.set ({_value:_blendValues (_this._startValue,_this._endValue,_blend)});
				},
				value:0
				/*?
					Set-get Properties
						progress
							A floating point value in the range of 0 to 1, specifying the fraction of the full fade process that is complete. A value of =0= indicates that the fade is at the start, a value of =1= indicates that the fade is at the end, and a value of =.5= indicates that the fade is halfway through.

							NOTES
							- the initial value is =0=
				*/
			},
			_reverse:{
				name:'reverse',
				value:false
				/*?
					Set-get Properties
						reverse
							A boolean value, specifying whether or not the fade process should operate in reverse.

							Because this flag complements the value of the =progress= set-get property, the =progress= property will have a value of =1= when the fade starts and =0= when the fade is done. This may also mean that the nature of the fade will not be exactly reversed and may actually be different if there is asymmetry resulting from the specific values of the =acceleration= and =deceleration= set-get properties.

							NOTES
							- the initial value is =false=
				*/
			},
			_startValue:{
				name:'startValue',
				onChange:_updateOptimizationProperties,
				value:0
				/*?
					Set-get Properties
						startValue
							The value (or set of values) at which the fade process should start.

							NOTES
							- the initial value is =0=
							- see also the =endValue= set-get property
				*/
			},
			_value:{
				name:'value',
				value:0
				/*?
					Set-get Properties
						value
							The value (or set of values) at the current point in a fade that is in progress, as interpolated between the values of the =startValue= and =endValue= set-get properties.

							NOTES
							- the initial value is =0=
				*/
			}
		});
}) ();

