Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "AndroidFlingPhysics.h"
#include <cmath>
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPrefs_apz.h"
#include "mozilla/StaticPtr.h"
namespace mozilla {
namespace layers {
// The fling physics calculations implemented here are adapted from
// Chrome's implementation of fling physics on Android:
static double ComputeDeceleration(float aDPI) {
const float kFriction = 0.84f;
const float kGravityEarth = 9.80665f;
return kGravityEarth // g (m/s^2)
* 39.37f // inch/meter
* aDPI // pixels/inch
* kFriction;
}
// == std::log(0.78f) / std::log(0.9f)
const float kDecelerationRate = 2.3582018f;
// Default friction constant in android.view.ViewConfiguration.
static float GetFlingFriction() {
return StaticPrefs::apz_android_chrome_fling_physics_friction();
}
// Tension lines cross at (GetInflexion(), 1).
static float GetInflexion() {
// Clamp the inflexion to the range [0,1]. Values outside of this range
// do not make sense in the physics model, and for negative values the
// approximation used to compute the spline curve does not converge.
const float inflexion =
StaticPrefs::apz_android_chrome_fling_physics_inflexion();
return std::clamp(inflexion, 0.f, 1.f);
}
// Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd|
// pixels or closer from the end.
static float GetThresholdForFlingEnd() {
return StaticPrefs::apz_android_chrome_fling_physics_stop_threshold();
}
static double ComputeSplineDeceleration(ParentLayerCoord aVelocity,
double aTuningCoeff) {
float velocityPerSec = aVelocity * 1000.0f;
return std::log(GetInflexion() * velocityPerSec /
(GetFlingFriction() * aTuningCoeff));
}
static TimeDuration ComputeFlingDuration(ParentLayerCoord aVelocity,
double aTuningCoeff) {
const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff);
const double timeSeconds = std::exp(splineDecel / (kDecelerationRate - 1.0));
return TimeDuration::FromSeconds(timeSeconds);
}
static ParentLayerCoord ComputeFlingDistance(ParentLayerCoord aVelocity,
double aTuningCoeff) {
const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff);
return GetFlingFriction() * aTuningCoeff *
std::exp(kDecelerationRate / (kDecelerationRate - 1.0) * splineDecel);
}
struct SplineConstants {
public:
SplineConstants() {
const float kStartTension = 0.5f;
const float kEndTension = 1.0f;
const float kP1 = kStartTension * GetInflexion();
const float kP2 = 1.0f - kEndTension * (1.0f - GetInflexion());
float xMin = 0.0f;
for (int i = 0; i < kNumSamples; i++) {
const float alpha = static_cast<float>(i) / kNumSamples;
float xMax = 1.0f;
float x, tx, coef;
// While the inflexion can be overridden by the user, it's clamped to
// [0,1]. For values in this range, the approximation algorithm below
// should converge in < 20 iterations. For good measure, we impose an
// iteration limit as well.
static const int sIterationLimit = 100;
int iterations = 0;
while (iterations++ < sIterationLimit) {
x = xMin + (xMax - xMin) / 2.0f;
coef = 3.0f * x * (1.0f - x);
tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x;
if (FuzzyEqualsAdditive(tx, alpha)) {
break;
}
if (tx > alpha) {
xMax = x;
} else {
xMin = x;
}
}
mSplinePositions[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x;
}
mSplinePositions[kNumSamples] = 1.0f;
}
void CalculateCoefficients(float aTime, float* aOutDistanceCoef,
float* aOutVelocityCoef) {
*aOutDistanceCoef = 1.0f;
*aOutVelocityCoef = 0.0f;
const int index = static_cast<int>(kNumSamples * aTime);
if (index < kNumSamples) {
const float tInf = static_cast<float>(index) / kNumSamples;
const float dInf = mSplinePositions[index];
const float tSup = static_cast<float>(index + 1) / kNumSamples;
const float dSup = mSplinePositions[index + 1];
*aOutVelocityCoef = (dSup - dInf) / (tSup - tInf);
*aOutDistanceCoef = dInf + (aTime - tInf) * *aOutVelocityCoef;
}
}
private:
static const int kNumSamples = 100;
float mSplinePositions[kNumSamples + 1];
};
StaticAutoPtr<SplineConstants> gSplineConstants;
/* static */
void AndroidFlingPhysics::InitializeGlobalState() {
gSplineConstants = new SplineConstants();
ClearOnShutdown(&gSplineConstants);
}
void AndroidFlingPhysics::Init(const ParentLayerPoint& aStartingVelocity,
float aPLPPI) {
mVelocity = aStartingVelocity.Length();
// We should not have created a fling animation if there is no velocity.
MOZ_ASSERT(mVelocity != 0.0f);
const double tuningCoeff = ComputeDeceleration(aPLPPI);
mTargetDuration = ComputeFlingDuration(mVelocity, tuningCoeff);
MOZ_ASSERT(!mTargetDuration.IsZero());
mDurationSoFar = TimeDuration();
mLastPos = ParentLayerPoint();
mCurrentPos = ParentLayerPoint();
float coeffX =
mVelocity == 0 ? 1.0f : aStartingVelocity.x.value / mVelocity.value;
float coeffY =
mVelocity == 0 ? 1.0f : aStartingVelocity.y.value / mVelocity.value;
mTargetDistance = ComputeFlingDistance(mVelocity, tuningCoeff);
mTargetPos =
ParentLayerPoint(mTargetDistance * coeffX, mTargetDistance * coeffY);
const float hyp = mTargetPos.Length();
if (FuzzyEqualsAdditive(hyp, 0.0f)) {
mDeltaNorm = ParentLayerPoint(1, 1);
} else {
mDeltaNorm = ParentLayerPoint(mTargetPos.x / hyp, mTargetPos.y / hyp);
}
}
void AndroidFlingPhysics::Sample(const TimeDuration& aDelta,
ParentLayerPoint* aOutVelocity,
ParentLayerPoint* aOutOffset) {
float newVelocity;
if (SampleImpl(aDelta, &newVelocity)) {
*aOutOffset = (mCurrentPos - mLastPos);
*aOutVelocity = ParentLayerPoint(mDeltaNorm.x * newVelocity,
mDeltaNorm.y * newVelocity);
mLastPos = mCurrentPos;
} else {
*aOutOffset = (mTargetPos - mLastPos);
*aOutVelocity = ParentLayerPoint();
}
}
bool AndroidFlingPhysics::SampleImpl(const TimeDuration& aDelta,
float* aOutVelocity) {
mDurationSoFar += aDelta;
if (mDurationSoFar >= mTargetDuration) {
return false;
}
const float timeRatio =
mDurationSoFar.ToSeconds() / mTargetDuration.ToSeconds();
float distanceCoef = 1.0f;
float velocityCoef = 0.0f;
gSplineConstants->CalculateCoefficients(timeRatio, &distanceCoef,
&velocityCoef);
// The caller expects the velocity in pixels per _millisecond_.
*aOutVelocity =
velocityCoef * mTargetDistance / mTargetDuration.ToMilliseconds();
mCurrentPos = mTargetPos * distanceCoef;
ParentLayerPoint remainder = mTargetPos - mCurrentPos;
const float threshold = GetThresholdForFlingEnd();
if (fabsf(remainder.x) < threshold && fabsf(remainder.y) < threshold) {
return false;
}
return true;
}
} // namespace layers
} // namespace mozilla