/** * The Animation Utility provides an API for creating advanced transitions. * @module anim */ /** * Provides the base Anim class, for animating numeric properties. * * @module anim * @submodule anim-base */ /** * A class for constructing animation instances. * @class Anim * @for Anim * @constructor * @extends Base */ var RUNNING = 'running', START_TIME = 'startTime', ELAPSED_TIME = 'elapsedTime', /** * @for Anim * @event start * @description fires when an animation begins. * @param {Event} ev The start event. * @type Event.Custom */ START = 'start', /** * @event tween * @description fires every frame of the animation. * @param {Event} ev The tween event. * @type Event.Custom */ TWEEN = 'tween', /** * @event end * @description fires after the animation completes. * @param {Event} ev The end event. * @type Event.Custom */ END = 'end', NODE = 'node', PAUSED = 'paused', REVERSE = 'reverse', // TODO: cleanup ITERATION_COUNT = 'iterationCount', NUM = Number; var _running = {}, _timer; Y.Anim = function() { Y.Anim.superclass.constructor.apply(this, arguments); Y.Anim._instances[Y.stamp(this)] = this; }; Y.Anim.NAME = 'anim'; Y.Anim._instances = {}; /** * Regex of properties that should use the default unit. * * @property RE_DEFAULT_UNIT * @static */ Y.Anim.RE_DEFAULT_UNIT = /^width|height|top|right|bottom|left|margin.*|padding.*|border.*$/i; /** * The default unit to use with properties that pass the RE_DEFAULT_UNIT test. * * @property DEFAULT_UNIT * @static */ Y.Anim.DEFAULT_UNIT = 'px'; Y.Anim.DEFAULT_EASING = function (t, b, c, d) { return c * t / d + b; // linear easing }; /** * Time in milliseconds passed to setInterval for frame processing * * @property intervalTime * @default 20 * @static */ Y.Anim._intervalTime = 20; /** * Bucket for custom getters and setters * * @property behaviors * @static */ Y.Anim.behaviors = { left: { get: function(anim, attr) { return anim._getOffset(attr); } } }; Y.Anim.behaviors.top = Y.Anim.behaviors.left; /** * The default setter to use when setting object properties. * * @property DEFAULT_SETTER * @static */ Y.Anim.DEFAULT_SETTER = function(anim, att, from, to, elapsed, duration, fn, unit) { var node = anim._node, domNode = node._node, val = fn(elapsed, NUM(from), NUM(to) - NUM(from), duration); if (domNode) { if ('style' in domNode && (att in domNode.style || att in Y.DOM.CUSTOM_STYLES)) { unit = unit || ''; node.setStyle(att, val + unit); } else if ('attributes' in domNode && att in domNode.attributes) { node.setAttribute(att, val); } else if (att in domNode) { domNode[att] = val; } } else if (node.set) { node.set(att, val); } else if (att in node) { node[att] = val; } }; /** * The default getter to use when getting object properties. * * @property DEFAULT_GETTER * @static */ Y.Anim.DEFAULT_GETTER = function(anim, att) { var node = anim._node, domNode = node._node, val = ''; if (domNode) { if ('style' in domNode && (att in domNode.style || att in Y.DOM.CUSTOM_STYLES)) { val = node.getComputedStyle(att); } else if ('attributes' in domNode && att in domNode.attributes) { val = node.getAttribute(att); } else if (att in domNode) { val = domNode[att]; } } else if (node.get) { val = node.get(att); } else if (att in node) { val = node[att]; } return val; }; Y.Anim.ATTRS = { /** * The object to be animated. * @attribute node * @type Node */ node: { setter: function(node) { if (node) { if (typeof node === 'string' || node.nodeType) { node = Y.one(node); } } this._node = node; if (!node) { Y.log(node + ' is not a valid node', 'warn', 'Anim'); } return node; } }, /** * The length of the animation. Defaults to "1" (second). * @attribute duration * @type NUM */ duration: { value: 1 }, /** * The method that will provide values to the attribute(s) during the animation. * Defaults to "Easing.easeNone". * @attribute easing * @type Function */ easing: { value: Y.Anim.DEFAULT_EASING, setter: function(val) { if (typeof val === 'string' && Y.Easing) { return Y.Easing[val]; } } }, /** * The starting values for the animated properties. * * Fields may be strings, numbers, or functions. * If a function is used, the return value becomes the from value. * If no from value is specified, the DEFAULT_GETTER will be used. * Supports any unit, provided it matches the "to" (or default) * unit (e.g. `{width: '10em', color: 'rgb(0, 0, 0)', borderColor: '#ccc'}`). * * If using the default ('px' for length-based units), the unit may be omitted * (e.g. `{width: 100}, borderColor: 'ccc'}`, which defaults to pixels * and hex, respectively). * * @attribute from * @type Object */ from: {}, /** * The ending values for the animated properties. * * Fields may be strings, numbers, or functions. * Supports any unit, provided it matches the "from" (or default) * unit (e.g. `{width: '50%', color: 'red', borderColor: '#ccc'}`). * * If using the default ('px' for length-based units), the unit may be omitted * (e.g. `{width: 100, borderColor: 'ccc'}`, which defaults to pixels * and hex, respectively). * * @attribute to * @type Object */ to: {}, /** * Date stamp for the first frame of the animation. * @attribute startTime * @type Int * @default 0 * @readOnly */ startTime: { value: 0, readOnly: true }, /** * Current time the animation has been running. * @attribute elapsedTime * @type Int * @default 0 * @readOnly */ elapsedTime: { value: 0, readOnly: true }, /** * Whether or not the animation is currently running. * @attribute running * @type Boolean * @default false * @readOnly */ running: { getter: function() { return !!_running[Y.stamp(this)]; }, value: false, readOnly: true }, /** * The number of times the animation should run * @attribute iterations * @type Int * @default 1 */ iterations: { value: 1 }, /** * The number of iterations that have occurred. * Resets when an animation ends (reaches iteration count or stop() called). * @attribute iterationCount * @type Int * @default 0 * @readOnly */ iterationCount: { value: 0, readOnly: true }, /** * How iterations of the animation should behave. * Possible values are "normal" and "alternate". * Normal will repeat the animation, alternate will reverse on every other pass. * * @attribute direction * @type String * @default "normal" */ direction: { value: 'normal' // | alternate (fwd on odd, rev on even per spec) }, /** * Whether or not the animation is currently paused. * @attribute paused * @type Boolean * @default false * @readOnly */ paused: { readOnly: true, value: false }, /** * If true, the `from` and `to` attributes are swapped, * and the animation is then run starting from `from`. * @attribute reverse * @type Boolean * @default false */ reverse: { value: false } }; /** * Runs all animation instances. * @method run * @static */ Y.Anim.run = function() { var instances = Y.Anim._instances, i; for (i in instances) { if (instances[i].run) { instances[i].run(); } } }; /** * Pauses all animation instances. * @method pause * @static */ Y.Anim.pause = function() { for (var i in _running) { // stop timer if nothing running if (_running[i].pause) { _running[i].pause(); } } Y.Anim._stopTimer(); }; /** * Stops all animation instances. * @method stop * @static */ Y.Anim.stop = function() { for (var i in _running) { // stop timer if nothing running if (_running[i].stop) { _running[i].stop(); } } Y.Anim._stopTimer(); }; Y.Anim._startTimer = function() { if (!_timer) { _timer = setInterval(Y.Anim._runFrame, Y.Anim._intervalTime); } }; Y.Anim._stopTimer = function() { clearInterval(_timer); _timer = 0; }; /** * Called per Interval to handle each animation frame. * @method _runFrame * @private * @static */ Y.Anim._runFrame = function() { var done = true, anim; for (anim in _running) { if (_running[anim]._runFrame) { done = false; _running[anim]._runFrame(); } } if (done) { Y.Anim._stopTimer(); } }; Y.Anim.RE_UNITS = /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/; var proto = { /** * Starts or resumes an animation. * @method run * @chainable */ run: function() { if (this.get(PAUSED)) { this._resume(); } else if (!this.get(RUNNING)) { this._start(); } return this; }, /** * Pauses the animation and * freezes it in its current state and time. * Calling run() will continue where it left off. * @method pause * @chainable */ pause: function() { if (this.get(RUNNING)) { this._pause(); } return this; }, /** * Stops the animation and resets its time. * @method stop * @param {Boolean} finish If true, the animation will move to the last frame * @chainable */ stop: function(finish) { if (this.get(RUNNING) || this.get(PAUSED)) { this._end(finish); } return this; }, _added: false, _start: function() { this._set(START_TIME, new Date() - this.get(ELAPSED_TIME)); this._actualFrames = 0; if (!this.get(PAUSED)) { this._initAnimAttr(); } _running[Y.stamp(this)] = this; Y.Anim._startTimer(); this.fire(START); }, _pause: function() { this._set(START_TIME, null); this._set(PAUSED, true); delete _running[Y.stamp(this)]; /** * @event pause * @description fires when an animation is paused. * @param {Event} ev The pause event. * @type Event.Custom */ this.fire('pause'); }, _resume: function() { this._set(PAUSED, false); _running[Y.stamp(this)] = this; this._set(START_TIME, new Date() - this.get(ELAPSED_TIME)); Y.Anim._startTimer(); /** * @event resume * @description fires when an animation is resumed (run from pause). * @param {Event} ev The pause event. * @type Event.Custom */ this.fire('resume'); }, _end: function(finish) { var duration = this.get('duration') * 1000; if (finish) { // jump to last frame this._runAttrs(duration, duration, this.get(REVERSE)); } this._set(START_TIME, null); this._set(ELAPSED_TIME, 0); this._set(PAUSED, false); delete _running[Y.stamp(this)]; this.fire(END, {elapsed: this.get(ELAPSED_TIME)}); }, _runFrame: function() { var d = this._runtimeAttr.duration, t = new Date() - this.get(START_TIME), reverse = this.get(REVERSE), done = (t >= d); this._runAttrs(t, d, reverse); this._actualFrames += 1; this._set(ELAPSED_TIME, t); this.fire(TWEEN); if (done) { this._lastFrame(); } }, _runAttrs: function(t, d, reverse) { var attr = this._runtimeAttr, customAttr = Y.Anim.behaviors, easing = attr.easing, lastFrame = d, done = false, attribute, setter, i; if (t >= d) { done = true; } if (reverse) { t = d - t; lastFrame = 0; } for (i in attr) { if (attr[i].to) { attribute = attr[i]; setter = (i in customAttr && 'set' in customAttr[i]) ? customAttr[i].set : Y.Anim.DEFAULT_SETTER; if (!done) { setter(this, i, attribute.from, attribute.to, t, d, easing, attribute.unit); } else { setter(this, i, attribute.from, attribute.to, lastFrame, d, easing, attribute.unit); } } } }, _lastFrame: function() { var iter = this.get('iterations'), iterCount = this.get(ITERATION_COUNT); iterCount += 1; if (iter === 'infinite' || iterCount < iter) { if (this.get('direction') === 'alternate') { this.set(REVERSE, !this.get(REVERSE)); // flip it } /** * @event iteration * @description fires when an animation begins an iteration. * @param {Event} ev The iteration event. * @type Event.Custom */ this.fire('iteration'); } else { iterCount = 0; this._end(); } this._set(START_TIME, new Date()); this._set(ITERATION_COUNT, iterCount); }, _initAnimAttr: function() { var from = this.get('from') || {}, to = this.get('to') || {}, attr = { duration: this.get('duration') * 1000, easing: this.get('easing') }, customAttr = Y.Anim.behaviors, node = this.get(NODE), // implicit attr init unit, begin, end; Y.each(to, function(val, name) { if (typeof val === 'function') { val = val.call(this, node); } begin = from[name]; if (begin === undefined) { begin = (name in customAttr && 'get' in customAttr[name]) ? customAttr[name].get(this, name) : Y.Anim.DEFAULT_GETTER(this, name); } else if (typeof begin === 'function') { begin = begin.call(this, node); } var mFrom = Y.Anim.RE_UNITS.exec(begin), mTo = Y.Anim.RE_UNITS.exec(val); begin = mFrom ? mFrom[1] : begin; end = mTo ? mTo[1] : val; unit = mTo ? mTo[2] : mFrom ? mFrom[2] : ''; // one might be zero TODO: mixed units if (!unit && Y.Anim.RE_DEFAULT_UNIT.test(name)) { unit = Y.Anim.DEFAULT_UNIT; } if (!begin || !end) { Y.error('invalid "from" or "to" for "' + name + '"', 'Anim'); return; } attr[name] = { from: Y.Lang.isObject(begin) ? Y.clone(begin) : begin, to: end, unit: unit }; }, this); this._runtimeAttr = attr; }, // TODO: move to computedStyle? (browsers dont agree on default computed offsets) _getOffset: function(attr) { var node = this._node, val = node.getComputedStyle(attr), get = (attr === 'left') ? 'getX': 'getY', set = (attr === 'left') ? 'setX': 'setY', position; if (val === 'auto') { position = node.getStyle('position'); if (position === 'absolute' || position === 'fixed') { val = node[get](); node[set](val); } else { val = 0; } } return val; }, destructor: function() { delete Y.Anim._instances[Y.stamp(this)]; } }; Y.extend(Y.Anim, Y.Base, proto);