289 lines
8.0 KiB
JavaScript
289 lines
8.0 KiB
JavaScript
// 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 genericSpec = /** @type {!goog.dom.animationFrame.Spec} */ (spec);
|
|
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_);
|
|
};
|