animationframe.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // Copyright 2014 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview goog.dom.animationFrame permits work to be done in-sync with
  16. * the render refresh rate of the browser and to divide work up globally based
  17. * on whether the intent is to measure or to mutate the DOM. The latter avoids
  18. * repeated style recalculation which can be really slow.
  19. *
  20. * Goals of the API:
  21. * <ul>
  22. * <li>Make it easy to schedule work for the next animation frame.
  23. * <li>Make it easy to only do work once per animation frame, even if two
  24. * events fire that trigger the same work.
  25. * <li>Make it easy to do all work in two phases to avoid repeated style
  26. * recalculation caused by interleaved reads and writes.
  27. * <li>Avoid creating closures per schedule operation.
  28. * </ul>
  29. *
  30. *
  31. * Programmatic:
  32. * <pre>
  33. * var animationTask = goog.dom.animationFrame.createTask({
  34. * measure: function(state) {
  35. * state.width = goog.style.getSize(elem).width;
  36. * this.animationTask();
  37. * },
  38. * mutate: function(state) {
  39. * goog.style.setWidth(elem, Math.floor(state.width / 2));
  40. * }
  41. * }, this);
  42. * });
  43. * </pre>
  44. *
  45. * See also
  46. * https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame
  47. */
  48. goog.provide('goog.dom.animationFrame');
  49. goog.provide('goog.dom.animationFrame.Spec');
  50. goog.provide('goog.dom.animationFrame.State');
  51. goog.require('goog.dom.animationFrame.polyfill');
  52. // Install the polyfill.
  53. goog.dom.animationFrame.polyfill.install();
  54. /**
  55. * @typedef {{
  56. * id: number,
  57. * fn: !Function,
  58. * context: (!Object|undefined)
  59. * }}
  60. * @private
  61. */
  62. goog.dom.animationFrame.Task_;
  63. /**
  64. * @typedef {{
  65. * measureTask: goog.dom.animationFrame.Task_,
  66. * mutateTask: goog.dom.animationFrame.Task_,
  67. * state: (!Object|undefined),
  68. * args: (!Array|undefined),
  69. * isScheduled: boolean
  70. * }}
  71. * @private
  72. */
  73. goog.dom.animationFrame.TaskSet_;
  74. /**
  75. * @typedef {{
  76. * measure: (!Function|undefined),
  77. * mutate: (!Function|undefined)
  78. * }}
  79. */
  80. goog.dom.animationFrame.Spec;
  81. /**
  82. * A type to represent state. Users may add properties as desired.
  83. * @constructor
  84. * @final
  85. */
  86. goog.dom.animationFrame.State = function() {};
  87. /**
  88. * Saves a set of tasks to be executed in the next requestAnimationFrame phase.
  89. * This list is initialized once before any event firing occurs. It is not
  90. * affected by the fired events or the requestAnimationFrame processing (unless
  91. * a new event is created during the processing).
  92. * @private {!Array<!Array<goog.dom.animationFrame.TaskSet_>>}
  93. */
  94. goog.dom.animationFrame.tasks_ = [[], []];
  95. /**
  96. * Values are 0 or 1, for whether the first or second array should be used to
  97. * lookup or add tasks.
  98. * @private {number}
  99. */
  100. goog.dom.animationFrame.doubleBufferIndex_ = 0;
  101. /**
  102. * Whether we have already requested an animation frame that hasn't happened
  103. * yet.
  104. * @private {boolean}
  105. */
  106. goog.dom.animationFrame.requestedFrame_ = false;
  107. /**
  108. * Counter to generate IDs for tasks.
  109. * @private {number}
  110. */
  111. goog.dom.animationFrame.taskId_ = 0;
  112. /**
  113. * Whether the animationframe runTasks_ loop is currently running.
  114. * @private {boolean}
  115. */
  116. goog.dom.animationFrame.running_ = false;
  117. /**
  118. * Returns a function that schedules the two passed-in functions to be run upon
  119. * the next animation frame. Calling the function again during the same
  120. * animation frame does nothing.
  121. *
  122. * The function under the "measure" key will run first and together with all
  123. * other functions scheduled under this key and the function under "mutate" will
  124. * run after that.
  125. *
  126. * @param {{
  127. * measure: (function(this:THIS, !goog.dom.animationFrame.State)|undefined),
  128. * mutate: (function(this:THIS, !goog.dom.animationFrame.State)|undefined)
  129. * }} spec
  130. * @param {THIS=} opt_context Context in which to run the function.
  131. * @return {function(...?)}
  132. * @template THIS
  133. */
  134. goog.dom.animationFrame.createTask = function(spec, opt_context) {
  135. var id = goog.dom.animationFrame.taskId_++;
  136. var measureTask = {id: id, fn: spec.measure, context: opt_context};
  137. var mutateTask = {id: id, fn: spec.mutate, context: opt_context};
  138. var taskSet = {
  139. measureTask: measureTask,
  140. mutateTask: mutateTask,
  141. state: {},
  142. args: undefined,
  143. isScheduled: false
  144. };
  145. return function() {
  146. // Save args and state.
  147. if (arguments.length > 0) {
  148. // The state argument goes last. That is kinda horrible but compatible
  149. // with {@see wiz.async.method}.
  150. if (!taskSet.args) {
  151. taskSet.args = [];
  152. }
  153. taskSet.args.length = 0;
  154. taskSet.args.push.apply(taskSet.args, arguments);
  155. taskSet.args.push(taskSet.state);
  156. } else {
  157. if (!taskSet.args || taskSet.args.length == 0) {
  158. taskSet.args = [taskSet.state];
  159. } else {
  160. taskSet.args[0] = taskSet.state;
  161. taskSet.args.length = 1;
  162. }
  163. }
  164. if (!taskSet.isScheduled) {
  165. taskSet.isScheduled = true;
  166. var tasksArray = goog.dom.animationFrame
  167. .tasks_[goog.dom.animationFrame.doubleBufferIndex_];
  168. tasksArray.push(
  169. /** @type {goog.dom.animationFrame.TaskSet_} */ (taskSet));
  170. }
  171. goog.dom.animationFrame.requestAnimationFrame_();
  172. };
  173. };
  174. /**
  175. * Run scheduled tasks.
  176. * @private
  177. */
  178. goog.dom.animationFrame.runTasks_ = function() {
  179. goog.dom.animationFrame.running_ = true;
  180. goog.dom.animationFrame.requestedFrame_ = false;
  181. var tasksArray = goog.dom.animationFrame
  182. .tasks_[goog.dom.animationFrame.doubleBufferIndex_];
  183. var taskLength = tasksArray.length;
  184. // During the runTasks_, if there is a recursive call to queue up more
  185. // task(s) for the next frame, we use double-buffering for that.
  186. goog.dom.animationFrame.doubleBufferIndex_ =
  187. (goog.dom.animationFrame.doubleBufferIndex_ + 1) % 2;
  188. var task;
  189. // Run all the measure tasks first.
  190. for (var i = 0; i < taskLength; ++i) {
  191. task = tasksArray[i];
  192. var measureTask = task.measureTask;
  193. task.isScheduled = false;
  194. if (measureTask.fn) {
  195. // TODO (perumaal): Handle any exceptions thrown by the lambda.
  196. measureTask.fn.apply(measureTask.context, task.args);
  197. }
  198. }
  199. // Run the mutate tasks next.
  200. for (var i = 0; i < taskLength; ++i) {
  201. task = tasksArray[i];
  202. var mutateTask = task.mutateTask;
  203. task.isScheduled = false;
  204. if (mutateTask.fn) {
  205. // TODO (perumaal): Handle any exceptions thrown by the lambda.
  206. mutateTask.fn.apply(mutateTask.context, task.args);
  207. }
  208. // Clear state for next vsync.
  209. task.state = {};
  210. }
  211. // Clear the tasks array as we have finished processing all the tasks.
  212. tasksArray.length = 0;
  213. goog.dom.animationFrame.running_ = false;
  214. };
  215. /**
  216. * @return {boolean} Whether the animationframe is currently running. For use
  217. * by callers who need not to delay tasks scheduled during runTasks_ for an
  218. * additional frame.
  219. */
  220. goog.dom.animationFrame.isRunning = function() {
  221. return goog.dom.animationFrame.running_;
  222. };
  223. /**
  224. * Request {@see goog.dom.animationFrame.runTasks_} to be called upon the
  225. * next animation frame if we haven't done so already.
  226. * @private
  227. */
  228. goog.dom.animationFrame.requestAnimationFrame_ = function() {
  229. if (goog.dom.animationFrame.requestedFrame_) {
  230. return;
  231. }
  232. goog.dom.animationFrame.requestedFrame_ = true;
  233. window.requestAnimationFrame(goog.dom.animationFrame.runTasks_);
  234. };