diff options
author | skobes@chromium.org <skobes@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-18 22:37:26 +0000 |
---|---|---|
committer | skobes@chromium.org <skobes@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-18 22:37:26 +0000 |
commit | 7a7d5bec832fd997b6b2eef3ec9c0906b74ef16d (patch) | |
tree | 618f497d0ca76b781df27da3ae6de9188b00bcdf | |
parent | 472de00aa7609c352c95d64abb125145296fc73a (diff) | |
download | chromium_src-7a7d5bec832fd997b6b2eef3ec9c0906b74ef16d.zip chromium_src-7a7d5bec832fd997b6b2eef3ec9c0906b74ef16d.tar.gz chromium_src-7a7d5bec832fd997b6b2eef3ec9c0906b74ef16d.tar.bz2 |
Scroll offset animation curve retargeting.
Adds support for updating the target of an in-progress input-triggered impl-side scroll offset animation, keeping the velocity function continuous.
BUG=575
Review URL: https://codereview.chromium.org/393713002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@284228 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | cc/animation/scroll_offset_animation_curve.cc | 78 | ||||
-rw-r--r-- | cc/animation/scroll_offset_animation_curve.h | 7 | ||||
-rw-r--r-- | cc/animation/scroll_offset_animation_curve_unittest.cc | 25 | ||||
-rw-r--r-- | cc/animation/timing_function.cc | 4 | ||||
-rw-r--r-- | cc/animation/timing_function.h | 2 | ||||
-rw-r--r-- | cc/trees/layer_tree_host_impl.cc | 24 | ||||
-rw-r--r-- | cc/trees/layer_tree_host_impl_unittest.cc | 15 | ||||
-rw-r--r-- | ui/gfx/geometry/cubic_bezier.cc | 41 | ||||
-rw-r--r-- | ui/gfx/geometry/cubic_bezier.h | 3 | ||||
-rw-r--r-- | ui/gfx/geometry/cubic_bezier_unittest.cc | 28 |
10 files changed, 190 insertions, 37 deletions
diff --git a/cc/animation/scroll_offset_animation_curve.cc b/cc/animation/scroll_offset_animation_curve.cc index a7283b8..8211a95 100644 --- a/cc/animation/scroll_offset_animation_curve.cc +++ b/cc/animation/scroll_offset_animation_curve.cc @@ -15,6 +15,35 @@ const double kDurationDivisor = 60.0; namespace cc { +namespace { + +static float MaximumDimension(gfx::Vector2dF delta) { + return std::max(std::abs(delta.x()), std::abs(delta.y())); +} + +static base::TimeDelta DurationFromDelta(gfx::Vector2dF delta) { + // The duration of a scroll animation depends on the size of the scroll. + // The exact relationship between the size and the duration isn't specified + // by the CSSOM View smooth scroll spec and is instead left up to user agents + // to decide. The calculation performed here will very likely be further + // tweaked before the smooth scroll API ships. + return base::TimeDelta::FromMicroseconds( + (std::sqrt(MaximumDimension(delta)) / kDurationDivisor) * + base::Time::kMicrosecondsPerSecond); +} + +static scoped_ptr<TimingFunction> EaseOutWithInitialVelocity(double velocity) { + // Based on EaseInOutTimingFunction::Create with first control point rotated. + const double r2 = 0.42 * 0.42; + const double v2 = velocity * velocity; + const double x1 = std::sqrt(r2 / (v2 + 1)); + const double y1 = std::sqrt(r2 * v2 / (v2 + 1)); + return CubicBezierTimingFunction::Create(x1, y1, 0.58, 1) + .PassAs<TimingFunction>(); +} + +} // namespace + scoped_ptr<ScrollOffsetAnimationCurve> ScrollOffsetAnimationCurve::Create( const gfx::Vector2dF& target_value, scoped_ptr<TimingFunction> timing_function) { @@ -33,22 +62,12 @@ ScrollOffsetAnimationCurve::~ScrollOffsetAnimationCurve() {} void ScrollOffsetAnimationCurve::SetInitialValue( const gfx::Vector2dF& initial_value) { initial_value_ = initial_value; - - // The duration of a scroll animation depends on the size of the scroll. - // The exact relationship between the size and the duration isn't specified - // by the CSSOM View smooth scroll spec and is instead left up to user agents - // to decide. The calculation performed here will very likely be further - // tweaked before the smooth scroll API ships. - float delta_x = std::abs(target_value_.x() - initial_value_.x()); - float delta_y = std::abs(target_value_.y() - initial_value_.y()); - float max_delta = std::max(delta_x, delta_y); - duration_ = base::TimeDelta::FromMicroseconds( - (std::sqrt(max_delta) / kDurationDivisor) * - base::Time::kMicrosecondsPerSecond); + total_animation_duration_ = DurationFromDelta(target_value_ - initial_value_); } gfx::Vector2dF ScrollOffsetAnimationCurve::GetValue(double t) const { - double duration = duration_.InSecondsF(); + double duration = (total_animation_duration_ - last_retarget_).InSecondsF(); + t -= last_retarget_.InSecondsF(); if (t <= 0) return initial_value_; @@ -64,7 +83,7 @@ gfx::Vector2dF ScrollOffsetAnimationCurve::GetValue(double t) const { } double ScrollOffsetAnimationCurve::Duration() const { - return duration_.InSecondsF(); + return total_animation_duration_.InSecondsF(); } AnimationCurve::CurveType ScrollOffsetAnimationCurve::Type() const { @@ -77,8 +96,37 @@ scoped_ptr<AnimationCurve> ScrollOffsetAnimationCurve::Clone() const { scoped_ptr<ScrollOffsetAnimationCurve> curve_clone = Create(target_value_, timing_function.Pass()); curve_clone->initial_value_ = initial_value_; - curve_clone->duration_ = duration_; + curve_clone->total_animation_duration_ = total_animation_duration_; + curve_clone->last_retarget_ = last_retarget_; return curve_clone.PassAs<AnimationCurve>(); } +void ScrollOffsetAnimationCurve::UpdateTarget( + double t, + const gfx::Vector2dF& new_target) { + gfx::Vector2dF current_position = GetValue(t); + gfx::Vector2dF old_delta = target_value_ - initial_value_; + gfx::Vector2dF new_delta = new_target - current_position; + + double old_duration = + (total_animation_duration_ - last_retarget_).InSecondsF(); + double new_duration = DurationFromDelta(new_delta).InSecondsF(); + + double old_velocity = timing_function_->Velocity( + (t - last_retarget_.InSecondsF()) / old_duration); + + // TimingFunction::Velocity gives the slope of the curve from 0 to 1. + // To match the "true" velocity in px/sec we must adjust this slope for + // differences in duration and scroll delta between old and new curves. + double new_velocity = + old_velocity * (new_duration / old_duration) * + (MaximumDimension(old_delta) / MaximumDimension(new_delta)); + + initial_value_ = current_position; + target_value_ = new_target; + total_animation_duration_ = base::TimeDelta::FromSecondsD(t + new_duration); + last_retarget_ = base::TimeDelta::FromSecondsD(t); + timing_function_ = EaseOutWithInitialVelocity(new_velocity); +} + } // namespace cc diff --git a/cc/animation/scroll_offset_animation_curve.h b/cc/animation/scroll_offset_animation_curve.h index 543ae37..3d71e13 100644 --- a/cc/animation/scroll_offset_animation_curve.h +++ b/cc/animation/scroll_offset_animation_curve.h @@ -24,6 +24,8 @@ class CC_EXPORT ScrollOffsetAnimationCurve : public AnimationCurve { void SetInitialValue(const gfx::Vector2dF& initial_value); gfx::Vector2dF GetValue(double t) const; + gfx::Vector2dF target_value() const { return target_value_; } + void UpdateTarget(double t, const gfx::Vector2dF& new_target); // AnimationCurve implementation virtual double Duration() const OVERRIDE; @@ -36,7 +38,10 @@ class CC_EXPORT ScrollOffsetAnimationCurve : public AnimationCurve { gfx::Vector2dF initial_value_; gfx::Vector2dF target_value_; - base::TimeDelta duration_; + base::TimeDelta total_animation_duration_; + + // Time from animation start to most recent UpdateTarget. + base::TimeDelta last_retarget_; scoped_ptr<TimingFunction> timing_function_; diff --git a/cc/animation/scroll_offset_animation_curve_unittest.cc b/cc/animation/scroll_offset_animation_curve_unittest.cc index 63445d9..1b8fdb5 100644 --- a/cc/animation/scroll_offset_animation_curve_unittest.cc +++ b/cc/animation/scroll_offset_animation_curve_unittest.cc @@ -119,5 +119,30 @@ TEST(ScrollOffsetAnimationCurveTest, Clone) { EXPECT_NEAR(37.4168f, value.y(), 0.00015f); } +TEST(ScrollOffsetAnimationCurveTest, UpdateTarget) { + gfx::Vector2dF initial_value(0.f, 0.f); + gfx::Vector2dF target_value(0.f, 3600.f); + scoped_ptr<ScrollOffsetAnimationCurve> curve( + ScrollOffsetAnimationCurve::Create( + target_value, EaseInOutTimingFunction::Create().Pass())); + curve->SetInitialValue(initial_value); + EXPECT_EQ(1.0, curve->Duration()); + EXPECT_EQ(1800.0, curve->GetValue(0.5).y()); + EXPECT_EQ(3600.0, curve->GetValue(1.0).y()); + + curve->UpdateTarget(0.5, gfx::Vector2dF(0.0, 9900.0)); + + EXPECT_EQ(2.0, curve->Duration()); + EXPECT_EQ(1800.0, curve->GetValue(0.5).y()); + EXPECT_NEAR(5566.49, curve->GetValue(1.0).y(), 0.01); + EXPECT_EQ(9900.0, curve->GetValue(2.0).y()); + + curve->UpdateTarget(1.0, gfx::Vector2dF(0.0, 7200.0)); + + EXPECT_NEAR(1.674, curve->Duration(), 0.01); + EXPECT_NEAR(5566.49, curve->GetValue(1.0).y(), 0.01); + EXPECT_EQ(7200.0, curve->GetValue(1.674).y()); +} + } // namespace } // namespace cc diff --git a/cc/animation/timing_function.cc b/cc/animation/timing_function.cc index bf11c20..71319e1 100644 --- a/cc/animation/timing_function.cc +++ b/cc/animation/timing_function.cc @@ -45,6 +45,10 @@ void CubicBezierTimingFunction::Range(float* min, float* max) const { *max = static_cast<float>(max_d); } +float CubicBezierTimingFunction::Velocity(double x) const { + return static_cast<float>(bezier_.Slope(x)); +} + // These numbers come from // http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag. scoped_ptr<TimingFunction> EaseTimingFunction::Create() { diff --git a/cc/animation/timing_function.h b/cc/animation/timing_function.h index 647701f..cc97939 100644 --- a/cc/animation/timing_function.h +++ b/cc/animation/timing_function.h @@ -22,6 +22,7 @@ class CC_EXPORT TimingFunction : public FloatAnimationCurve { // The smallest and largest values returned by GetValue for inputs in // [0, 1]. virtual void Range(float* min, float* max) const = 0; + virtual float Velocity(double time) const = 0; protected: TimingFunction(); @@ -41,6 +42,7 @@ class CC_EXPORT CubicBezierTimingFunction : public TimingFunction { virtual scoped_ptr<AnimationCurve> Clone() const OVERRIDE; virtual void Range(float* min, float* max) const OVERRIDE; + virtual float Velocity(double time) const OVERRIDE; protected: CubicBezierTimingFunction(double x1, double y1, double x2, double y2); diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc index a2c8111..f137cb4 100644 --- a/cc/trees/layer_tree_host_impl.cc +++ b/cc/trees/layer_tree_host_impl.cc @@ -2302,9 +2302,25 @@ InputHandler::ScrollStatus LayerTreeHostImpl::ScrollBegin( InputHandler::ScrollStatus LayerTreeHostImpl::ScrollAnimated( const gfx::Point& viewport_point, const gfx::Vector2dF& scroll_delta) { - if (CurrentlyScrollingLayer()) { - // TODO(skobes): Update the target of the existing animation. - return ScrollIgnored; + if (LayerImpl* layer_impl = CurrentlyScrollingLayer()) { + Animation* animation = + layer_impl->layer_animation_controller()->GetAnimation( + Animation::ScrollOffset); + if (!animation) + return ScrollIgnored; + + ScrollOffsetAnimationCurve* curve = + animation->curve()->ToScrollOffsetAnimationCurve(); + + gfx::Vector2dF new_target = curve->target_value() + scroll_delta; + new_target.SetToMax(gfx::Vector2dF()); + new_target.SetToMin(layer_impl->MaxScrollOffset()); + + curve->UpdateTarget( + animation->TrimTimeToCurrentIteration(CurrentFrameTimeTicks()), + new_target); + + return ScrollStarted; } // ScrollAnimated is only used for wheel scrolls. We use the same bubbling // behavior as ScrollBy to determine which layer to animate, but we do not @@ -2341,7 +2357,7 @@ InputHandler::ScrollStatus LayerTreeHostImpl::ScrollAnimated( curve->SetInitialValue(current_offset); scoped_ptr<Animation> animation = - Animation::Create(curve->Clone().Pass(), + Animation::Create(curve.PassAs<AnimationCurve>(), AnimationIdProvider::NextAnimationId(), AnimationIdProvider::NextGroupId(), Animation::ScrollOffset); diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc index 05fc478..d05a493 100644 --- a/cc/trees/layer_tree_host_impl_unittest.cc +++ b/cc/trees/layer_tree_host_impl_unittest.cc @@ -6670,7 +6670,7 @@ TEST_F(LayerTreeHostImplTest, ExternalTransformReflectedInNextDraw) { } TEST_F(LayerTreeHostImplTest, ScrollAnimated) { - SetupScrollAndContentsLayers(gfx::Size(100, 100)); + SetupScrollAndContentsLayers(gfx::Size(100, 150)); host_impl_->SetViewportSize(gfx::Size(50, 50)); DrawFrame(); @@ -6693,10 +6693,21 @@ TEST_F(LayerTreeHostImplTest, ScrollAnimated) { float y = scrolling_layer->TotalScrollOffset().y(); EXPECT_TRUE(y > 1 && y < 49); + // Update target. + EXPECT_EQ(InputHandler::ScrollStarted, + host_impl_->ScrollAnimated(gfx::Point(), gfx::Vector2d(0, 50))); + host_impl_->Animate(start_time + base::TimeDelta::FromMilliseconds(200)); host_impl_->UpdateAnimationState(true); - EXPECT_EQ(gfx::Vector2dF(0, 50), scrolling_layer->TotalScrollOffset()); + y = scrolling_layer->TotalScrollOffset().y(); + EXPECT_TRUE(y > 50 && y < 100); + EXPECT_EQ(scrolling_layer, host_impl_->CurrentlyScrollingLayer()); + + host_impl_->Animate(start_time + base::TimeDelta::FromMilliseconds(250)); + host_impl_->UpdateAnimationState(true); + + EXPECT_EQ(gfx::Vector2dF(0, 100), scrolling_layer->TotalScrollOffset()); EXPECT_EQ(NULL, host_impl_->CurrentlyScrollingLayer()); } diff --git a/ui/gfx/geometry/cubic_bezier.cc b/ui/gfx/geometry/cubic_bezier.cc index 3d7e8fe..f9f786e 100644 --- a/ui/gfx/geometry/cubic_bezier.cc +++ b/ui/gfx/geometry/cubic_bezier.cc @@ -16,19 +16,26 @@ namespace { static const double kBezierEpsilon = 1e-7; static const int MAX_STEPS = 30; -static double eval_bezier(double x1, double x2, double t) { - const double x1_times_3 = 3.0 * x1; - const double x2_times_3 = 3.0 * x2; - const double h3 = x1_times_3; - const double h1 = x1_times_3 - x2_times_3 + 1.0; - const double h2 = x2_times_3 - 6.0 * x1; +static double eval_bezier(double p1, double p2, double t) { + const double p1_times_3 = 3.0 * p1; + const double p2_times_3 = 3.0 * p2; + const double h3 = p1_times_3; + const double h1 = p1_times_3 - p2_times_3 + 1.0; + const double h2 = p2_times_3 - 6.0 * p1; return t * (t * (t * h1 + h2) + h3); } +static double eval_bezier_derivative(double p1, double p2, double t) { + const double h1 = 9.0 * p1 - 9.0 * p2 + 3.0; + const double h2 = 6.0 * p2 - 12.0 * p1; + const double h3 = 3.0 * p1; + return t * (t * h1 + h2) + h3; +} + +// Finds t such that eval_bezier(x1, x2, t) = x. +// There is a unique solution if x1 and x2 lie within (0, 1). static double bezier_interp(double x1, - double y1, double x2, - double y2, double x) { DCHECK_GE(1.0, x1); DCHECK_LE(0.0, x1); @@ -39,10 +46,6 @@ static double bezier_interp(double x1, x2 = std::min(std::max(x2, 0.0), 1.0); x = std::min(std::max(x, 0.0), 1.0); - // Step 1. Find the t corresponding to the given x. I.e., we want t such that - // eval_bezier(x1, x2, t) = x. There is a unique solution if x1 and x2 lie - // within (0, 1). - // // We're just going to do bisection for now (for simplicity), but we could // easily do some newton steps if this turns out to be a bottleneck. double t = 0.0; @@ -58,8 +61,7 @@ static double bezier_interp(double x1, // because we exceeded MAX_STEPS. Do a DCHECK here to confirm. DCHECK_GT(kBezierEpsilon, std::abs(eval_bezier(x1, x2, t) - x)); - // Step 2. Return the interpolated y values at the t we computed above. - return eval_bezier(y1, y2, t); + return t; } } // namespace @@ -75,7 +77,14 @@ CubicBezier::~CubicBezier() { } double CubicBezier::Solve(double x) const { - return bezier_interp(x1_, y1_, x2_, y2_, x); + return eval_bezier(y1_, y2_, bezier_interp(x1_, x2_, x)); +} + +double CubicBezier::Slope(double x) const { + double t = bezier_interp(x1_, x2_, x); + double dx_dt = eval_bezier_derivative(x1_, x2_, t); + double dy_dt = eval_bezier_derivative(y1_, y2_, t); + return dy_dt / dx_dt; } void CubicBezier::Range(double* min, double* max) const { @@ -85,6 +94,8 @@ void CubicBezier::Range(double* min, double* max) const { return; // Represent the function's derivative in the form at^2 + bt + c. + // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros + // but does not actually give the slope of the curve.) double a = 3 * (y1_ - y2_) + 1; double b = 2 * (y2_ - 2 * y1_); double c = y1_; diff --git a/ui/gfx/geometry/cubic_bezier.h b/ui/gfx/geometry/cubic_bezier.h index 645dfb7..66d5e8a 100644 --- a/ui/gfx/geometry/cubic_bezier.h +++ b/ui/gfx/geometry/cubic_bezier.h @@ -18,6 +18,9 @@ class GFX_EXPORT CubicBezier { // Returns an approximation of y at the given x. double Solve(double x) const; + // Returns an approximation of dy/dx at the given x. + double Slope(double x) const; + // Sets |min| and |max| to the bezier's minimum and maximium y values in the // interval [0, 1]. void Range(double* min, double* max) const; diff --git a/ui/gfx/geometry/cubic_bezier_unittest.cc b/ui/gfx/geometry/cubic_bezier_unittest.cc index 4fd60e8..168817c 100644 --- a/ui/gfx/geometry/cubic_bezier_unittest.cc +++ b/ui/gfx/geometry/cubic_bezier_unittest.cc @@ -136,5 +136,33 @@ TEST(CubicBezierTest, Range) { EXPECT_EQ(1.f, max); } +TEST(CubicBezierTest, Slope) { + CubicBezier function(0.25, 0.0, 0.75, 1.0); + + double epsilon = 0.00015; + + EXPECT_NEAR(function.Slope(0), 0, epsilon); + EXPECT_NEAR(function.Slope(0.05), 0.42170, epsilon); + EXPECT_NEAR(function.Slope(0.1), 0.69778, epsilon); + EXPECT_NEAR(function.Slope(0.15), 0.89121, epsilon); + EXPECT_NEAR(function.Slope(0.2), 1.03184, epsilon); + EXPECT_NEAR(function.Slope(0.25), 1.13576, epsilon); + EXPECT_NEAR(function.Slope(0.3), 1.21239, epsilon); + EXPECT_NEAR(function.Slope(0.35), 1.26751, epsilon); + EXPECT_NEAR(function.Slope(0.4), 1.30474, epsilon); + EXPECT_NEAR(function.Slope(0.45), 1.32628, epsilon); + EXPECT_NEAR(function.Slope(0.5), 1.33333, epsilon); + EXPECT_NEAR(function.Slope(0.55), 1.32628, epsilon); + EXPECT_NEAR(function.Slope(0.6), 1.30474, epsilon); + EXPECT_NEAR(function.Slope(0.65), 1.26751, epsilon); + EXPECT_NEAR(function.Slope(0.7), 1.21239, epsilon); + EXPECT_NEAR(function.Slope(0.75), 1.13576, epsilon); + EXPECT_NEAR(function.Slope(0.8), 1.03184, epsilon); + EXPECT_NEAR(function.Slope(0.85), 0.89121, epsilon); + EXPECT_NEAR(function.Slope(0.9), 0.69778, epsilon); + EXPECT_NEAR(function.Slope(0.95), 0.42170, epsilon); + EXPECT_NEAR(function.Slope(1), 0, epsilon); +} + } // namespace } // namespace gfx |