blob: b9bccff37cf5a69fdeecefc6fa9b576c59af5ca9 [file] [log] [blame]
// 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_);
};