/* global platypus */
import Data from '../Data.js';
import TweenJS from '@tweenjs/tween.js';
import {arrayCache} from '../utils/array.js';
import createComponentClass from '../factory.js';
const
Easing = TweenJS.Easing,
Group = TweenJS.Group,
Interpolation = TweenJS.Interpolation,
Tween = TweenJS.Tween,
eases = (function () {
const easing = {};
for (const key in Easing) {
if (Easing.hasOwnProperty(key)) {
for (const type in Easing[key]) {
if (Easing[key].hasOwnProperty(type)) {
easing[key + '.' + type] = Easing[key][type];
}
}
}
}
return easing;
}()),
trigger = function () {
this.trigger.apply(this, arguments);
};
export default createComponentClass(/** @lends platypus.components.Tween.prototype */{
id: 'Tween',
properties: {
/**
* Required. A key/value list of events and an array or object representing the tween they should trigger.
*
* {
* "begin-flying": { // When "begin-flying" is triggered on this entity, the following tween begins. Tween definitions adhere to a similar structure outlined by the [TweenJS documentation](https://github.com/tweenjs/tween.js/blob/master/docs/user_guide.md).
* "target": "entityId", // This defaults to the entity that this component is on, but can be the id of any entity in this layer.
* "to": { // Specifies the values to change and what they should tween to.
* "scaleY": 1,
* "y": [400, 450, 425]
* },
* "time": 1000, // Time in MS to make transition.
* "easing": "Quadratic.In", Easing function to use.
* "onUpdate": "flying", // Event to trigger while transition is running.
* "onStart": "wave", // Event to trigger when tween begins (after delay).
* "onStop": "whoa", // Event to trigger when tween is stopped (not completed normally).
* "onComplete": "done", // Event to trigger when tween is complete.
* "onRepeat": "going-again", // Event to trigger when tween is beginning again.
* "chain": "stop-flying", // Specifies a tween to use next.
* "repeat": 0, // Sets how many times to repeat this tween once it completes.
* "yoyo": false, // If this tween repeats, yoyo makes it transition back-and-forth.
* "delay": 500, // Time in MS to delay before starting transition.
* "repeatDelay": 0, // Time in MS that the transition should wait between repeats if it shouldn't be the `delay` value.
* "interpolation": "Linear" // Interpolation method to use for an array of values.
* },
*
* "stop-flying": [{ // May also chain tweens by specifying an array.
* "to": {"y": 100},
* "time": 250
* }, {
* "to": {"y": 0},
* "time": 250
* }]
* }
*
* @property events
* @type Object
* @default null
*/
events: null
},
/**
* This component takes a list of tween definitions and plays them as needed. This component requires TweenJS.
*
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#handle-logic
* @listens platypus.Entity#pause-tween
* @listens platypus.Entity#stop-tween
* @listens platypus.Entity#tween
* @listens platypus.Entity#unpause-tween
*/
initialize: function () {
var event = '',
events = this.events;
this.group = new Group();
this.waitingToChain = Data.setUp();
this.tweens = Data.setUp();
this.time = 0;
this.paused = false;
if (events) {
for (event in events) {
if (events.hasOwnProperty(event)) {
const tween = this.tweens[event] = this.createTweens(events[event]);
this.addEventListener(event, this.runTween.bind(this, event));
if (this.waitingToChain[event]) {
const waits = this.waitingToChain[event];
for (let i = 0; i < waits.length; i++) {
waits[i].chain(tween);
}
arrayCache.recycle(waits);
delete this.waitingToChain[event];
}
}
}
}
},
events: {
/**
* Trigger this event to play a tween using the same spec used for a tween on this component's `events` property.
*
* @event platypus.Entity#tween
* @param {Object|Array} tween
*/
'tween': function (tween) {
this.runTween(tween);
},
'handle-logic': function (tick) {
if (!this.paused) {
this.time += tick.delta;
this.group.update(this.time);
}
},
/**
* This event stops all running tweens on this component.
*
* @event platypus.Entity#stop-tween
*/
'stop-tween': function () {
this.group.removeAll();
},
/**
* This event pauses all running tweens on this component.
*
* @event platypus.Entity#pause-tween
*/
"pause-tween": function () {
this.paused = true;
},
/**
* This event unpauses all running tweens on this component.
*
* @event platypus.Entity#unpause-tween
*/
"unpause-tween": function () {
this.paused = false;
}
},
methods: {
createTween: function (tweenDefinition, chainable) {
const owner = this.owner,
entity = tweenDefinition.target ? (typeof tweenDefinition.target === 'string' ? owner.parent.getEntityById(tweenDefinition.target) : tweenDefinition.target) : owner;
if (!entity) {
platypus.debug.warn('Component Tween: Could not find entity as specified by `target` - ' + tweenDefinition.target);
return null;
} else if (!tweenDefinition.to || !tweenDefinition.time) {
platypus.debug.warn('Component Tween: Both `time` and `to` must be specified to create tween.');
return null;
} else {
const tween = new Tween(entity, this.group);
tween.to(tweenDefinition.to, tweenDefinition.time);
if (tweenDefinition.onUpdate) {
tween.onUpdate((typeof tweenDefinition.onUpdate !== 'function') ? trigger.bind(owner, tweenDefinition.onUpdate) : tweenDefinition.onUpdate);
}
if (tweenDefinition.onStart) {
tween.onStart((typeof tweenDefinition.onStart !== 'function') ? trigger.bind(owner, tweenDefinition.onStart) : tweenDefinition.onStart);
}
if (tweenDefinition.onStop) {
tween.onStop((typeof tweenDefinition.onStop !== 'function') ? trigger.bind(owner, tweenDefinition.onStop) : tweenDefinition.onStop);
}
if (tweenDefinition.onComplete || tweenDefinition.yoyo) { // need fix for repeating an event that yoyo's
tween.onComplete(() => {
if (tweenDefinition.yoyo) { // reset tween
tween.to(tweenDefinition.to);
tween.repeat(tweenDefinition.repeat);
}
if (tweenDefinition.onComplete) {
if (typeof tweenDefinition.onComplete !== 'function') {
trigger.call(owner, tweenDefinition.onComplete);
} else {
tweenDefinition.onComplete();
}
}
});
}
if (tweenDefinition.onRepeat) {
tween.onRepeat((typeof tweenDefinition.onRepeat !== 'function') ? trigger.bind(owner, tweenDefinition.onRepeat) : tweenDefinition.onRepeat);
}
if (tweenDefinition.chain) {
if (!chainable) {
platypus.debug.warn('Component Tween: ignoring `chain` on tween since it is part of an array of tweens.');
} else if (typeof tweenDefinition.chain === 'string') {
if (this.tweens[tweenDefinition.chain]) {
tween.chain(this.tweens[tweenDefinition.chain]);
} else {
if (!this.waitingToChain[tweenDefinition.chain]) {
this.waitingToChain[tweenDefinition.chain] = arrayCache.setUp();
}
this.waitingToChain[tweenDefinition.chain].push(tween);
}
} else {
tween.chain(tweenDefinition.chain);
}
}
if (tweenDefinition.repeat) {
tween.repeat(tweenDefinition.repeat);
}
if (tweenDefinition.yoyo) {
tween.yoyo(true);
}
if (tweenDefinition.delay) {
tween.delay(tweenDefinition.delay);
}
if (tweenDefinition.repeatDelay) {
tween.repeatDelay(tweenDefinition.repeatDelay);
}
if (tweenDefinition.interpolation) {
if (typeof tweenDefinition.interpolation === 'function') {
tween.interpolation(tweenDefinition.interpolation);
} else if (Interpolation[tweenDefinition.interpolation]) {
tween.interpolation(Interpolation[tweenDefinition.interpolation]);
} else {
platypus.debug.warn('Component Tween: "' + tweenDefinition.interpolation + '" is not a valid interpolation value; must be "Linear", "Bezier", or "CatmullRom".');
}
}
if (tweenDefinition.easing) {
if (typeof tweenDefinition.easing === 'function') {
tween.easing(tweenDefinition.easing);
} else if (eases[tweenDefinition.easing]) {
tween.easing(eases[tweenDefinition.easing]);
} else {
let str = '".',
join = '", or "';
for (const key in eases) {
if (eases.hasOwnProperty(key)) {
str = join + key + str;
join = '", "';
}
}
platypus.debug.warn('Component Tween: "' + tweenDefinition.easing + '" is not a valid easing value; must be ' + str.substring(3));
}
}
return tween;
}
},
createTweens: function (tween) {
if (Array.isArray(tween)) {
let i = tween.length,
lastTween = null;
while (i--) {
const newTween = this.createTween(tween[i], !lastTween);
if (lastTween) {
newTween.chain(lastTween);
}
lastTween = newTween;
}
return lastTween;
} else {
return this.createTween(tween, true);
}
},
destroy: function () {
this.group.removeAll();
},
runTween: function (tweenDefinition) {
var tween = typeof tweenDefinition === 'string' ? this.tweens[tweenDefinition] : this.createTweens(tweenDefinition);
if (tween) {
// Clean out old values
for (const key in tween._valuesStart) {
if (tween._valuesStart.hasOwnProperty(key)) {
delete tween._valuesStart[key];
}
}
// Run
tween.start(this.time);
} else {
platypus.debug.warn('Component Tween: Unable to run requested tween.', tweenDefinition);
}
}
}
});