1 /** 2 * @fileOverview animation framework for KISSY 3 * @author yiminghe@gmail.com,lifesinger@gmail.com 4 */ 5 KISSY.add('anim/base', function (S, DOM, Event, Easing, UA, AM, Fx, Q) { 6 7 var camelCase = DOM._camelCase, 8 specialVals = ["hide", "show", "toggle"], 9 // shorthand css properties 10 SHORT_HANDS = { 11 // http://www.w3.org/Style/CSS/Tracker/issues/9 12 // http://snook.ca/archives/html_and_css/background-position-x-y 13 // backgroundPositionX backgroundPositionY does not support 14 background:[ 15 "backgroundPosition" 16 ], 17 border:[ 18 "borderBottomWidth", 19 "borderLeftWidth", 20 'borderRightWidth', 21 // 'borderSpacing', 组合属性? 22 'borderTopWidth' 23 ], 24 borderBottom:["borderBottomWidth"], 25 borderLeft:["borderLeftWidth"], 26 borderTop:["borderTopWidth"], 27 borderRight:["borderRightWidth"], 28 font:[ 29 'fontSize', 30 'fontWeight' 31 ], 32 margin:[ 33 'marginBottom', 34 'marginLeft', 35 'marginRight', 36 'marginTop' 37 ], 38 padding:[ 39 'paddingBottom', 40 'paddingLeft', 41 'paddingRight', 42 'paddingTop' 43 ] 44 }, 45 defaultConfig = { 46 duration:1, 47 easing:'easeNone' 48 }, 49 rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i; 50 51 Anim.SHORT_HANDS = SHORT_HANDS; 52 53 54 /** 55 * @class A class for constructing animation instances. 56 * @param {HTMLElement|window} elem Html dom node or window 57 * (window can only animate scrollTop/scrollLeft) 58 * @param {Object} props style map 59 * @param {Number|Object} [duration] duration(s) or anim config 60 * @param {String|Function} [duration.easing] easing fn or string 61 * @param {Function} [duration.complete] callback function when this animation is complete 62 * @param {Number} [duration.duration] duration(s) 63 * @param {String|Boolean} [duration.queue] current animation's queue, if false then no queue 64 * @param {Function|String} [easing] easing fn or string 65 * @param {Function} [callback] callback function when this animation is complete 66 * @extends Event.Target 67 * @name Anim 68 * 69 */ 70 function Anim(elem, props, duration, easing, callback) { 71 var self = this, config; 72 73 // ignore non-exist element 74 if (!(elem = DOM.get(elem))) { 75 return; 76 } 77 78 // factory or constructor 79 if (!(self instanceof Anim)) { 80 return new Anim(elem, props, duration, easing, callback); 81 } 82 83 /** 84 * the transition properties 85 */ 86 if (S.isString(props)) { 87 props = S.unparam(String(props), ";", ":"); 88 } else { 89 // clone to prevent collision within multiple instance 90 props = S.clone(props); 91 } 92 93 /** 94 * 驼峰属性名 95 */ 96 for (var prop in props) { 97 var camelProp = camelCase(S.trim(prop)); 98 if (prop != camelProp) { 99 props[camelProp] = props[prop]; 100 delete props[prop]; 101 } 102 } 103 104 /** 105 * animation config 106 */ 107 if (S.isPlainObject(duration)) { 108 config = S.clone(duration); 109 } else { 110 config = { 111 duration:parseFloat(duration) || undefined, 112 easing:easing, 113 complete:callback 114 }; 115 } 116 117 config = S.merge(defaultConfig, config); 118 self.config = config; 119 config.duration *= 1000; 120 121 // domEl deprecated! 122 self.elem = self['domEl'] = elem; 123 self.props = props; 124 125 // 实例属性 126 self._backupProps = {}; 127 self._fxs = {}; 128 129 // register callback 130 self.on("complete", onComplete); 131 } 132 133 134 function onComplete(e) { 135 var self = this, 136 _backupProps, 137 config = self.config; 138 139 // only recover after complete anim 140 if (!S.isEmptyObject(_backupProps = self._backupProps)) { 141 DOM.css(self.elem, _backupProps); 142 } 143 144 if (config.complete) { 145 config.complete.call(self, e); 146 } 147 } 148 149 function runInternal() { 150 var self = this, 151 config = self.config, 152 _backupProps = self._backupProps, 153 elem = self.elem, 154 elemStyle, 155 hidden, 156 val, 157 prop, 158 specialEasing = (config['specialEasing'] || {}), 159 fxs = self._fxs, 160 props = self.props; 161 162 // 进入该函数即代表执行(q[0] 已经是 ...) 163 saveRunning(self); 164 165 if (self.fire("start") === false) { 166 // no need to invoke complete 167 self.stop(0); 168 return; 169 } 170 171 if (elem.nodeType == DOM.ELEMENT_NODE) { 172 hidden = (DOM.css(elem, "display") === "none"); 173 for (prop in props) { 174 val = props[prop]; 175 // 直接结束 176 if (val == "hide" && hidden || val == 'show' && !hidden) { 177 // need to invoke complete 178 self.stop(1); 179 return; 180 } 181 } 182 } 183 184 // 放在前面,设置 overflow hidden,否则后面 ie6 取 width/height 初值导致错误 185 // <div style='width:0'><div style='width:100px'></div></div> 186 if (elem.nodeType == DOM.ELEMENT_NODE && 187 (props.width || props.height)) { 188 // Make sure that nothing sneaks out 189 // Record all 3 overflow attributes because IE does not 190 // change the overflow attribute when overflowX and 191 // overflowY are set to the same value 192 elemStyle = elem.style; 193 S.mix(_backupProps, { 194 overflow:elemStyle.overflow, 195 overflow-x:elemStyle.overflowX, 196 overflow-y:elemStyle.overflowY 197 }); 198 elemStyle.overflow = "hidden"; 199 // inline element should has layout/inline-block 200 if (DOM.css(elem, "display") === "inline" && 201 DOM.css(elem, "float") === "none") { 202 if (UA['ie']) { 203 elemStyle.zoom = 1; 204 } else { 205 elemStyle.display = "inline-block"; 206 } 207 } 208 } 209 210 // 分离 easing 211 S.each(props, function (val, prop) { 212 if (!props.hasOwnProperty(prop)) { 213 return; 214 } 215 var easing; 216 if (S.isArray(val)) { 217 easing = specialEasing[prop] = val[1]; 218 props[prop] = val[0]; 219 } else { 220 easing = specialEasing[prop] = (specialEasing[prop] || config.easing); 221 } 222 if (S.isString(easing)) { 223 easing = specialEasing[prop] = Easing[easing]; 224 } 225 specialEasing[prop] = easing || Easing['easeNone']; 226 }); 227 228 229 // 扩展分属性 230 S.each(SHORT_HANDS, function (shortHands, p) { 231 var sh, 232 origin, 233 val; 234 if (val = props[p]) { 235 origin = {}; 236 S.each(shortHands, function (sh) { 237 // 得到原始分属性之前值 238 origin[sh] = DOM.css(elem, sh); 239 specialEasing[sh] = specialEasing[p]; 240 }); 241 DOM.css(elem, p, val); 242 for (sh in origin) { 243 // 得到期待的分属性最后值 244 props[sh] = DOM.css(elem, sh); 245 // 还原 246 DOM.css(elem, sh, origin[sh]); 247 } 248 // 删除复合属性 249 delete props[p]; 250 } 251 }); 252 253 // 取得单位,并对单个属性构建 Fx 对象 254 for (prop in props) { 255 256 if (!props.hasOwnProperty(prop)) { 257 continue; 258 } 259 260 val = S.trim(props[prop]); 261 262 var to, 263 from, 264 propCfg = { 265 prop:prop, 266 anim:self, 267 easing:specialEasing[prop] 268 }, 269 fx = Fx.getFx(propCfg); 270 271 // hide/show/toggle : special treat! 272 if (S.inArray(val, specialVals)) { 273 // backup original value 274 _backupProps[prop] = DOM.style(elem, prop); 275 if (val == "toggle") { 276 val = hidden ? "show" : "hide"; 277 } 278 if (val == "hide") { 279 to = 0; 280 from = fx.cur(); 281 // 执行完后隐藏 282 _backupProps.display = 'none'; 283 } else { 284 from = 0; 285 to = fx.cur(); 286 // prevent flash of content 287 DOM.css(elem, prop, from); 288 DOM.show(elem); 289 } 290 val = to; 291 } else { 292 to = val; 293 from = fx.cur(); 294 } 295 296 val += ""; 297 298 var unit = "", 299 parts = val.match(rfxnum); 300 301 if (parts) { 302 to = parseFloat(parts[2]); 303 unit = parts[3]; 304 305 // 有单位但单位不是 px 306 if (unit && unit !== "px") { 307 DOM.css(elem, prop, val); 308 from = (to / fx.cur()) * from; 309 DOM.css(elem, prop, from + unit); 310 } 311 312 // 相对 313 if (parts[1]) { 314 to = ( (parts[ 1 ] === "-=" ? -1 : 1) * to ) + from; 315 } 316 } 317 318 propCfg.from = from; 319 propCfg.to = to; 320 propCfg.unit = unit; 321 fx.load(propCfg); 322 fxs[prop] = fx; 323 } 324 325 self._startTime = S.now(); 326 327 AM.start(self); 328 } 329 330 331 S.augment(Anim, Event.Target, 332 /** 333 * @lends Anim.prototype 334 */ 335 { 336 337 /** 338 * @return {Boolean} whether this animation is running 339 */ 340 isRunning:function () { 341 return isRunning(this); 342 }, 343 344 isPaused:function () { 345 return isPaused(this); 346 }, 347 348 pause:function () { 349 var self = this; 350 if (self.isRunning()) { 351 self._pauseDiff = S.now() - self._startTime; 352 AM.stop(self); 353 removeRunning(self); 354 savePaused(self); 355 } 356 return self; 357 }, 358 359 resume:function () { 360 var self = this; 361 if (self.isPaused()) { 362 self._startTime = S.now() - self._pauseDiff; 363 removePaused(self); 364 saveRunning(self); 365 AM.start(self); 366 } 367 return self; 368 }, 369 370 _runInternal:runInternal, 371 372 /** 373 * start this animation 374 */ 375 run:function () { 376 var self = this, 377 queueName = self.config.queue; 378 379 if (queueName === false) { 380 runInternal.call(self); 381 } else { 382 // 当前动画对象加入队列 383 Q.queue(self); 384 } 385 386 return self; 387 }, 388 389 _frame:function () { 390 391 var self = this, 392 prop, 393 config = self.config, 394 end = 1, 395 c, 396 fx, 397 fxs = self._fxs; 398 399 for (prop in fxs) { 400 if (fxs.hasOwnProperty(prop) && 401 // 当前属性没有结束 402 !((fx = fxs[prop]).finished)) { 403 // 非短路 404 if (config.frame) { 405 c = config.frame(fx); 406 } 407 // 结束 408 if (c == 1 || 409 // 不执行自带 410 c == 0) { 411 fx.finished = c; 412 end &= c; 413 } else { 414 end &= fx.frame(); 415 // 最后通知下 416 if (end && config.frame) { 417 config.frame(fx); 418 } 419 } 420 } 421 } 422 423 if ((self.fire("step") === false) || end) { 424 // complete 事件只在动画到达最后一帧时才触发 425 self.stop(end); 426 } 427 }, 428 429 /** 430 * stop this animation 431 * @param {Boolean} [finish] whether jump to the last position of this animation 432 */ 433 stop:function (finish) { 434 var self = this, 435 config = self.config, 436 queueName = config.queue, 437 prop, 438 fx, 439 fxs = self._fxs; 440 441 // already stopped 442 if (!self.isRunning()) { 443 // 从自己的队列中移除 444 if (queueName !== false) { 445 Q.remove(self); 446 } 447 return; 448 } 449 450 if (finish) { 451 for (prop in fxs) { 452 if (fxs.hasOwnProperty(prop) && 453 // 当前属性没有结束 454 !((fx = fxs[prop]).finished)) { 455 // 非短路 456 if (config.frame) { 457 config.frame(fx, 1); 458 } else { 459 fx.frame(1); 460 } 461 } 462 } 463 self.fire("complete"); 464 } 465 466 AM.stop(self); 467 468 removeRunning(self); 469 470 if (queueName !== false) { 471 // notify next anim to run in the same queue 472 Q.dequeue(self); 473 } 474 475 return self; 476 } 477 }); 478 479 var runningKey = S.guid("ks-anim-unqueued-" + S.now() + "-"); 480 481 function saveRunning(anim) { 482 var elem = anim.elem, 483 allRunning = DOM.data(elem, runningKey); 484 if (!allRunning) { 485 DOM.data(elem, runningKey, allRunning = {}); 486 } 487 allRunning[S.stamp(anim)] = anim; 488 } 489 490 function removeRunning(anim) { 491 var elem = anim.elem, 492 allRunning = DOM.data(elem, runningKey); 493 if (allRunning) { 494 delete allRunning[S.stamp(anim)]; 495 if (S.isEmptyObject(allRunning)) { 496 DOM.removeData(elem, runningKey); 497 } 498 } 499 } 500 501 function isRunning(anim) { 502 var elem = anim.elem, 503 allRunning = DOM.data(elem, runningKey); 504 if (allRunning) { 505 return !!allRunning[S.stamp(anim)]; 506 } 507 return 0; 508 } 509 510 511 var pausedKey = S.guid("ks-anim-paused-" + S.now() + "-"); 512 513 function savePaused(anim) { 514 var elem = anim.elem, 515 paused = DOM.data(elem, pausedKey); 516 if (!paused) { 517 DOM.data(elem, pausedKey, paused = {}); 518 } 519 paused[S.stamp(anim)] = anim; 520 } 521 522 function removePaused(anim) { 523 var elem = anim.elem, 524 paused = DOM.data(elem, pausedKey); 525 if (paused) { 526 delete paused[S.stamp(anim)]; 527 if (S.isEmptyObject(paused)) { 528 DOM.removeData(elem, pausedKey); 529 } 530 } 531 } 532 533 function isPaused(anim) { 534 var elem = anim.elem, 535 paused = DOM.data(elem, pausedKey); 536 if (paused) { 537 return !!paused[S.stamp(anim)]; 538 } 539 return 0; 540 } 541 542 /** 543 * stop all the anims currently running 544 * @param {HTMLElement} elem element which anim belongs to 545 * @param {Boolean} end whether jump to last position 546 * @param {Boolean} clearQueue whether clean current queue 547 * @param {String|Boolean} queueName current queue's name to be cleared 548 * @private 549 */ 550 Anim.stop = function (elem, end, clearQueue, queueName) { 551 if ( 552 // default queue 553 queueName === null || 554 // name of specified queue 555 S.isString(queueName) || 556 // anims not belong to any queue 557 queueName === false 558 ) { 559 return stopQueue.apply(undefined, arguments); 560 } 561 // first stop first anim in queues 562 if (clearQueue) { 563 Q.removeQueues(elem); 564 } 565 var allRunning = DOM.data(elem, runningKey), 566 // can not stop in for/in , stop will modified allRunning too 567 anims = S.merge(allRunning); 568 for (var k in anims) { 569 anims[k].stop(end); 570 } 571 }; 572 573 S.each(["pause", "resume"], function (action) { 574 Anim[action] = function (elem, queueName) { 575 if ( 576 // default queue 577 queueName === null || 578 // name of specified queue 579 S.isString(queueName) || 580 // anims not belong to any queue 581 queueName === false 582 ) { 583 return pauseResumeQueue(elem, queueName, action); 584 } 585 pauseResumeQueue(elem, undefined, action); 586 }; 587 }); 588 589 function pauseResumeQueue(elem, queueName, action) { 590 var allAnims = DOM.data(elem, action == 'resume' ? pausedKey : runningKey), 591 // can not stop in for/in , stop will modified allRunning too 592 anims = S.merge(allAnims); 593 for (var k in anims) { 594 var anim = anims[k]; 595 if (queueName === undefined || 596 anim.config.queue == queueName) { 597 anim[action](); 598 } 599 } 600 } 601 602 /** 603 * 604 * @param elem element which anim belongs to 605 * @param queueName queue'name if set to false only remove 606 * @param end 607 * @param clearQueue 608 * @private 609 */ 610 function stopQueue(elem, end, clearQueue, queueName) { 611 if (clearQueue && queueName !== false) { 612 Q.removeQueue(elem, queueName); 613 } 614 var allRunning = DOM.data(elem, runningKey), 615 anims = S.merge(allRunning); 616 for (var k in anims) { 617 var anim = anims[k]; 618 if (anim.config.queue == queueName) { 619 anim.stop(end); 620 } 621 } 622 } 623 624 /** 625 * whether elem is running anim 626 * @param {HTMLElement} elem 627 * @private 628 */ 629 Anim.isRunning = function (elem) { 630 var allRunning = DOM.data(elem, runningKey); 631 return allRunning && !S.isEmptyObject(allRunning); 632 }; 633 634 /** 635 * whether elem has paused anim 636 * @param {HTMLElement} elem 637 * @private 638 */ 639 Anim.isPaused = function (elem) { 640 var paused = DOM.data(elem, pausedKey); 641 return paused && !S.isEmptyObject(paused); 642 }; 643 644 Anim.Q = Q; 645 646 if (SHORT_HANDS) { 647 } 648 return Anim; 649 }, { 650 requires:["dom", "event", "./easing", "ua", "./manager", "./fx", "./queue"] 651 }); 652 653 /** 654 * 2011-11 655 * - 重构,抛弃 emile,优化性能,只对需要的属性进行动画 656 * - 添加 stop/stopQueue/isRunning,支持队列管理 657 * 658 * 2011-04 659 * - 借鉴 yui3 ,中央定时器,否则 ie6 内存泄露? 660 * - 支持配置 scrollTop/scrollLeft 661 * 662 * 663 * TODO: 664 * - 效率需要提升,当使用 nativeSupport 时仍做了过多动作 665 * - opera nativeSupport 存在 bug ,浏览器自身 bug ? 666 * - 实现 jQuery Effects 的 queue / specialEasing / += / 等特性 667 * 668 * NOTES: 669 * - 与 emile 相比,增加了 borderStyle, 使得 border: 5px solid #ccc 能从无到有,正确显示 670 * - api 借鉴了 YUI, jQuery 以及 http://www.w3.org/TR/css3-transitions/ 671 * - 代码实现了借鉴了 Emile.js: http://github.com/madrobby/emile * 672 */ 673