const EventEmitter = require('engine/EventEmitter');
const { getTargetAndKey } = require('./utils');
const { Easing, Interpolation } = require('./easing');
/**
* Action type enums
* @enum {number}
* @memberof Tween
*/
const ACTION_TYPES = {
REPEAT: 0,
WAIT: 1,
ANIMATE: 2,
};
// TODO: better easing support (https://github.com/rezoner/ease)
/**
* @class Tween
* @extends {EventEmitter}
*/
class Tween extends EventEmitter {
/**
* @constructor
* @param {object} context Object to apply this tween to.
*/
constructor(context) {
super();
this.context = context;
/**
* List of actions.
* @type {array}
*/
this.actions = [];
this.index = -1;
this.current = null;
this.currentAction = null;
/**
* Delta cache for updating
* @type {number}
*/
this.delta = 0;
/**
* Tween duration.
* @type {number}
* @default 500
*/
this.duration = 500;
/**
* Progress of current performing action
* @type {number}
*/
this.progress = 0;
/**
* Tween's easing function.
* @property {function} easing
*/
this.easing = Easing.Linear.None;
/**
* Tween's interpolation function.
* @property {function} interpolationFn
*/
this.interpolation = Interpolation.Linear;
/**
* Whether this tween is finished
* @type {boolean}
*/
this.isFinished = false;
/**
* Whether this tween is removed
* @type {boolean}
*/
this.isRemoved = false;
/**
* Whether this tween is paused
* @type {boolean}
*/
this.isPaused = false;
// Interal variables
this.repeatCount = 0;
this.propCtx = []; // Property context list
this.propKey = []; // Property key list
this.before = []; // Target property list
this.change = []; // Property change list
this.types = []; // Property type list
}
/**
* Initialize this tween
* @param {object} context Target object.
*/
init(context) {
this.removeAllListeners();
this.context = context;
this.actions.length = 0;
this.index = -1;
this.current = null;
this.currentAction = null;
this.delta = 0;
this.duration = 500;
this.progress = 0;
this.easing = Easing.Linear.None;
this.interpolation = Interpolation.Linear;
this.isFinished = false;
this.isRemoved = false;
this.isPaused = false;
this.repeatCount = 0;
this.propCtx.length = 0;
this.propKey.length = 0;
this.before.length = 0;
this.change.length = 0;
this.types.length = 0;
}
/**
* Push a new action to the tween.
* @memberof Tween#
* @method to
* @param {Object} properties Target properties
* @param {Number} duration Duration of the action in ms
* @param {String|Function} easing Easing function
* @param {String|Function} interpolation Interpolation function
* @return {Tween} Tween itself for chaining.
*/
to(properties, duration, easing = Easing.Linear.None, interpolation = Interpolation.Linear) {
let easingFn = easing, interpolationFn = interpolation;
if (typeof easing === 'string') {
easing = easing.split('.');
easingFn = Easing[easing[0]][easing[1]];
}
if (typeof interpolation === 'string') {
interpolationFn = Interpolation[interpolation];
}
/**
* props [
* propertyContext1, propertyKey1, targetValue1,
* propertyContext2, propertyKey2, targetValue2,
* ...
* ]
*/
let props = [], keys = Object.keys(properties), pair;
for (let i = 0; i < keys.length; i++) {
pair = getTargetAndKey(this.context, keys[i]);
props.push(pair[0], pair[1], properties[keys[i]]);
}
this.actions.push([props, duration, easingFn, interpolationFn]);
return this;
}
/**
* Repeat the tween for times.
* @memberof Tween#
* @method repeat
* @param {number} times How many times to repeat.
* @return {Tween} Tween itself.
*/
repeat(times) {
this.actions.push([ACTION_TYPES.REPEAT, times]);
return this;
}
/**
* Wait a short time before next action.
* @memberof Tween#
* @method wait
* @param {number} time Time to wait in ms.
* @return {Tween} Tween itself for chaining.
*/
wait(time) {
this.actions.push([ACTION_TYPES.WAIT, time]);
return this;
}
/**
* Stop this tween.
* @memberof Tween#
* @method stop
* @return {Tween} Tween itself for chaining.
*/
stop() {
this.isRemoved = true;
this.removeAllListeners();
return this;
}
/**
* Pause this tween.
* @memberof Tween#
* @method pause
* @return {Tween} Tween itself for chaining.
*/
pause() {
this.isPaused = true;
return this;
}
/**
* Resume this tween from pausing.
* @memberof Tween#
* @method resume
* @return {Tween} Tween itself for chaining.
*/
resume() {
this.isPaused = false;
return this;
}
/**
* Do next action.
* @private
*/
_next() {
this.delta = 0;
this.index++;
if (this.index >= this.actions.length) {
this.isFinished = true;
this.isRemoved = true;
this.emit('finish', this);
return;
}
this.current = this.actions[this.index];
if (this.current[0] === ACTION_TYPES.WAIT) {
this.duration = this.current[1];
this.currentAction = ACTION_TYPES.WAIT;
}
else if (this.current[0] === ACTION_TYPES.REPEAT) {
if (!this.current.counter) {
this.current.counter = this.current[1];
}
this.current.counter--;
if (this.current.counter > 0) {
this.emit('repeat', this);
// Restart from beginning
this.index = -1;
this.current = null;
this._step(0);
}
else {
// Reset counter for next repeat if exists
this.current.counter = this.current[1];
// Clear for next action
this.current = null;
this.currentAction = null;
this._step(0);
}
}
else {
this.properties = this.current[0];
this.propCtx.length = 0;
this.propKey.length = 0;
this.change.length = 0;
this.before.length = 0;
this.types.length = 0;
for (let i = 0; i < this.properties.length; i += 3) {
// Property context
let context = this.properties[i];
// Property key
let key = this.properties[i + 1];
// Current value
let currValue = context[key];
// Target value
let targetValue = this.properties[i + 2];
// Construct action lists
this.propKey.push(key);
this.propCtx.push(context);
// Number
if (typeof(currValue) === 'number') {
this.before.push(currValue);
this.change.push(targetValue - currValue);
this.types.push(0);
}
// String
else if (typeof(currValue) === 'string') {
this.before.push(currValue);
this.change.push(targetValue);
this.types.push(1);
}
// Boolean or object
else {
this.before.push(currValue);
this.change.push(targetValue);
this.types.push(2);
}
}
this.currentAction = ACTION_TYPES.ANIMATE;
this.duration = this.current[1];
this.easing = this.current[2];
this.interpolation = this.current[3];
}
}
/**
* Update.
* @param {number} delta Delta time
* @protected
*/
_step(delta) {
if (this.isRemoved || this.isPaused) {return;}
this.delta += delta;
if (!this.current) {
this._next();
}
switch (this.currentAction) {
case ACTION_TYPES.ANIMATE:
this._doAnimate();
break;
case ACTION_TYPES.WAIT:
this._doWait();
break;
}
}
/**
* Do current action.
* @private
*/
_doAnimate() {
this.progress = Math.min(1, this.delta / this.duration);
let mod = this.easing(this.progress);
let i, key;
for (i = 0; i < this.change.length; i++) {
key = this.propKey[i];
switch (this.types[i]) {
// Number tweening
case 0:
this.propCtx[i][key] = this.before[i] + this.change[i] * mod;
break;
// Tweening text content
case 1:
this.propCtx[i][key] = this.change[i].slice(0, Math.floor(this.change[i].length * mod));
break;
// Instantly value changing for boolean and objects
case 2:
if (this.progress >= 1) {this.propCtx[i][key] = this.change[i];}
break;
}
}
if (this.progress >= 1) {
this._next();
}
}
/**
* Do wait action.
* @private
*/
_doWait() {
if (this.delta >= this.duration) {
this._next();
}
}
/**
* Recycle this tween for later use.
*/
recycle() {
pool.push(this);
}
}
// Object recycle
const pool = [];
for (let i = 0; i < 20; i++) {
pool.push(new Tween(null));
}
/**
* Tween factory method.
* @memberOf Tween
* @param {object} context Target object.
* @return {module:engine/animation/tween~Tween} Tween instance.
*/
Tween.create = function(context) {
let t = pool.pop();
if (!t) {
t = new Tween(context);
}
t.init(context);
return t;
};
/**
* Classic tween animation.
* Use {@link SystemAnime#tween} to create a new tween and start it immediately.
*
* @exports engine/anime/tween
*
* @requires engine/EventEmitter
* @requires engine/anime/utils
* @requires engine/anime/easing
*/
module.exports = Tween;
module.exports.ACTION_TYPES = ACTION_TYPES;