Source: hook/pauseResume.js

  1. /**
  2. * Copyright (c) 2015 NAVER Corp.
  3. * egjs projects are licensed under the MIT license
  4. */
  5. eg.module("pauseResume", ["jQuery"], function($) {
  6. "use strict";
  7. var animateFn = $.fn.animate;
  8. var stopFn = $.fn.stop;
  9. var delayFn = $.fn.delay;
  10. var uuid = 1;
  11. function AniProperty(type, el, prop, optall) {
  12. this.el = el;
  13. this.opt = optall;
  14. this.start = -1;
  15. this.elapsed = 0;
  16. this.paused = false;
  17. this.uuid = uuid++;
  18. this.easingNames = [];
  19. this.prop = prop;
  20. this.type = type;
  21. }
  22. /**
  23. * Generate a new absolute value maker.
  24. *
  25. * function to avoid JS Hint error "Don't make functions within a loop"
  26. */
  27. function generateAbsoluteValMaker(prevValue, propName, sign) {
  28. return function absoluteValMaker(match) {
  29. if (!prevValue || prevValue === "auto") {
  30. // Empty strings, null, undefined and "auto" are converted to 0.
  31. // This solution is somewhat extracted from jQuery Tween.propHooks._default.get
  32. // TODO: Should we consider adopting a Tween.propHooks?
  33. prevValue = 0;
  34. } else {
  35. prevValue = parseFloat(prevValue);
  36. }
  37. return prevValue + (match * sign);
  38. };
  39. }
  40. AniProperty.prototype.init = function() {
  41. var currValue;
  42. this.start = $.now();
  43. this.elapsed = 0;
  44. for (var propName in this.prop) {
  45. var propValue = this.prop[propName];
  46. var markIndex;
  47. var sign;
  48. // DO NOT SUPPORT TRANSFORM YET
  49. // TODO: convert from relative value to absolute value on transform
  50. if (propName === "transform") {
  51. continue;
  52. }
  53. //If it has a absoulte value.
  54. if (typeof propValue !== "string" ||
  55. (markIndex = propValue.search(/[+|-]=/)) < 0) {
  56. // this.prop[propName] = propValue;
  57. continue;
  58. }
  59. //If it has a relative value
  60. sign = propValue.charAt(markIndex) === "-" ? -1 : 1;
  61. // Current value
  62. currValue = $.css(this.el, propName);
  63. // CurrValue + (relativeValue)
  64. this.prop[propName] = propValue
  65. .replace(/([-|+])*([\d|\.])+/g,
  66. generateAbsoluteValMaker(currValue, propName, sign))
  67. .replace(/[-|+]+=/g, "");
  68. }
  69. };
  70. AniProperty.prototype.addEasingFn = function(easingName) {
  71. this.easingNames.push(easingName);
  72. };
  73. AniProperty.prototype.clearEasingFn = function() {
  74. var easing;
  75. while (easing = this.easingNames.shift()) {
  76. delete $.easing[easing];
  77. }
  78. this.easingNames = [];
  79. };
  80. function addAniProperty(type, el, prop, optall) {
  81. var newProp;
  82. newProp = new AniProperty(type, el, prop, optall);
  83. el.__aniProps = el.__aniProps || [];
  84. //Animation is excuted immediately.
  85. if (el.__aniProps.length === 0) {
  86. newProp.init();
  87. }
  88. el.__aniProps.push(newProp);
  89. }
  90. function removeAniProperty(el) {
  91. var removeProp = el.__aniProps.shift();
  92. removeProp && removeProp.clearEasingFn();
  93. el.__aniProps[0] && el.__aniProps[0].init();
  94. }
  95. $.fn.animate = function(prop, speed, easing, callback) {
  96. return this.each(function() {
  97. //optall should be made for each elements.
  98. var optall = $.speed(speed, easing, callback);
  99. // prepare next animation when current animation completed.
  100. optall.complete = function() {
  101. prepareNextAniProp(this);
  102. };
  103. //Queue animation property to recover the current animation.
  104. addAniProperty("animate", this, prop, optall);
  105. animateFn.call($(this), prop, optall);
  106. });
  107. // TODO: Below code is more reasonable?
  108. // return animateFn.call(this, prop, optall); // and declare optall at outside this.each loop.
  109. };
  110. // Check if this element can be paused/resume.
  111. function getStatus(el) {
  112. if (!el.__aniProps || el.__aniProps.length === 0) {
  113. // Current element doesn't have animation information.
  114. // Check 'animate' is applied to this element.
  115. return "empty";
  116. }
  117. return el.__aniProps[0].paused ? "paused" : "inprogress";
  118. }
  119. /**
  120. * Set a timer to delay execution of subsequent items in the queue.
  121. * And it internally manages "fx"queue to support pause/resume if "fx" type.
  122. *
  123. * @param {Number} An integer indicating the number of milliseconds to delay execution of the next item in the queue.
  124. * @param {String} A string containing the name of the queue. Defaults to fx, the standard effects queue.
  125. */
  126. $.fn.delay = function(time, type) {
  127. var t;
  128. var isCallByResume = arguments[2];//internal used value.
  129. if (type && type !== "fx") {
  130. return delayFn.call(this, time, type);
  131. }
  132. t = parseInt(time, 10);
  133. t = isNaN(t) ? 0 : t;
  134. return this.each(function() {
  135. if (!isCallByResume) {
  136. // Queue delay property to recover the current animation.
  137. // Don't add property when delay is called by resume.
  138. addAniProperty("delay", this, null, {duration: t});
  139. }
  140. var self = this;
  141. delayFn.call($(this), time).queue(function(next) {
  142. next();
  143. // Remove delay property when delay has been expired.
  144. removeAniProperty(self);
  145. });
  146. });
  147. };
  148. /**
  149. * Pauses the animation executed through a call to the jQuery <a href=http://api.jquery.com/animate/>.animate()</a> method.
  150. * @ko jQuery의<a href=http://api.jquery.com/animate/>animate() 메서드</a>가 실행한 애니메이션을 일시 정지한다
  151. *
  152. * @name jQuery#pause
  153. * @method
  154. * @support {"ie": "10+", "ch" : "latest", "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"}
  155. * @example
  156. * $("#box").pause(); //paused the current animation
  157. */
  158. $.fn.pause = function() {
  159. return this.each(function() {
  160. var p;
  161. var type = "fx";
  162. if (getStatus(this) !== "inprogress") {
  163. return;
  164. }
  165. //Clear fx-queue except 1 dummy function
  166. //for promise not to be expired when calling stop()
  167. $.queue(this, type, [$.noop]);
  168. stopFn.call($(this));
  169. //Remember current animation property
  170. if (p = this.__aniProps[0]) {
  171. p.elapsed += $.now() - p.start;
  172. // Complement native timer's inaccuracy (complete timer can be different from your request.)
  173. // (eg. your request:400ms -> real :396 ~ 415 ms ))
  174. if (p.elapsed >= p.opt.duration) {
  175. p = prepareNextAniProp(this);
  176. }
  177. p && (p.paused = true);
  178. }
  179. });
  180. };
  181. function prepareNextAniProp(el) {
  182. var removeProp;
  183. var userCallback;
  184. // Dequeue animation property that was ended.
  185. removeProp = el.__aniProps.shift();
  186. removeProp.clearEasingFn();
  187. userCallback = removeProp.opt.old;
  188. // Callback should be called before aniProps.init()
  189. if (userCallback && typeof userCallback === "function") {
  190. userCallback.call(el);
  191. }
  192. // If next ani property exists
  193. el.__aniProps[0] && el.__aniProps[0].init();
  194. return el.__aniProps[0];
  195. }
  196. /**
  197. * Resumes the animation paused through a call to the pause() method.
  198. * @ko pause() 메서드가 일시 정지한 애니메이션을 다시 실행한다
  199. *
  200. * @name jQuery#resume
  201. * @method
  202. * @support {"ie": "10+", "ch" : "latest", "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"}
  203. * @example
  204. * $("#box").resume(); //resume the paused animation
  205. */
  206. $.fn.resume = function() {
  207. return this.each(function() {
  208. var type = "fx";
  209. var p;
  210. var i;
  211. if (getStatus(this) !== "paused") {
  212. return;
  213. }
  214. //Clear fx-queue,
  215. //And this queue will be initialized by animate call.
  216. $.queue(this, type, []);
  217. // Restore __aniProps
  218. i = 0;
  219. while (p = this.__aniProps[i]) {
  220. // Restore easing status
  221. if (p.elapsed > 0 && p.opt.easing) {
  222. var resumePercent = p.elapsed / p.opt.duration;
  223. var remainPercent = 1 - resumePercent;
  224. var originalEasing = $.easing[p.opt.easing];
  225. var startEasingValue = originalEasing(resumePercent);
  226. var scale = scaler([startEasingValue, 1], [0, 1]);
  227. var newEasingName = p.opt.easing + "_" + p.uuid;
  228. // Make new easing function that continues from pause point.
  229. $.easing[newEasingName] = generateNewEasingFunc(
  230. resumePercent, remainPercent, scale, originalEasing);
  231. p.opt.easing = newEasingName;
  232. //Store new easing function to clear it later.
  233. p.addEasingFn(newEasingName);
  234. }
  235. p.paused = false;
  236. p.opt.duration -= p.elapsed;
  237. // If duration remains, request 'animate' with storing aniProps
  238. if (p.opt.duration > 0 || p.elapsed === 0) {
  239. i === 0 && p.init();
  240. if (p.type === "delay") {
  241. // pass last parameter 'true' not to add an aniProperty.
  242. $(this).delay(p.opt.duration, "fx", true);
  243. } else {
  244. animateFn.call($(this), p.prop, p.opt);
  245. }
  246. }
  247. i++;
  248. }
  249. });
  250. };
  251. /**
  252. * Generate a new easing function.
  253. *
  254. * function to avoid JS Hint error "Don't make functions within a loop"
  255. */
  256. function generateNewEasingFunc(resumePercent, remainPercent, scale, originalEasing) {
  257. return function easingFunc(percent) {
  258. var newPercent = resumePercent + remainPercent * percent;
  259. return scale(originalEasing(newPercent));
  260. };
  261. }
  262. $.fn.stop = function(type, clearQueue) {
  263. var clearQ = clearQueue;
  264. stopFn.apply(this, arguments);
  265. if (typeof type !== "string") {
  266. clearQ = type;
  267. }
  268. return this.each(function() {
  269. var p;
  270. // When this element was not animated properly, do nothing.
  271. if (getStatus(this) === "empty") {
  272. return;
  273. }
  274. if (!clearQ) {
  275. p = this.__aniProps.shift();
  276. p && p.clearEasingFn();
  277. } else {
  278. //If clearQueue is requested,
  279. //then all properties must be initialized
  280. //for element not to be resumed.
  281. while (p = this.__aniProps.shift()) {
  282. p.clearEasingFn();
  283. }
  284. this.__aniProps = [];
  285. }
  286. });
  287. };
  288. jQuery.expr.filters.paused = function(elem) {
  289. return getStatus(elem) === "paused";
  290. };
  291. //Adopt linear scale from d3
  292. function scaler(domain, range) {
  293. var u = uninterpolateNumber(domain[0], domain[1]);
  294. var i = interpolateNumber(range[0], range[1]);
  295. return function(x) {
  296. return i(u(x));
  297. };
  298. }
  299. function interpolateNumber(a, b) {
  300. a = +a, b = +b;
  301. return function(t) {
  302. return a * (1 - t) + b * t;
  303. };
  304. }
  305. function uninterpolateNumber(a, b) {
  306. b = (b -= a = +a) || 1 / b;
  307. return function(x) {
  308. return (x - a) / b;
  309. };
  310. }
  311. });
comments powered by Disqus