import Easing from './Easing';
/**
* Fired when the tween starts. If the tween has a delay, this event fires when the delay time is ended.
*
* @event PIXI.tween.Tween#start
*/
/**
* Fired when the tween is over. If the .loop option it's true, this event never will be fired.
* If the tween has an .repeat number, this event will be fired just when all the repeats are done.
*
* @event PIXI.tween.Tween#end
*/
/**
* Fired at every repeat cycle, if your tween has .repeat=5 this events will be fired 5 times.
*
* @event PIXI.tween.Tween#repeat
* @param {number} repeat - Number of times this tween has repeated
*/
/**
* Fired at each frame
*
* @event PIXI.tween.Tween#update
* @param {number} progress - 0-1 decimal value representing proportion of completion.
* @param {number} elapsedTime - How much time in ms that has passed since the tween started.
*/
/**
* Fired only when it's used the .stop() method. It's useful to know when a timer is cancelled.
*
* @event PIXI.tween.Tween#stop
*/
/**
* If the pingPong option it's true, this events will be fired when the tweens returns back.
*
* @event PIXI.tween.Tween#pingpong
*/
/**
* Quickly configure a tween via an object / json
*
* @typedef {Object} PIXI.tween.Tween#tweenConfig
* @property {Object} [from]
* @property {Object} [to]
* @property {number} [delay]
* @property {function|string} [easing]
* @property {boolean} [loop]
* @property {Object} [path]
* @property {boolean} [pathReverse]
* @property {boolean} [pingPong]
* @property {number} [repeat]
* @property {number} [time]
* @property {number} [speed]
* @property {Object} [on]
* @property {function} [on.end]
* @property {function} [on.pingpong]
* @property {function} [on.repeat]
* @property {function} [on.start]
* @property {function} [on.stop]
* @property {function} [on.update]
*/
/**
* Tween class
*
* @class
* @memberof PIXI.tween
*/
export default class Tween extends PIXI.utils.EventEmitter {
/**
* @param {*} target - Target object to tween
* @param {PIXI.tween.TweenManager} [manager] - Tween manager to handle this tween
* @param {PIXI.tween.Tween#tweenConfig} [config] - object to configure the tween
*/
constructor(target, manager, config) {
super();
this.target = target;
if (manager) {
this.manager = manager;
}
this.clear();
if (config) {
this.config(config);
}
}
/**
* Clears all class data, meaning that the tween will now do nothing if start is called
*
* @returns {PIXI.tween.Tween} - This tween instance
*/
clear() {
/** @member {PIXI.tween.Easing} - Either an easing function from PIXI.tween.Easing or a custom easing */
this.easing = Easing.linear();
/** @member {number} - Times to repeat this tween */
this.repeat = 0;
/** @member {boolean} - Set true if you want to loop this tween forever */
this.loop = false;
/** @member {number} - Set a delay time in milliseconds before the tween starts */
this.delay = 0;
/** @member {boolean} - Set true to repeat the tween from the end point back to the start point */
this.pingPong = false;
/** @member {PIXI.tween.TweenPath} - Set an instance of TweenPath to animate an object along the path */
this.path = null;
/** @member {boolean} - Set true to reverse the direction along the path */
this.pathReverse = false;
/** @member {number} - How long to animate this tween over */
this.time = 0;
/** @member {number} - The speed that the tween will play at. 0 effectively pauses it, 1 is normal speed */
this.speed = 1;
this._active = false;
this._isStarted = false;
this._isEnded = false;
this._to = {};
this._from = {};
this._resetFromOnStart = false;
this._delayTime = 0;
this._elapsedTime = 0;
this._progress = 0;
this._repeat = 0;
this._pingPong = false;
this._pathFrom = 0;
this._pathTo = 0;
this._chainTween = null;
this._resolvePromise = null;
return this;
}
/**
* Configures the tween via a config object
*
* @param {PIXI.tween.Tween#tweenConfig} config - object to configure the tween
* @returns {PIXI.tween.Tween} - This tween instance
*/
config(config) {
if (!config || typeof config !== 'object') {
return this;
}
if (typeof config.from === 'object') {
this.from(config.from);
}
if (config.to && typeof config.to === 'object') {
this.to(config.to);
}
if (typeof config.delay === 'number') {
this.delay = config.delay;
}
if (config.easing) {
if (typeof config.easing === 'string' && Easing[config.easing]) {
this.easing = Easing[config.easing]();
} else if (typeof config.easing === 'function') {
this.easing = config.easing;
}
}
if (typeof config.loop === 'boolean') {
this.loop = config.loop;
}
if (typeof config.path === 'object') {
this.path = config.path;
}
if (typeof config.pathReverse === 'boolean') {
this.pathReverse = config.pathReverse;
}
if (typeof config.pingPong === 'boolean') {
this.pingPong = config.pingPong;
}
if (typeof config.repeat === 'number') {
this.repeat = config.repeat;
}
if (typeof config.time === 'number') {
this.time = config.time;
}
if (typeof config.speed === 'number') {
this.speed = config.speed;
}
if (config.on && typeof config.on === 'object') {
if (typeof config.on.end === 'function') {
this.on('end', config.on.end);
}
if (typeof config.on.pingpong === 'function') {
this.on('pingpong', config.on.pingpong);
}
if (typeof config.on.repeat === 'function') {
this.on('repeat', config.on.repeat);
}
if (typeof config.on.start === 'function') {
this.on('start', config.on.start);
}
if (typeof config.on.stop === 'function') {
this.on('stop', config.on.stop);
}
if (typeof config.on.update === 'function') {
this.on('update', config.on.update);
}
}
return this;
}
/**
* True if the tween is running
*
* @member {boolean}
* @readonly
*/
get active() {
return this._active;
}
/**
* How much time has passed on an active tween
*
* @member {number}
* @readonly
*/
get elapsedTime() {
return this._elapsedTime;
}
/**
* 0-1 decimal value representing proportion of completion
*
* @member {number}
* @readonly
*/
get progress() {
return this._progress;
}
/**
* True if the tween has started running
*
* @member {boolean}
* @readonly
*/
get isStarted() {
return this._isStarted;
}
/**
* True if a tween has ended running
*
* @member {boolean}
* @readonly
*/
get isEnded() {
return this._isEnded;
}
/**
* Remove the tween from the manager if it has one
*
* @returns {PIXI.tween.Tween} - This tween instance
*/
remove() {
if (!this.manager) {
return this;
}
this.manager.removeTween(this);
return this;
}
/**
* Starts the tween
*
* @param {Promise} resolve - Promise to resolve when the tween has ended
* @returns {PIXI.tween.Tween} - This tween instance
*/
start(resolve) {
this._active = true;
this._isStarted = false;
if (this._resetFromOnStart) {
this._from = {};
}
if (!this._resolvePromise && resolve) {
this._resolvePromise = resolve;
}
this.manager.addTween(this);
return this;
}
/**
* Starts the tween, whilst returning a new promise
*
* @returns {Promise} - Promsie that will resolve when the tween has finished
*/
startPromise() {
if (!Promise) {
return this.start();
}
if (this._resolvePromise) {
return Promise.resolve();
}
return new Promise((resolve) => {
this.start(resolve);
});
}
/**
* Stop the tweens progress
*
* @fires PIXI.tween.Tween#stop
*
* @param {boolean} [end=false] - Force end to be called
* @returns {PIXI.tween.Tween} - This tween instance
*/
stop(end = false) {
this._active = false;
this.emit('stop');
if (end) {
this._end();
}
return this;
}
/**
* Set the end data for the tween
*
* @example
* tween.to({ x:100, y:100 });
*
* @param {Object} data - Object containing end point data for the tween
* @returns {PIXI.tween.Tween} - This tween instance
*/
to(data = {}) {
this._to = data;
return this;
}
/**
* Set the start point data for the tween.
* If nothing is set, data is reset so that starting the tween will use the objects current state as the start point
*
* @example
* tween.from({ x:50, y:50 });
*
* @param {Object} [data] - Object containing start point data for the tween
* @returns {PIXI.tween.Tween} - This tween instance
*/
from(data) {
if (!data || typeof data !== 'object') {
this._resetFromOnStart = true;
this._from = {};
} else {
this._resetFromOnStart = false;
this._from = data;
}
return this;
}
/**
* Chain another tween to play after this tween has ended
*
* @param {PIXI.tween.Tween} tween - Tween to chain
* @returns {PIXI.tween.Tween} - This tween instance
*/
chain(tween) {
if (!tween) {
tween = new Tween(this.target);
}
this._chainTween = tween;
return tween;
}
/**
* Resets the tween to it's default state, but keeping any to and from data, so start can be called to replay the tween
*
* @returns {PIXI.tween.Tween} - This tween instance
*/
reset() {
this._elapsedTime = 0;
this._progress = 0;
this._repeat = 0;
this._delayTime = 0;
this._isStarted = false;
this._isEnded = false;
if (this.pingPong && this._pingPong) {
const _to = this._to;
const _from = this._from;
this._to = _from;
this._from = _to;
this._pingPong = false;
}
return this;
}
/**
* Updating of the tween; usually automatically called by its manager
*
* @fires PIXI.tween.Tween#start
* @fires PIXI.tween.Tween#update
* @fires PIXI.tween.Tween#pingpong
* @fires PIXI.tween.Tween#repeat
*
* @param {number} deltaMS - Time elapsed in milliseconds from last update to this update.
*/
update(deltaMS) {
if (!this._canUpdate() && (this._to || this.path)) {
return;
}
deltaMS *= this.speed;
if (this.delay > this._delayTime) {
this._delayTime += deltaMS;
return;
}
if (!this._isStarted) {
this._parseData();
this._isStarted = true;
this._isEnded = false;
this.emit('start');
}
const time = (this.pingPong) ? this.time / 2 : this.time;
let _to;
let _from;
if (time >= this._elapsedTime) {
const t = this._elapsedTime + deltaMS;
const ended = (t >= time);
this._elapsedTime = ended ? time : t;
this._apply(time);
const realElapsed = this._pingPong ? time + this._elapsedTime : this._elapsedTime;
this._progress = this.time > 0 ? Math.min(realElapsed / this.time, 1) : 1;
this.emit('update', this._progress, realElapsed);
if (ended) {
if (this.pingPong && !this._pingPong) {
this._pingPong = true;
_to = this._to;
_from = this._from;
this._from = _to;
this._to = _from;
if (this.path) {
_to = this._pathTo;
_from = this._pathFrom;
this._pathTo = _from;
this._pathFrom = _to;
}
this.emit('pingpong');
this._elapsedTime = 0;
this._progress = 0.5;
return;
}
if (this.loop || this.repeat > this._repeat) {
++this._repeat;
this.emit('repeat', this._repeat);
this._elapsedTime = 0;
this._progress = 0;
if (this.pingPong && this._pingPong) {
_to = this._to;
_from = this._from;
this._to = _from;
this._from = _to;
if (this.path) {
_to = this._pathTo;
_from = this._pathFrom;
this._pathTo = _from;
this._pathFrom = _to;
}
this._pingPong = false;
}
return;
}
this._end();
}
return;
}
}
/**
* Called when the tween has finished
*
* @fires PIXI.tween.Tween#end
*
* @private
*/
_end() {
this._isEnded = true;
this._active = false;
this.emit('end');
this._elapsedTime = 0;
if (this._chainTween) {
if (!this._chainTween.manager) {
this._chainTween.manager = this.manager;
}
this._chainTween.start(this._resolvePromise);
this._resolvePromise = null;
} else if (this._resolvePromise) {
const resolvePromise = this._resolvePromise;
this._resolvePromise = null;
resolvePromise();
}
}
/**
* Parses the from and to data to extract details about how the tween should progress
*
* @private
*/
_parseData() {
if (this._isStarted) {
return;
}
_parseRecursiveData(this._to, this._from, this.target);
if (this.path) {
const distance = this.path.totalDistance();
if (this.pathReverse) {
this._pathFrom = distance;
this._pathTo = 0;
} else {
this._pathFrom = 0;
this._pathTo = distance;
}
}
}
/**
* Updates the object with the tween settings
*
* @param {number} time - Time duration for the tween
* @private
*/
_apply(time) {
_recursiveApplyTween(this._to, this._from, this.target, time, this._elapsedTime, this.easing);
if (this.path) {
const time = (this.pingPong) ? this.time / 2 : this.time;
const b = this._pathFrom;
const c = this._pathTo - this._pathFrom;
const d = time;
const t = time ? this._elapsedTime / d : 1;
const distance = b + (c * this.easing(t));
const pos = this.path.getPointAtDistance(distance);
this.target.position.set(pos.x, pos.y);
}
}
/**
* Can this tween be updated (must be active and have a target destination)
*
* @returns {boolean} - True if this tween can be updated
* @private
*/
_canUpdate() {
return (this._active && this.target);
}
}
function _recursiveApplyTween(to, from, target, time, elapsedTime, easing) {
for (const k in to) {
if (!_isObject(to[k])) {
const b = from[k];
const c = to[k] - from[k];
const d = time;
const t = time ? elapsedTime / d : 1;
target[k] = b + (c * easing(t));
} else {
_recursiveApplyTween(to[k], from[k], target[k], time, elapsedTime, easing);
}
}
}
function _parseRecursiveData(to, from, target) {
for (const k in to) {
if (from[k] !== 0 && !from[k]) {
if (_isObject(target[k])) {
from[k] = {};
_parseRecursiveData(to[k], from[k], target[k]);
} else {
from[k] = target[k];
}
}
}
}
function _isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}