/**
* The Animation modifier.
*
* Sencha Charts allow users to use transitional animation on sprites. Simply set the duration
* and easing in the animation modifier, then all the changes to the sprites will be animated.
*
* @example
* var drawCt = Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 400,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91'
* }]
* });
*
* var rect = drawCt.getSurface().getItems()[0];
*
* rect.setAnimation({
* duration: 1000,
* easing: 'elasticOut'
* });
*
* Ext.defer(function () {
* rect.setAttributes({
* width: 250
* });
* }, 500);
*
* Also, you can use different durations and easing functions on different attributes by using
* {@link #customDurations} and {@link #customEasings}.
*
* By default, an animation modifier will be created during the initialization of a sprite.
* You can get the animation modifier of a sprite via its
* {@link Ext.draw.sprite.Sprite#method-getAnimation getAnimation} method.
*/
Ext.define('Ext.draw.modifier.Animation', {
extend: 'Ext.draw.modifier.Modifier',
alias: 'modifier.animation',
requires: [
'Ext.draw.TimingFunctions',
'Ext.draw.Animator'
],
config: {
/**
* @cfg {Function} easing
* Default easing function.
*/
easing: Ext.identityFn,
/**
* @cfg {Number} duration
* Default duration time (ms).
*/
duration: 0,
/**
* @cfg {Object} customEasings Overrides the default easing function for defined attributes.
* E.g.:
*
* // Assuming the sprite the modifier is applied to is a 'circle'.
* customEasings: {
* r: 'easeOut',
* 'fillStyle,strokeStyle': 'linear',
* 'cx,cy': function (p, n) {
* p = 1 - p;
* n = n || 1.616;
* return 1 - p * p * ((n + 1) * p - n);
* }
* }
*/
customEasings: {},
/**
* @cfg {Object} customDurations Overrides the default duration for defined attributes.
* E.g.:
*
* // Assuming the sprite the modifier is applied to is a 'circle'.
* customDurations: {
* r: 1000,
* 'fillStyle,strokeStyle': 2000,
* 'cx,cy': 1000
* }
*/
customDurations: {}
},
constructor: function(config) {
var me = this;
me.anyAnimation = me.anySpecialAnimations = false;
me.animating = 0;
me.animatingPool = [];
me.callParent([config]);
},
prepareAttributes: function(attr) {
if (!attr.hasOwnProperty('timers')) {
attr.animating = false;
attr.timers = {};
// The 'targets' object is used to hold the target values for the
// attributes while they are being animated from source to target values.
// The 'targets' is pushed down to the lower level modifiers,
// instead of the actual attr object, to hide the fact that the
// attributes are being animated.
attr.targets = Ext.Object.chain(attr);
attr.targets.prototype = attr;
}
if (this._lower) {
this._lower.prepareAttributes(attr.targets);
}
},
updateSprite: function(sprite) {
this.setConfig(sprite.config.animation);
},
updateDuration: function(duration) {
this.anyAnimation = duration > 0;
},
applyEasing: function(easing) {
if (typeof easing === 'string') {
easing = Ext.draw.TimingFunctions.easingMap[easing];
}
return easing;
},
applyCustomEasings: function(newEasings, oldEasings) {
var any, key, attrs, easing, i, ln;
oldEasings = oldEasings || {};
for (key in newEasings) {
any = true;
easing = newEasings[key];
attrs = key.split(',');
if (typeof easing === 'string') {
easing = Ext.draw.TimingFunctions.easingMap[easing];
}
for (i = 0, ln = attrs.length; i < ln; i++) {
oldEasings[attrs[i]] = easing;
}
}
if (any) {
this.anySpecialAnimations = any;
}
return oldEasings;
},
/**
* Set special easings on the given attributes. E.g.:
*
* circleSprite.getAnimation().setEasingOn('r', 'elasticIn');
*
* @param {String/Array} attrs The source attribute(s).
* @param {String} easing The special easings.
*/
setEasingOn: function(attrs, easing) {
var customEasings = {},
i, ln;
attrs = Ext.Array.from(attrs).slice();
for (i = 0, ln = attrs.length; i < ln; i++) {
customEasings[attrs[i]] = easing;
}
this.setCustomEasings(customEasings);
},
/**
* Remove special easings on the given attributes.
* @param {String/Array} attrs The source attribute(s).
*/
clearEasingOn: function(attrs) {
var i, ln;
attrs = Ext.Array.from(attrs, true);
for (i = 0, ln = attrs.length; i < ln; i++) {
delete this._customEasings[attrs[i]];
}
},
applyCustomDurations: function(newDurations, oldDurations) {
var any, key, duration, attrs, i, ln;
oldDurations = oldDurations || {};
for (key in newDurations) {
any = true;
duration = newDurations[key];
attrs = key.split(',');
for (i = 0, ln = attrs.length; i < ln; i++) {
oldDurations[attrs[i]] = duration;
}
}
if (any) {
this.anySpecialAnimations = any;
}
return oldDurations;
},
/**
* Set special duration on the given attributes. E.g.:
*
* rectSprite.getAnimation().setDurationOn('height', 2000);
*
* @param {String/Array} attrs The source attributes.
* @param {Number} duration The special duration.
*/
setDurationOn: function(attrs, duration) {
var customDurations = {},
i, ln;
attrs = Ext.Array.from(attrs).slice();
for (i = 0, ln = attrs.length; i < ln; i++) {
customDurations[attrs[i]] = duration;
}
this.setCustomDurations(customDurations);
},
/**
* Remove special easings on the given attributes.
* @param {Object} attrs The source attributes.
*/
clearDurationOn: function(attrs) {
var i, ln;
attrs = Ext.Array.from(attrs, true);
for (i = 0, ln = attrs.length; i < ln; i++) {
delete this._customDurations[attrs[i]];
}
},
/**
* @private
* Initializes Animator for the animation.
* @param {Object} attr The source attributes.
* @param {Boolean} animating The animating flag.
*/
setAnimating: function(attr, animating) {
var me = this,
pool = me.animatingPool,
i;
if (attr.animating !== animating) {
attr.animating = animating;
if (animating) {
pool.push(attr);
if (me.animating === 0) {
Ext.draw.Animator.add(me);
}
me.animating++;
}
else {
for (i = pool.length; i--;) {
if (pool[i] === attr) {
pool.splice(i, 1);
}
}
me.animating = pool.length;
}
}
},
/**
* @private
* Set the attr with given easing and duration.
* @param {Object} attr The attributes collection.
* @param {Object} changes The changes that popped up from lower modifier.
* @return {Object} The changes to pop up.
*/
setAttrs: function(attr, changes) {
var me = this,
timers = attr.timers,
parsers = me._sprite.self.def._animationProcessors,
defaultEasing = me._easing,
defaultDuration = me._duration,
customDurations = me._customDurations,
customEasings = me._customEasings,
anySpecial = me.anySpecialAnimations,
any = me.anyAnimation || anySpecial,
targets = attr.targets,
ignite = false,
timer, name, newValue, startValue, parser, easing, duration, initial;
if (!any) { // If there is no animation enabled.
// When applying changes to attributes, simply stop current animation
// and set the value.
for (name in changes) {
if (attr[name] === changes[name]) {
delete changes[name];
}
else {
attr[name] = changes[name];
}
delete targets[name];
delete timers[name];
}
return changes;
}
else { // If any animation.
for (name in changes) {
newValue = changes[name];
startValue = attr[name];
if (newValue !== startValue && startValue !== undefined && startValue !== null &&
(parser = parsers[name])) {
// If this property is animating.
// Figure out the desired duration and easing.
easing = defaultEasing;
duration = defaultDuration;
if (anySpecial) {
// Deducing the easing function and duration
if (name in customEasings) {
easing = customEasings[name];
}
if (name in customDurations) {
duration = customDurations[name];
}
}
// Transitions betweens color and gradient or between gradients
// are not supported.
if (startValue && startValue.isGradient || newValue && newValue.isGradient) {
duration = 0;
}
// If the property is animating
if (duration) {
if (!timers[name]) {
timers[name] = {};
}
timer = timers[name];
timer.start = 0;
timer.easing = easing;
timer.duration = duration;
timer.compute = parser.compute;
timer.serve = parser.serve || Ext.identityFn;
timer.remove = changes.removeFromInstance &&
changes.removeFromInstance[name];
if (parser.parseInitial) {
initial = parser.parseInitial(startValue, newValue);
timer.source = initial[0];
timer.target = initial[1];
}
else if (parser.parse) {
timer.source = parser.parse(startValue);
timer.target = parser.parse(newValue);
}
else {
timer.source = startValue;
timer.target = newValue;
}
// The animation started. Change to originalVal.
targets[name] = newValue;
delete changes[name];
ignite = true;
continue;
}
else {
delete targets[name];
}
}
else {
delete targets[name];
}
// If the property is not animating.
delete timers[name];
}
}
if (ignite && !attr.animating) {
me.setAnimating(attr, true);
}
return changes;
},
/**
* @private
*
* Update attributes to current value according to current animation time.
* This method will not affect the values of lower layers, but may delete a
* value from it.
* @param {Object} attr The source attributes.
* @return {Object} The changes to pop up or null.
*/
updateAttributes: function(attr) {
if (!attr.animating) {
return {};
}
// eslint-disable-next-line vars-on-top
var changes = {},
any = false,
timers = attr.timers,
targets = attr.targets,
now = Ext.draw.Animator.animationTime(),
name, timer, delta;
// If updated in the same frame, return.
if (attr.lastUpdate === now) {
return null;
}
for (name in timers) {
timer = timers[name];
if (!timer.start) {
timer.start = now;
delta = 0;
}
else {
delta = (now - timer.start) / timer.duration;
}
if (delta >= 1) {
changes[name] = targets[name];
delete targets[name];
if (timers[name].remove) {
changes.removeFromInstance = changes.removeFromInstance || {};
changes.removeFromInstance[name] = true;
}
delete timers[name];
}
else {
changes[name] = timer.serve(
timer.compute(timer.source, timer.target, timer.easing(delta), attr[name])
);
any = true;
}
}
attr.lastUpdate = now;
this.setAnimating(attr, any);
return changes;
},
pushDown: function(attr, changes) {
changes = this.callParent([attr.targets, changes]);
return this.setAttrs(attr, changes);
},
popUp: function(attr, changes) {
attr = attr.prototype;
changes = this.setAttrs(attr, changes);
if (this._upper) {
return this._upper.popUp(attr, changes);
}
else {
return Ext.apply(attr, changes);
}
},
/**
* @private
* This is called as an animated object in `Ext.draw.Animator`.
*/
step: function(frameTime) {
var me = this,
pool = me.animatingPool.slice(),
ln = pool.length,
i = 0,
attr, changes;
for (; i < ln; i++) {
attr = pool[i];
changes = me.updateAttributes(attr);
if (changes && me._upper) {
me._upper.popUp(attr, changes);
}
}
},
/**
* Stop all animations affected by this modifier.
*/
stop: function() {
var me = this,
pool = me.animatingPool,
i, ln;
this.step();
for (i = 0, ln = pool.length; i < ln; i++) {
pool[i].animating = false;
}
me.animatingPool.length = 0;
me.animating = 0;
Ext.draw.Animator.remove(me);
},
destroy: function() {
Ext.draw.Animator.remove(this);
this.callParent();
}
});