Source: components/ModelAnimationComponent.js

import { CommandExecutor } from '@uino/base-thing';
import { Utils } from '../common/Utils'
import { BaseComponent } from './BaseComponent';
import { EventType, AnimationDirectionType, LoopType, PlayStateType } from '../const';

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

let _animationNames = [];

/**
 * @class ModelAnimationComponent
 * The model animation component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class ModelAnimationComponent extends BaseComponent {

	static cDummyAnimationInterface = {
		'animations': [],
		'animationNames': [],
		'playAnimation': function () { return false; },
		'playAnimationAsync': function () { return Promise.resolve(); },
		'blendAnimation': function () { return false; },
		'blendAnimationAsync': function () { return Promise.resolve(); },
		'hasAnimation': function () { return false; },
		'pauseAnimation': function () { return false; },
		'pauseAllAnimations': function () { return false; },
		'resumeAnimation': function () { return false; },
		'resumeAllAnimations': function () { return false; },
		'stopAnimation': function () { return false; },
		'stopAllAnimations': function () { return false; },
		'isAnimationPlaying': function () { return false; },
		'getAnimation': function () { return null; },
		'getAnimationState': function () { return 'Stopped'; },
		'getPlayingAnimations': function () { return []; },
		'getAnimationDirectionType': function () { return 'Normal'; },
		'setAnimationDirectionType': function () { return false; },
		'getAnimationSpeed': function () { return 1; },
		'setAnimationSpeed': function () { return false; },
		'runDelayedCommands': function () { },
	};

	/**
	 * The model animation player of entity object.
	 */
	constructor() {
		super();

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

		_private.commandExecutor = null;

		_private.animations = new Map();
		_private.animationPlayer = null;
	}

	// #region Private Functions

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

		let animationPlayer = _private.animationPlayer;
		if (!animationPlayer) {
			return;
		}

		// When play animation finished
		animationPlayer.addEventListener('finished', (ev) => {
			let name = ev.name;

			let animation = _private.animations.get(name);
			if (!animation) {
				return;
			}

			animation.state = PlayStateType.Finished;

			if (animation.onComplete) {
				animation.onComplete(ev);
			}
		});

		// When loop once finished
		animationPlayer.addEventListener('loop', (ev) => {

		});
	}

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

		let animationPlayer = _private.animationPlayer;
		if (!animationPlayer) {
			return;
		}

		_animationNames.length = 0;
		animationPlayer.getNames(_animationNames);

		_animationNames.forEach(name => {
			let duration = animationPlayer.getAttribute(name, 'Duration');

			// Pay attention: duration could be 0, but it's correct, just means it would play very fast !
			_private.animations.set(name, {
				name,
				state: PlayStateType.Ready,
				duration,
				directionType: AnimationDirectionType.Normal,
				speed: 1.0,
			});
		});
	}

	_playAnimation(param) {
		this._init();

		let _private = this[__.private];

		// Get the playing parameters
		let name = param['name'];
		let loopType = param['loopType'];
		let times = param['times'];
		let reverse = param['reverse'];
		let speed = Utils.parseValue(param['speed'], 1);
		let frames = param['frames'] || null;
		let onComplete = Utils.parseValue(param['onComplete'], param['complete']);

		// Get animation info
		let animation = _private.animations.get(name);
		if (!animation) {
			return;
		}

		// Stop animation playing
		this.stopAnimation(name);

		// Update animation info
		animation.state = PlayStateType.Playing;
		animation.speed = speed;
		animation.onComplete = onComplete;

		// Check whether it's reversed play mode
		if (reverse) {
			speed *= -1;
		}

		// Update player attributes
		let animationPlayer = _private.animationPlayer;
		animationPlayer.setAttribute(name, 'TimeScale', speed);

		// Fix loop times
		if (!times) {
			if (loopType == LoopType.Repeat || loopType == LoopType.PingPong) {
				times = Infinity;
			}
			else {
				times = 1;
			}
		}

		// Fix loop type
		if (!loopType) {
			loopType = LoopType.Repeat;
		}

		// Setup play options
		let options = {
			loopType,
			loopTimes: times
		};

		// Set frames range
		let start = 0.0, end = 1.0;
		if (frames) {
			if (Utils.isArray(frames)) {
				start = frames[0];
				end = frames.length > 1 ? frames[1] : 1.0;
			}
			else if (Utils.isNumber(frames)) {
				start = frames;
			}
			else {
				Utils.error(`The animation frames '${frames}' is invalid when play '${name}' animation`);
				return;
			}

			// Set the start and end frames
			options['framesRange'] = [start, end];
		}

		// Start to play animation
		animationPlayer.play(name, options);

		// Bind style with animation
		let object = this.object;
		object.body.style.bindAnimationEvents(object);

		// Notify animation had played
		object.trigger(EventType.PlayAnimation, { name });
	}

	_pauseAnimation(name, paused) {
		let _private = this[__.private];

		// Get animation info
		let animation = _private.animations.get(name);
		if (!animation) {
			return;
		}

		// Update animation player state
		let animationPlayer = _private.animationPlayer;
		if (paused) {
			animation.state = PlayStateType.Paused;

			animationPlayer.pause(name);
		}
		else {
			animation.state = PlayStateType.Playing;

			animationPlayer.resume(name);
		}
	}

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

		// Get animation info
		let animation = _private.animations.get(name);
		if (!animation) {
			return;
		}

		let state = animation.state;
		if (state == PlayStateType.Ready || state == PlayStateType.Stopped) {
			return;
		}

		// Update animation info state
		animation.state = PlayStateType.Stopped;
		animation.onComplete = null;

		// Update animation player state
		_private.animationPlayer.stop(name);

		// Notify animation had stopped
		this.object.trigger(EventType.StopAnimation, { name });
	}

	_checkAnimationState(name, state) {
		let _private = this[__.private];

		if (!_private.animationPlayer) {
			return false;
		}

		if (name) {
			let animationState = _private.animationPlayer.getState(name);
			if (animationState == state) {
				return true;
			}
		}
		else {
			for (let [key, animation] of _private.animations) {
				if (animation.state == state) {
					return true;
				}
			}
		}

		return false;
	}

	_addDelayedCommand(funcName, args) {
		let _private = this[__.private];

		_private.commandExecutor = _private.commandExecutor || new CommandExecutor();
		_private.commandExecutor.addCommand(funcName, args);
	}

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

		if (_private.animationPlayer) {
			return;
		}

		_private.animationPlayer = this.object.bodyNode.getAttribute('AnimationPlayer');

		this._initEvents();
		this._collectAnimiations();
	}

	// #endregion

	// #region BaseComponent Interface

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

		if (_private.animationPlayer) {
			_private.animationPlayer.dispose();
			_private.animationPlayer = null;
		}

		super.onRemove();
	}

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

		this.stopAllAnimations();

		if (_private.animationPlayer) {
			_private.animationPlayer.dispose();
			_private.animationPlayer = null;
		}

		_private.animations.clear();
	}

	// #endregion

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

		if (_private.animationPlayer) {
			return true;
		}

		return false;
	}

	/**
	 * Check whether has animation by name.
	 * @param {String} name The animation name
	 * @returns {Boolean}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   let ret = UinoSpaceman.hasAnimation('Walk');
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	hasAnimation(name) {
		return this.animationNames.indexOf(name) !== -1;
	}

	pauseAnimation(name) {
		if (this.object.loaded) {
			// Process animations by name lists
			if (Utils.isArray(name)) {
				name.forEach(animationName => {
					this._pauseAnimation(animationName, true);
				});
			}
			// Process one animation
			else if (Utils.isString(name)) {
				this._pauseAnimation(name, true);
			}
		}
		else {
			this._addDelayedCommand('pauseAnimation', arguments);
		}
	}

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

		_private.animations.forEach(animation => {
			if (animation.state == PlayStateType.Paused) {
				return;
			}

			this._pauseAnimation(animation.name, true);
		});
	}

	resumeAnimation(name) {
		if (this.object.loaded) {
			// Process animations by name lists
			if (Utils.isArray(name)) {
				name.forEach(animationName => {
					this._pauseAnimation(animationName, false);
				});
			}
			// Process one animation
			else if (Utils.isString(name)) {
				this._pauseAnimation(name, false);
			}
		}
		else {
			this._addDelayedCommand('resumeAnimation', arguments);
		}
	}

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

		_private.animations.forEach(animation => {
			if (animation.state != PlayStateType.Paused) {
				return;
			}

			this._pauseAnimation(animation.name, false);
		});
	}

	/**
	 * Stop animation.
	 * @param {String} name The animation name.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.stopAnimation('Walk');
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Stopped';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 * @public
	 */
	stopAnimation(name) {
		if (this.object.loaded) {
			// Pause spcecified animations by name lists
			if (Utils.isArray(name)) {
				name.forEach(animationName => {
					this._stopAnimation(animationName);
				});
			}
			// Pause one animation
			else if (Utils.isString(name)) {
				this._stopAnimation(name);
			}
		}
		else {
			this._addDelayedCommand('stopAnimation', arguments);
		}
	}

	/**
	 * Stop all animations.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.stopAllAnimations();
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Stopped';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 * @public
	 */
	stopAllAnimations() {
		let _private = this[__.private];

		_private.animations.forEach(animation => {
			this._stopAnimation(animation.name);
		});
	}

	playAnimation(param = {}) {
		if (this.object.loaded) {
			// Stop all animations
			this.stopAllAnimations();

			// Use only one animation name to play
			if (Utils.isString(param)) {
				param = { name: param };
			}

			// Start to play animation
			this._playAnimation(param);
		}
		else {
			this._addDelayedCommand('playAnimation', arguments);
		}
	}

	playAnimationAsync(param = {}) {
		return new Promise((resolve, reject) => {
			let onComplete = Utils.parseValue(param['onComplete'], param['complete']);
			param['onComplete'] = function (ev) {
				if (onComplete) {
					onComplete(ev);
				}

				resolve();
			};

			this.playAnimation(param);
		});
	}

	blendAnimation(param = {}) {
		if (this.object.loaded) {
			// Use only one animation name to play
			if (Utils.isString(param)) {
				param = { name: param };
			}

			// Start to play animation
			this._playAnimation(param);
		}
		else {
			this._addDelayedCommand('blendAnimation', arguments);
		}
	}

	blendAnimationAsync(param = {}) {
		return new Promise((resolve, reject) => {
			let onComplete = Utils.parseValue(param['onComplete'], param['complete']);
			param['onComplete'] = function (ev) {
				if (onComplete) {
					onComplete(ev);
				}

				resolve();
			};

			this.blendAnimation(param);
		});
	}

	isAnimationPlaying(name) {
		return this._checkAnimationState(name, PlayStateType.Playing);
	}

	/**
	 * Check whether all animations are ready.
	 * @returns {Boolean}
	 * @private
	 */
	isAllAnimationsReady() {
		let _private = this[__.private];

		if (_private.animationPlayer) {
			for (let [key, animation] of _private.animations) {
				if (animation.state != PlayStateType.Ready && animation.state != PlayStateType.Stopped) {
					return false;
				}
			}
		}

		return true;
	}

	getAnimation(name) {
		this._init();

		let _private = this[__.private];

		return _private.animations.get(name);
	}

	getAnimationState(name) {
		this._init();

		let _private = this[__.private];

		let animation = _private.animations.get(name);
		if (!animation) {
			return PlayStateType.Stopped;
		}

		return animation.state;
	}

	getPlayingAnimations() {
		this._init();

		let _private = this[__.private];

		let playingAnimations = [];
		_private.animations.forEach(animation => {
			if (animation.state == PlayStateType.Playing) {
				playingAnimations.push(animation);
			}
		});

		return playingAnimations;
	}

	getAnimationDirectionType(name) {
		this._init();

		let _private = this[__.private];

		let animation = _private.animations.get(name);
		if (!animation) {
			return AnimationDirectionType.Normal;
		}

		return animation.directionType;
	}

	setAnimationDirectionType(name, value) {
		if (this.object.loaded) {
			this._init();

			let _private = this[__.private];

			let animation = _private.animations.get(name);
			if (!animation) {
				return false;
			}

			animation.directionType = value;

			let speed = animation.speed;
			if (value == AnimationDirectionType.Reverse) {
				speed *= -1;
			}

			let animationPlayer = _private.animationPlayer;
			animationPlayer.setAttribute(name, 'TimeScale', speed);
		}
		else {
			this._addDelayedCommand('setAnimationDirectionType', arguments);
		}

		return true;
	}

	getAnimationSpeed(name) {
		this._init();

		let _private = this[__.private];

		let animation = _private.animations.get(name);
		if (!animation) {
			return 0;
		}

		return animation.speed;
	}

	setAnimationSpeed(name, value) {
		if (this.object.loaded) {
			this._init();

			let _private = this[__.private];

			let animation = _private.animations.get(name);
			if (!animation) {
				return false;
			}

			animation.speed = value;

			let animationPlayer = _private.animationPlayer;
			animationPlayer.setAttribute(name, 'TimeScale', value);
		}
		else {
			this._addDelayedCommand('setAnimationDirectionType', arguments);
		}

		return true;
	}

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

		if (_private.commandExecutor) {
			_private.commandExecutor.runCommands(this);
			_private.commandExecutor = null;
		}
	}

	get animationPlayer() {
		this._init();

		let _private = this[__.private];

		return _private.animationPlayer;
	}

	/**
	 * Get all animations info.
	 * @type {Array<AnimationResult>}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   let animations = UinoSpaceman.animations;
	 *   let ret = animations[0].name == 'Walk';
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	get animations() {
		this._init();

		let _private = this[__.private];

		return [..._private.animations.values()];
	}

	/**
	 * Get the animation names.
	 * @type {Array<String>}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   let animations = UinoSpaceman.animationNames;
	 *   let ret = animations[0] == 'Walk';
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	get animationNames() {
		this._init();

		let _private = this[__.private];

		return [..._private.animations.keys()];
	}

}

ModelAnimationComponent.exportProperties = [
	'animations',
	'animationNames'
];

ModelAnimationComponent.exportFunctions = [
	'playAnimation',
	'playAnimationAsync',
	'blendAnimation',
	'blendAnimationAsync',
	'hasAnimation',
	'isAnimationPlaying',
	'pauseAnimation',
	'pauseAllAnimations',
	'resumeAnimation',
	'resumeAllAnimations',
	'stopAnimation',
	'stopAllAnimations',
	'getAnimation',
	'getAnimationState',
	'getPlayingAnimations',
	'getAnimationDirectionType',
	'setAnimationDirectionType',
	'getAnimationSpeed',
	'setAnimationSpeed'
];

export { ModelAnimationComponent }