Bug 1335958 - Simplify APZ overscroll physics. r=kats draft
authorBotond Ballo <botond@mozilla.com>
Wed, 01 Feb 2017 19:47:26 -0500
changeset 470462 6cea43d993ce7ebfeab8a6cf3ebb3c5a7c8f29df
parent 467243 07d7ecbf77e3be59797f16234d357a02bb38ed8b
child 544482 11082d0ec418bf5c60050d1cd5c8a90ada87c6cc
push id44034
push userbballo@mozilla.com
push dateFri, 03 Feb 2017 19:16:28 +0000
reviewerskats
bugs1335958
milestone54.0a1
Bug 1335958 - Simplify APZ overscroll physics. r=kats Replace the oscillating spring physics used for B2G with simpler physics where we just snap back without oscillating, using an MSD model. This is not currently used, but will be once we enable overscroll for macOS. MozReview-Commit-ID: 8g2Sjdj002Z
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/Axis.cpp
gfx/layers/apz/src/Axis.h
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -3042,52 +3042,20 @@ AsyncPanZoomController::GetOverscrollTra
   if (aMode == RESPECT_FORCE_DISABLE && mScrollMetadata.IsApzForceDisabled()) {
     return AsyncTransformComponentMatrix();
   }
 
   if (!IsOverscrolled()) {
     return AsyncTransformComponentMatrix();
   }
 
-  // The overscroll effect is a uniform stretch along the overscrolled axis,
-  // with the edge of the content where we have reached the end of the
-  // scrollable area pinned into place.
-
-  // The kStretchFactor parameter determines how much overscroll can stretch the
-  // content.
-  const float kStretchFactor = gfxPrefs::APZOverscrollStretchFactor();
-
-  // Compute the amount of the stretch along each axis. The stretch is
-  // proportional to the amount by which we are overscrolled along that axis.
-  ParentLayerSize compositionSize(mX.GetCompositionLength(), mY.GetCompositionLength());
-  float scaleX = 1 + kStretchFactor * fabsf(mX.GetOverscroll()) / mX.GetCompositionLength();
-  float scaleY = 1 + kStretchFactor * fabsf(mY.GetOverscroll()) / mY.GetCompositionLength();
-
-  // The scale is applied relative to the origin of the composition bounds, i.e.
-  // it keeps the top-left corner of the content in place. This is fine if we
-  // are overscrolling at the top or on the left, but if we are overscrolling
-  // at the bottom or on the right, we want the bottom or right edge of the
-  // content to stay in place instead, so we add a translation to compensate.
-  ParentLayerPoint translation;
-  bool overscrolledOnRight = mX.GetOverscroll() > 0;
-  if (overscrolledOnRight) {
-    ParentLayerCoord overscrolledCompositionWidth = scaleX * compositionSize.width;
-    ParentLayerCoord extraCompositionWidth = overscrolledCompositionWidth - compositionSize.width;
-    translation.x = -extraCompositionWidth;
-  }
-  bool overscrolledAtBottom = mY.GetOverscroll() > 0;
-  if (overscrolledAtBottom) {
-    ParentLayerCoord overscrolledCompositionHeight = scaleY * compositionSize.height;
-    ParentLayerCoord extraCompositionHeight = overscrolledCompositionHeight - compositionSize.height;
-    translation.y = -extraCompositionHeight;
-  }
-
-  // Combine the transformations into a matrix.
-  return AsyncTransformComponentMatrix::Scaling(scaleX, scaleY, 1)
-                    .PostTranslate(translation.x, translation.y, 0);
+  // The overscroll effect is a simple translation by the overscroll offset.
+  ParentLayerPoint overscrollOffset(-mX.GetOverscroll(), -mY.GetOverscroll());
+  return AsyncTransformComponentMatrix()
+      .PostTranslate(overscrollOffset.x, overscrollOffset.y, 0);
 }
 
 bool AsyncPanZoomController::AdvanceAnimations(const TimeStamp& aSampleTime)
 {
   APZThreadUtils::AssertOnCompositorThread();
 
   // Don't send any state-change notifications until the end of the function,
   // because we may go through some intermediate states while we finish
--- a/gfx/layers/apz/src/Axis.cpp
+++ b/gfx/layers/apz/src/Axis.cpp
@@ -48,19 +48,17 @@ extern StaticAutoPtr<ComputedTimingFunct
 Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
   : mPos(0),
     mVelocitySampleTimeMs(0),
     mVelocitySamplePos(0),
     mVelocity(0.0f),
     mAxisLocked(false),
     mAsyncPanZoomController(aAsyncPanZoomController),
     mOverscroll(0),
-    mFirstOverscrollAnimationSample(0),
-    mLastOverscrollPeak(0),
-    mOverscrollScale(1.0f)
+    mMSDModel(0.0, 0.0, 0.0, 400.0, 1.2)
 {
 }
 
 float Axis::ToLocalVelocity(float aVelocityInchesPerMs) const {
   ScreenPoint velocity = MakePoint(aVelocityInchesPerMs * APZCTreeManager::GetDPI());
   // Use ToScreenCoordinates() to convert a point rather than a vector by
   // treating the point as a vector, and using (0, 0) as the anchor.
   ScreenPoint panStart = mAsyncPanZoomController->ToScreenCoordinates(
@@ -201,24 +199,25 @@ bool Axis::AdjustDisplacement(ParentLaye
     mVelocity = 0.0f;
     displacement -= aOverscrollAmountOut;
   }
   aDisplacementOut = displacement;
   return fabsf(consumedOverscroll) > EPSILON;
 }
 
 ParentLayerCoord Axis::ApplyResistance(ParentLayerCoord aRequestedOverscroll) const {
-  // 'resistanceFactor' is a value between 0 and 1, which:
-  //   - tends to 1 as the existing overscroll tends to 0
+  // 'resistanceFactor' is a value between 0 and 1/16, which:
+  //   - tends to 1/16 as the existing overscroll tends to 0
   //   - tends to 0 as the existing overscroll tends to the composition length
   // The actual overscroll is the requested overscroll multiplied by this
-  // factor; this should prevent overscrolling by more than the composition
-  // length.
-  float resistanceFactor = 1 - fabsf(GetOverscroll()) / GetCompositionLength();
-  return resistanceFactor < 0 ? ParentLayerCoord(0) : aRequestedOverscroll * resistanceFactor;
+  // factor.
+  float resistanceFactor = (1 - fabsf(GetOverscroll()) / GetCompositionLength()) / 16;
+  float result = resistanceFactor < 0 ? ParentLayerCoord(0) : aRequestedOverscroll * resistanceFactor;
+  result = clamped(result, -8.0f, 8.0f);
+  return result;
 }
 
 void Axis::OverscrollBy(ParentLayerCoord aOverscroll) {
   MOZ_ASSERT(CanScroll());
   // We can get some spurious calls to OverscrollBy() with near-zero values
   // due to rounding error. Ignore those (they might trip the asserts below.)
   if (FuzzyEqualsAdditive(aOverscroll.value, 0.0f, COORDINATE_EPSILON)) {
     return;
@@ -245,147 +244,37 @@ void Axis::OverscrollBy(ParentLayerCoord
     }
 #endif
     MOZ_ASSERT(mOverscroll <= 0);
   }
   mOverscroll += aOverscroll;
 }
 
 ParentLayerCoord Axis::GetOverscroll() const {
-  ParentLayerCoord result = (mOverscroll - mLastOverscrollPeak) / mOverscrollScale;
-
-  // Assert that we return overscroll in the correct direction
-#ifdef DEBUG
-  if ((result.value * mFirstOverscrollAnimationSample.value) < 0.0f) {
-    nsPrintfCString message("GetOverscroll() (%f) and first overscroll animation sample (%f) have different signs\n",
-                            result.value, mFirstOverscrollAnimationSample.value);
-    NS_ASSERTION(false, message.get());
-    MOZ_CRASH("GFX: Overscroll issue");
-  }
-#endif
-
-  return result;
+  return mOverscroll;
 }
 
 void Axis::StartOverscrollAnimation(float aVelocity) {
-  // Make sure any state from a previous animation has been cleared.
-  MOZ_ASSERT(mFirstOverscrollAnimationSample == 0 &&
-             mLastOverscrollPeak == 0 &&
-             mOverscrollScale == 1);
-
+  aVelocity = clamped(aVelocity / 2.0f, -20.0f, 20.0f);
   SetVelocity(aVelocity);
+  mMSDModel.SetPosition(mOverscroll);
+  // Convert velocity from ParentLayerCoords/millisecond to ParentLayerCoords/second.
+  mMSDModel.SetVelocity(mVelocity * 1000.0);
 }
 
 void Axis::EndOverscrollAnimation() {
-  ParentLayerCoord overscroll = GetOverscroll();
-  mFirstOverscrollAnimationSample = 0;
-  mLastOverscrollPeak = 0;
-  mOverscrollScale = 1.0f;
-  mOverscroll = overscroll;
-}
-
-void Axis::StepOverscrollAnimation(double aStepDurationMilliseconds) {
-  // Apply spring physics to the overscroll as time goes on.
-  // Note: this method of sampling isn't perfectly smooth, as it assumes
-  // a constant velocity over 'aDelta', instead of an accelerating velocity.
-  // (The way we applying friction to flings has the same issue.)
-  // Hooke's law with damping:
-  //   F = -kx - bv
-  // where
-  //   k is a constant related to the stiffness of the spring
-  //     The larger the constant, the stiffer the spring.
-  //   x is the displacement of the end of the spring from its equilibrium
-  //     In our scenario, it's the amount of overscroll on the axis.
-  //   b is a constant that provides damping (friction)
-  //   v is the velocity of the point at the end of the spring
-  // See http://gafferongames.com/game-physics/spring-physics/
-  const float kSpringStiffness = gfxPrefs::APZOverscrollSpringStiffness();
-  const float kSpringFriction = gfxPrefs::APZOverscrollSpringFriction();
-
-  // Apply spring force.
-  float springForce = -1 * kSpringStiffness * mOverscroll;
-  // Assume unit mass, so force = acceleration.
-  float oldVelocity = mVelocity;
-  mVelocity += springForce * aStepDurationMilliseconds;
-
-  // Apply dampening.
-  mVelocity *= pow(double(1 - kSpringFriction), aStepDurationMilliseconds);
-  AXIS_LOG("%p|%s sampled overscroll animation, leaving velocity at %f\n",
-    mAsyncPanZoomController, Name(), mVelocity);
-
-  // At the peak of each oscillation, record new offset and scaling factors for
-  // overscroll, to ensure that GetOverscroll always returns a value of the
-  // same sign, and that this value is correctly adjusted as the spring is
-  // dampened.
-  // To handle the case where one of the velocity samples is exaclty zero,
-  // consider a sign change to have occurred when the outgoing velocity is zero.
-  bool velocitySignChange = (oldVelocity * mVelocity) < 0 || mVelocity == 0;
-  if (mFirstOverscrollAnimationSample == 0.0f) {
-    mFirstOverscrollAnimationSample = mOverscroll;
-
-    // It's possible to start sampling overscroll with velocity == 0, or
-    // velocity in the opposite direction of overscroll, so make sure we
-    // correctly record the peak in this case.
-    if (mOverscroll != 0 && ((mOverscroll > 0 ? oldVelocity : -oldVelocity) <= 0.0f)) {
-      velocitySignChange = true;
-    }
-  }
-  if (velocitySignChange) {
-    bool oddOscillation = (mOverscroll.value * mFirstOverscrollAnimationSample.value) < 0.0f;
-    mLastOverscrollPeak = oddOscillation ? mOverscroll : -mOverscroll;
-    mOverscrollScale = 2.0f;
-  }
-
-  // Adjust the amount of overscroll based on the velocity.
-  // Note that we allow for oscillations.
-  mOverscroll += (mVelocity * aStepDurationMilliseconds);
-
-  // Our mechanism for translating a set of mOverscroll values that oscillate
-  // around zero to a set of GetOverscroll() values that have the same sign
-  // (so content is always stretched, never compressed) assumes that
-  // mOverscroll does not exceed mLastOverscrollPeak in magnitude. If our
-  // calculations were exact, this would be the case, as a dampened spring
-  // should never attain a displacement greater in magnitude than a previous
-  // peak. In our approximation calculations, however, this may not hold
-  // exactly. To ensure the assumption is not violated, we clamp the magnitude
-  // of mOverscroll.
-  if (mLastOverscrollPeak != 0 && fabs(mOverscroll) > fabs(mLastOverscrollPeak)) {
-    mOverscroll = (mOverscroll >= 0) ? fabs(mLastOverscrollPeak) : -fabs(mLastOverscrollPeak);
-  }
+  mMSDModel.SetPosition(0.0);
+  mMSDModel.SetVelocity(0.0);
 }
 
 bool Axis::SampleOverscrollAnimation(const TimeDuration& aDelta) {
-  // Short-circuit early rather than running through all the sampling code.
-  if (mVelocity == 0.0f && mOverscroll == 0.0f) {
-    return false;
-  }
+  mMSDModel.Simulate(aDelta);
+  mOverscroll = mMSDModel.GetPosition();
 
-  // We approximate the curve traced out by the velocity of the spring
-  // over time by breaking up the curve into small segments over which we
-  // consider the velocity to be constant. If the animation is sampled
-  // sufficiently often, then treating |aDelta| as a single segment of this
-  // sort would be fine, but the frequency at which the animation is sampled
-  // can be affected by external factors, and as the segment size grows larger,
-  // the approximation gets worse and the approximated curve can even diverge
-  // (i.e. oscillate forever, with displacements of increasing absolute value)!
-  // To avoid this, we break up |aDelta| into smaller segments of length 1 ms
-  // each, and a segment of any remaining fractional milliseconds.
-  double milliseconds = aDelta.ToMilliseconds();
-  int wholeMilliseconds = (int) aDelta.ToMilliseconds();
-  double fractionalMilliseconds = milliseconds - wholeMilliseconds;
-  for (int i = 0; i < wholeMilliseconds; ++i) {
-    StepOverscrollAnimation(1);
-  }
-  StepOverscrollAnimation(fractionalMilliseconds);
-
-  // If both the velocity and the displacement fall below a threshold, stop
-  // the animation so we don't continue doing tiny oscillations that aren't
-  // noticeable.
-  if (fabs(mOverscroll) < gfxPrefs::APZOverscrollStopDistanceThreshold() &&
-      fabs(mVelocity) < gfxPrefs::APZOverscrollStopVelocityThreshold()) {
+  if (mMSDModel.IsFinished(1.0)) {
     // "Jump" to the at-rest state. The jump shouldn't be noticeable as the
     // velocity and overscroll are already low.
     AXIS_LOG("%p|%s oscillation dropped below threshold, going to rest\n",
       mAsyncPanZoomController, Name());
     ClearOverscroll();
     mVelocity = 0;
     return false;
   }
--- a/gfx/layers/apz/src/Axis.h
+++ b/gfx/layers/apz/src/Axis.h
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_layers_Axis_h
 #define mozilla_layers_Axis_h
 
 #include <sys/types.h>                  // for int32_t
 #include "APZUtils.h"
+#include "AxisPhysicsMSDModel.h"
 #include "Units.h"
 #include "mozilla/TimeStamp.h"          // for TimeDuration
 #include "nsTArray.h"                   // for nsTArray
 
 namespace mozilla {
 namespace layers {
 
 const float EPSILON = 0.0001f;
@@ -267,32 +268,21 @@ protected:
   uint32_t mVelocitySampleTimeMs;
   ParentLayerCoord mVelocitySamplePos;
 
   ParentLayerCoord mStartPos;
   float mVelocity;      // Units: ParentLayerCoords per millisecond
   bool mAxisLocked;     // Whether movement on this axis is locked.
   AsyncPanZoomController* mAsyncPanZoomController;
 
-  // mOverscroll is the displacement of an oscillating spring from its resting
-  // state. The resting state moves as the overscroll animation progresses.
+  // The amount by which we are overscrolled; see GetOverscroll().
   ParentLayerCoord mOverscroll;
-  // Used to record the initial overscroll when we start sampling for animation.
-  ParentLayerCoord mFirstOverscrollAnimationSample;
-  // These two variables are used in combination to make sure that
-  // GetOverscroll() never changes sign during animation. This is necessary,
-  // as mOverscroll itself oscillates around zero during animation.
-  // If we're not sampling overscroll animation, mOverscrollScale will be 1.0
-  // and mLastOverscrollPeak will be zero.
-  // If we are animating, after the overscroll reaches its peak,
-  // mOverscrollScale will be 2.0 and mLastOverscrollPeak will store the amount
-  // of overscroll at the last peak of the oscillation. Together, these values
-  // guarantee that the result of GetOverscroll() never changes sign.
-  ParentLayerCoord mLastOverscrollPeak;
-  float mOverscrollScale;
+
+  // The mass-spring-damper model for overscroll physics.
+  AxisPhysicsMSDModel mMSDModel;
 
   // A queue of (timestamp, velocity) pairs; these are the historical
   // velocities at the given timestamps. Timestamps are in milliseconds,
   // velocities are in screen pixels per ms. This member can only be
   // accessed on the controller/UI thread.
   nsTArray<std::pair<uint32_t, float> > mVelocityQueue;
 
   const FrameMetrics& GetFrameMetrics() const;