/** * @projectDescription * Класс для пакетной анимации кучи объектов и их свойств. * Базируется на коде * caurina.transitions.Tweener (http://code.google.com/p/tweener/) * и JSTweener (http://coderepos.org/share/wiki/JSTweener)<br><br> * @author Sergey Chikuyonok (sc@design.ru) * @copyright Art.Lebedev Studio (http://www.artlebedev.ru) * @link http://code.google.com/p/jtweener/ * @version 0.2.1 * * TODO включить в документацию анимационные функции и функции Безье *//** * Класс для пакетной анимации кучи объектов и их свойств. * В большинстве случаев для создания анимации нужно всего лишь * вызвать метод <code>jTweener.addTween()</code> */var jTweener = function() {	var looping = false;	var frameRate = 60;	var userAgent = navigator.userAgent.toLowerCase();	/** @type {Boolean} Говорит, что текущий браузер — Internet Explorer */	var is_msie = /msie/.test(userAgent) && !/opera/.test(userAgent);	/**	 * Хэш всех анимируемых в данный момент объектов, разбитые по нэймспэйсам.	 * Ключом хэша является нэймспэйс, а значением — массив анимируемых объектов	 */	var objects = {};	/** Действия для нэймспэйсов */	var nsActions = {};	/**	 * Стандартные значения анимации, который будут подставляться в каждый	 * <code>addTween()</code>, если они не указаны	 */	var defaultOptions = {		time: 1,		transition: 'easeoutexpo',		namespace: 'default',		delay: 0,		prefix: {},		suffix: {},		onStart: undefined,		onStartParams: undefined,		onUpdate: undefined,		onUpdateParams: undefined,		onComplete: undefined,		onCompleteParams: undefined	};	/**	 * Названия параметров, при анимации которых будет менятся цвет.	 */	var color_properties = ['backgroundColor', 'borderBottomColor',		'borderLeftColor', 'borderRightColor', 'borderTopColor',		'color', 'outlineColor', 'borderColor'];	/**	 * Регулярное выражение для разбора значения, до которого нужно анимировать.	 * Используется для разбора значений типа '+=30', '-=20'	 */	var re_value = /^\s*([+\-])=\s*(\-?\d+)/;	var inited = false;	var easingFunctionsLowerCase = {};	/**	 * Первичная инициализация объекта, вызывается только один раз при создании	 * первой анимации.	 */	function init() {		for (var key in jTweener.easingFunctions) {			easingFunctionsLowerCase[key.toLowerCase()] = jTweener.easingFunctions[key];		}		inited = true;	}	;	/**	 * Вспомогательный метод для callback-функций	 * @param {Function} func Функция, котрую нужно вызвать	 * @param {Array} [params] Агрументы функции	 * @param {Object} [context] Контекст выполнения функции	 */	function callback(func, params, context) {		if (typeof func == 'function') {			func.apply(context || window, params || []);		}	}	/**	 * Возвращает значение CSS-свойства <b>name</b> элемента <b>elem</b>	 * @author John Resig (http://ejohn.org)	 * @param {Element} elem Элемент, у которого нужно получить значение CSS-свойства	 * @param {String} name Название CSS-свойства	 * @return {String}	 */	function getStyle(elem, name) {		// If the property exists in style[], then it's been set		// recently (and is current)		if (elem.style[name]) {			return elem.style[name];		}		//Otherwise, try to use IE's method		else if (is_msie) {			var cs = elem.currentStyle;			if (name == 'opacity') {				// если хотим полцчить opacity — наверняка хотим анимировать это свойство,				// поэтому принудительно ставлю zoom = 1				elem.style.zoom = 1;				/**				 * взял код с jQuery				 * @author John Resig				 */				return cs.filter && cs.filter.indexOf("opacity=") >= 0 ?					   parseFloat(cs.filter.match(/opacity=([^)]*)/)[1]) / 100 :					   1;			} else {				return elem.currentStyle[name];			}		}		// Or the W3C's method, if it exists		else if (document.defaultView && document.defaultView.getComputedStyle) {				//It uses the traditional 'text-align' style of rule writing,				//instead of textAlign				name = name.replace(/([A-Z])/g, "-$1").toLowerCase();				// Get the style object and get the value of the property (if it exists)				var s = document.defaultView.getComputedStyle(elem, "");				return s && s.getPropertyValue(name);			}			//Otherwise, we're using some other browser			else {				return null;			}	}	/**	 * Проверяет, является ли переданный элемент массивом, и если нет,	 * то создает новый массив, элементом которого является переданный аргумент	 * @param {Object}			* @return {Array}	 */	function toArray(obj) {		return (!(obj instanceof Array) && !obj.jquery) ? [obj] : obj;	}	/**	 * Проверяет, является ли переданный объект html-нодом	 * @return {Boolean}	 */	function isNode(obj) {		return obj.nodeType ? true : false;	}	/**	 * Проверяет, является ли анимируемое свойство цветовым	 * @param {String} prop Название свойства	 * @return {Boolean}	 */	function isColorProperty(prop) {		for (var i = 0; i < color_properties.length; i++) {			if (color_properties[i] == prop) {				return true;			}		}		return false;	}	/**	 * Проверяет, является ли переданный объект функцией	 * @param {Object} obj	 * @return {Boolean}	 */	function isFunction(obj) {		return (typeof obj == 'function');	}	/**	 * Получить значение свойства <b>key</b> объекта <b>obj</b>.	 * Если объект является html-элементом, достается css-свойство.	 * @param {Object, Element} obj	 * @param {String} key	 * @return {String}	 */	function getValue(obj, key) {		var val = 0;		if (isNode(obj)) { //это html-элемент			val = getStyle(obj, key);		} else if (isFunction(obj[key])) { // используем свойство как getter			val = obj[key]();		} else { //это обычный объект			val = obj[key];		}		return val;	}	/**	 * Получить числовое значение свойства <b>key</b> объекта <b>obj</b>.	 * Аналогичен <code>getValue()</code>, только возвращает число.	 * Если объект является html-элементом, достается css-свойство.	 * @param {Object|Element} obj	 * @param {String} key	 * @return {Number}	 */	function getNumericValue(obj, key) {		return parseFloat(getValue(obj, key)) || 0;	}	/**	 * Запускает функции нэймспэйса	 * @param {String} ns	 * @param {String} action_name	 */	function runNSAction(ns, action_name) {		if (nsActions[ns] && nsActions[ns][action_name]) {			var actions = nsActions[ns][action_name];			for (var i = 0; i < actions.length; i++) {				callback(actions[i].func, actions[i].params);			}		}	}	/**	 * Ставит новое значение анимируемому свойству объекта.	 * Перед установкой проверяется, является ли это свойство цветовым,	 * getter/setter или opacity. В зависимости от этого выбираются разные	 * стратегии установки значения	 * @param {Object} o Анимационный объект, возвращаемый <code>prepareOptions()</code>	 * @param {String} property Анимируемое свойство	 * @param {Number} val Новое значение свойства	 */	function setProperty(o, property, val) {		var new_value = (o.suffix[property]) ? val + o.suffix[property] : val;		if (isFunction(o.target[property])) {			// используем свойство объекта как setter			o.target[property].call(o.rawTarget, new_value);		} else if (o.targetPropeties[property].func) {			// используем анимируемое свойство как setter			o.targetPropeties[property].func.call(o.rawTarget, val);		} else if (isColorProperty(property)) {			// анимируем цвет			var tP = o.targetPropeties[property];			o.target[property] = jTweener.Utils.Color.blend(tP.start_color, tP.end_color, val) + '';		} else {			// обычное или CSS-свойство			// FIXME:For IE. A Few times IE (style.width||style.height) = value is throw error...			try {				if (is_msie && property == 'opacity' && isNode(o.rawTarget)) {					// устанавливаем opacity для IE					// для фильтров, установленных через CSS, нужно использовать currentStyle					var flt = o.target.filter || o.rawTarget.currentStyle.filter;					o.target.filter = (flt || "").replace(/alpha\([^)]*\)/, "") +									  (parseFloat(val).toString() == "NaN" ? "" : "alpha(opacity=" + val * 100 + ")");				} else {					o.target[property] = new_value;				}			} catch(e) {			}		}	}	/**	 * Основной метод, который работает с анимациями. Все анимации отрабатываются	 * и останавливаются именно в нем.	 */	function eventLoop() {		var now = (new Date() - 0);		// Количество namespaces в объектах. Если равно нулю после завершения		// основного цикла, значит, нужно прекращать анимацию		var ns_len = 0;		for (var ns in objects) {			var ns_obj = objects[ns];			ns_len++;			for (var i = 0; i < ns_obj.length; i++) {				var o = ns_obj[i];				var t = now - o.startTime;				var d = o.endTime - o.startTime;				if (t >= d) { //завершаем анимацию					for (var property in o.targetPropeties) {						var tP = o.targetPropeties[property];						setProperty(o, property, tP.b + tP.c);					}					ns_obj.splice(i, 1);					callback(o.onUpdate, o.onUpdateParams, o.rawTarget);					callback(o.onComplete, o.onCompleteParams, o.rawTarget);				} else {					for (var property in o.targetPropeties) {						var tP = o.targetPropeties[property];						setProperty(o, property, o.easing(t, tP.b, tP.c, d));					}					callback(o.onUpdate, o.onUpdateParams, o.rawTarget);				}			}			runNSAction(ns, 'onUpdate');			if (!ns_obj.length) {				ns_obj = null;				delete objects[ns];				ns_len--;				runNSAction(ns, 'onComplete');			}		}		if (ns_len > 0) {			//еще есть объекты для анимирования			setTimeout(eventLoop, 1000 / frameRate);		} else {			//объектов больше нет, останавливаемся			looping = false;		}	}	/**	 * Останавливает все анимации для объекта <b>obj</b> путем удаления	 * соответствующих объектов из хэша <code>objects</code>. Возвращает	 * количество удаленных анимаций у объекта	 * @param {Object} obj Объект, для которого нужно остановить все анимации	 * @param {String} [ns] Удалять анимации только из этого нэймспэйса	 * @return {Number}	 */	function stopAnimation(obj, ns) {		var how_many = 0;		if (obj && isNode(obj)) {			obj = obj.style;		}		function rm(items) {			for (var i = items.length - 1; i >= 0; i--) {				if (items[i].target == obj) {					items.splice(i, 1);					how_many++;				}			}		}		;		if (!obj && ns) { // удаляем все анимации из нэймспэйса			objects[ns] = [];		} else if (ns && objects[ns]) { // если указали нэймспэйс, удаляем анимации только из него			rm(objects[ns]);		} else {			for (var n in objects) {				rm(objects[n]);			}		}		return how_many;	}	/**	 * Метод извлекает системные свойства анимации из объекта (такие как	 * <b>time</b>, <b>transition</b>, <b>onStart</b> и т.д), переданного	 * пользоватем, <b>модифицируя аргумент <code>options</code></b>. Если	 * системные свойства не были найдены — подставятся значения по умолчанию.	 * @param {Object} options Параметры анимации, переданные пользователем	 * @return {Object} Хэш системных свойств анимации	 */	function extractSystemOptions(options) {		var result = {};		for (var key in defaultOptions) {			result[key] = options[key] || defaultOptions[key];			delete options[key];		}		if (isFunction(result.transition)) {			result.easing = result.transition;		} else {			result.easing = easingFunctionsLowerCase[result.transition.toLowerCase()];		}		delete options.easing;		return result;	}	/**	 * Создание копии объекта	 * @param {Object} obj Объект, который нужно скопировать	 * @return {Object}	 */	function copyObject(obj) {		var result = {};		for (var a in obj) if (obj.hasOwnProperty(a)) {			result[a] = obj[a];		}		return result;	}	/**	 * Преобразует переданные пользователем опции в вид, пригодный для анимации	 * @param {Object} obj Объект, для которого нужно подготовить опции	 * @param {Object} options Опции, пришедшие от пользователя	 * @return {Object}	 */	function prepareOptions(obj, options) {		// создаю копию объекта, так как эти опции могут использоваться		// для нескольких объектов, а мне нужно модифицировать объект		options = copyObject(options);		var is_node = isNode(obj);		var o = extractSystemOptions(options);		o.rawTarget = obj;		o.target = (is_node) ? obj.style : obj;		o.targetPropeties = {};		/** @type {Array} */		var m;		// прохожусь по всем свойствам, которые нужно анимировать		for (var key in options) {			// TODO: удалить суффиксы и префиксы из параметров, данные брать из значения			if (!o.prefix[key])				o.prefix[key] = '';			if (!o.suffix[key])				o.suffix[key] = (is_node && key != 'opacity') ? 'px' : '';			var option_value = options[key];			if (option_value === null)				continue;			// если анимируем html-элемент, нужно преобразовать название css-свойства:			// 'background-color' => 'backgroundColor'			if (is_node) {				key = key.replace(/\-(\w)/g, function(/* String */ str, /* String */ p) {					return p.toUpperCase();				});			}			if (isColorProperty(key)) {				// хотим анимировать цвет				o.targetPropeties[key] = {					b: 0, //base — начальное значение					c: 1, //change — изменение значения					start_color: jTweener.Utils.getRGB(getValue(obj, key)),					end_color: jTweener.Utils.getRGB(option_value)				};			} else if (isFunction(option_value)) {				/*				 * Анимируемый параметр является функцией — это особая нотация,				 * которая подразумевает, что пользователь хочет запустить				 * процентную анимацию (значение от 0 до 1)				 */				o.targetPropeties[key] = {					func: option_value,					b: 0, //base — начальное значение					c: 1 //change — изменение значения				};			} else {				var base_value = getNumericValue(obj, key);				var end_value = option_value;				if ((m = re_value.exec(end_value))) {					end_value = base_value + (m[1] == '-' ? -1 : 1) * parseFloat(m[2]);				} else {					end_value = parseFloat(end_value);				}				o.targetPropeties[key] = {					b: base_value, //base — начальное значение					c: end_value - base_value //change — изменение значения				};			}		}		return o;	}	/**	 * Создание новой анимации. Параметры такие же, как	 * и в <code>jTweener.addTween</code> с одним исключением: <b>obj</b> —	 * именно тот объект, который нужно анимировать (а не массив объектов)	 * @param {Object} obj Объект, свойства которого нужно анимировать.	 * Если нужно анимировать css-свойства элемента, передать <code>obj.style</code>	 * @param {Object} options Параметры анимации	 */	function createTween(obj, options) {		if (!inited)			init();		var delay = options.delay || defaultOptions.delay;		function startTween() {			var o = prepareOptions(obj, options);			o.startTime = (new Date() - 0);			o.endTime = o.time * 1000 + o.startTime;			callback(o.onStart, o.onStartParams, o.rawTarget);			if (!objects[o.namespace]) {				objects[o.namespace] = [];			}			objects[o.namespace].push(o);			if (!looping) {				looping = true;				eventLoop();			}		}		if( delay > 0 ) {			setTimeout( startTween, delay * 1000);		} else {			startTween.apply(window);		}	}	return {		/**		 * @param {Object, Array} obj Объект или массив объектов, свойства которых нужно анимировать.		 * @param {Object} options Параметры анимации		 */		addTween: function(obj, options) {			obj = toArray(obj);			for (var i = 0; i < obj.length; i++) {				createTween(obj[i], options);			}		},		/**		 * @param {Object} options Параметры анимации		 * @see jTweener#addTween		 * @return {Object} Внутренний объект, созданный при добавлении анимации. Его можно использовать в <code>jTweener.removeTween()</code>		 */		addPercent: function(options) {			var anim_obj = {};			if (arguments.length == 2) {				// передали анимируемые объекты и параметры анимации				anim_obj = arguments[0];				options = arguments[1];			}			createTween(anim_obj, options);			return anim_obj;		},		/**		 * @param {Object} actions Действия		 * @param {String} [ns] Нэймспэйс, которому нужно добавить действия (по умолчанию: 'default')		 */		addNSAction: function(actions, ns) {			ns = ns || defaultOptions.namespace;			if (!nsActions[ns]) {				nsActions[ns] = {};			}			var nsa = nsActions[ns];			for (var a in actions) if (a.indexOf('Params') == -1) {				if (!nsa[a]) {					nsa[a] = [];				}				nsa[a].push({func: actions[a], params: actions[a + 'Params']});			}		},		removeNSActions: function() {			switch (arguments.length) {				case 0: //удаляем все действия					nsActions = {};					break;				default:					var ns = arguments[0];					var actions = [].splice.call(arguments, 1);					if (nsActions[ns]) {						if (actions && actions.length) { //указали действия — удаляем их							var nsa = nsActions[ns];							for (var i = 0; i < actions.length; i++) {								delete nsa[actions[i]];							}						} else { //удаляем все действия из нэймспэйса							delete nsActions[ns]						}					}			}		},		removeTween: function() {			switch (arguments.length) {				case 0:					objects = {};					break;				default:					var ns, items;					if (arguments.length == 1) {						if (typeof arguments[0] == 'string') {							ns = arguments[0];						} else {							items = arguments[0];						}					} else {						ns = arguments[0];						items = arguments[1];					}					if (items && (items instanceof Array || items.jquery)) {						for (var i = 0; i < items.length; i++) {							stopAnimation(items[i], ns);						}					} else {						stopAnimation(items, ns);					}			}		}	};}();/** * Утилиты для анимаций */jTweener.Utils = {	bezier2: function(t, p0, p1, p2) {		return (1 - t) * (1 - t) * p0 + 2 * t * (1 - t) * p1 + t * t * p2;	},	/**	 * @param {Number} t Время	 * @param {Number} p0 Начальная точка	 * @param {Number} p1 Контрольная точка 1	 * @param {Number} p2 Контрольная точка 2	 * @param {Number} p3 Конечная точка	 */	bezier3: function(t, p0, p1, p2, p3) {		return Math.pow(1 - t, 3) * p0 + 3 * t * Math.pow(1 - t, 2) * p1 + 3 * t * t * (1 - t) * p2 + t * t * t * p3;	},	/**	 * Объединяет несколько объектов-хэшей в один	 * @param {Object} ... Объекты, которые нужно объединить	 * @return {Object}	 */	mergeObjects: function() {		var result = {};		for (var i = 0; i < arguments.length; i++) {			var obj = arguments[i];			if (!obj)				continue;			for (var a in obj)				result[a] = obj[a];		}		return result;	},	// Color Conversion functions from highlightFade	// By Blair Mitchelmore	// http://jquery.offput.ca/highlightFade/	// Parse strings looking for color tuples [255,255,255]	/**	 * Разбирает строку и возвращает объект, содержащий цветовые компоненты.	 * Понимает следующие форматы:<br>	 * <b>rgb(120,50,255)</b><br>	 * <b>rgb(88%,100%,39%)</b><br>	 * <b>#a0b1c2</b><br>	 * <b>#fff</b><br>	 * Если строка не может быть разобрана — возвращается черный цвет	 * @author Blair Mitchelmore (http://jquery.offput.ca/highlightFade/)	 * @param {String} color Строка с цветом.	 * @return jTweener.Util.Color	 */	getRGB: function(color) {		var result;		// Если на передели объект Color, значит, делать ничего не нужно		if (color && color.constructor == jTweener.Utils.Color)			return color;		// Look for rgb(num,num,num)		if (result = /rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/.exec(color))			return new jTweener.Utils.Color(					parseInt(result[1], 10),					parseInt(result[2], 10),					parseInt(result[3], 10)					);		// Look for rgb(num%,num%,num%)		if (result = /rgb\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*\)/.exec(color))			return new jTweener.Utils.Color(					parseFloat(result[1], 10) * 2.55,					parseFloat(result[2], 10) * 2.55,					parseFloat(result[3], 10) * 2.55					);		// Look for #a0b1c2		if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))			return new jTweener.Utils.Color(					parseInt(result[1], 16),					parseInt(result[2], 16),					parseInt(result[3], 16)					);		// Look for #fff		if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))			return new jTweener.Utils.Color(					parseInt(result[1] + result[1], 16),					parseInt(result[2] + result[2], 16),					parseInt(result[3] + result[3], 16)					);		// Otherwise, return black color		return new jTweener.Utils.Color(0, 0, 0);	}};/** * @class Вспомогательный класс для работы с цветом * @param {Number} r Красная компонента (0—255) * @param {Number} g Зеленая компонента (0—255) * @param {Number} b Синяя компонента (0—255) */jTweener.Utils.Color = function(r, g, b) {	this.r = Math.max(Math.min(Math.round(r), 255), 0);	this.g = Math.max(Math.min(Math.round(g), 255), 0);	this.b = Math.max(Math.min(Math.round(b), 255), 0);};/** * Смешивает два цвета в указанной пропорции. * @param {jTweener.Utils.Color} color1 Первичный цвет * @param {jTweener.Utils.Color} color2 Вторичный цвет * @param {Number} ratio Пропорция, в которой нужно смешать цвет (0 — чистый первичный цвет, 1 — чистый вторичный цвет). * @return {jTweener.Utils.Color} */jTweener.Utils.Color.blend = function(color1, color2, ratio) {	ratio = ratio || 0;	return new jTweener.Utils.Color(			color1.r + (color2.r - color1.r) * ratio,			color1.g + (color2.g - color1.g) * ratio,			color1.b + (color2.b - color1.b) * ratio			);};jTweener.Utils.Color.prototype = {	/** Красная компонента цвета */	r: 0,	/** Зеленая компонента цвета */	g: 0,	/** Синяя компонента цвета */	b: 0,	/**	 * Возвращает цвет в формате RGB, например, 'rgb(129,250,0)'	 * @return {String}	 */	toString: function() {		return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';	}}
