Bug 538682 - Skip frames during kinetic panning [r=mfinkle, froystig]

This commit is contained in:
Benjamin Stover 2010-01-12 11:22:48 -08:00
Родитель 633aee553b
Коммит b05c1daee3
2 изменённых файлов: 101 добавлений и 73 удалений

Просмотреть файл

@ -352,9 +352,10 @@ pref("dom.max_script_run_time", 20);
pref("browser.console.showInPanel", false);
// kinetic tweakables
pref("browser.ui.kinetic.updateInterval", 33);
pref("browser.ui.kinetic.ema.alphaValue", 8);
pref("browser.ui.kinetic.decelerationRate", 15);
pref("browser.ui.kinetic.updateInterval", 30);
pref("browser.ui.kinetic.decelerationRate", 20);
pref("browser.ui.kinetic.speedSensitivity", 80);
pref("browser.ui.kinetic.swipeLength", 160);
// Disable default plugin
pref("plugin.default_plugin_disabled", true);

Просмотреть файл

@ -42,24 +42,12 @@
*
* ***** END LICENSE BLOCK ***** */
// how much movement input to take before mouse up for calculating kinetic speed
const kSwipeLength = 160;
// how many msecs elapse before two taps are not a double tap
const kDoubleClickInterval = 400;
// threshold in pixels for sensing a tap as opposed to a pan
const kTapRadius = 25;
// how many milliseconds between each kinetic pan update
const kKineticUpdateInterval = 25;
// How much speed is removed every update
const kDecelerationRate = .09;
// How sensitive kinetic scroll is to mouse movement
const kSpeedSensitivity = 1.1;
// Same as NS_EVENT_STATE_ACTIVE from nsIEventStateManager.h
const kStateActive = 0x00000001;
@ -996,37 +984,33 @@ DragData.prototype = {
* aPanBy is a function that will be called with the dx and dy
* generated by the kinetic algorithm. It should return true if the
* object was panned, false if there was no movement.
*
* There are two complicated things done here. One is calculating the
* initial velocity of the movement based on user input. Two is
* calculating the distance to move every frame.
*/
function KineticController(aPanBy, aEndCallback) {
this._panBy = aPanBy;
this._timer = null;
this._beforeEnd = aEndCallback;
try {
this._updateInterval = gPrefService.getIntPref("browser.ui.kinetic.updateInterval");
} catch(e) {
this._updateInterval = kKineticUpdateInterval;
}
// These are used to calculate the position of the scroll panes during kinetic panning. Think of
// these points as vectors that are added together and multiplied by scalars.
this._position = new Point(0, 0);
this._velocity = new Point(0, 0);
this._acceleration = new Point(0, 0);
this._time = 0;
this._timeStart = 0;
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;
}
try {
this._swipeLength = gPrefService.getIntPref("browser.ui.kinetic.swipelength");
} catch(e) {
this._swipeLength = kSwipeLength;
}
// How often do we change the position of the scroll pane? Too often and panning may jerk near
// the end. Too little and panning will be choppy. In milliseconds.
this._updateInterval = gPrefService.getIntPref("browser.ui.kinetic.updateInterval");
// "Friction" of the scroll pane. The lower, the less friction and the further distance traveled.
this._decelerationRate = gPrefService.getIntPref("browser.ui.kinetic.decelerationRate") / 10000;
// A multiplier for the initial velocity of the movement.
this._speedSensitivity = gPrefService.getIntPref("browser.ui.kinetic.speedSensitivity") / 100;
// Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded.
this._swipeLength = gPrefService.getIntPref("browser.ui.kinetic.swipeLength");
this._reset();
}
@ -1039,8 +1023,7 @@ KineticController.prototype = {
}
this.momentumBuffer = [];
this._speedX = 0;
this._speedY = 0;
this._velocity.set(0, 0);
},
isActive: function isActive() {
@ -1048,6 +1031,35 @@ KineticController.prototype = {
},
_startTimer: function _startTimer() {
// Use closed form of a parabola to calculate each position for panning.
// x(t) = v0*t + .5*t^2*a
// where: v0 is initial velocity
// a is acceleration
// t is time elapsed
//
// x(t)
// ^
// | |
// |
// | |
// | ....^^^^....
// | ...^^ | ^^...
// | ...^ ^...
// |.. | ..
// -----------------------------------> t
// t0 tf=-v0/a
//
// Using this formula, distance moved is independent of the time between each frame, unlike time
// step approaches. Once the time is up, set the position to x(tf) and stop the timer.
let lastx = this._position; // track last position vector because pan takes differences
let v0 = this._velocity; // initial velocity
let a = this._acceleration; // acceleration
// Temporary "bins" so that we don't create new objects during pan.
let aBin = new Point(0, 0);
let v0Bin = new Point(0, 0);
let callback = {
_self: this,
notify: function kineticTimerCallback(timer) {
@ -1056,34 +1068,40 @@ KineticController.prototype = {
if (!self.isActive()) // someone called end() on us between timer intervals
return;
//dump(" speeds: " + self._speedX + " " + self._speedY + "\n");
// To make animation end fast enough but to keep smoothness, average the ideal
// time frame (smooth animation) with the actual time lapse (end fast enough).
// Animation will never take longer than 2 times the ideal length of time.
let realt = Date.now() - self._initialTime;
self._time += self._updateInterval;
let t = (self._time + realt) / 2;
if (self._speedX == 0 && self._speedY == 0) {
self.end();
return;
// Calculate new position using x(t) formula.
let x = v0Bin.set(v0).scale(t).add(aBin.set(a).scale(0.5 * t * t));
let dx = x.x - lastx.x;
let dy = x.y - lastx.y;
lastx.set(x);
// Test to see if movement is finished for each component. As seen in graph, we want the
// final position to be at tf.
if (t >= -v0.x / a.x) {
// Plug in t=-v0/a into x(t) to get final position.
dx = -v0.x * v0.x / 2 / a.x - lastx.x;
// Reset components. Next frame: a's component will be 0 and t >= NaN will be false.
lastx.x = 0;
v0.x = 0;
a.x = 0;
}
// Symmetric to above case.
if (t >= -v0.y / a.y) {
dy = -v0.y * v0.y / 2 / a.y - lastx.y;
lastx.y = 0;
v0.y = 0;
a.y = 0;
}
let dx = Math.round(self._speedX * self._updateInterval);
let dy = Math.round(self._speedY * self._updateInterval);
let panned = false;
try { panned = self._panBy(-dx, -dy); } catch (e) {}
if (!panned) {
self.end();
return;
}
if (self._speedX < 0) {
self._speedX = Math.min(self._speedX + self._decelerationRate, 0);
} else if (self._speedX > 0) {
self._speedX = Math.max(self._speedX - self._decelerationRate, 0);
}
if (self._speedY < 0) {
self._speedY = Math.min(self._speedY + self._decelerationRate, 0);
} else if (self._speedY > 0) {
self._speedY = Math.max(self._speedY - self._decelerationRate, 0);
}
if (self._speedX == 0 && self._speedY == 0)
try { panned = self._panBy(Math.round(-dx), Math.round(-dy)); } catch (e) {}
if (!panned)
self.end();
}
};
@ -1096,6 +1114,10 @@ KineticController.prototype = {
},
start: function start() {
function sign(x) {
return x ? ((x > 0) ? 1 : -1) : 0;
}
let mb = this.momentumBuffer;
let mblen = this.momentumBuffer.length;
@ -1115,12 +1137,19 @@ KineticController.prototype = {
}
// Only allow kinetic scrolling to speed up if kinetic scrolling is active.
this._speedX = (distanceX < 0 ? Math.min : Math.max)((distanceX / swipeLength) * this._speedSensitivity, this._speedX);
this._speedY = (distanceY < 0 ? Math.min : Math.max)((distanceY / swipeLength) * this._speedSensitivity, this._speedY);
this._velocity.x = (distanceX < 0 ? Math.min : Math.max)((distanceX / swipeLength) * this._speedSensitivity, this._velocity.x);
this._velocity.y = (distanceY < 0 ? Math.min : Math.max)((distanceY / swipeLength) * this._speedSensitivity, this._velocity.y);
// Set acceleration vector to opposite signs of velocity
this._acceleration.set(this._velocity.clone().map(sign).scale(-this._decelerationRate));
this._position.set(0, 0);
this._initialTime = Date.now();
this._time = 0;
this.momentumBuffer = [];
if (!this.isActive()) {
if (!this.isActive())
this._startTimer();
}
return true;
},
@ -1137,10 +1166,8 @@ KineticController.prototype = {
if (this.isActive()) {
// Stop active movement when dragging in other direction.
if (dx * this._speedX < 0)
this._speedX = 0;
if (dy * this._speedY < 0)
this._speedY = 0;
if (dx * this._velocity.x < 0 || dy * this._velocity.y < 0)
this.end();
}
this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy});