Commit 7a7d5bec authored by skobes@chromium.org's avatar skobes@chromium.org

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
parent 472de00a
......@@ -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
......@@ -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_;
......
......@@ -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
......@@ -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() {
......
......@@ -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);
......
......@@ -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);
......
......@@ -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());
}
......
......@@ -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_;
......
......@@ -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;
......
......@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment