Source: managers/TweenManager.js

import { Flags, TWEEN } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { LoopType } from '../const';

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

const Flag = {
	Enable: 1 << 0
}

// #region Private Functions

// Build tween values.
function _buildTweenValues(object) {
	if (Utils.isNumber(object)) {
		return { value: object };
	}
	else if (Utils.isArray(object)) {
		return { value: object };
	}

	return { value: object };
}

// Update tween repeat.
function _updateTweenRepeat(tween) {
	let repeat = tween._times_value;
	if (repeat === undefined) {
		tween.repeat(Infinity);
	}
	else if (repeat < 0) {
		tween.repeat(Infinity);
	}
}

// Create tween.
function _createTween(from, to, duration, delayTime, lerpType, noInterp) {
	return new TWEEN.Tween(from)
		.to(to, duration)
		.delay(delayTime)
		.easing(lerpType)
		.noInterp(noInterp);
}

// #endregion

/**
 * @class TweenManager
 * The tween manager.
 * @memberof THING
 */
class TweenManager {

	/**
	 * The tween manager to update value(s) smoothly.
	 */
	constructor() {
		this[__.private] = {};
		let _private = this[__.private];

		_private.flags = new Flags();
		_private.flags.enable(Flag.Enable, true);

		_private.elapsedTimeMilliseconds = 0;
		_private.tickTimeMilliseconds = 0;
	}

	dispose() {
		TWEEN.removeAll();
	}

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

		// We must care about the elapsed time in 1 frame, so need to calculate elapsed time from current frame begin
		let elapsedTimeMilliseconds = Utils.getCurrentTimeMilliseconds() - _private.tickTimeMilliseconds;

		return _private.elapsedTimeMilliseconds + elapsedTimeMilliseconds;
	}

	/**
	 * Update.
	 * @param {Number} deltaTime The delta time in seconds.
	 * @private
	 */
	update(deltaTime) {
		let _private = this[__.private];

		if (!_private.flags.has(Flag.Enable)) {
			return;
		}

		_private.tickTimeMilliseconds = Utils.getCurrentTimeMilliseconds();

		_private.elapsedTimeMilliseconds += deltaTime * 1000;

		TWEEN.update(_private.elapsedTimeMilliseconds);
	}

	/**
	 * @typedef {Object} LerpToResult
	 * @property {Function} start The start function.
	 * @property {Function} stop The stop function.
	 * @property {Function} times The repeat times setting function.
	 * @property {Function} easing The lerp setting function.
	 * @property {Function} looping The looping setting function.
	 * @property {Function} onRepeat When repeat callback function.
	 * @property {Function} onStart When start callback function.
	 * @property {Function} onStop When start callback function.
	 * @property {Function} onUpdate When update callback function.
	 * @property {Function} onComplete When complete callback function.
	 */

	/**
	 * Start lerp to.
	 * @param {Number|Object} from The source.
	 * @param {Number|Object} to The target.
	 * @param {Number} [duration=1000] The lerp time in milliseconds.
	 * @param {Number} [delayTime=0] The delay time in milliseconds.
	 * @param {Object} options The optional parameters.
	 * @param {Boolean} [options.noInterp = false] Whether to set it directly without interpolation.
	 * @returns {LerpToResult}
	 * @example
	 * const box = new THING.Box();
	 * THING.App.current.tween.lerpTo({ opacity: 1}, { opacity: 0.2}, 2000).onUpdate(ev => {
	 * 	box.style.opacity = ev.value.opacity;
	 * });
	 * @public
	 */
	lerpTo(from, to, duration = 1000, delayTime = 0, options = {}) {
		let source = _buildTweenValues(from);
		let target = _buildTweenValues(to);
		let noInterp = Utils.parseBoolean(options.noInterp, false);

		let tween;

		let that = this;
		let result = {
			_from: source,
			_to: target,
			start: function () {
				tween.start(that.now());

				return this;
			},
			stop: function () {
				Utils.clearTimeout(tween._startTimer);

				tween.stop();

				return this;
			},
			pause: function () {
				if (this._onPause) {
					this._onPause({});
				}

				tween.pause();

				return this;
			},
			resume: function () {
				if (this._onResume) {
					this._onResume({});
				}

				tween.resume();

				return this;
			},
			times: function (value) {
				tween._times_value = value;

				// In tween, times always add more 1 time, so we need to decrease it
				if (value > 0) {
					value--;
				}

				tween.repeat(value);

				return this;
			},
			easing: function (value) {
				tween.easing(value);

				return this;
			},
			looping: function (value) {
				switch (value) {
					case LoopType.Repeat:
						_updateTweenRepeat(tween);

						tween.yoyo(false);
						break;

					case LoopType.PingPong:
						_updateTweenRepeat(tween);

						tween.yoyo(true);
						break;

					default:
						tween.yoyo(false);
						break;
				}

				return this;
			},
			onRepeat: function (callback) {
				this._onRepeat = callback;

				return this;
			},
			onParseValue: function (callback) {
				this._onParseValue = callback;

				return this;
			},
			onStart: function (callback) {
				this._onStart = callback;

				return this;
			},
			onStop: function (callback) {
				this._onStop = callback;

				return this;
			},
			onResume: function (callback) {
				this._onResume = callback;

				return this;
			},
			onPause: function (callback) {
				this._onPause = callback;

				return this;
			},
			onUpdate: function (callback) {
				this._onUpdate = callback;

				return this;
			},
			onComplete: function (callback) {
				this._onComplete = callback;

				return this;
			},
		};

		tween = _createTween(source, target, duration, delayTime, TWEEN.Easing.Linear.None, noInterp);
		tween.onRepeat(ev => {
			if (result._onRepeat) {
				result._onRepeat(ev);
			}
		});
		tween.onStart(ev => {
			if (result._onStart) {
				result._onStart(ev);
			}
		});
		tween.onParseValue((start, end, key) => {
			if (result._onParseValue) {
				result._onParseValue(start, end, key);
			}
		});
		tween.onStop(ev => {
			if (result._onStop) {
				result._onStop(ev);
			}
		});
		tween.onUpdate((ev, progress) => {
			if (result._onUpdate) {
				result._onUpdate(ev, progress);
			}
		});
		tween.onComplete(ev => {
			if (result._onComplete) {
				result._onComplete(ev);
			}
		});

		// Here we make it a little delay to start in order to set attribute by result operations
		tween._startTimer = Utils.setTimeout(() => {
			tween.start(that.now());
		}, 0); // We use '0' here to make sure start timer is available

		if (_DEBUG) {
			result._tween = tween;
		}

		return result;
	}

	// lerpPoints(value, time = 1000, delayTime = 0) {
	// 	let result = this.lerpTo(0, 1, time, delayTime);
	// 	result.onUpdatePoint = function (callback) {
	// 		result._onUpdatePoint = callback;
	// 	};

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

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

	// 	// Get the steps of path
	// 	let steps = MathUtils.getPointsSteps(path);
	// 	if (steps.length) {
	// 		// The reverse mode
	// 		let reverseMode = false;

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

	// 		result.onRepeat(() => {
	// 			// Reset start index
	// 			curIndex = -1;

	// 			if (loopType == LoopType.PingPong) {
	// 				reverseMode = !reverseMode;
	// 			}
	// 		});
	// 		result.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 (next) {
	// 						next({ object, from, to });
	// 					}
	// 				}

	// 				curIndex = index;
	// 			}

	// 			// Notify outside
	// 			if (result._onUpdatePoint) {
	// 				result._onUpdatePoint({ point, from, to, progress });
	// 			}
	// 		});
	// 	}

	// 	return result;
	// }

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

		return _private.flags.has(Flag.Enable);
	}
	set enable(value) {
		let _private = this[__.private];

		_private.flags.enable(Flag.Enable, value);
	}

}

export { TweenManager }