(function() {
var Y = YAHOO.util;
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
*/
/**
* The animation module provides allows effects to be added to HTMLElements.
* @module animation
* @requires yahoo, event, dom
*/
/**
*
* Base animation class that provides the interface for building animated effects.
* <p>Usage: var myAnim = new YAHOO.util.Anim(el, { width: { from: 10, to: 100 } }, 1, YAHOO.util.Easing.easeOut);</p>
* @class Anim
* @namespace YAHOO.util
* @requires YAHOO.util.AnimMgr
* @requires YAHOO.util.Easing
* @requires YAHOO.util.Dom
* @requires YAHOO.util.Event
* @requires YAHOO.util.CustomEvent
* @constructor
* @param {String | HTMLElement} el Reference to the element that will be animated
* @param {Object} attributes The attribute(s) to be animated.
* Each attribute is an object with at minimum a "to" or "by" member defined.
* Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
* All attribute names use camelCase.
* @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
* @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
*/
var Anim = function(el, attributes, duration, method) {
if (!el) {
YAHOO.log('element required to create Anim instance', 'error', 'Anim');
}
this.init(el, attributes, duration, method);
};
Anim.NAME = 'Anim';
Anim.prototype = {
/**
* Provides a readable name for the Anim instance.
* @method toString
* @return {String}
*/
toString: function() {
var el = this.getEl() || {};
var id = el.id || el.tagName;
return (this.constructor.NAME + ': ' + id);
},
patterns: { // cached for performance
noNegatives: /width|height|opacity|padding/i, // keep at zero or above
offsetAttribute: /^((width|height)|(top|left))$/, // use offsetValue as default
defaultUnit: /width|height|top$|bottom$|left$|right$/i, // use 'px' by default
offsetUnit: /\d+(em|%|en|ex|pt|in|cm|mm|pc)$/i // IE may return these, so convert these to offset
},
/**
* Returns the value computed by the animation's "method".
* @method doMethod
* @param {String} attr The name of the attribute.
* @param {Number} start The value this attribute should start from for this animation.
* @param {Number} end The value this attribute should end at for this animation.
* @return {Number} The Value to be applied to the attribute.
*/
doMethod: function(attr, start, end) {
return this.method(this.currentFrame, start, end - start, this.totalFrames);
},
/**
* Applies a value to an attribute.
* @method setAttribute
* @param {String} attr The name of the attribute.
* @param {Number} val The value to be applied to the attribute.
* @param {String} unit The unit ('px', '%', etc.) of the value.
*/
setAttribute: function(attr, val, unit) {
if ( this.patterns.noNegatives.test(attr) ) {
val = (val > 0) ? val : 0;
}
Y.Dom.setStyle(this.getEl(), attr, val + unit);
},
/**
* Returns current value of the attribute.
* @method getAttribute
* @param {String} attr The name of the attribute.
* @return {Number} val The current value of the attribute.
*/
getAttribute: function(attr) {
var el = this.getEl();
var val = Y.Dom.getStyle(el, attr);
if (val !== 'auto' && !this.patterns.offsetUnit.test(val)) {
return parseFloat(val);
}
var a = this.patterns.offsetAttribute.exec(attr) || [];
var pos = !!( a[3] ); // top or left
var box = !!( a[2] ); // width or height
// use offsets for width/height and abs pos top/left
if ( box || (Y.Dom.getStyle(el, 'position') == 'absolute' && pos) ) {
val = el['offset' + a[0].charAt(0).toUpperCase() + a[0].substr(1)];
} else { // default to zero for other 'auto'
val = 0;
}
return val;
},
/**
* Returns the unit to use when none is supplied.
* @method getDefaultUnit
* @param {attr} attr The name of the attribute.
* @return {String} The default unit to be used.
*/
getDefaultUnit: function(attr) {
if ( this.patterns.defaultUnit.test(attr) ) {
return 'px';
}
return '';
},
/**
* Sets the actual values to be used during the animation. Should only be needed for subclass use.
* @method setRuntimeAttribute
* @param {Object} attr The attribute object
* @private
*/
setRuntimeAttribute: function(attr) {
var start;
var end;
var attributes = this.attributes;
this.runtimeAttributes[attr] = {};
var isset = function(prop) {
return (typeof prop !== 'undefined');
};
if ( !isset(attributes[attr]['to']) && !isset(attributes[attr]['by']) ) {
return false; // note return; nothing to animate to
}
start = ( isset(attributes[attr]['from']) ) ? attributes[attr]['from'] : this.getAttribute(attr);
// To beats by, per SMIL 2.1 spec
if ( isset(attributes[attr]['to']) ) {
end = attributes[attr]['to'];
} else if ( isset(attributes[attr]['by']) ) {
if (start.constructor == Array) {
end = [];
for (var i = 0, len = start.length; i < len; ++i) {
end[i] = start[i] + attributes[attr]['by'][i] * 1; // times 1 to cast "by"
}
} else {
end = start + attributes[attr]['by'] * 1;
}
}
this.runtimeAttributes[attr].start = start;
this.runtimeAttributes[attr].end = end;
// set units if needed
this.runtimeAttributes[attr].unit = ( isset(attributes[attr].unit) ) ?
attributes[attr]['unit'] : this.getDefaultUnit(attr);
return true;
},
/**
* Constructor for Anim instance.
* @method init
* @param {String | HTMLElement} el Reference to the element that will be animated
* @param {Object} attributes The attribute(s) to be animated.
* Each attribute is an object with at minimum a "to" or "by" member defined.
* Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
* All attribute names use camelCase.
* @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
* @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
*/
init: function(el, attributes, duration, method) {
/**
* Whether or not the animation is running.
* @property isAnimated
* @private
* @type Boolean
*/
var isAnimated = false;
/**
* A Date object that is created when the animation begins.
* @property startTime
* @private
* @type Date
*/
var startTime = null;
/**
* The number of frames this animation was able to execute.
* @property actualFrames
* @private
* @type Int
*/
var actualFrames = 0;
/**
* The element to be animated.
* @property el
* @private
* @type HTMLElement
*/
el = Y.Dom.get(el);
/**
* The collection of attributes to be animated.
* Each attribute must have at least a "to" or "by" defined in order to animate.
* If "to" is supplied, the animation will end with the attribute at that value.
* If "by" is supplied, the animation will end at that value plus its starting value.
* If both are supplied, "to" is used, and "by" is ignored.
* Optional additional member include "from" (the value the attribute should start animating from, defaults to current value), and "unit" (the units to apply to the values).
* @property attributes
* @type Object
*/
this.attributes = attributes || {};
/**
* The length of the animation. Defaults to "1" (second).
* @property duration
* @type Number
*/
this.duration = !YAHOO.lang.isUndefined(duration) ? duration : 1;
/**
* The method that will provide values to the attribute(s) during the animation.
* Defaults to "YAHOO.util.Easing.easeNone".
* @property method
* @type Function
*/
this.method = method || Y.Easing.easeNone;
/**
* Whether or not the duration should be treated as seconds.
* Defaults to true.
* @property useSeconds
* @type Boolean
*/
this.useSeconds = true; // default to seconds
/**
* The location of the current animation on the timeline.
* In time-based animations, this is used by AnimMgr to ensure the animation finishes on time.
* @property currentFrame
* @type Int
*/
this.currentFrame = 0;
/**
* The total number of frames to be executed.
* In time-based animations, this is used by AnimMgr to ensure the animation finishes on time.
* @property totalFrames
* @type Int
*/
this.totalFrames = Y.AnimMgr.fps;
/**
* Changes the animated element
* @method setEl
*/
this.setEl = function(element) {
el = Y.Dom.get(element);
};
/**
* Returns a reference to the animated element.
* @method getEl
* @return {HTMLElement}
*/
this.getEl = function() { return el; };
/**
* Checks whether the element is currently animated.
* @method isAnimated
* @return {Boolean} current value of isAnimated.
*/
this.isAnimated = function() {
return isAnimated;
};
/**
* Returns the animation start time.
* @method getStartTime
* @return {Date} current value of startTime.
*/
this.getStartTime = function() {
return startTime;
};
this.runtimeAttributes = {};
var logger = {};
logger.log = function() {YAHOO.log.apply(window, arguments)};
logger.log('creating new instance of ' + this);
/**
* Starts the animation by registering it with the animation manager.
* @method animate
*/
this.animate = function() {
if ( this.isAnimated() ) {
return false;
}
this.currentFrame = 0;
this.totalFrames = ( this.useSeconds ) ? Math.ceil(Y.AnimMgr.fps * this.duration) : this.duration;
if (this.duration === 0 && this.useSeconds) { // jump to last frame if zero second duration
this.totalFrames = 1;
}
Y.AnimMgr.registerElement(this);
return true;
};
/**
* Stops the animation. Normally called by AnimMgr when animation completes.
* @method stop
* @param {Boolean} finish (optional) If true, animation will jump to final frame.
*/
this.stop = function(finish) {
if (!this.isAnimated()) { // nothing to stop
return false;
}
if (finish) {
this.currentFrame = this.totalFrames;
this._onTween.fire();
}
Y.AnimMgr.stop(this);
};
var onStart = function() {
this.onStart.fire();
this.runtimeAttributes = {};
for (var attr in this.attributes) {
this.setRuntimeAttribute(attr);
}
isAnimated = true;
actualFrames = 0;
startTime = new Date();
};
/**
* Feeds the starting and ending values for each animated attribute to doMethod once per frame, then applies the resulting value to the attribute(s).
* @private
*/
var onTween = function() {
var data = {
duration: new Date() - this.getStartTime(),
currentFrame: this.currentFrame
};
data.toString = function() {
return (
'duration: ' + data.duration +
', currentFrame: ' + data.currentFrame
);
};
this.onTween.fire(data);
var runtimeAttributes = this.runtimeAttributes;
for (var attr in runtimeAttributes) {
this.setAttribute(attr, this.doMethod(attr, runtimeAttributes[attr].start, runtimeAttributes[attr].end), runtimeAttributes[attr].unit);
}
actualFrames += 1;
};
var onComplete = function() {
var actual_duration = (new Date() - startTime) / 1000 ;
var data = {
duration: actual_duration,
frames: actualFrames,
fps: actualFrames / actual_duration
};
data.toString = function() {
return (
'duration: ' + data.duration +
', frames: ' + data.frames +
', fps: ' + data.fps
);
};
isAnimated = false;
actualFrames = 0;
this.onComplete.fire(data);
};
/**
* Custom event that fires after onStart, useful in subclassing
* @private
*/
this._onStart = new Y.CustomEvent('_start', this, true);
/**
* Custom event that fires when animation begins
* Listen via subscribe method (e.g. myAnim.onStart.subscribe(someFunction)
* @event onStart
*/
this.onStart = new Y.CustomEvent('start', this);
/**
* Custom event that fires between each frame
* Listen via subscribe method (e.g. myAnim.onTween.subscribe(someFunction)
* @event onTween
*/
this.onTween = new Y.CustomEvent('tween', this);
/**
* Custom event that fires after onTween
* @private
*/
this._onTween = new Y.CustomEvent('_tween', this, true);
/**
* Custom event that fires when animation ends
* Listen via subscribe method (e.g. myAnim.onComplete.subscribe(someFunction)
* @event onComplete
*/
this.onComplete = new Y.CustomEvent('complete', this);
/**
* Custom event that fires after onComplete
* @private
*/
this._onComplete = new Y.CustomEvent('_complete', this, true);
this._onStart.subscribe(onStart);
this._onTween.subscribe(onTween);
this._onComplete.subscribe(onComplete);
}
};
Y.Anim = Anim;
})();