/** * @ignore * animation framework for KISSY * @author yiminghe@gmail.com, lifesinger@gmail.com */ KISSY.add('anim/base', function (S, DOM, Event, Easing, AM, Fx, Q) { var UA = S.UA, camelCase = DOM._camelCase, NodeType = DOM.NodeType, specialVals = ['hide', 'show', 'toggle'], // shorthand css properties SHORT_HANDS = { // http://www.w3.org/Style/CSS/Tracker/issues/9 // http://snook.ca/archives/html_and_css/background-position-x-y // backgroundPositionX backgroundPositionY does not support background: [ 'backgroundPosition' ], border: [ 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth', // 'borderSpacing', 组合属性? 'borderTopWidth' ], 'borderBottom': ['borderBottomWidth'], 'borderLeft': ['borderLeftWidth'], borderTop: ['borderTopWidth'], borderRight: ['borderRightWidth'], font: [ 'fontSize', 'fontWeight' ], margin: [ 'marginBottom', 'marginLeft', 'marginRight', 'marginTop' ], padding: [ 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop' ] }, defaultConfig = { duration: 1, easing: 'easeNone' }, NUMBER_REG = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i; Anim.SHORT_HANDS = SHORT_HANDS; /** * @class KISSY.Anim * A class for constructing animation instances. * @mixins KISSY.Event.Target * @cfg {HTMLElement|window} el html dom node or window * (window can only animate scrollTop/scrollLeft) * @cfg {Object} props end css style value. * @cfg {Number} [duration=1] duration(second) or anim config * @cfg {String|Function} [easing='easeNone'] easing fn or string * @cfg {Function} [complete] callback function when this animation is complete * @cfg {String|Boolean} [queue] current animation's queue, if false then no queue */ function Anim(el, props, duration, easing, complete) { if (el.el) { var realEl = el.el; props = el.props; delete el.el; delete el.props; return new Anim(realEl, props, el); } var self = this, config; // ignore non-exist element if (!(el = DOM.get(el))) { return; } // factory or constructor if (!(self instanceof Anim)) { return new Anim(el, props, duration, easing, complete); } // the transition properties if (typeof props == 'string') { props = S.unparam(String(props), ';', ':'); } else { // clone to prevent collision within multiple instance props = S.clone(props); } // camel case uniformity S.each(props, function (v, prop) { var camelProp = S.trim(camelCase(prop)); if (!camelProp) { delete props[prop]; } else if (prop != camelProp) { props[camelProp] = props[prop]; delete props[prop]; } }); // animation config if (S.isPlainObject(duration)) { config = S.clone(duration); } else { config = { duration: parseFloat(duration) || undefined, easing: easing, complete: complete }; } config = S.merge(defaultConfig, config); config.el = el; config.props = props; /** * config object of current anim instance * @type {Object} */ self.config = config; self._duration = config.duration * 1000; // domEl deprecated! self['domEl'] = el; // self.props = props; // 实例属性 self._backupProps = {}; self._fxs = {}; // register complete self.on('complete', onComplete); } function onComplete(e) { var self = this, _backupProps, complete, config = self.config; // only recover after complete anim if (!S.isEmptyObject(_backupProps = self._backupProps)) { DOM.css(config.el, _backupProps); } if (complete = config.complete) { complete.call(self, e); } } function runInternal() { var self = this, config = self.config, _backupProps = self._backupProps, el = config.el, elStyle, hidden, val, prop, specialEasing = (config['specialEasing'] || {}), fxs = self._fxs, props = config.props; // 进入该函数即代表执行(q[0] 已经是 ...) saveRunning(self); if (self.fire('beforeStart') === false) { // no need to invoke complete self.stop(0); return; } if (el.nodeType == NodeType.ELEMENT_NODE) { hidden = (DOM.css(el, 'display') === 'none'); for (prop in props) { val = props[prop]; // 直接结束 if (val == 'hide' && hidden || val == 'show' && !hidden) { // need to invoke complete self.stop(1); return; } } } // 放在前面,设置 overflow hidden,否则后面 ie6 取 width/height 初值导致错误 // <div style='width:0'><div style='width:100px'></div></div> if (el.nodeType == NodeType.ELEMENT_NODE && (props.width || props.height)) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and // overflowY are set to the same value elStyle = el.style; S.mix(_backupProps, { overflow: elStyle.overflow, 'overflow-x': elStyle.overflowX, 'overflow-y': elStyle.overflowY }); elStyle.overflow = 'hidden'; // inline element should has layout/inline-block if (DOM.css(el, 'display') === 'inline' && DOM.css(el, 'float') === 'none') { if (UA['ie']) { elStyle.zoom = 1; } else { elStyle.display = 'inline-block'; } } } // 分离 easing S.each(props, function (val, prop) { var easing; if (S.isArray(val)) { easing = specialEasing[prop] = val[1]; props[prop] = val[0]; } else { easing = specialEasing[prop] = (specialEasing[prop] || config.easing); } if (typeof easing == 'string') { easing = specialEasing[prop] = Easing[easing]; } specialEasing[prop] = easing || Easing['easeNone']; }); // 扩展分属性 S.each(SHORT_HANDS, function (shortHands, p) { var origin, val; if (val = props[p]) { origin = {}; S.each(shortHands, function (sh) { // 得到原始分属性之前值 origin[sh] = DOM.css(el, sh); specialEasing[sh] = specialEasing[p]; }); DOM.css(el, p, val); S.each(origin, function (val, sh) { // 得到期待的分属性最后值 props[sh] = DOM.css(el, sh); // 还原 DOM.css(el, sh, val); }); // 删除复合属性 delete props[p]; } }); // 取得单位,并对单个属性构建 Fx 对象 for (prop in props) { val = S.trim(props[prop]); var to, from, propCfg = { prop: prop, anim: self, easing: specialEasing[prop] }, fx = Fx.getFx(propCfg); // hide/show/toggle : special treat! if (S.inArray(val, specialVals)) { // backup original inline css value _backupProps[prop] = DOM.style(el, prop); if (val == 'toggle') { val = hidden ? 'show' : 'hide'; } if (val == 'hide') { to = 0; from = fx.cur(); // 执行完后隐藏 _backupProps.display = 'none'; } else { from = 0; to = fx.cur(); // prevent flash of content DOM.css(el, prop, from); DOM.show(el); } val = to; } else { to = val; from = fx.cur(); } val += ''; var unit = '', parts = val.match(NUMBER_REG); if (parts) { to = parseFloat(parts[2]); unit = parts[3]; // 有单位但单位不是 px if (unit && unit !== 'px') { DOM.css(el, prop, val); from = (to / fx.cur()) * from; DOM.css(el, prop, from + unit); } // 相对 if (parts[1]) { to = ( (parts[ 1 ] === '-=' ? -1 : 1) * to ) + from; } } propCfg.from = from; propCfg.to = to; propCfg.unit = unit; fx.load(propCfg); fxs[prop] = fx; } self._startTime = S.now(); AM.start(self); } Anim.prototype = { constructor: Anim, /** * whether this animation is running * @return {Boolean} */ isRunning: function () { return isRunning(this); }, /** * whether this animation is paused * @return {Boolean} */ isPaused: function () { return isPaused(this); }, /** * pause current anim * @chainable */ pause: function () { var self = this; if (self.isRunning()) { self._pauseDiff = S.now() - self._startTime; AM.stop(self); removeRunning(self); savePaused(self); } return self; }, /** * resume current anim * @chainable */ resume: function () { var self = this; if (self.isPaused()) { self._startTime = S.now() - self._pauseDiff; removePaused(self); saveRunning(self); AM.start(self); } return self; }, /** * @ignore */ _runInternal: runInternal, /** * start this animation * @chainable */ run: function () { var self = this, queueName = self.config.queue; if (queueName === false) { runInternal.call(self); } else { // 当前动画对象加入队列 Q.queue(self); } return self; }, /** * @ignore */ _frame: function () { var self = this, prop, config = self.config, end = 1, c, fx, fxs = self._fxs; for (prop in fxs) { // 当前属性没有结束 if (!((fx = fxs[prop]).finished)) { // 非短路 if (config.frame) { c = config.frame(fx); } // 结束 if (c == 1 || // 不执行自带 c == 0) { fx.finished = c; end &= c; } else { end &= fx.frame(); // 最后通知下 if (end && config.frame) { config.frame(fx); } } } } if ((self.fire('step') === false) || end) { // complete 事件只在动画到达最后一帧时才触发 self.stop(end); } }, /** * stop this animation * @param {Boolean} [finish] whether jump to the last position of this animation * @chainable */ stop: function (finish) { var self = this, config = self.config, queueName = config.queue, prop, fx, fxs = self._fxs; // already stopped if (!self.isRunning()) { // 从自己的队列中移除 if (queueName !== false) { Q.remove(self); } return self; } if (finish) { for (prop in fxs) { // 当前属性没有结束 if (!((fx = fxs[prop]).finished)) { // 非短路 if (config.frame) { config.frame(fx, 1); } else { fx.frame(1); } } } self.fire('complete'); } AM.stop(self); removeRunning(self); if (queueName !== false) { // notify next anim to run in the same queue Q.dequeue(self); } return self; } }; S.augment(Anim, Event.Target); var runningKey = S.guid('ks-anim-unqueued-' + S.now() + '-'); function saveRunning(anim) { var el = anim.config.el, allRunning = DOM.data(el, runningKey); if (!allRunning) { DOM.data(el, runningKey, allRunning = {}); } allRunning[S.stamp(anim)] = anim; } function removeRunning(anim) { var el = anim.config.el, allRunning = DOM.data(el, runningKey); if (allRunning) { delete allRunning[S.stamp(anim)]; if (S.isEmptyObject(allRunning)) { DOM.removeData(el, runningKey); } } } function isRunning(anim) { var el = anim.config.el, allRunning = DOM.data(el, runningKey); if (allRunning) { return !!allRunning[S.stamp(anim)]; } return 0; } var pausedKey = S.guid('ks-anim-paused-' + S.now() + '-'); function savePaused(anim) { var el = anim.config.el, paused = DOM.data(el, pausedKey); if (!paused) { DOM.data(el, pausedKey, paused = {}); } paused[S.stamp(anim)] = anim; } function removePaused(anim) { var el = anim.config.el, paused = DOM.data(el, pausedKey); if (paused) { delete paused[S.stamp(anim)]; if (S.isEmptyObject(paused)) { DOM.removeData(el, pausedKey); } } } function isPaused(anim) { var el = anim.config.el, paused = DOM.data(el, pausedKey); if (paused) { return !!paused[S.stamp(anim)]; } return 0; } /** * stop all the anims currently running * @static * @param {HTMLElement} el element which anim belongs to * @param {Boolean} end whether jump to last position * @param {Boolean} clearQueue whether clean current queue * @param {String|Boolean} queueName current queue's name to be cleared */ Anim.stop = function (el, end, clearQueue, queueName) { if ( // default queue queueName === null || // name of specified queue typeof queueName == 'string' || // anims not belong to any queue queueName === false ) { return stopQueue.apply(undefined, arguments); } // first stop first anim in queues if (clearQueue) { Q.removeQueues(el); } var allRunning = DOM.data(el, runningKey), // can not stop in for/in , stop will modified allRunning too anims = S.merge(allRunning); S.each(anims, function (anim) { anim.stop(end); }); }; /** * pause all the anims currently running * @param {HTMLElement} el element which anim belongs to * @param {String|Boolean} queueName current queue's name to be cleared * @method pause * @member KISSY.Anim * @static */ /** * resume all the anims currently running * @param {HTMLElement} el element which anim belongs to * @param {String|Boolean} queueName current queue's name to be cleared * @method resume * @member KISSY.Anim * @static */ S.each(['pause', 'resume'], function (action) { Anim[action] = function (el, queueName) { if ( // default queue queueName === null || // name of specified queue typeof queueName == 'string' || // anims not belong to any queue queueName === false ) { return pauseResumeQueue(el, queueName, action); } pauseResumeQueue(el, undefined, action); }; }); function pauseResumeQueue(el, queueName, action) { var allAnims = DOM.data(el, action == 'resume' ? pausedKey : runningKey), // can not stop in for/in , stop will modified allRunning too anims = S.merge(allAnims); S.each(anims, function (anim) { if (queueName === undefined || anim.config.queue == queueName) { anim[action](); } }); } /** * * @param el element which anim belongs to * @param queueName queue'name if set to false only remove * @param end * @param clearQueue * @ignore */ function stopQueue(el, end, clearQueue, queueName) { if (clearQueue && queueName !== false) { Q.removeQueue(el, queueName); } var allRunning = DOM.data(el, runningKey), anims = S.merge(allRunning); S.each(anims, function (anim) { if (anim.config.queue == queueName) { anim.stop(end); } }); } /** * whether el is running anim * @param {HTMLElement} el * @return {Boolean} * @static */ Anim.isRunning = function (el) { var allRunning = DOM.data(el, runningKey); return allRunning && !S.isEmptyObject(allRunning); }; /** * whether el has paused anim * @param {HTMLElement} el * @return {Boolean} * @static */ Anim.isPaused = function (el) { var paused = DOM.data(el, pausedKey); return paused && !S.isEmptyObject(paused); }; /** * @ignore */ Anim.Q = Q; if (SHORT_HANDS) { } return Anim; }, { requires: ['dom', 'event', './easing', './manager', './fx', './queue'] }); /* 2011-11 - 重构,抛弃 emile,优化性能,只对需要的属性进行动画 - 添加 stop/stopQueue/isRunning,支持队列管理 2011-04 - 借鉴 yui3 ,中央定时器,否则 ie6 内存泄露? - 支持配置 scrollTop/scrollLeft TODO: - 效率需要提升,当使用 nativeSupport 时仍做了过多动作 - opera nativeSupport 存在 bug ,浏览器自身 bug ? - 实现 jQuery Effects 的 queue / specialEasing / += / 等特性 NOTES: - 与 emile 相比,增加了 borderStyle, 使得 border: 5px solid #ccc 能从无到有,正确显示 - api 借鉴了 YUI, jQuery 以及 http://www.w3.org/TR/css3-transitions/ - 代码实现了借鉴了 Emile.js: http://github.com/madrobby/emile * */