fx.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. define(["./kernel", "./config", /*===== "./declare", =====*/ "./lang", "../Evented", "./Color", "../aspect", "../sniff", "../dom", "../dom-style"],
  2. function(dojo, config, /*===== declare, =====*/ lang, Evented, Color, aspect, has, dom, style){
  3. // module:
  4. // dojo/_base/fx
  5. // notes:
  6. // Animation loosely package based on Dan Pupius' work, contributed under CLA; see
  7. // http://pupius.co.uk/js/Toolkit.Drawing.js
  8. var _mixin = lang.mixin;
  9. // Module export
  10. var basefx = {
  11. // summary:
  12. // This module defines the base dojo/_base/fx implementation.
  13. };
  14. var _Line = basefx._Line = function(/*int*/ start, /*int*/ end){
  15. // summary:
  16. // Object used to generate values from a start value to an end value
  17. // start: int
  18. // Beginning value for range
  19. // end: int
  20. // Ending value for range
  21. this.start = start;
  22. this.end = end;
  23. };
  24. _Line.prototype.getValue = function(/*float*/ n){
  25. // summary:
  26. // Returns the point on the line
  27. // n:
  28. // a floating point number greater than 0 and less than 1
  29. return ((this.end - this.start) * n) + this.start; // Decimal
  30. };
  31. var Animation = basefx.Animation = function(args){
  32. // summary:
  33. // A generic animation class that fires callbacks into its handlers
  34. // object at various states.
  35. // description:
  36. // A generic animation class that fires callbacks into its handlers
  37. // object at various states. Nearly all dojo animation functions
  38. // return an instance of this method, usually without calling the
  39. // .play() method beforehand. Therefore, you will likely need to
  40. // call .play() on instances of `Animation` when one is
  41. // returned.
  42. // args: Object
  43. // The 'magic argument', mixing all the properties into this
  44. // animation instance.
  45. _mixin(this, args);
  46. if(lang.isArray(this.curve)){
  47. this.curve = new _Line(this.curve[0], this.curve[1]);
  48. }
  49. };
  50. Animation.prototype = new Evented();
  51. lang.extend(Animation, {
  52. // duration: Integer
  53. // The time in milliseconds the animation will take to run
  54. duration: 350,
  55. /*=====
  56. // curve: _Line|Array
  57. // A two element array of start and end values, or a `_Line` instance to be
  58. // used in the Animation.
  59. curve: null,
  60. // easing: Function?
  61. // A Function to adjust the acceleration (or deceleration) of the progress
  62. // across a _Line
  63. easing: null,
  64. =====*/
  65. // repeat: Integer?
  66. // The number of times to loop the animation
  67. repeat: 0,
  68. // rate: Integer?
  69. // the time in milliseconds to wait before advancing to next frame
  70. // (used as a fps timer: 1000/rate = fps)
  71. rate: 20 /* 50 fps */,
  72. /*=====
  73. // delay: Integer?
  74. // The time in milliseconds to wait before starting animation after it
  75. // has been .play()'ed
  76. delay: null,
  77. // beforeBegin: Event?
  78. // Synthetic event fired before a Animation begins playing (synchronous)
  79. beforeBegin: null,
  80. // onBegin: Event?
  81. // Synthetic event fired as a Animation begins playing (useful?)
  82. onBegin: null,
  83. // onAnimate: Event?
  84. // Synthetic event fired at each interval of the Animation
  85. onAnimate: null,
  86. // onEnd: Event?
  87. // Synthetic event fired after the final frame of the Animation
  88. onEnd: null,
  89. // onPlay: Event?
  90. // Synthetic event fired any time the Animation is play()'ed
  91. onPlay: null,
  92. // onPause: Event?
  93. // Synthetic event fired when the Animation is paused
  94. onPause: null,
  95. // onStop: Event
  96. // Synthetic event fires when the Animation is stopped
  97. onStop: null,
  98. =====*/
  99. _percent: 0,
  100. _startRepeatCount: 0,
  101. _getStep: function(){
  102. var _p = this._percent,
  103. _e = this.easing
  104. ;
  105. return _e ? _e(_p) : _p;
  106. },
  107. _fire: function(/*Event*/ evt, /*Array?*/ args){
  108. // summary:
  109. // Convenience function. Fire event "evt" and pass it the
  110. // arguments specified in "args".
  111. // description:
  112. // Convenience function. Fire event "evt" and pass it the
  113. // arguments specified in "args".
  114. // Fires the callback in the scope of this Animation
  115. // instance.
  116. // evt:
  117. // The event to fire.
  118. // args:
  119. // The arguments to pass to the event.
  120. var a = args||[];
  121. if(this[evt]){
  122. if(config.debugAtAllCosts){
  123. this[evt].apply(this, a);
  124. }else{
  125. try{
  126. this[evt].apply(this, a);
  127. }catch(e){
  128. // squelch and log because we shouldn't allow exceptions in
  129. // synthetic event handlers to cause the internal timer to run
  130. // amuck, potentially pegging the CPU. I'm not a fan of this
  131. // squelch, but hopefully logging will make it clear what's
  132. // going on
  133. console.error("exception in animation handler for:", evt);
  134. console.error(e);
  135. }
  136. }
  137. }
  138. return this; // Animation
  139. },
  140. play: function(/*int?*/ delay, /*Boolean?*/ gotoStart){
  141. // summary:
  142. // Start the animation.
  143. // delay:
  144. // How many milliseconds to delay before starting.
  145. // gotoStart:
  146. // If true, starts the animation from the beginning; otherwise,
  147. // starts it from its current position.
  148. // returns: Animation
  149. // The instance to allow chaining.
  150. var _t = this;
  151. if(_t._delayTimer){ _t._clearTimer(); }
  152. if(gotoStart){
  153. _t._stopTimer();
  154. _t._active = _t._paused = false;
  155. _t._percent = 0;
  156. }else if(_t._active && !_t._paused){
  157. return _t;
  158. }
  159. _t._fire("beforeBegin", [_t.node]);
  160. var de = delay || _t.delay,
  161. _p = lang.hitch(_t, "_play", gotoStart);
  162. if(de > 0){
  163. _t._delayTimer = setTimeout(_p, de);
  164. return _t;
  165. }
  166. _p();
  167. return _t; // Animation
  168. },
  169. _play: function(gotoStart){
  170. var _t = this;
  171. if(_t._delayTimer){ _t._clearTimer(); }
  172. _t._startTime = new Date().valueOf();
  173. if(_t._paused){
  174. _t._startTime -= _t.duration * _t._percent;
  175. }
  176. _t._active = true;
  177. _t._paused = false;
  178. var value = _t.curve.getValue(_t._getStep());
  179. if(!_t._percent){
  180. if(!_t._startRepeatCount){
  181. _t._startRepeatCount = _t.repeat;
  182. }
  183. _t._fire("onBegin", [value]);
  184. }
  185. _t._fire("onPlay", [value]);
  186. _t._cycle();
  187. return _t; // Animation
  188. },
  189. pause: function(){
  190. // summary:
  191. // Pauses a running animation.
  192. var _t = this;
  193. if(_t._delayTimer){ _t._clearTimer(); }
  194. _t._stopTimer();
  195. if(!_t._active){ return _t; /*Animation*/ }
  196. _t._paused = true;
  197. _t._fire("onPause", [_t.curve.getValue(_t._getStep())]);
  198. return _t; // Animation
  199. },
  200. gotoPercent: function(/*Decimal*/ percent, /*Boolean?*/ andPlay){
  201. // summary:
  202. // Sets the progress of the animation.
  203. // percent:
  204. // A percentage in decimal notation (between and including 0.0 and 1.0).
  205. // andPlay:
  206. // If true, play the animation after setting the progress.
  207. var _t = this;
  208. _t._stopTimer();
  209. _t._active = _t._paused = true;
  210. _t._percent = percent;
  211. if(andPlay){ _t.play(); }
  212. return _t; // Animation
  213. },
  214. stop: function(/*boolean?*/ gotoEnd){
  215. // summary:
  216. // Stops a running animation.
  217. // gotoEnd:
  218. // If true, the animation will end.
  219. var _t = this;
  220. if(_t._delayTimer){ _t._clearTimer(); }
  221. if(!_t._timer){ return _t; /* Animation */ }
  222. _t._stopTimer();
  223. if(gotoEnd){
  224. _t._percent = 1;
  225. }
  226. _t._fire("onStop", [_t.curve.getValue(_t._getStep())]);
  227. _t._active = _t._paused = false;
  228. return _t; // Animation
  229. },
  230. destroy: function(){
  231. // summary:
  232. // cleanup the animation
  233. this.stop();
  234. },
  235. status: function(){
  236. // summary:
  237. // Returns a string token representation of the status of
  238. // the animation, one of: "paused", "playing", "stopped"
  239. if(this._active){
  240. return this._paused ? "paused" : "playing"; // String
  241. }
  242. return "stopped"; // String
  243. },
  244. _cycle: function(){
  245. var _t = this;
  246. if(_t._active){
  247. var curr = new Date().valueOf();
  248. // Allow durations of 0 (instant) by setting step to 1 - see #13798
  249. var step = _t.duration === 0 ? 1 : (curr - _t._startTime) / (_t.duration);
  250. if(step >= 1){
  251. step = 1;
  252. }
  253. _t._percent = step;
  254. // Perform easing
  255. if(_t.easing){
  256. step = _t.easing(step);
  257. }
  258. _t._fire("onAnimate", [_t.curve.getValue(step)]);
  259. if(_t._percent < 1){
  260. _t._startTimer();
  261. }else{
  262. _t._active = false;
  263. if(_t.repeat > 0){
  264. _t.repeat--;
  265. _t.play(null, true);
  266. }else if(_t.repeat == -1){
  267. _t.play(null, true);
  268. }else{
  269. if(_t._startRepeatCount){
  270. _t.repeat = _t._startRepeatCount;
  271. _t._startRepeatCount = 0;
  272. }
  273. }
  274. _t._percent = 0;
  275. _t._fire("onEnd", [_t.node]);
  276. !_t.repeat && _t._stopTimer();
  277. }
  278. }
  279. return _t; // Animation
  280. },
  281. _clearTimer: function(){
  282. // summary:
  283. // Clear the play delay timer
  284. clearTimeout(this._delayTimer);
  285. delete this._delayTimer;
  286. }
  287. });
  288. // the local timer, stubbed into all Animation instances
  289. var ctr = 0,
  290. timer = null,
  291. runner = {
  292. run: function(){}
  293. };
  294. lang.extend(Animation, {
  295. _startTimer: function(){
  296. if(!this._timer){
  297. this._timer = aspect.after(runner, "run", lang.hitch(this, "_cycle"), true);
  298. ctr++;
  299. }
  300. if(!timer){
  301. timer = setInterval(lang.hitch(runner, "run"), this.rate);
  302. }
  303. },
  304. _stopTimer: function(){
  305. if(this._timer){
  306. this._timer.remove();
  307. this._timer = null;
  308. ctr--;
  309. }
  310. if(ctr <= 0){
  311. clearInterval(timer);
  312. timer = null;
  313. ctr = 0;
  314. }
  315. }
  316. });
  317. var _makeFadeable =
  318. has("ie") ? function(node){
  319. // only set the zoom if the "tickle" value would be the same as the
  320. // default
  321. var ns = node.style;
  322. // don't set the width to auto if it didn't already cascade that way.
  323. // We don't want to f anyones designs
  324. if(!ns.width.length && style.get(node, "width") == "auto"){
  325. ns.width = "auto";
  326. }
  327. } :
  328. function(){};
  329. basefx._fade = function(/*Object*/ args){
  330. // summary:
  331. // Returns an animation that will fade the node defined by
  332. // args.node from the start to end values passed (args.start
  333. // args.end) (end is mandatory, start is optional)
  334. args.node = dom.byId(args.node);
  335. var fArgs = _mixin({ properties: {} }, args),
  336. props = (fArgs.properties.opacity = {});
  337. props.start = !("start" in fArgs) ?
  338. function(){
  339. return +style.get(fArgs.node, "opacity")||0;
  340. } : fArgs.start;
  341. props.end = fArgs.end;
  342. var anim = basefx.animateProperty(fArgs);
  343. aspect.after(anim, "beforeBegin", lang.partial(_makeFadeable, fArgs.node), true);
  344. return anim; // Animation
  345. };
  346. /*=====
  347. var __FadeArgs = declare(null, {
  348. // node: DOMNode|String
  349. // The node referenced in the animation
  350. // duration: Integer?
  351. // Duration of the animation in milliseconds.
  352. // easing: Function?
  353. // An easing function.
  354. });
  355. =====*/
  356. basefx.fadeIn = function(/*__FadeArgs*/ args){
  357. // summary:
  358. // Returns an animation that will fade node defined in 'args' from
  359. // its current opacity to fully opaque.
  360. return basefx._fade(_mixin({ end: 1 }, args)); // Animation
  361. };
  362. basefx.fadeOut = function(/*__FadeArgs*/ args){
  363. // summary:
  364. // Returns an animation that will fade node defined in 'args'
  365. // from its current opacity to fully transparent.
  366. return basefx._fade(_mixin({ end: 0 }, args)); // Animation
  367. };
  368. basefx._defaultEasing = function(/*Decimal?*/ n){
  369. // summary:
  370. // The default easing function for Animation(s)
  371. return 0.5 + ((Math.sin((n + 1.5) * Math.PI)) / 2); // Decimal
  372. };
  373. var PropLine = function(properties){
  374. // PropLine is an internal class which is used to model the values of
  375. // an a group of CSS properties across an animation lifecycle. In
  376. // particular, the "getValue" function handles getting interpolated
  377. // values between start and end for a particular CSS value.
  378. this._properties = properties;
  379. for(var p in properties){
  380. var prop = properties[p];
  381. if(prop.start instanceof Color){
  382. // create a reusable temp color object to keep intermediate results
  383. prop.tempColor = new Color();
  384. }
  385. }
  386. };
  387. PropLine.prototype.getValue = function(r){
  388. var ret = {};
  389. for(var p in this._properties){
  390. var prop = this._properties[p],
  391. start = prop.start;
  392. if(start instanceof Color){
  393. ret[p] = Color.blendColors(start, prop.end, r, prop.tempColor).toCss();
  394. }else if(!lang.isArray(start)){
  395. ret[p] = ((prop.end - start) * r) + start + (p != "opacity" ? prop.units || "px" : 0);
  396. }
  397. }
  398. return ret;
  399. };
  400. /*=====
  401. var __AnimArgs = declare(__FadeArgs, {
  402. // properties: Object?
  403. // A hash map of style properties to Objects describing the transition,
  404. // such as the properties of _Line with an additional 'units' property
  405. properties: {}
  406. //TODOC: add event callbacks
  407. });
  408. =====*/
  409. basefx.animateProperty = function(/*__AnimArgs*/ args){
  410. // summary:
  411. // Returns an animation that will transition the properties of
  412. // node defined in `args` depending how they are defined in
  413. // `args.properties`
  414. //
  415. // description:
  416. // Foundation of most `dojo/_base/fx`
  417. // animations. It takes an object of "properties" corresponding to
  418. // style properties, and animates them in parallel over a set
  419. // duration.
  420. //
  421. // example:
  422. // A simple animation that changes the width of the specified node.
  423. // | basefx.animateProperty({
  424. // | node: "nodeId",
  425. // | properties: { width: 400 },
  426. // | }).play();
  427. // Dojo figures out the start value for the width and converts the
  428. // integer specified for the width to the more expressive but
  429. // verbose form `{ width: { end: '400', units: 'px' } }` which you
  430. // can also specify directly. Defaults to 'px' if omitted.
  431. //
  432. // example:
  433. // Animate width, height, and padding over 2 seconds... the
  434. // pedantic way:
  435. // | basefx.animateProperty({ node: node, duration:2000,
  436. // | properties: {
  437. // | width: { start: '200', end: '400', units:"px" },
  438. // | height: { start:'200', end: '400', units:"px" },
  439. // | paddingTop: { start:'5', end:'50', units:"px" }
  440. // | }
  441. // | }).play();
  442. // Note 'paddingTop' is used over 'padding-top'. Multi-name CSS properties
  443. // are written using "mixed case", as the hyphen is illegal as an object key.
  444. //
  445. // example:
  446. // Plug in a different easing function and register a callback for
  447. // when the animation ends. Easing functions accept values between
  448. // zero and one and return a value on that basis. In this case, an
  449. // exponential-in curve.
  450. // | basefx.animateProperty({
  451. // | node: "nodeId",
  452. // | // dojo figures out the start value
  453. // | properties: { width: { end: 400 } },
  454. // | easing: function(n){
  455. // | return (n==0) ? 0 : Math.pow(2, 10 * (n - 1));
  456. // | },
  457. // | onEnd: function(node){
  458. // | // called when the animation finishes. The animation
  459. // | // target is passed to this function
  460. // | }
  461. // | }).play(500); // delay playing half a second
  462. //
  463. // example:
  464. // Like all `Animation`s, animateProperty returns a handle to the
  465. // Animation instance, which fires the events common to Dojo FX. Use `aspect.after`
  466. // to access these events outside of the Animation definition:
  467. // | var anim = basefx.animateProperty({
  468. // | node:"someId",
  469. // | properties:{
  470. // | width:400, height:500
  471. // | }
  472. // | });
  473. // | aspect.after(anim, "onEnd", function(){
  474. // | console.log("animation ended");
  475. // | }, true);
  476. // | // play the animation now:
  477. // | anim.play();
  478. //
  479. // example:
  480. // Each property can be a function whose return value is substituted along.
  481. // Additionally, each measurement (eg: start, end) can be a function. The node
  482. // reference is passed directly to callbacks.
  483. // | basefx.animateProperty({
  484. // | node:"mine",
  485. // | properties:{
  486. // | height:function(node){
  487. // | // shrink this node by 50%
  488. // | return domGeom.position(node).h / 2
  489. // | },
  490. // | width:{
  491. // | start:function(node){ return 100; },
  492. // | end:function(node){ return 200; }
  493. // | }
  494. // | }
  495. // | }).play();
  496. //
  497. var n = args.node = dom.byId(args.node);
  498. if(!args.easing){ args.easing = dojo._defaultEasing; }
  499. var anim = new Animation(args);
  500. aspect.after(anim, "beforeBegin", lang.hitch(anim, function(){
  501. var pm = {};
  502. for(var p in this.properties){
  503. // Make shallow copy of properties into pm because we overwrite
  504. // some values below. In particular if start/end are functions
  505. // we don't want to overwrite them or the functions won't be
  506. // called if the animation is reused.
  507. if(p == "width" || p == "height"){
  508. this.node.display = "block";
  509. }
  510. var prop = this.properties[p];
  511. if(lang.isFunction(prop)){
  512. prop = prop(n);
  513. }
  514. prop = pm[p] = _mixin({}, (lang.isObject(prop) ? prop: { end: prop }));
  515. if(lang.isFunction(prop.start)){
  516. prop.start = prop.start(n);
  517. }
  518. if(lang.isFunction(prop.end)){
  519. prop.end = prop.end(n);
  520. }
  521. var isColor = (p.toLowerCase().indexOf("color") >= 0);
  522. function getStyle(node, p){
  523. // domStyle.get(node, "height") can return "auto" or "" on IE; this is more reliable:
  524. var v = { height: node.offsetHeight, width: node.offsetWidth }[p];
  525. if(v !== undefined){ return v; }
  526. v = style.get(node, p);
  527. return (p == "opacity") ? +v : (isColor ? v : parseFloat(v));
  528. }
  529. if(!("end" in prop)){
  530. prop.end = getStyle(n, p);
  531. }else if(!("start" in prop)){
  532. prop.start = getStyle(n, p);
  533. }
  534. if(isColor){
  535. prop.start = new Color(prop.start);
  536. prop.end = new Color(prop.end);
  537. }else{
  538. prop.start = (p == "opacity") ? +prop.start : parseFloat(prop.start);
  539. }
  540. }
  541. this.curve = new PropLine(pm);
  542. }), true);
  543. aspect.after(anim, "onAnimate", lang.hitch(style, "set", anim.node), true);
  544. return anim; // Animation
  545. };
  546. basefx.anim = function( /*DOMNode|String*/ node,
  547. /*Object*/ properties,
  548. /*Integer?*/ duration,
  549. /*Function?*/ easing,
  550. /*Function?*/ onEnd,
  551. /*Integer?*/ delay){
  552. // summary:
  553. // A simpler interface to `animateProperty()`, also returns
  554. // an instance of `Animation` but begins the animation
  555. // immediately, unlike nearly every other Dojo animation API.
  556. // description:
  557. // Simpler (but somewhat less powerful) version
  558. // of `animateProperty`. It uses defaults for many basic properties
  559. // and allows for positional parameters to be used in place of the
  560. // packed "property bag" which is used for other Dojo animation
  561. // methods.
  562. //
  563. // The `Animation` object returned will be already playing, so
  564. // calling play() on it again is (usually) a no-op.
  565. // node:
  566. // a DOM node or the id of a node to animate CSS properties on
  567. // duration:
  568. // The number of milliseconds over which the animation
  569. // should run. Defaults to the global animation default duration
  570. // (350ms).
  571. // easing:
  572. // An easing function over which to calculate acceleration
  573. // and deceleration of the animation through its duration.
  574. // A default easing algorithm is provided, but you may
  575. // plug in any you wish. A large selection of easing algorithms
  576. // are available in `dojo/fx/easing`.
  577. // onEnd:
  578. // A function to be called when the animation finishes
  579. // running.
  580. // delay:
  581. // The number of milliseconds to delay beginning the
  582. // animation by. The default is 0.
  583. // example:
  584. // Fade out a node
  585. // | basefx.anim("id", { opacity: 0 });
  586. // example:
  587. // Fade out a node over a full second
  588. // | basefx.anim("id", { opacity: 0 }, 1000);
  589. return basefx.animateProperty({ // Animation
  590. node: node,
  591. duration: duration || Animation.prototype.duration,
  592. properties: properties,
  593. easing: easing,
  594. onEnd: onEnd
  595. }).play(delay || 0);
  596. };
  597. if(has("extend-dojo")){
  598. _mixin(dojo, basefx);
  599. // Alias to drop come 2.0:
  600. dojo._Animation = Animation;
  601. }
  602. return basefx;
  603. });