| // Copyright 2014 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview goog.dom.animationFrame permits work to be done in-sync with |
| * the render refresh rate of the browser and to divide work up globally based |
| * on whether the intent is to measure or to mutate the DOM. The latter avoids |
| * repeated style recalculation which can be really slow. |
| * |
| * Goals of the API: |
| * <ul> |
| * <li>Make it easy to schedule work for the next animation frame. |
| * <li>Make it easy to only do work once per animation frame, even if two |
| * events fire that trigger the same work. |
| * <li>Make it easy to do all work in two phases to avoid repeated style |
| * recalculation caused by interleaved reads and writes. |
| * <li>Avoid creating closures per schedule operation. |
| * </ul> |
| * |
| * |
| * Programmatic: |
| * <pre> |
| * var animationTask = goog.dom.animationFrame.createTask({ |
| * measure: function(state) { |
| * state.width = goog.style.getSize(elem).width; |
| * this.animationTask(); |
| * }, |
| * mutate: function(state) { |
| * goog.style.setWidth(elem, Math.floor(state.width / 2)); |
| * } |
| * }, this); |
| * }); |
| * </pre> |
| * |
| * See also |
| * https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame |
| */ |
| |
| goog.provide('goog.dom.animationFrame'); |
| goog.provide('goog.dom.animationFrame.Spec'); |
| goog.provide('goog.dom.animationFrame.State'); |
| |
| goog.require('goog.dom.animationFrame.polyfill'); |
| |
| // Install the polyfill. |
| goog.dom.animationFrame.polyfill.install(); |
| |
| |
| /** |
| * @typedef {{ |
| * id: number, |
| * fn: !Function, |
| * context: (!Object|undefined) |
| * }} |
| * @private |
| */ |
| goog.dom.animationFrame.Task_; |
| |
| |
| /** |
| * @typedef {{ |
| * measureTask: goog.dom.animationFrame.Task_, |
| * mutateTask: goog.dom.animationFrame.Task_, |
| * state: (!Object|undefined), |
| * args: (!Array|undefined), |
| * isScheduled: boolean |
| * }} |
| * @private |
| */ |
| goog.dom.animationFrame.TaskSet_; |
| |
| |
| /** |
| * @typedef {{ |
| * measure: (!Function|undefined), |
| * mutate: (!Function|undefined) |
| * }} |
| */ |
| goog.dom.animationFrame.Spec; |
| |
| |
| |
| /** |
| * A type to represent state. Users may add properties as desired. |
| * @constructor |
| * @final |
| */ |
| goog.dom.animationFrame.State = function() {}; |
| |
| |
| /** |
| * Saves a set of tasks to be executed in the next requestAnimationFrame phase. |
| * This list is initialized once before any event firing occurs. It is not |
| * affected by the fired events or the requestAnimationFrame processing (unless |
| * a new event is created during the processing). |
| * @private {!Array<!Array<goog.dom.animationFrame.TaskSet_>>} |
| */ |
| goog.dom.animationFrame.tasks_ = [[], []]; |
| |
| |
| /** |
| * Values are 0 or 1, for whether the first or second array should be used to |
| * lookup or add tasks. |
| * @private {number} |
| */ |
| goog.dom.animationFrame.doubleBufferIndex_ = 0; |
| |
| |
| /** |
| * Whether we have already requested an animation frame that hasn't happened |
| * yet. |
| * @private {boolean} |
| */ |
| goog.dom.animationFrame.requestedFrame_ = false; |
| |
| |
| /** |
| * Counter to generate IDs for tasks. |
| * @private {number} |
| */ |
| goog.dom.animationFrame.taskId_ = 0; |
| |
| |
| /** |
| * Whether the animationframe runTasks_ loop is currently running. |
| * @private {boolean} |
| */ |
| goog.dom.animationFrame.running_ = false; |
| |
| |
| /** |
| * Returns a function that schedules the two passed-in functions to be run upon |
| * the next animation frame. Calling the function again during the same |
| * animation frame does nothing. |
| * |
| * The function under the "measure" key will run first and together with all |
| * other functions scheduled under this key and the function under "mutate" will |
| * run after that. |
| * |
| * @param {{ |
| * measure: (function(this:THIS, !goog.dom.animationFrame.State)|undefined), |
| * mutate: (function(this:THIS, !goog.dom.animationFrame.State)|undefined) |
| * }} spec |
| * @param {THIS=} opt_context Context in which to run the function. |
| * @return {function(...?)} |
| * @template THIS |
| */ |
| goog.dom.animationFrame.createTask = function(spec, opt_context) { |
| var id = goog.dom.animationFrame.taskId_++; |
| var measureTask = { |
| id: id, |
| fn: spec.measure, |
| context: opt_context |
| }; |
| var mutateTask = { |
| id: id, |
| fn: spec.mutate, |
| context: opt_context |
| }; |
| |
| var taskSet = { |
| measureTask: measureTask, |
| mutateTask: mutateTask, |
| state: {}, |
| args: undefined, |
| isScheduled: false |
| }; |
| |
| return function() { |
| // Default the context to the one that was used to call the tasks scheduler |
| // (this function). |
| if (!opt_context) { |
| measureTask.context = this; |
| mutateTask.context = this; |
| } |
| |
| // Save args and state. |
| if (arguments.length > 0) { |
| // The state argument goes last. That is kinda horrible but compatible |
| // with {@see wiz.async.method}. |
| if (!taskSet.args) { |
| taskSet.args = []; |
| } |
| taskSet.args.length = 0; |
| taskSet.args.push.apply(taskSet.args, arguments); |
| taskSet.args.push(taskSet.state); |
| } else { |
| if (!taskSet.args || taskSet.args.length == 0) { |
| taskSet.args = [taskSet.state]; |
| } else { |
| taskSet.args[0] = taskSet.state; |
| taskSet.args.length = 1; |
| } |
| } |
| if (!taskSet.isScheduled) { |
| taskSet.isScheduled = true; |
| var tasksArray = goog.dom.animationFrame.tasks_[ |
| goog.dom.animationFrame.doubleBufferIndex_]; |
| tasksArray.push(taskSet); |
| } |
| goog.dom.animationFrame.requestAnimationFrame_(); |
| }; |
| }; |
| |
| |
| /** |
| * Run scheduled tasks. |
| * @private |
| */ |
| goog.dom.animationFrame.runTasks_ = function() { |
| goog.dom.animationFrame.running_ = true; |
| goog.dom.animationFrame.requestedFrame_ = false; |
| var tasksArray = goog.dom.animationFrame |
| .tasks_[goog.dom.animationFrame.doubleBufferIndex_]; |
| var taskLength = tasksArray.length; |
| |
| // During the runTasks_, if there is a recursive call to queue up more |
| // task(s) for the next frame, we use double-buffering for that. |
| goog.dom.animationFrame.doubleBufferIndex_ = |
| (goog.dom.animationFrame.doubleBufferIndex_ + 1) % 2; |
| |
| var task; |
| |
| // Run all the measure tasks first. |
| for (var i = 0; i < taskLength; ++i) { |
| task = tasksArray[i]; |
| var measureTask = task.measureTask; |
| task.isScheduled = false; |
| if (measureTask.fn) { |
| // TODO (perumaal): Handle any exceptions thrown by the lambda. |
| measureTask.fn.apply(measureTask.context, task.args); |
| } |
| } |
| |
| // Run the mutate tasks next. |
| for (var i = 0; i < taskLength; ++i) { |
| task = tasksArray[i]; |
| var mutateTask = task.mutateTask; |
| task.isScheduled = false; |
| if (mutateTask.fn) { |
| // TODO (perumaal): Handle any exceptions thrown by the lambda. |
| mutateTask.fn.apply(mutateTask.context, task.args); |
| } |
| |
| // Clear state for next vsync. |
| task.state = {}; |
| } |
| |
| // Clear the tasks array as we have finished processing all the tasks. |
| tasksArray.length = 0; |
| goog.dom.animationFrame.running_ = false; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the animationframe is currently running. For use |
| * by callers who need not to delay tasks scheduled during runTasks_ for an |
| * additional frame. |
| */ |
| goog.dom.animationFrame.isRunning = function() { |
| return goog.dom.animationFrame.running_; |
| }; |
| |
| |
| /** |
| * Request {@see goog.dom.animationFrame.runTasks_} to be called upon the |
| * next animation frame if we haven't done so already. |
| * @private |
| */ |
| goog.dom.animationFrame.requestAnimationFrame_ = function() { |
| if (goog.dom.animationFrame.requestedFrame_) { |
| return; |
| } |
| goog.dom.animationFrame.requestedFrame_ = true; |
| window.requestAnimationFrame(goog.dom.animationFrame.runTasks_); |
| }; |