Bug 720992 - Tilt should use mozAnimation for all its animations, instead of intervals; r=rcampbell
authorVictor Porof <vporof@mozilla.com>
Fri, 03 Feb 2012 12:29:02 +0200
changeset 87770 3b55713894a6d9d569a4972739b141e6e8590a7a
parent 87769 22be219de9a63a4c8b9ba73ffef7291efb9be4a4
child 87771 4e33018f2643a7f6991f40804f6500bfe644fc7a
push id22146
push usertim.taubert@gmx.de
push dateMon, 27 Feb 2012 08:44:45 +0000
treeherdermozilla-central@85e309ee6d34 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs720992
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 720992 - Tilt should use mozAnimation for all its animations, instead of intervals; r=rcampbell
browser/devtools/tilt/Tilt.jsm
browser/devtools/tilt/TiltVisualizer.jsm
browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
browser/devtools/tilt/test/browser_tilt_arcball-reset.js
browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
browser/devtools/tilt/test/browser_tilt_visualizer.js
--- a/browser/devtools/tilt/Tilt.jsm
+++ b/browser/devtools/tilt/Tilt.jsm
@@ -122,17 +122,16 @@ Tilt.prototype = {
       return;
     }
 
     // create a visualizer instance for the current tab
     this.visualizers[id] = new TiltVisualizer({
       chromeWindow: this.chromeWindow,
       contentWindow: this.chromeWindow.gBrowser.selectedBrowser.contentWindow,
       parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
-      requestAnimationFrame: this.chromeWindow.mozRequestAnimationFrame,
       notifications: this.NOTIFICATIONS
     });
 
     // make sure the visualizer object was initialized properly
     if (!this.visualizers[id].isInitialized()) {
       this.destroy(id);
       return;
     }
--- a/browser/devtools/tilt/TiltVisualizer.jsm
+++ b/browser/devtools/tilt/TiltVisualizer.jsm
@@ -52,31 +52,31 @@ const INVISIBLE_ELEMENTS = {
   "option": true,
   "script": true,
   "style": true,
   "title": true
 };
 
 const STACK_THICKNESS = 15;
 const WIREFRAME_COLOR = [0, 0, 0, 0.25];
-const INTRO_TRANSITION_DURATION = 50;
-const OUTRO_TRANSITION_DURATION = 40;
+const INTRO_TRANSITION_DURATION = 1000;
+const OUTRO_TRANSITION_DURATION = 800;
 const INITIAL_Z_TRANSLATION = 400;
 const MOVE_INTO_VIEW_ACCURACY = 50;
 
 const MOUSE_CLICK_THRESHOLD = 10;
-const MOUSE_INTRO_DELAY = 10;
+const MOUSE_INTRO_DELAY = 200;
 const ARCBALL_SENSITIVITY = 0.5;
 const ARCBALL_ROTATION_STEP = 0.15;
 const ARCBALL_TRANSLATION_STEP = 35;
 const ARCBALL_ZOOM_STEP = 0.1;
 const ARCBALL_ZOOM_MIN = -3000;
 const ARCBALL_ZOOM_MAX = 500;
-const ARCBALL_RESET_FACTOR = 0.9;
-const ARCBALL_RESET_INTERVAL = 1000 / 60;
+const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1;
+const ARCBALL_RESET_LINEAR_FACTOR = 0.01;
 
 const TILT_CRAFTER = "resource:///modules/devtools/TiltWorkerCrafter.js";
 const TILT_PICKER = "resource:///modules/devtools/TiltWorkerPicker.js";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/TiltGL.jsm");
 Cu.import("resource:///modules/devtools/TiltMath.jsm");
 Cu.import("resource:///modules/devtools/TiltUtils.jsm");
@@ -87,17 +87,16 @@ let EXPORTED_SYMBOLS = ["TiltVisualizer"
 /**
  * Initializes the visualization presenter and controller.
  *
  * @param {Object} aProperties
  *                 an object containing the following properties:
  *        {Window} chromeWindow: a reference to the top level window
  *        {Window} contentWindow: the content window holding the visualized doc
  *       {Element} parentNode: the parent node to hold the visualization
- *      {Function} requestAnimationFrame: responsible with scheduling loops
  *        {Object} notifications: necessary notifications for Tilt
  *      {Function} onError: optional, function called if initialization failed
  *      {Function} onLoad: optional, function called if initialization worked
  */
 function TiltVisualizer(aProperties)
 {
   // make sure the properties parameter is a valid object
   aProperties = aProperties || {};
@@ -116,17 +115,16 @@ function TiltVisualizer(aProperties)
   });
 
   /**
    * Visualization logic and drawing loop.
    */
   this.presenter = new TiltVisualizer.Presenter(this.canvas,
     aProperties.chromeWindow,
     aProperties.contentWindow,
-    aProperties.requestAnimationFrame,
     aProperties.notifications,
     aProperties.onError || null,
     aProperties.onLoad || null);
 
   /**
    * Visualization mouse and keyboard controller.
    */
   this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter);
@@ -179,28 +177,25 @@ TiltVisualizer.prototype = {
  * This object manages the visualization logic and drawing loop.
  *
  * @param {HTMLCanvasElement} aCanvas
  *                            the canvas element used for rendering
  * @param {Window} aChromeWindow
  *                 a reference to the top-level window
  * @param {Window} aContentWindow
  *                 the content window holding the document to be visualized
- * @param {Function} aRequestAnimationFrame
- *                   function responsible with scheduling loop frames
  * @param {Object} aNotifications
  *                 necessary notifications for Tilt
  * @param {Function} onError
  *                   function called if initialization failed
  * @param {Function} onLoad
  *                   function called if initialization worked
  */
 TiltVisualizer.Presenter = function TV_Presenter(
-  aCanvas, aChromeWindow, aContentWindow, aRequestAnimationFrame, aNotifications,
-  onError, onLoad)
+  aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad)
 {
   /**
    * A canvas overlay used for drawing the visualization.
    */
   this.canvas = aCanvas;
 
   /**
    * Save a reference to the top-level window, to access InspectorUI or Tilt.
@@ -266,24 +261,39 @@ TiltVisualizer.Presenter = function TV_P
 
   /**
    * Variable specifying if the scene should be redrawn.
    * This should happen usually when the visualization is translated/rotated.
    */
   this.redraw = true;
 
   /**
-   * A frame counter, incremented each time the scene is redrawn.
+   * Total time passed since the rendering started.
+   * If the rendering is paused, this property won't get updated.
+   */
+  this.time = 0;
+
+  /**
+   * Frame delta time (the ammount of time passed for each frame).
+   * This is used to smoothly interpolate animation transfroms.
    */
-  this.frames = 0;
+  this.delta = 0;
+  this.prevFrameTime = 0;
+  this.currFrameTime = 0;
+
+  this.setup();
+  this.loop();
+};
+
+TiltVisualizer.Presenter.prototype = {
 
   /**
    * The initialization logic.
    */
-  let setup = function TVP_setup()
+  setup: function TVP_setup()
   {
     let renderer = this.renderer;
     let inspector = this.chromeWindow.InspectorUI;
 
     // if the renderer was destroyed, don't continue setup
     if (!renderer || !renderer.context) {
       return;
     }
@@ -296,56 +306,65 @@ TiltVisualizer.Presenter = function TV_P
       uniforms: ["mvMatrix", "projMatrix", "sampler"]
     });
 
     // get the document zoom to properly scale the visualization
     if (inspector.highlighter) {
       this.transforms.zoom = inspector.highlighter.zoom;
     }
 
+    // bind the owner object to the necessary functions
+    TiltUtils.bindObjectFunc(this, "^on");
+    TiltUtils.bindObjectFunc(this, "loop");
+
     this.setupTexture();
     this.setupMeshData();
     this.setupEventListeners();
     this.canvas.focus();
-  }.bind(this);
+  },
 
   /**
    * The animation logic.
    */
-  let loop = function TVP_loop()
+  loop: function TVP_loop()
   {
     let renderer = this.renderer;
 
     // if the renderer was destroyed, don't continue rendering
     if (!renderer || !renderer.context) {
       return;
     }
 
     // prepare for the next frame of the animation loop
-    aRequestAnimationFrame(loop);
+    this.chromeWindow.mozRequestAnimationFrame(this.loop);
 
     // only redraw if we really have to
     if (this.redraw) {
       this.redraw = false;
       this.drawVisualization();
     }
 
     // call the attached ondraw function and handle all keyframe notifications
     if ("function" === typeof this.ondraw) {
-      this.ondraw(this.frames);
+      this.ondraw(this.time, this.delta);
     }
 
+    this.handleFrameDelta();
     this.handleKeyframeNotifications();
-  }.bind(this);
+  },
 
-  setup();
-  loop();
-};
-
-TiltVisualizer.Presenter.prototype = {
+  /**
+   * Calculates the current frame delta time.
+   */
+  handleFrameDelta: function TVP_handleFrameDelta()
+  {
+    this.prevFrameTime = this.currFrameTime;
+    this.currFrameTime = this.chromeWindow.mozAnimationStartTime;
+    this.delta = this.currFrameTime - this.prevFrameTime;
+  },
 
   /**
    * Draws the visualization mesh and highlight quad.
    */
   drawVisualization: function TVP_drawVisualization()
   {
     let renderer = this.renderer;
     let transforms = this.transforms;
@@ -360,20 +379,20 @@ TiltVisualizer.Presenter.prototype = {
     // clear the context to an opaque black background
     renderer.clear();
     renderer.perspective();
 
     // apply a transition transformation using an ortho and perspective matrix
     let ortho = mat4.ortho(0, w, h, 0, -1000, 1000);
 
     if (!this.isExecutingDestruction) {
-      let f = this.frames / INTRO_TRANSITION_DURATION;
+      let f = this.time / INTRO_TRANSITION_DURATION;
       renderer.lerp(renderer.projMatrix, ortho, f, 8);
     } else {
-      let f = this.frames / OUTRO_TRANSITION_DURATION;
+      let f = this.time / OUTRO_TRANSITION_DURATION;
       renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8);
     }
 
     // apply the preliminary transformations to the model view
     renderer.translate(w * 0.5, h * 0.5, -INITIAL_Z_TRANSLATION);
 
     // calculate the camera matrix using the rotation and translation
     renderer.translate(transforms.translation[0], 0,
@@ -390,21 +409,21 @@ TiltVisualizer.Presenter.prototype = {
     // draw the visualization mesh
     renderer.strokeWeight(2);
     renderer.depthTest(true);
     this.drawMeshStacks();
     this.drawMeshWireframe();
     this.drawHighlight();
 
     // make sure the initial transition is drawn until finished
-    if (this.frames < INTRO_TRANSITION_DURATION ||
-        this.frames < OUTRO_TRANSITION_DURATION) {
+    if (this.time < INTRO_TRANSITION_DURATION ||
+        this.time < OUTRO_TRANSITION_DURATION) {
       this.redraw = true;
     }
-    this.frames++;
+    this.time += this.delta;
   },
 
   /**
    * Draws the meshStacks object.
    */
   drawMeshStacks: function TVP_drawMeshStacks()
   {
     let renderer = this.renderer;
@@ -610,19 +629,16 @@ TiltVisualizer.Presenter.prototype = {
     });
   },
 
   /**
    * Sets up event listeners necessary for the presenter.
    */
   setupEventListeners: function TVP_setupEventListeners()
   {
-    // bind the owner object to the necessary functions
-    TiltUtils.bindObjectFunc(this, "^on");
-
     this.contentWindow.addEventListener("resize", this.onResize, false);
   },
 
   /**
    * Called when the content window of the current browser is resized.
    */
   onResize: function TVP_onResize(e)
   {
@@ -788,18 +804,17 @@ TiltVisualizer.Presenter.prototype = {
     for (let i = 0, k = 36 * aNodeIndex; i < 36; i++) {
       meshData.vertices[i + k] = 0;
     }
 
     this.meshStacks.vertices = new renderer.VertexBuffer(meshData.vertices, 3);
     this.highlight.disabled = true;
     this.redraw = true;
 
-    Services.obs.notifyObservers(null,
-      this.NOTIFICATIONS.NODE_REMOVED, null);
+    Services.obs.notifyObservers(null, this.NOTIFICATIONS.NODE_REMOVED, null);
   },
 
   /**
    * Picks a stacked dom node at the x and y screen coordinates and issues
    * a callback function with the found intersection.
    *
    * @param {Number} x
    *                 the current horizontal coordinate of the mouse
@@ -904,35 +919,39 @@ TiltVisualizer.Presenter.prototype = {
   },
 
   /**
    * Handles notifications at specific frame counts.
    */
   handleKeyframeNotifications: function TV_handleKeyframeNotifications()
   {
     if (!TiltVisualizer.Prefs.introTransition && !this.isExecutingDestruction) {
-      this.frames = INTRO_TRANSITION_DURATION;
+      this.time = INTRO_TRANSITION_DURATION;
     }
     if (!TiltVisualizer.Prefs.outroTransition && this.isExecutingDestruction) {
-      this.frames = OUTRO_TRANSITION_DURATION;
+      this.time = OUTRO_TRANSITION_DURATION;
     }
 
-    if (this.frames === INTRO_TRANSITION_DURATION &&
+    if (this.time >= INTRO_TRANSITION_DURATION &&
+       !this.initializationFinished &&
        !this.isExecutingDestruction) {
 
+      this.initializationFinished = true;
       Services.obs.notifyObservers(null, this.NOTIFICATIONS.INITIALIZED, null);
 
       if ("function" === typeof this.onInitializationFinished) {
         this.onInitializationFinished();
       }
     }
 
-    if (this.frames === OUTRO_TRANSITION_DURATION &&
+    if (this.time >= OUTRO_TRANSITION_DURATION &&
+       !this.destructionFinished &&
         this.isExecutingDestruction) {
 
+      this.destructionFinished = true;
       Services.obs.notifyObservers(null, this.NOTIFICATIONS.BEFORE_DESTROYED, null);
 
       if ("function" === typeof this.onDestructionFinished) {
         this.onDestructionFinished();
       }
     }
   },
 
@@ -948,18 +967,18 @@ TiltVisualizer.Presenter.prototype = {
     if (!this.isExecutingDestruction) {
       this.isExecutingDestruction = true;
       this.onDestructionFinished = aCallback;
 
       // if we execute the destruction after the initialization finishes,
       // proceed normally; otherwise, skip everything and immediately issue
       // the callback
 
-      if (this.frames > OUTRO_TRANSITION_DURATION) {
-        this.frames = 0;
+      if (this.time > OUTRO_TRANSITION_DURATION) {
+        this.time = 0;
         this.redraw = true;
       } else {
         aCallback();
       }
     }
   },
 
   /**
@@ -1047,17 +1066,17 @@ TiltVisualizer.Controller = function TV_
   // bind the owner object to the necessary functions
   TiltUtils.bindObjectFunc(this, "update");
   TiltUtils.bindObjectFunc(this, "^on");
 
   // add the necessary event listeners
   this.addEventListeners();
 
   // attach this controller's update function to the presenter ondraw event
-  aPresenter.ondraw = this.update;
+  this.presenter.ondraw = this.update;
 };
 
 TiltVisualizer.Controller.prototype = {
 
   /**
    * Adds events listeners required by this controller.
    */
   addEventListeners: function TVC_addEventListeners()
@@ -1101,38 +1120,40 @@ TiltVisualizer.Controller.prototype = {
     canvas.removeEventListener("blur", this.onBlur, false);
 
     presenter.contentWindow.removeEventListener("resize", this.onResize,false);
   },
 
   /**
    * Function called each frame, updating the visualization camera transforms.
    *
-   * @param {Number} aFrames
-   *                 the current animation frame count
+   * @param {Number} aTime
+   *                 total time passed since rendering started
+   * @param {Number} aDelta
+   *                 the current animation frame delta
    */
-  update: function TVC_update(aFrames)
+  update: function TVC_update(aTime, aDelta)
   {
-    this.frames = aFrames;
-    this.coordinates = this.arcball.update();
+    this.time = aTime;
+    this.coordinates = this.arcball.update(aDelta);
 
     this.presenter.setRotation(this.coordinates.rotation);
     this.presenter.setTranslation(this.coordinates.translation);
   },
 
   /**
    * Called once after every time a mouse button is pressed.
    */
   onMouseDown: function TVC_onMouseDown(e)
   {
     e.target.focus();
     e.preventDefault();
     e.stopPropagation();
 
-    if (this.frames < MOUSE_INTRO_DELAY) {
+    if (this.time < MOUSE_INTRO_DELAY) {
       return;
     }
 
     // calculate x and y coordinates using using the client and target offset
     let button = e.which;
     this._downX = e.clientX - e.target.offsetLeft;
     this._downY = e.clientY - e.target.offsetTop;
 
@@ -1142,17 +1163,17 @@ TiltVisualizer.Controller.prototype = {
   /**
    * Called every time a mouse button is released.
    */
   onMouseUp: function TVC_onMouseUp(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
-    if (this.frames < MOUSE_INTRO_DELAY) {
+    if (this.time < MOUSE_INTRO_DELAY) {
       return;
     }
 
     // calculate x and y coordinates using using the client and target offset
     let button = e.which;
     let upX = e.clientX - e.target.offsetLeft;
     let upY = e.clientY - e.target.offsetTop;
 
@@ -1170,17 +1191,17 @@ TiltVisualizer.Controller.prototype = {
   /**
    * Called every time the mouse moves.
    */
   onMouseMove: function TVC_onMouseMove(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
-    if (this.frames < MOUSE_INTRO_DELAY) {
+    if (this.time < MOUSE_INTRO_DELAY) {
       return;
     }
 
     // calculate x and y coordinates using using the client and target offset
     let moveX = e.clientX - e.target.offsetLeft;
     let moveY = e.clientY - e.target.offsetTop;
 
     this.arcball.mouseMove(moveX, moveY);
@@ -1299,16 +1320,17 @@ TiltVisualizer.Controller.prototype = {
    * Function called when this object is destroyed.
    */
   finalize: function TVC_finalize()
   {
     TiltUtils.destroyObject(this.arcball);
     TiltUtils.destroyObject(this.coordinates);
 
     this.removeEventListeners();
+    this.presenter.controller = null;
     this.presenter.ondraw = null;
   }
 };
 
 /**
  * This is a general purpose 3D rotation controller described by Ken Shoemake
  * in the Graphics Interface ’92 Proceedings. It features good behavior
  * easy implementation, cheap execution.
@@ -1389,19 +1411,22 @@ TiltVisualizer.Arcball = function TV_Arc
 
 TiltVisualizer.Arcball.prototype = {
 
   /**
    * Call this function whenever you need the updated rotation quaternion
    * and the zoom amount. These values will be returned as "rotation" and
    * "translation" properties inside an object.
    *
+   * @param {Number} aDelta
+   *                 the current animation frame delta
+   *
    * @return {Object} the rotation quaternion and the translation amount
    */
-  update: function TVA_update()
+  update: function TVA_update(aDelta)
   {
     let mousePress = this._mousePress;
     let mouseRelease = this._mouseRelease;
     let mouseMove = this._mouseMove;
     let mouseLerp = this._mouseLerp;
     let mouseButton = this._mouseButton;
 
     // smoothly update the mouse coordinates
@@ -1554,16 +1579,21 @@ TiltVisualizer.Arcball.prototype = {
       (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY;
 
     // create an additional rotation based on the key events
     quat4.fromEuler(deltaAdditionalRot[0], deltaAdditionalRot[1], 0, deltaRot);
 
     // create an additional translation based on the key events
     vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans);
 
+    // handle the reset animation steps if necessary
+    if (this._resetInProgress) {
+      this._nextResetStep(aDelta || 1);
+    }
+
     // return the current rotation and translation
     return {
       rotation: quat4.multiply(deltaRot, currentRot),
       translation: vec3.add(deltaTrans, currentTrans)
     };
   },
 
   /**
@@ -1578,17 +1608,17 @@ TiltVisualizer.Arcball.prototype = {
    *                 which mouse button was pressed
    */
   mouseDown: function TVA_mouseDown(x, y, aButton)
   {
     // save the mouse down state and prepare for rotations or translations
     this._mousePress[0] = x;
     this._mousePress[1] = y;
     this._mouseButton = aButton;
-    this._cancelResetInterval();
+    this._cancelReset();
     this._save();
 
     // find the sphere coordinates of the mouse positions
     this.pointToSphere(
       x, y, this.width, this.height, this.radius, this._startVec);
 
     quat4.set(this._currentRot, this._lastRot);
   },
@@ -1654,31 +1684,31 @@ TiltVisualizer.Arcball.prototype = {
    * Call this, for example, when the mouse wheel was scrolled or zoom keys
    * were pressed.
    *
    * @param {Number} aZoom
    *                 the zoom direction and speed
    */
   zoom: function TVA_zoom(aZoom)
   {
-    this._cancelResetInterval();
+    this._cancelReset();
     this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom,
       ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX);
   },
 
   /**
    * Function handling the keyDown event.
    * Call this when a key was pressed.
    *
    * @param {Number} aCode
    *                 the code corresponding to the key pressed
    */
   keyDown: function TVA_keyDown(aCode)
   {
-    this._cancelResetInterval();
+    this._cancelReset();
     this._keyCode[aCode] = true;
   },
 
   /**
    * Function handling the keyUp event.
    * Call this when a key was released.
    *
    * @param {Number} aCode
@@ -1803,79 +1833,88 @@ TiltVisualizer.Arcball.prototype = {
    */
   reset: function TVA_reset(aFinalTranslation, aFinalRotation)
   {
     if ("function" === typeof this.onResetStart) {
       this.onResetStart();
       this.onResetStart = null;
     }
 
-    let func = this._nextResetIntervalStep.bind(this);
-
     this.cancelMouseEvents();
     this.cancelKeyEvents();
-    this._cancelResetInterval();
+    this._cancelReset();
 
     this._save();
     this._resetFinalTranslation = vec3.create(aFinalTranslation);
     this._resetFinalRotation = quat4.create(aFinalRotation);
-    this._resetInterval =
-      this.chromeWindow.setInterval(func, ARCBALL_RESET_INTERVAL);
+    this._resetInProgress = true;
   },
 
   /**
    * Cancels the current arcball reset animation if there is one.
    */
-  _cancelResetInterval: function TVA__cancelResetInterval()
+  _cancelReset: function TVA__cancelReset()
   {
-    if (this._resetInterval) {
-      this.chromeWindow.clearInterval(this._resetInterval);
-
-      this._resetInterval = null;
+    if (this._resetInProgress) {
+      this._resetInProgress = false;
       this._save();
 
       if ("function" === typeof this.onResetFinish) {
         this.onResetFinish();
         this.onResetFinish = null;
+        this.onResetStep = null;
       }
     }
   },
 
   /**
    * Executes the next step in the arcball reset animation.
+   *
+   * @param {Number} aDelta
+   *                 the current animation frame delta
    */
-  _nextResetIntervalStep: function TVA__nextResetIntervalStep()
+  _nextResetStep: function TVA__nextResetStep(aDelta)
   {
-    let fDelta = EPSILON * EPSILON;
+    // a very large animation frame delta (in case of seriously low framerate)
+    // would cause all the interpolations to become highly unstable
+    aDelta = TiltMath.clamp(aDelta, 1, 100);
+
+    let fNearZero = EPSILON * EPSILON;
+    let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta;
+    let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR;
     let fTran = this._resetFinalTranslation;
     let fRot = this._resetFinalRotation;
 
     let t = vec3.create(fTran);
     let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot);
 
     // reset the rotation quaternion and translation vector
-    vec3.lerp(this._currentTrans, t, ARCBALL_RESET_FACTOR / 4);
-    quat4.slerp(this._currentRot, r, 1 - ARCBALL_RESET_FACTOR);
+    vec3.lerp(this._currentTrans, t, fInterpLin);
+    quat4.slerp(this._currentRot, r, fInterpSph);
 
     // also reset any additional transforms by the keyboard or mouse
-    vec3.scale(this._additionalTrans, ARCBALL_RESET_FACTOR);
-    vec3.scale(this._additionalRot, ARCBALL_RESET_FACTOR);
-    this._zoomAmount *= ARCBALL_RESET_FACTOR;
+    vec3.scale(this._additionalTrans, fInterpLin);
+    vec3.scale(this._additionalRot, fInterpLin);
+    this._zoomAmount *= fInterpLin;
 
     // clear the loop if the all values are very close to zero
-    if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fDelta &&
-        vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fDelta &&
-        vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fDelta &&
-        vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fDelta &&
-        vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fDelta &&
-        vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fDelta &&
-        vec3.length(this._additionalRot) < fDelta &&
-        vec3.length(this._additionalTrans) < fDelta) {
+    if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero &&
+        vec3.length(this._additionalRot) < fNearZero &&
+        vec3.length(this._additionalTrans) < fNearZero) {
 
-      this._cancelResetInterval();
+      this._cancelReset();
+    }
+
+    if ("function" === typeof this.onResetStep) {
+      this.onResetStep();
     }
   },
 
   /**
    * Loads the keys to control this arcball.
    */
   _loadKeys: function TVA__loadKeys()
   {
@@ -1924,17 +1963,17 @@ TiltVisualizer.Arcball.prototype = {
     }
   },
 
   /**
    * Function called when this object is destroyed.
    */
   finalize: function TVA_finalize()
   {
-    this._cancelResetInterval();
+    this._cancelReset();
   }
 };
 
 /**
  * Tilt configuration preferences.
  */
 TiltVisualizer.Prefs = {
 
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
@@ -61,16 +61,28 @@ function performTest(canvas, arcball, ca
 
       window.setTimeout(function() {
         info("Synthesizing arcball reset key press.");
 
         arcball.onResetStart = function() {
           info("Starting arcball reset animation.");
         };
 
+        arcball.onResetStep = function() {
+          info("\nlastRot: " + quat4.str(arcball._lastRot) +
+               "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+               "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+               "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+               "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+               "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+               "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+               "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+               "\nzoomAmount: " + arcball._zoomAmount);
+        };
+
         arcball.onResetFinish = function() {
           ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
             "The arcball _lastRot field wasn't reset correctly.");
           ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
             "The arcball _deltaRot field wasn't reset correctly.");
           ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
             "The arcball _currentRot field wasn't reset correctly.");
 
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
@@ -59,16 +59,28 @@ function performTest(canvas, arcball, ca
 
       window.setTimeout(function() {
         info("Synthesizing arcball reset key press.");
 
         arcball.onResetStart = function() {
           info("Starting arcball reset animation.");
         };
 
+        arcball.onResetStep = function() {
+          info("\nlastRot: " + quat4.str(arcball._lastRot) +
+               "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+               "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+               "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+               "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+               "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+               "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+               "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+               "\nzoomAmount: " + arcball._zoomAmount);
+        };
+
         arcball.onResetFinish = function() {
           ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
             "The arcball _lastRot field wasn't reset correctly.");
           ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
             "The arcball _deltaRot field wasn't reset correctly.");
           ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
             "The arcball _currentRot field wasn't reset correctly.");
 
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
@@ -35,17 +35,17 @@ function test() {
   });
 }
 
 function whenHighlighting() {
   ok(presenter._currentSelection > 0,
     "Highlighting a node didn't work properly.");
   ok(!presenter.highlight.disabled,
     "After highlighting a node, it should be highlighted. D'oh.");
-  ok(presenter.controller.arcball._resetInterval,
+  ok(presenter.controller.arcball._resetInProgress,
     "Highlighting a node that's not already visible should trigger a reset!");
 
   executeSoon(function() {
     Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
     presenter.highlightNode(null);
   });
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
@@ -34,17 +34,17 @@ function test() {
   });
 }
 
 function whenHighlighting() {
   ok(presenter._currentSelection > 0,
     "Highlighting a node didn't work properly.");
   ok(!presenter.highlight.disabled,
     "After highlighting a node, it should be highlighted. D'oh.");
-  ok(!presenter.controller.arcball._resetInterval,
+  ok(!presenter.controller.arcball._resetInProgress,
     "Highlighting a node that's already visible shouldn't trigger a reset.");
 
   executeSoon(function() {
     Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
     presenter.highlightNode(null);
   });
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_visualizer.js
+++ b/browser/devtools/tilt/test/browser_tilt_visualizer.js
@@ -14,17 +14,16 @@ function test() {
 
   let webGLError = false;
   let webGLLoad = false;
 
   let visualizer = new TiltVisualizer({
     chromeWindow: window,
     contentWindow: gBrowser.selectedBrowser.contentWindow,
     parentNode: gBrowser.selectedBrowser.parentNode,
-    requestAnimationFrame: window.mozRequestAnimationFrame,
     inspectorUI: window.InspectorUI,
 
     onError: function onWebGLError()
     {
       webGLError = true;
     },
 
     onLoad: function onWebGLLoad()