Bug 522504: Panning speed is sometimes erratic [r=bcombee]
authorBenjamin Stover <bstover@mozilla.com>
Mon, 19 Oct 2009 16:33:34 -0400
changeset 65683 95aed39e31ffdbef96f5135edc1dcfbc5fb77137
parent 65682 31a9537c6978fdb938d405e441cc9f1c4b52e8f8
child 65684 a8fb14782bfa9d53e45cce93e56e49c8c7a39032
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbcombee
bugs522504
Bug 522504: Panning speed is sometimes erratic [r=bcombee]
mobile/chrome/content/InputHandler.js
--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -37,16 +37,37 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
+// how many msecs elapse before two taps are not a double tap
+const kDoubleClickInterval = 400;
+
+// milliseconds between mouse down and drag direction determined
+const kMsUntilLock = 50;
+
+// threshold in pixels for sensing a tap as opposed to a pan
+const kTapRadius = 15;
+
+// how many milliseconds between each kinetic pan update
+const kKineticUpdateInterval = 25;
+
+// How much speed is removed every update
+const kDecelerationRate = .10;
+
+// How sensitive kinetic scroll is to mouse movement
+const kSpeedSensitivity = .8;
+
+// How relevant x earlier milliseconds is to determining the speed.
+const kTimeRelevance = .01;
+
 /**
  * InputHandler
  *
  * The input handler is an arbiter between the Fennec chrome window inputs and any
  * registered input modules.  It keeps an array of input module objects.  Incoming
  * input events are wrapped in an EventInfo object and passed down to the input modules
  * in the order of the modules array.  Every registed module thus gets called with
  * an EventInfo for each event that the InputHandler is registered to listen for.
@@ -394,28 +415,28 @@ InputHandler.EventInfo.prototype = {
  *     cx1, cy1, and second click at client coordinates cx2, cy2.
  *
  * There is a default dragger in case a scrollable element is dragged --- see
  * the defaultDragger prototype property.  There is no default clicker.
  */
 function MouseModule(owner, browserViewContainer) {
   this._owner = owner;
   this._browserViewContainer = browserViewContainer;
-  this._dragData = new DragData(this, 15, 200);
+  this._dragData = new DragData(this, kTapRadius);
 
   this._dragger = null;
   this._clicker = null;
 
   this._downUpEvents = [];
   this._targetScrollInterface = null;
 
   var self = this;
   this._kinetic = new KineticController(
     function _dragByBound(dx, dy) { return self._dragBy(dx, dy); },
-    function _dragStopBound() { return self._doDragStop(0, 0, true); }
+    function _dragStopBound() { return self._doDragStop(0, 0, 0, true); }
   );
 }
 
 
 MouseModule.prototype = {
   handleEvent: function handleEvent(evInfo) {
     if (evInfo.event.button !== 0) // avoid all but a clean left click
       return;
@@ -511,18 +532,18 @@ MouseModule.prototype = {
    */
   _onMouseUp: function _onMouseUp(evInfo) {
     let dragData = this._dragData;
 
     let [sX, sY] = dragData.lockAxis(evInfo.event.screenX, evInfo.event.screenY);
 
     this._movedOutOfRadius = this._movedOutOfRadius || dragData.isPointOutsideRadius(sX, sY);
 
-    if (dragData.dragging)       // XXX same check as this._dragger but we
-      this._doDragStop(sX, sY);  //  are using both, no good reason
+    if (dragData.dragging)                               // XXX same check as this._dragger but we
+      this._doDragStop(sX, sY, evInfo.event.timeStamp);  //  are using both, no good reason
 
     if (this._clicker)
       this._clicker.mouseUp(evInfo.event.clientX, evInfo.event.clientY);
 
     if (this._targetIsContent(evInfo.event)) {
       this._recordEvent(evInfo);
       this._doClick(this._movedOutOfRadius);
     }
@@ -537,17 +558,17 @@ MouseModule.prototype = {
    */
   _onMouseMove: function _onMouseMove(evInfo) {
     let dragData = this._dragData;
 
     if (dragData.dragging) {
       let [sX, sY] = dragData.lockAxis(evInfo.event.screenX, evInfo.event.screenY);
       evInfo.event.stopPropagation();
       evInfo.event.preventDefault();
-      this._doDragMove(sX, sY);
+      this._doDragMove(sX, sY, evInfo.event.timeStamp);
     }
 
     this._movedOutOfRadius = this._movedOutOfRadius || 
       dragData.isPointOutsideRadius(evInfo.event.screenX, evInfo.event.screenY);
   },
 
   /**
    * Check if the event concern the browser content
@@ -567,53 +588,53 @@ MouseModule.prototype = {
 
   /**
    * Inform our dragger of a dragStart and update kinetic with new data.
    */
   _doDragStart: function _doDragStart(event) {
     let dragData = this._dragData;
 
     dragData.setDragStart(event.screenX, event.screenY);
-    this._kinetic.addData(event.screenX, event.screenY);
+    this._kinetic.addData(event.screenX, event.screenY, event.timeStamp);
 
     this._dragger.dragStart(event.clientX, event.clientY, event.target, this._targetScrollInterface);
   },
 
   /**
    * Finish a drag.  The third parameter is a secret one used to distinguish
    * between the supposed end of drag caused by a mouseup and the real end
    * of drag which happens when KineticController::end() is called.
    */
-  _doDragStop: function _doDragStop(sX, sY, kineticStop) {
+  _doDragStop: function _doDragStop(sX, sY, t, kineticStop) {
     let dragData = this._dragData;
 
     if (!kineticStop) {    // we're not really done, since now it is
                            // kinetic's turn to scroll about
       let dx = dragData.sX - sX;
       let dy = dragData.sY - sY;
 
       dragData.endDrag();
 
-      this._kinetic.addData(sX, sY);
+      this._kinetic.addData(sX, sY, t);
 
       this._kinetic.start();
     } else {               // now we're done, says our secret 3rd argument
       this._dragger.dragStop(0, 0, this._targetScrollInterface);
       dragData.reset();
     }
   },
 
   /**
    * Update kinetic with new data and drag.
    */
-  _doDragMove: function _doDragMove(sX, sY) {
+  _doDragMove: function _doDragMove(sX, sY, t) {
     let dragData = this._dragData;
     let dX = dragData.sX - sX;
     let dY = dragData.sY - sY;
-    this._kinetic.addData(sX, sY);
+    this._kinetic.addData(sX, sY, t);
     return this._dragBy(dX, dY);
   },
 
   /**
    * Used by _doDragMove() above and by KineticController's timer to do the
    * actual dragMove signalling to the dragger.  We'd put this in _doDragMove()
    * but then KineticController would be adding to its own data as it signals
    * the dragger of dragMove()s.
@@ -646,24 +667,23 @@ MouseModule.prototype = {
   /**
    * Commit another click event to our click buffer.  The `click buffer' is a
    * timeout initiated by the first click.  If the timeout is still alive when
    * another click is committed, then the click buffer forms a double click, and
    * the timeout is cancelled.  Otherwise, the timeout issues a single click to
    * the clicker.
    */
   _commitAnotherClick: function _commitAnotherClick() {
-    const doubleClickInterval = 400;
 
     if (this._clickTimeout) {   // we're waiting for a second click for double
       window.clearTimeout(this._clickTimeout);
       this._doDoubleClick();
     } else {
       this._clickTimeout = window.setTimeout(function _clickTimeout(self) { self._doSingleClick(); },
-                                             doubleClickInterval, this);
+                                             kDoubleClickInterval, this);
     }
   },
 
   /**
    * Endpoint of _commitAnotherClick().  Finalize a single click and tell the clicker.
    */
   _doSingleClick: function _doSingleClick() {
     //dump('doing single click with ' + this._downUpEvents.length + '\n');
@@ -819,25 +839,22 @@ MouseModule.prototype = {
       + '\n\tclickTimeout=' + this._clickTimeout + '\n  }';
   }
 };
 
 /**
  * DragData handles processing drags on the screen, handling both
  * locking of movement on one axis, and click detection.
  */
-function DragData(owner, dragRadius, dragStartTimeoutLength) {
+function DragData(owner, dragRadius) {
   this._owner = owner;
   this._dragRadius = dragRadius;
   this.reset();
 };
 
-/* milliseconds between mouse down and drag direction determined */
-const kMsUntilLock = 50;
-
 DragData.prototype = {
   reset: function reset() {
     this.dragging = false;
     this.sX = null;
     this.sY = null;
     this.alreadyLocked = false;
     this.lockedX = null;
     this.lockedY = null;
@@ -907,17 +924,17 @@ DragData.prototype = {
 
     return [sX, sY];
   },
 
   isPointOutsideRadius: function isPointOutsideRadius(sX, sY) {
     if (this._originX === null)
       return false;
     return (Math.pow(sX - this._originX, 2) + Math.pow(sY - this._originY, 2)) >
-      (2 * Math.pow(this._dragRadius, 2));
+      (Math.pow(this._dragRadius, 2));
   },
 
   toString: function toString() {
     return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }';
   }
 };
 
 
@@ -931,26 +948,39 @@ DragData.prototype = {
  */
 function KineticController(aPanBy, aEndCallback) {
   this._panBy = aPanBy;
   this._timer = null;
   this._beforeEnd = aEndCallback;
 
   try {
     this._updateInterval = gPrefService.getIntPref("browser.ui.kinetic.updateInterval");
-    // In preferences this value is an int.  We divide so that it can be between 0 and 1;
-    this._emaAlpha = gPrefService.getIntPref("browser.ui.kinetic.ema.alphaValue") / 10;
+  } catch(e) {
+    this._updateInterval = kKineticUpdateInterval;
+  }
+
+  try {
     // In preferences this value is an int.  We divide so that it can be a percent.
     this._decelerationRate = gPrefService.getIntPref("browser.ui.kinetic.decelerationRate") / 100;
+  } catch (e) {
+    this._decelerationRate = kDecelerationRate;
+  };
+
+  try {
+    // In preferences this value is an int.  We divide so that it can be a percent.
+    this._speedSensitivity = gPrefService.getIntPref("browser.ui.kinetic.speedsensitivity") / 100;
+  } catch(e) {
+    this._speedSensitivity = kSpeedSensitivity;
   }
-  catch (e) {
-    this._updateInterval = 33;
-    this._emaAlpha = .8;
-    this._decelerationRate = .15;
-  };
+
+  try {
+    this._timeRelevance = gPrefService.getIntPref("browser.ui.kinetic.timerelevance") / 100;
+  } catch(e) {
+    this._timeRelevance = kTimeRelevance;
+  }
 
   this._reset();
 }
 
 KineticController.prototype = {
   _reset: function _reset() {
     if (this._timer != null) {
       this._timer.cancel();
@@ -978,17 +1008,16 @@ KineticController.prototype = {
         //dump("             speeds: " + self._speedX + " " + self._speedY + "\n");
 
         if (self._speedX == 0 && self._speedY == 0) {
           self.end();
           return;
         }
         let dx = Math.round(self._speedX * self._updateInterval);
         let dy = Math.round(self._speedY * self._updateInterval);
-        //dump("dx, dy: " + dx + " " + dy + "\n");
 
         let panned = false;
         try { panned = self._panBy(-dx, -dy); } catch (e) {}
         if (!panned) {
           self.end();
           return;
         }
 
@@ -1020,58 +1049,66 @@ KineticController.prototype = {
     let mblen = this.momentumBuffer.length;
 
     // If we don't have at least 2 events do not do kinetic panning
     if (mblen < 2) {
       this.end();
       return false;
     }
 
-    function ema(currentSpeed, lastSpeed, alpha) {
-      return alpha * currentSpeed + (1 - alpha) * lastSpeed;
-    };
+    let lastTime = mb[mblen - 1].t;
+    let weightedSpeedSumX = 0;
+    let weightedSpeedSumY = 0;
+    let weightSum = 0;
+    let prev = mb[0];
 
-    // build arrays of each movement's speed in pixels/ms
-    let prev = mb[0];
+    // determine speed based on recorded input, giving the most weight to inputs
+    // closest to the end
     for (let i = 1; i < mblen; i++) {
       let me = mb[i];
-
+      let weight = Math.exp((me.t - lastTime) * this._timeRelevance);
       let timeDiff = me.t - prev.t;
-
-      this._speedX = ema( ((me.sx - prev.sx) / timeDiff), this._speedX, this._emaAlpha);
-      this._speedY = ema( ((me.sy - prev.sy) / timeDiff), this._speedY, this._emaAlpha);
-
+      weightSum += weight;
+      weightedSpeedSumX += weight * (me.sx - prev.sx) / timeDiff;
+      weightedSpeedSumY += weight * (me.sy - prev.sy) / timeDiff;
       prev = me;
     }
 
+    this._speedX = (weightedSpeedSumX / weightSum) * this._speedSensitivity;
+    this._speedY = (weightedSpeedSumY / weightSum) * this._speedSensitivity;
+
     // fire off our kinetic timer which will do all the work
     this._startTimer();
 
     return true;
   },
 
   end: function end() {
     if (this._beforeEnd)
       this._beforeEnd();
     this._reset();
   },
 
-  addData: function addData(sx, sy) {
+  addData: function addData(sx, sy, t) {
     // if we're active, end that move before adding data
     if (this.isActive())
       this.end();
 
     let mbLength = this.momentumBuffer.length;
+    let now = t || Date.now();
+ 
     // avoid adding duplicates which would otherwise slow down the speed
-    let now = Date.now();
- 
     if (mbLength > 0) {
       let mbLast = this.momentumBuffer[mbLength - 1];
-      if ((mbLast.sx == sx && mbLast.sy == sy) || mbLast.t == now)
+      if ((mbLast.sx == sx && mbLast.sy == sy) || mbLast.t == now) {
+        mbLast.sx = sx;
+        mbLast.sy = sy;
+        mbLast.t = t;
         return;
+      }
     }
 
     // Util.dumpLn("adding t:", now, ", sx: ", sx, ", sy: ", sy);
     this.momentumBuffer.push({'t': now, 'sx' : sx, 'sy' : sy});
   }
 };
 
 /**