// 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: * * * * Programmatic: *
 * 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);
 * });
 * 
* * 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>} */ 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() { // 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( /** @type {goog.dom.animationFrame.TaskSet_} */ (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_); };