Source: components/LerpComponent.js

import { Flags } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { BaseComponent } from './BaseComponent';
import { LerpType, LoopType, EventType, SpaceType } from '../const';

const __ = {
	private: Symbol('private'),
}

const cMoveToActionName = '__moveTo__';
const cMovePathActionName = '__movePath__';
const cScaleToActionName = '__scaleTo__';
const cRotateToActionName = '__rotateTo__';
const cFadingActionName = '__fading__';
const cFlyingActionNames = ['__positionFlying__', '__targetFlying__'];

const Flag = {
	Flying: 1 << 0,
};

let _lerpResults = [];

// #region Private Functions

function _buildAttribute(keys, object, attribute, callback) {
	if (Utils.isArray(attribute)) {
		return attribute.slice(0);
	}

	let result = {};
	keys.forEach(key => {
		let value = attribute[key];
		if (Utils.isNull(value)) {
			value = object.getAttribute(key);
		}
		else {
			if (object.hasAttribute(key)) {
				if (callback) {
					callback(key, value);
				}
			}
		}

		result[key] = value;
	});

	return result;
}

function _buildAsyncTrigger(resolve, reject) {
	return {
		start: () => {

		},
		stop: () => {
			resolve('stop');
		},
		update: () => {

		},
		complete: () => {
			resolve('complete');
		},
		onStart: () => {

		},
		onStop: () => {
			resolve('onStop');
		},
		onUpdate: () => {

		},
		onComplete: () => {
			resolve('onComplete');
		},
	};
}

function _invokeCallback(object, value, callback, trigger, key) {
	if (callback) {
		callback({ object, value });
	}

	if (trigger && trigger[key]) {
		trigger[key]();
	}
}

function _update(object, from, to, values, onUpdate, progress, beforeCallback, afterCallback) {
	let results;

	// It's array
	if (values.length) {
		_lerpResults.length = 0;

		results = _lerpResults;
	}
	// It's object
	else {
		results = {};
	}

	// Update values
	for (let key in values) {
		let value;

		// Use slerp to work with quaternion
		if (key == 'quaternion' || key == 'localQuaternion') {
			value = MathUtils.slerp(to[key + '_start'], to[key], progress);
		}
		else {
			value = values[key];
		}

		if (beforeCallback) {
			beforeCallback(key, value);
		}

		results[key] = value;

		if (afterCallback) {
			afterCallback(key, value);
		}
	}

	// Notify outside
	if (onUpdate) {
		onUpdate({ object, from, to, progress, value: results });
	}
}

// Parse lerp to arguments.
function _parseLerpArgs(param, name, trigger) {
	let options;

	// Check whether it's number->number lerping mode
	if (Utils.isNumber(param) && Utils.isNumber(name)) {
		let fromNumber = param;
		let toNumber = name;

		options = {
			from: { 'current': fromNumber },
			to: { 'current': toNumber }
		};

		// Use trigger as lerping name
		if (Utils.isString(trigger)) {
			name = trigger;
		}
		else {
			name = '';

			// Check whether trigger is options
			if (Utils.isObject(trigger)) {
				for (let key in trigger) {
					options[key] = trigger[key];
				}

				trigger = {};
			}
		}
	}
	else {
		options = Object.assign({}, param);
	}

	options['name'] = name;
	options['trigger'] = trigger;

	return options;
}

function _buildUVTransformValues(from, to) {
	let uv = {};

	if (Utils.isValid(to.rotation)) {
		uv.rotation = from.rotation;
	}

	if (to.offset) {
		uv.offset = from.offset.slice(0);
	}

	if (to.repeat) {
		uv.repeat = from.repeat.slice(0);
	}

	if (to.center) {
		uv.center = from.center.slice(0);
	}

	return uv;
}

function _buildUVTransformName(slotType) {
	return `__UVTransform_${slotType.toLowerCase()}__`;
}

function _parseLerpingOptions(options = {}) {
	return {
		loopType: Utils.parseLoopType(options['loopType']),
		lerpType: Utils.parseLerpType(options['lerpType']),
		time: options['time'],
		duration: options['duration'],
		times: options['times'],
		loop: options['loop']
	};
}

// #endregion

/**
 * @class LerpComponent
 * The object lerp component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class LerpComponent extends BaseComponent {

	static mustCopyWithInstance = true;

	/**
	 * The interpolation of object what could do all smoothing transforming jobs.
	 */
	constructor() {
		super();

		this[__.private] = {};
		let _private = this[__.private];

		_private.flags = new Flags();

		_private.tweens = [];
	}

	// #region Private

	_buildLerpParam(trigger, param) {
		let { lerpType, loop, loopType, times, duration, time, delayTime } = param;

		let onRepeat = Utils.parseValue(param['onRepeat'], param['repeat']);
		let onStart = Utils.parseValue(param['onStart'], param['start']);
		let onStop = Utils.parseValue(param['onStop'], param['stop']);
		let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
		let onComplete = Utils.parseValue(param['onComplete'], param['complete']);

		let canTriggerRepeat = false;

		return {
			lerpType,
			loop,
			loopType,
			times,
			duration,
			time,
			delayTime,
			onRepeat: () => {
				canTriggerRepeat = true;
			},
			onStart: () => {
				if (onStart) {
					onStart({ object: this.object });
				}

				if (trigger && trigger.onStart) {
					trigger.onStart();
				}
			},
			onStop: () => {
				if (onStop) {
					onStop({ object: this.object });
				}

				if (trigger && trigger.onStop) {
					trigger.onStop();
				}
			},
			onUpdate: (ev) => {
				if (canTriggerRepeat) {
					canTriggerRepeat = false;

					if (onRepeat) {
						onRepeat({ object: this.object });
					}

					if (trigger && trigger.onRepeat) {
						trigger.onRepeat();
					}
				}

				if (onUpdate) {
					onUpdate(ev, ev.progress);
				}

				if (trigger && trigger.onUpdate) {
					trigger.onUpdate();
				}
			},
			onComplete: () => {
				if (onComplete) {
					onComplete({ object: this.object });
				}

				if (trigger && trigger.onComplete) {
					trigger.onComplete();
				}
			}
		};
	}

	_stopTweenByName(name) {
		let _private = this[__.private];

		let tweens = _private.tweens;
		let index = tweens.findIndex(a => { return a.name == name; });
		if (index === -1) {
			return false;
		}

		tweens[index].tween.stop();

		return true;
	}

	_removeTweenByName(name) {
		let _private = this[__.private];

		let tweens = _private.tweens;
		let index = tweens.findIndex(a => { return a.name == name; });
		if (index === -1) {
			return false;
		}

		tweens._removeAt(index);

		return true;
	}

	_getTweenByName(name) {
		let _private = this[__.private];

		let tweens = _private.tweens;
		let index = tweens.findIndex(a => { return a.name == name; });
		if (index === -1) {
			return null;
		}

		return tweens[index].tween;
	}

	_lerpPoints(value, param, name, trigger) {
		// Parse arguments
		let loopType = Utils.parseLoopType(param['loopType']);
		let onRepeat = Utils.parseValue(param['onRepeat'], param['repeat']);
		let onStart = Utils.parseValue(param['onStart'], param['start']);
		let onStop = Utils.parseValue(param['onStop'], param['stop']);
		let onNext = Utils.parseValue(param['onNext'], param['next']);
		let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
		let onComplete = Utils.parseValue(param['onComplete'], param['complete']);

		// Copy path to move
		let path = value.slice(0);

		// Check whether it's closure path
		if (param['closure']) {
			path.push(value[0]);
		}

		// Move object to start position
		let object = this.object;

		// The reverse mode
		let reverseMode = false;

		// The current index of path
		let curIndex = -1;

		// Build lerp params
		let lerpParam = Object.assign({}, param);
		lerpParam['onRepeat'] = () => {
			// Reset start index
			curIndex = -1;

			if (loopType == LoopType.PingPong) {
				reverseMode = !reverseMode;
			}
			else {
				// Set the initial position of the object as the starting point of the path.
				object.position = path[0];
			}

			if (onRepeat) {
				onRepeat({ object, from: path[0], to: path[1] });
			}
		};
		lerpParam['onStart'] = () => {
			// Set the initial position of the object as the starting point of the path.
			object.position = path[0];

			if (onStart) {
				onStart({ object, from: path[0], to: path[1] });
			}
		};
		lerpParam['onStop'] = () => {
			if (onStop) {
				onStop({ object });
			}
		};
		lerpParam['onUpdate'] = (ev) => {
			let progress = ev.value.progress;

			// Get the position by progress
			let { point, index, from, to } = MathUtils.lerpPoints(path, steps, progress);

			// Exchange from and to if it's in reserve mode
			if (reverseMode) {
				let cur = from;
				from = to;
				to = cur;
			}

			// Check whether reach next path
			if (curIndex != index) {
				if (to) {
					if (onNext) {
						onNext({ object, from, to });
					}
				}

				curIndex = index;
			}

			// Notify outside
			if (onUpdate) {
				onUpdate({ object, point, from, to, progress });
			}
		};
		lerpParam['onComplete'] = () => {
			if (onComplete) {
				onComplete({ object, progress: 1 });
			}
		};

		// Start to lerp points
		let options = this._buildLerpParam(trigger, lerpParam);

		// Get the total distance of path
		let steps = MathUtils.getPointsSteps(path);
		if (!steps.length) {
			if (options.onComplete) {
				options.onComplete();
			}

			return;
		}

		options['from'] = { progress: 0 };
		options['to'] = { progress: 1 };
		this.to(options, name);
	}

	_lookAt(object, from, to, up) {
		up = up || object.up;

		object.quaternion = MathUtils.getQuatFromTarget(from, to, up);
	}

	// #endregion

	// #region BaseComponent Interface

	onImport(param) {
		let uv = param['uv'];
		if (uv) {
			for (let key in uv) {
				let value = uv[key]['value'];
				let options = _parseLerpingOptions(uv[key]['options']);

				this.uvTransformTo(key, value, options);
			}
		}
	}

	onRemove() {
		this.stopAll();

		super.onRemove();
	}

	// #endregion

	// #region Common

	stopAll() {
		let _private = this[__.private];

		// We must copy tweens here to prevent missing stop tween action with 'tween.stop()' interface
		let tweens = _private.tweens.slice(0);
		tweens.forEach(object => {
			object.tween.stop();
		});

		_private.tweens.length = 0;
	}

	pauseAll() {
		let _private = this[__.private];

		_private.tweens.forEach(object => {
			object.tween.pause();
		});
	}

	resumeAll() {
		let _private = this[__.private];

		_private.tweens.forEach(object => {
			object.tween.resume();
		});
	}

	pause(name) {
		if (!name) {
			return;
		}

		if (Utils.isArray(name)) {
			name.forEach(n => {
				this.pause(n);
			});
		}
		else {
			let tween = this._getTweenByName(name);
			if (tween) {
				tween.pause();
			}
		}
	}

	resume(name) {
		if (!name) {
			return;
		}

		if (Utils.isArray(name)) {
			name.forEach(n => {
				this.resume(n);
			});
		}
		else {
			let tween = this._getTweenByName(name);
			if (tween) {
				tween.resume();
			}
		}
	}

	stop(name) {
		if (!name) {
			return;
		}

		if (Utils.isArray(name)) {
			name.forEach(n => {
				this._stopTweenByName(n);
			});
		}
		else {
			this._stopTweenByName(name);
		}
	}

	/**
	 * Start lerp.
	 * @param {LerpArgs} param The parameters.
	 * @param {String} name The lerp name, if user want to stop it later then need to provide it.
	 * @public
	 */
	to(param = {}, name, trigger) {
		let options = _parseLerpArgs(param, name, trigger);

		// Get the binding object
		let object = this.object;

		// Parse arguments
		let syncMode = Utils.parseValue(options['syncMode'], true);
		let from = options['from'] || {};
		let to = options['to'] || {};
		let lerpType = Utils.parseLerpType(options['lerpType']) || LerpType.Linear.None;
		let loopType = Utils.parseLoopType(options['loopType']);
		let times = Utils.parseValue(options['times'], -1);
		let duration = Utils.parseNumber(options['duration'], Utils.parseNumber(options['time'], 1000));
		let delayTime = Utils.parseValue(options['delayTime'], 0);
		let onRepeat = Utils.parseValue(options['onRepeat'], options['repeat']);
		let onStart = Utils.parseValue(options['onStart'], options['start']);
		let onStop = Utils.parseValue(options['onStop'], options['stop']);
		let onResume = Utils.parseValue(options['onResume'], options['resume']);
		let onPause = Utils.parseValue(options['onPause'], options['pause']);
		let onUpdate = Utils.parseValue(options['onUpdate'], options['update']);
		let onComplete = Utils.parseValue(options['onComplete'], options['complete']);
		let noInterp = Utils.parseBoolean(options['noInterp'], false);

		let loop = options['loop'];
		if (Utils.isValid(loop)) {
			// true indicates -1, false indicates 1
			if (Utils.isBoolean(loop)) {
				times = loop ? -1 : 1;
			} else {
				times = loop;
			}
			if (!Utils.isValid(loopType)) {
				loopType = LoopType.Repeat;
			}
		}

		// Get unique keys
		let keys = Utils.getUnionKeys(from, to);

		// Build the source attributes
		let fromAttribute = _buildAttribute(keys, object, from, (key, value) => {
			if (syncMode) {
				object.setAttribute(key, value);
			}
		});

		// Build the target attributes
		let toAttribute = _buildAttribute(keys, object, to);

		// Copy quaterion for lerping
		if (fromAttribute['quaternion']) {
			toAttribute['quaternion_start'] = fromAttribute['quaternion'].slice(0);
		}
		else if (fromAttribute['localQuaternion']) {
			toAttribute['localQuaternion_start'] = fromAttribute['localQuaternion'].slice(0);
		}

		// Clone original data
		let origin = Object.assign({}, fromAttribute);

		// Stop current action
		if (options['name']) {
			this.stop(options['name']);
		}

		// Get the update attribute callback function
		let _onBeforeUpdateAttributeCallback = options['onBeforeUpdateAttribute'];
		let _onAfterUpdateAttributeCallback = syncMode ? (key, value) => {
			object.setAttribute(key, value);
		} : options['onAfterUpdateAttribute'];

		let _private = this[__.private];

		// Do it now, because it not provide any time info
		if (!duration && !delayTime) {
			_invokeCallback(object, fromAttribute, onStart, options['trigger'], 'onStart');

			_update(object, origin, toAttribute, toAttribute, onUpdate, 1, _onBeforeUpdateAttributeCallback, _onAfterUpdateAttributeCallback);

			_invokeCallback(object, fromAttribute, onComplete, options['trigger'], 'onComplete');
		}
		// Start to lerp
		else {
			let tween = this.app.tweenManager.lerpTo(fromAttribute, toAttribute, duration, delayTime, { noInterp })
				.times(times)
				.easing(lerpType)
				.looping(loopType)
				.onRepeat(ev => {
					_invokeCallback(object, ev.value, onRepeat, options['trigger'], 'onRepeat');
				})
				.onStart(ev => {
					_invokeCallback(object, ev.value, onStart, options['trigger'], 'onStart');
				})
				.onParseValue((start, end, key) => {
					if (key === 'color') {
						start[key] = Utils.parseColor(start[key]);
						end[key] = Utils.parseColor(end[key]);
					}
				})
				.onStop(ev => {
					_invokeCallback(object, ev.value, onStop, options['trigger'], 'onStop');

					this._removeTweenByName(options['name']);
				})
				.onResume(ev => {
					_invokeCallback(object, ev.value, onResume, options['trigger'], 'onResume');
				})
				.onPause(ev => {
					_invokeCallback(object, ev.value, onPause, options['trigger'], 'onPause');
				})
				.onUpdate((ev, progress) => {
					_update(object, origin, toAttribute, ev.value, onUpdate, progress, _onBeforeUpdateAttributeCallback, _onAfterUpdateAttributeCallback);
				})
				.onComplete(ev => {
					this._removeTweenByName(options['name']);

					_invokeCallback(object, ev.value, onComplete, options['trigger'], 'onComplete');
				})
				.start();

			// Bind name and easy to debug
			if (_DEBUG) {
				tween._tween._name = options['name'];
			}

			// Update tweens
			_private.tweens.push({ name: options['name'], tween });
		}
	}

	/**
	 * Lerp to in duration (async).
	 * @param {LerpArgs} param The parameters.
	 * @param {String} name The lerp name, if user want to stop it later then need to provide it.
	 * @returns {Promise<any>}
	 * @private
	 */
	toAsync(param = {}, name) {
		return new Promise((resolve, reject) => {
			this.to(param, name, _buildAsyncTrigger(resolve, reject));
		});
	}

	lerpPoints(value, param = {}, name = '', trigger = null) {
		if (name) {
			this.stop(name);
		}

		this._lerpPoints(value, param, name, trigger);
	}

	lerpPointsAsync(value, param = {}, name = '') {
		return new Promise((resolve, reject) => {
			this.lerpPoints(value, param, name, _buildAsyncTrigger(resolve, reject));
		});
	}

	// #endregion

	// #region Moving

	stopMoving() {
		this.stop(cMoveToActionName);
		this.stop(cMovePathActionName);
	}

	pauseMoving() {
		this.pause(cMoveToActionName);
		this.pause(cMovePathActionName);
	}

	resumeMoving() {
		this.resume(cMoveToActionName);
		this.resume(cMovePathActionName);
	}

	moveForward(distance, param = {}) {
		let object = this.object;
		let target = MathUtils.getPositionOnDirection(object.position, object.forward, distance);

		this.moveTo(target, param);
	}

	moveTo(value, param = {}, trigger = null) {
		this.stopMoving();

		if (!Utils.isArray(value)) {
			param = value;
			value = param['to'];
		}

		let object = this._object;

		// Build lerp params
		let lerpParam = this._buildLerpParam(trigger, param);

		// Set the target position
		let spaceType = Utils.parseValue(param['spaceType'], SpaceType.World);
		if (spaceType == SpaceType.World) {
			lerpParam['to'] = { position: value };
		}
		else {
			lerpParam['to'] = { localPosition: value };
		}

		// Set object's quaternion
		let orientToPath = Utils.parseValue(param['orientToPath'], true);
		if (orientToPath) {
			let from = object.position;
			let to = lerpParam['to'];
			let up = param['up'];

			if (!MathUtils.equalsVector3(object.position, value)) {
				this._lookAt(object, object.position, value, up);
			}

			if (lerpParam['loopType'] == LoopType.PingPong) {
				let onRepeat = lerpParam['onRepeat'];
				lerpParam['onRepeat'] = (ev) => {
					// Swap from and to position
					let temp = from;
					from = to.position || to;
					to = temp;

					// Set object's quaternion
					this._lookAt(object, from, to, up);

					if (onRepeat) {
						onRepeat(ev);
					}
				};
			}
		}

		// Start to move
		this.to(lerpParam, cMoveToActionName);
	}

	moveToAsync(value, param = {}) {
		return new Promise((resolve, reject) => {
			this.moveTo(value, param, _buildAsyncTrigger(resolve, reject));
		});
	}

	movePath(value, param = {}, trigger = null) {
		this.stopMoving();

		if (Utils.isObject(value)) {
			param = value;
			value = param['path'];
		}

		let orientToPath = Utils.parseValue(param['orientToPath'], true);
		let spaceType = Utils.parseValue(param['spaceType'], SpaceType.World);
		let up = param['up'];

		let object = this._object;

		let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
		param['onUpdate'] = (ev) => {
			let point = ev.point;

			// Set the target position
			if (spaceType == SpaceType.World) {
				object.position = point;
			}
			else {
				object.localPosition = point;
			}

			if (onUpdate) {
				onUpdate(ev);
			}
		};

		let onNext = Utils.parseValue(param['onNext'], param['next']);
		param['onNext'] = (ev) => {
			let { from, to } = ev;

			// Set object's quaternion
			if (orientToPath) {
				this._lookAt(object, from, to, up);
			}

			if (onNext) {
				onNext(ev);
			}
		};

		this._lerpPoints(value, param, cMovePathActionName, trigger);
	}

	movePathAsync(value, param = {}) {
		return new Promise((resolve, reject) => {
			this.movePath(value, param, _buildAsyncTrigger(resolve, reject));
		});
	}

	// #endregion

	// #region Scaling

	stopScaling() {
		this.stop(cScaleToActionName);
	}

	pauseScaling() {
		this.pause(cScaleToActionName);
	}

	resumeScaling() {
		this.resume(cScaleToActionName);
	}

	scaleTo(value, param = {}, trigger) {
		this.stopScaling();

		if (!Utils.isArray(value)) {
			param = value;
			value = param['to'];
		}

		// Build lerp params
		let lerpParam = this._buildLerpParam(trigger, param);

		// Set the target scale
		let spaceType = Utils.parseValue(param['spaceType'], SpaceType.World);
		if (spaceType == SpaceType.World) {
			lerpParam['to'] = { scale: value };
		}
		else {
			lerpParam['to'] = { localScale: value };
		}

		// Start to scale
		this.to(lerpParam, cScaleToActionName);
	}

	scaleToAsync(value, param = {}) {
		return new Promise((resolve, reject) => {
			this.scaleTo(value, param, _buildAsyncTrigger(resolve, reject));
		});
	}

	// #endregion

	// #region Rotating

	stopRotating() {
		this.stop(cRotateToActionName);
	}

	pauseRotating() {
		this.pause(cRotateToActionName);
	}

	resumeRotating() {
		this.resume(cRotateToActionName);
	}

	rotateTo(value, param = {}, trigger) {
		this.stopRotating();

		if (!Utils.isArray(value)) {
			param = value;
			value = param['to'];
		}

		// Build lerp params
		let lerpParam = this._buildLerpParam(trigger, param);

		// Set the target angels
		let spaceType = Utils.parseValue(param['spaceType'], SpaceType.Local);
		if (spaceType == SpaceType.World) {
			if (value.length == 3) {
				lerpParam['to'] = { angles: value };
			}
			else {
				lerpParam['to'] = { quaternion: MathUtils.getQuatFromAngles(value) };
			}
		}
		else {
			if (value.length == 3) {
				lerpParam['to'] = { localAngles: value };
			}
			else {
				lerpParam['to'] = { localQuaternion: MathUtils.getQuatFromAngles(value) };
			}
		}

		// Start to rotate
		this.to(lerpParam, cRotateToActionName);
	}

	rotateToAsync(value, param = {}) {
		return new Promise((resolve, reject) => {
			this.rotateTo(value, param, _buildAsyncTrigger(resolve, reject));
		});
	}

	// #endregion

	// #region Fading

	stopFading() {
		this.stop(cFadingActionName);
	}

	pauseFading() {
		this.pause(cFadingActionName);
	}

	resumeFading() {
		this.resume(cFadingActionName);
	}

	fadeIn(param = {}, trigger) {
		this.stopFading();

		// Get the binding object
		let object = this.object;
		if (!object.style) {
			return;
		}

		// Build lerp params
		let lerpParam = this._buildLerpParam(trigger, param);
		lerpParam['from'] = {
			style: {
				opacity: 0
			}
		};
		lerpParam['to'] = {
			style: {
				opacity: 1
			}
		};

		// Start to fade
		this.to(lerpParam, cFadingActionName);
	}

	fadeOut(param = {}, trigger) {
		this.stopFading();

		// Get the binding object
		let object = this.object;
		if (!object.style) {
			return;
		}

		// Build lerp params
		let lerpParam = this._buildLerpParam(trigger, param);
		lerpParam['from'] = {
			style: {
				opacity: 1
			}
		};
		lerpParam['to'] = {
			style: {
				opacity: 0
			}
		};

		// Start to fade
		this.to(lerpParam, cFadingActionName);
	}

	fadeInAsync(param = {}) {
		return new Promise((resolve, reject) => {
			this.fadeIn(param, _buildAsyncTrigger(resolve, reject));
		});
	}

	fadeOutAsync(param = {}) {
		return new Promise((resolve, reject) => {
			this.fadeOut(param, _buildAsyncTrigger(resolve, reject));
		});
	}

	// #endregion

	// #region Flying

	/**
	 * Get the fly info.
	 * @param {Object} param The parameters.
	 * @param {Array<Number>} position The start position.
	 * @param {Array<Number>|THING.BaseObject} target The target position.
	 * @param {Array<Number>} up The up direction.
	 * @param {Number} distance The distance(only works for object target mode).
	 * @param {Number} horzAngle The horz angle(only works for object target mode).
	 * @param {Number} vertAngle The vert angle(only works for object target mode).
	 * @returns {Object}
	 * @private
	 */
	getFlyInfo(param = {}) {
		let position = param['position'];
		let target = param['target'];
		let up = param['up'];

		let object = this.object;

		// Build position info
		let result = {
			position: {
				from: object.position,
				to: position ? position : object.position,
			}
		};

		// Build target info
		let targetPosition = object.target;
		if (targetPosition) {
			result.target = {
				from: targetPosition,
				to: null,
			};
		}

		// Build up info
		if (up) {
			result.up = {
				from: object.up,
				to: up,
			};
		}

		// It's object
		if (target.isBaseObject) {
			let aabb = target.getAABB();

			// If we do not provide position then try to calculate by angles and distance
			if (!position) {
				// If do not provide any angles then use 45 as default degree
				let defaultDegree = 0;
				if (Utils.isNull(param['horzAngle']) && Utils.isNull(param['vertAngle'])) {
					defaultDegree = 45;
				}

				// We make distance a little more far
				let radiusFactor = Utils.parseValue(param['radiusFactor'], 2.5);
				let distance = Utils.parseValue(param['distance'], aabb.radius * radiusFactor);
				let horzAngle = Utils.parseValue(param['horzAngle'], defaultDegree);
				let vertAngle = Utils.parseValue(param['vertAngle'], defaultDegree);

				// Prevent horz angles calculation bug
				if (vertAngle == 90) {
					vertAngle -= 0.1;
				}

				let offset = MathUtils.getOffsetFromAngles(horzAngle, vertAngle, distance);
				result.position.to = MathUtils.addVector(aabb.center, offset);
			}

			if (result.target) {
				result.target.to = aabb.center;
			}
		}
		// It's position
		else {
			if (result.target) {
				result.target.to = target;
			}
		}

		return result;
	}

	stopFlying() {
		this.stop(cFlyingActionNames);
	}

	pauseFlying() {
		this.pause(cFlyingActionNames);
	}

	resumeFlying() {
		this.resume(cFlyingActionNames);
	}

	flyTo(param = {}, trigger) {
		let _private = this[__.private];

		param = Utils.parseFlyParam(param);

		// Parse arguments
		let duration = param['duration'];
		let time = param['time'];
		let delayTime = Utils.parseValue(param['delayTime'], 0);
		let lerpType = param['lerpType'] || LerpType.Linear.None;
		let positionLerpType = Utils.parseValue(param['positionLerpType'], lerpType);
		let targetLerpType = Utils.parseValue(param['targetLerpType'], lerpType);
		let upLerpType = Utils.parseValue(param['upLerpType'], lerpType);
		let onStart = Utils.parseValue(param['onStart'], param['start']);
		let onStop = Utils.parseValue(param['onStop'], param['stop']);
		let onResume = Utils.parseValue(param['onResume'], param['resume']);
		let onPause = Utils.parseValue(param['onPause'], param['pause']);
		let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
		let onComplete = Utils.parseValue(param['onComplete'], param['complete']);

		// Prepare for flying
		let that = this;

		// Stop previous flying action
		this.stop(cFlyingActionNames);

		// Parse fly info
		let info = this.getFlyInfo(param);

		// Start to fly with position
		this.to({
			syncMode: false,
			from: info.position.from,
			to: info.position.to,
			duration,
			time,
			delayTime,
			lerpType: positionLerpType,
			onStart: function () {
				_private.flags.enable(Flag.Flying, true);

				that.object.trigger(EventType.StartFlying);

				if (onStart) {
					onStart({ object: that.object });
				}

				if (trigger && trigger.onStart) {
					trigger.onStart();
				}
			},
			onStop: function () {
				_private.flags.enable(Flag.Flying, false);

				that.object.trigger(EventType.StopFlying);

				if (onStop) {
					onStop({ object: that.object });
				}

				if (trigger && trigger.onStop) {
					trigger.onStop();
				}
			},
			onResume: function () {
				_private.flags.enable(Flag.Flying, true);

				if (onResume) {
					onResume({ object: that.object });
				}
			},
			onPause: function () {
				_private.flags.enable(Flag.Flying, false);

				if (onPause) {
					onPause({ object: that.object });
				}
			},
			onUpdate: function (ev) {
				that.object.position = ev.value;

				that.object.trigger(EventType.Flying);

				if (onUpdate) {
					onUpdate({ progress: ev.progress, object: that.object });
				}

				if (trigger && trigger.onUpdate) {
					trigger.onUpdate();
				}
			},
			onComplete: function () {
				_private.flags.enable(Flag.Flying, false);

				that.object.trigger(EventType.CompleteFlying);

				if (onComplete) {
					onComplete({ object: that.object });
				}

				if (trigger && trigger.onComplete) {
					trigger.onComplete();
				}
			}
		}, cFlyingActionNames[0]);

		// Start to fly with target
		if (info.target) {
			// Start to fly with up
			if (info.up) {
				this.to({
					syncMode: false,
					from: { up: info.up.from, target: info.target.from },
					to: { up: info.up.to, target: info.target.to },
					duration,
					time,
					delayTime,
					lerpType: upLerpType,
					onUpdate: function (ev) {
						let up = ev.value.up;
						that.object.lookAt(ev.value.target, { up });
					}
				}, cFlyingActionNames[1]);
			}
			else {
				this.to({
					syncMode: false,
					from: info.target.from,
					to: info.target.to,
					duration,
					time,
					delayTime,
					lerpType: targetLerpType,
					onUpdate: function (ev) {
						that.object.lookAt(ev.value);
					}
				}, cFlyingActionNames[1]);
			}
		}
	}

	flyToAsync(param = {}) {
		return new Promise((resolve, reject) => {
			this.flyTo(param, _buildAsyncTrigger(resolve, reject));
		});
	}

	fit(param = {}) {
		this.stopFlying();

		param = Utils.parseFlyParam(param);
		param['target'] = param['target'] || this.app.root;

		let flyInfo = this.getFlyInfo(param);

		this.object.position = flyInfo.position.to;

		if (flyInfo.target) {
			this.object.lookAt(flyInfo.target.to);
		}
	}

	get isFlying() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Flying);
	}

	// #endregion

	// #region UV

	stopUVTransform(slotType) {
		if (!slotType) {
			return;
		}

		this.stop(_buildUVTransformName(slotType));
	}

	pauseUVTransform(slotType) {
		if (!slotType) {
			return;
		}

		this.pause(_buildUVTransformName(slotType));
	}

	resumeUVTransform(slotType) {
		if (!slotType) {
			return;
		}

		this.resume(_buildUVTransformName(slotType));
	}

	uvTransformTo(slotType, value, param, trigger) {
		if (!slotType) {
			return;
		}

		// We can provide value as param to run
		if (param === undefined) {
			param = value;
		}

		this.stopUVTransform();

		// Get the UV transform info
		let object = this.object;
		let style = object.style;
		let from = param['from'] || style.getUV(slotType);
		let to = param['to'] || value;

		// Build lerp params
		let lerpParam = this._buildLerpParam(trigger, param);
		lerpParam['from'] = _buildUVTransformValues(from, to);
		lerpParam['to'] = to;
		lerpParam['syncMode'] = false;
		lerpParam['onAfterUpdateAttribute'] = (key, value) => {
			if (object.invisible) {
				return;
			}

			style.setUV(slotType, key, value);
		};

		// Start to rotate
		this.to(lerpParam, _buildUVTransformName(slotType));
	}

	uvTransformToAsync(slotType, value, param) {
		if (!slotType) {
			return;
		}

		return new Promise((resolve, reject) => {
			this.uvTransformTo(slotType, value, param, _buildAsyncTrigger(resolve, reject));
		});
	}

	// #endregion

}

export { LerpComponent }