| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cc/animation/scroll_offset_animation_curve.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/numerics/ranges.h" |
| #include "ui/gfx/animation/keyframe/timing_function.h" |
| #include "ui/gfx/animation/tween.h" |
| |
| const double kConstantDuration = 9.0; |
| const double kDurationDivisor = 60.0; |
| |
| // 0.7 seconds limit for long-distance programmatic scrolls |
| const double kDeltaBasedMaxDuration = 0.7 * kDurationDivisor; |
| |
| const double kInverseDeltaRampStartPx = 120.0; |
| const double kInverseDeltaRampEndPx = 480.0; |
| const double kInverseDeltaMinDuration = 6.0; |
| const double kInverseDeltaMaxDuration = 12.0; |
| |
| const double kInverseDeltaSlope = |
| (kInverseDeltaMinDuration - kInverseDeltaMaxDuration) / |
| (kInverseDeltaRampEndPx - kInverseDeltaRampStartPx); |
| |
| const double kInverseDeltaOffset = |
| kInverseDeltaMaxDuration - kInverseDeltaRampStartPx * kInverseDeltaSlope; |
| |
| using gfx::CubicBezierTimingFunction; |
| using gfx::LinearTimingFunction; |
| using gfx::TimingFunction; |
| |
| namespace cc { |
| |
| namespace { |
| |
| constexpr double kImpulseCurveX1 = 0.25; |
| constexpr double kImpulseCurveX2 = 0.0; |
| constexpr double kImpulseCurveY2 = 1.0; |
| |
| constexpr double kImpulseMinDurationMs = 200.0; |
| constexpr double kImpulseMaxDurationMs = 500.0; |
| constexpr double kImpulseMillisecondsPerPixel = 1.5; |
| |
| const double kEpsilon = 0.01f; |
| |
| static float MaximumDimension(const gfx::Vector2dF& delta) { |
| return std::abs(delta.x()) > std::abs(delta.y()) ? delta.x() : delta.y(); |
| } |
| |
| static std::unique_ptr<TimingFunction> EaseInOutWithInitialSlope(double slope) { |
| // Clamp slope to a sane value. |
| slope = base::ClampToRange(slope, -1000.0, 1000.0); |
| |
| // Based on CubicBezierTimingFunction::EaseType::EASE_IN_OUT preset |
| // with first control point scaled. |
| const double x1 = 0.42; |
| const double y1 = slope * x1; |
| return CubicBezierTimingFunction::Create(x1, y1, 0.58, 1); |
| } |
| |
| std::unique_ptr<TimingFunction> ImpulseCurveWithInitialSlope(double slope) { |
| DCHECK_GE(slope, 0); |
| |
| double x1 = kImpulseCurveX1; |
| double y1 = 1.0; |
| if (x1 * slope < 1.0) { |
| y1 = x1 * slope; |
| } else { |
| x1 = y1 / slope; |
| } |
| |
| const double x2 = kImpulseCurveX2; |
| const double y2 = kImpulseCurveY2; |
| return CubicBezierTimingFunction::Create(x1, y1, x2, y2); |
| } |
| |
| bool IsNewTargetInOppositeDirection(const gfx::ScrollOffset& current_position, |
| const gfx::ScrollOffset& old_target, |
| const gfx::ScrollOffset& new_target) { |
| gfx::Vector2dF old_delta = old_target.DeltaFrom(current_position); |
| gfx::Vector2dF new_delta = new_target.DeltaFrom(current_position); |
| |
| // We only declare the new target to be in the "opposite" direction when |
| // one of the dimensions doesn't change at all. This may sound a bit strange, |
| // but it avoids lots of issues. |
| // For instance, if we are moving to the down & right and we are updated to |
| // move down & left, then are we moving in the opposite direction? If we don't |
| // do the check this way, then it would be considered in the opposite |
| // direction and the velocity gets set to 0. The update would therefore look |
| // pretty janky. |
| if (std::abs(old_delta.x() - new_delta.x()) < kEpsilon) { |
| return (old_delta.y() >= 0.0f) != (new_delta.y() >= 0.0f); |
| } else if (std::abs(old_delta.y() - new_delta.y()) < kEpsilon) { |
| return (old_delta.x() >= 0.0f) != (new_delta.x() >= 0.0f); |
| } else { |
| return false; |
| } |
| } |
| |
| base::TimeDelta VelocityBasedDurationBound(gfx::Vector2dF old_delta, |
| double velocity, |
| gfx::Vector2dF new_delta) { |
| double new_delta_max_dimension = MaximumDimension(new_delta); |
| |
| // If we are already at the target, stop animating. |
| if (std::abs(new_delta_max_dimension) < kEpsilon) |
| return base::TimeDelta(); |
| |
| // Guard against division by zero. |
| if (std::abs(velocity) < kEpsilon) { |
| return base::TimeDelta::Max(); |
| } |
| |
| // Estimate how long it will take to reach the new target at our present |
| // velocity, with some fudge factor to account for the "ease out". |
| double bound = (new_delta_max_dimension / velocity) * 2.5f; |
| |
| // If bound < 0 we are moving in the opposite direction. |
| return bound < 0 ? base::TimeDelta::Max() |
| : base::TimeDelta::FromSecondsD(bound); |
| } |
| |
| } // namespace |
| |
| base::Optional<double> |
| ScrollOffsetAnimationCurve::animation_duration_for_testing_; |
| |
| ScrollOffsetAnimationCurve::ScrollOffsetAnimationCurve( |
| const gfx::ScrollOffset& target_value, |
| AnimationType animation_type, |
| base::Optional<DurationBehavior> duration_behavior) |
| : target_value_(target_value), |
| animation_type_(animation_type), |
| duration_behavior_(duration_behavior), |
| has_set_initial_value_(false) { |
| DCHECK_EQ((animation_type == AnimationType::kEaseInOut || |
| animation_type == AnimationType::kImpulse), |
| duration_behavior.has_value()); |
| switch (animation_type) { |
| case AnimationType::kEaseInOut: |
| timing_function_ = CubicBezierTimingFunction::CreatePreset( |
| CubicBezierTimingFunction::EaseType::EASE_IN_OUT); |
| break; |
| case AnimationType::kLinear: |
| timing_function_ = LinearTimingFunction::Create(); |
| break; |
| case AnimationType::kImpulse: |
| timing_function_ = ImpulseCurveWithInitialSlope(0); |
| break; |
| } |
| } |
| |
| ScrollOffsetAnimationCurve::ScrollOffsetAnimationCurve( |
| const gfx::ScrollOffset& target_value, |
| std::unique_ptr<TimingFunction> timing_function, |
| AnimationType animation_type, |
| base::Optional<DurationBehavior> duration_behavior) |
| : target_value_(target_value), |
| timing_function_(std::move(timing_function)), |
| animation_type_(animation_type), |
| duration_behavior_(duration_behavior), |
| has_set_initial_value_(false) { |
| DCHECK_EQ((animation_type == AnimationType::kEaseInOut || |
| animation_type == AnimationType::kImpulse), |
| duration_behavior.has_value()); |
| } |
| |
| ScrollOffsetAnimationCurve::~ScrollOffsetAnimationCurve() = default; |
| |
| // static |
| base::TimeDelta ScrollOffsetAnimationCurve::EaseInOutSegmentDuration( |
| const gfx::Vector2dF& delta, |
| DurationBehavior duration_behavior, |
| base::TimeDelta delayed_by) { |
| double duration = kConstantDuration; |
| if (!animation_duration_for_testing_) { |
| switch (duration_behavior) { |
| case DurationBehavior::CONSTANT: |
| duration = kConstantDuration; |
| break; |
| case DurationBehavior::DELTA_BASED: |
| duration = |
| std::min<double>(std::sqrt(std::abs(MaximumDimension(delta))), |
| kDeltaBasedMaxDuration); |
| break; |
| case DurationBehavior::INVERSE_DELTA: |
| duration = kInverseDeltaOffset + |
| std::abs(MaximumDimension(delta)) * kInverseDeltaSlope; |
| duration = base::ClampToRange(duration, kInverseDeltaMinDuration, |
| kInverseDeltaMaxDuration); |
| break; |
| } |
| duration /= kDurationDivisor; |
| } else { |
| duration = animation_duration_for_testing_.value(); |
| } |
| |
| base::TimeDelta delay_adjusted_duration = |
| base::TimeDelta::FromSecondsD(duration) - delayed_by; |
| return (delay_adjusted_duration >= base::TimeDelta()) |
| ? delay_adjusted_duration |
| : base::TimeDelta(); |
| } |
| |
| base::TimeDelta ScrollOffsetAnimationCurve::EaseInOutBoundedSegmentDuration( |
| const gfx::Vector2dF& new_delta, |
| base::TimeDelta t, |
| base::TimeDelta delayed_by) { |
| gfx::Vector2dF old_delta = target_value_.DeltaFrom(initial_value_); |
| double velocity = CalculateVelocity(t); |
| |
| // Use the velocity-based duration bound when it is less than the constant |
| // segment duration. This minimizes the "rubber-band" bouncing effect when |
| // |velocity| is large and |new_delta| is small. |
| return std::min(EaseInOutSegmentDuration( |
| new_delta, duration_behavior_.value(), delayed_by), |
| VelocityBasedDurationBound(old_delta, velocity, new_delta)); |
| } |
| |
| base::TimeDelta ScrollOffsetAnimationCurve::SegmentDuration( |
| const gfx::Vector2dF& delta, |
| base::TimeDelta delayed_by, |
| base::Optional<double> velocity) { |
| switch (animation_type_) { |
| case AnimationType::kEaseInOut: |
| DCHECK(duration_behavior_.has_value()); |
| return EaseInOutSegmentDuration(delta, duration_behavior_.value(), |
| delayed_by); |
| case AnimationType::kLinear: |
| DCHECK(velocity.has_value()); |
| return LinearSegmentDuration(delta, delayed_by, velocity.value()); |
| case AnimationType::kImpulse: |
| return ImpulseSegmentDuration(delta, delayed_by); |
| } |
| } |
| |
| // static |
| base::TimeDelta ScrollOffsetAnimationCurve::LinearSegmentDuration( |
| const gfx::Vector2dF& delta, |
| base::TimeDelta delayed_by, |
| float velocity) { |
| double duration_in_seconds = |
| (animation_duration_for_testing_.has_value()) |
| ? animation_duration_for_testing_.value() |
| : std::abs(MaximumDimension(delta) / velocity); |
| base::TimeDelta delay_adjusted_duration = |
| base::TimeDelta::FromSecondsD(duration_in_seconds) - delayed_by; |
| return (delay_adjusted_duration >= base::TimeDelta()) |
| ? delay_adjusted_duration |
| : base::TimeDelta(); |
| } |
| |
| // static |
| base::TimeDelta ScrollOffsetAnimationCurve::ImpulseSegmentDuration( |
| const gfx::Vector2dF& delta, |
| base::TimeDelta delayed_by) { |
| base::TimeDelta duration; |
| if (animation_duration_for_testing_.has_value()) { |
| duration = |
| base::TimeDelta::FromSecondsD(animation_duration_for_testing_.value()); |
| } else { |
| double duration_in_milliseconds = |
| kImpulseMillisecondsPerPixel * std::abs(MaximumDimension(delta)); |
| duration_in_milliseconds = base::ClampToRange( |
| duration_in_milliseconds, kImpulseMinDurationMs, kImpulseMaxDurationMs); |
| duration = base::TimeDelta::FromMillisecondsD(duration_in_milliseconds); |
| } |
| |
| duration -= delayed_by; |
| return (duration >= base::TimeDelta()) ? duration : base::TimeDelta(); |
| } |
| |
| void ScrollOffsetAnimationCurve::SetInitialValue( |
| const gfx::ScrollOffset& initial_value, |
| base::TimeDelta delayed_by, |
| float velocity) { |
| initial_value_ = initial_value; |
| has_set_initial_value_ = true; |
| |
| gfx::Vector2dF delta = target_value_.DeltaFrom(initial_value); |
| total_animation_duration_ = SegmentDuration(delta, delayed_by, velocity); |
| } |
| |
| bool ScrollOffsetAnimationCurve::HasSetInitialValue() const { |
| return has_set_initial_value_; |
| } |
| |
| void ScrollOffsetAnimationCurve::ApplyAdjustment( |
| const gfx::Vector2dF& adjustment) { |
| initial_value_ = ScrollOffsetWithDelta(initial_value_, adjustment); |
| target_value_ = ScrollOffsetWithDelta(target_value_, adjustment); |
| } |
| |
| gfx::ScrollOffset ScrollOffsetAnimationCurve::GetValue( |
| base::TimeDelta t) const { |
| const base::TimeDelta duration = total_animation_duration_ - last_retarget_; |
| t -= last_retarget_; |
| |
| if (duration.is_zero() || (t >= duration)) |
| return target_value_; |
| if (t <= base::TimeDelta()) |
| return initial_value_; |
| |
| const double progress = timing_function_->GetValue(t / duration); |
| return gfx::ScrollOffset( |
| gfx::Tween::FloatValueBetween(progress, initial_value_.x(), |
| target_value_.x()), |
| gfx::Tween::FloatValueBetween(progress, initial_value_.y(), |
| target_value_.y())); |
| } |
| |
| base::TimeDelta ScrollOffsetAnimationCurve::Duration() const { |
| return total_animation_duration_; |
| } |
| |
| int ScrollOffsetAnimationCurve::Type() const { |
| return AnimationCurve::SCROLL_OFFSET; |
| } |
| |
| const char* ScrollOffsetAnimationCurve::TypeName() const { |
| return "ScrollOffset"; |
| } |
| |
| std::unique_ptr<gfx::AnimationCurve> ScrollOffsetAnimationCurve::Clone() const { |
| return CloneToScrollOffsetAnimationCurve(); |
| } |
| |
| void ScrollOffsetAnimationCurve::Tick( |
| base::TimeDelta t, |
| int property_id, |
| gfx::KeyframeModel* keyframe_model) const { |
| if (target_) { |
| target_->OnScrollOffsetAnimated(GetValue(t), property_id, keyframe_model); |
| } |
| } |
| |
| std::unique_ptr<ScrollOffsetAnimationCurve> |
| ScrollOffsetAnimationCurve::CloneToScrollOffsetAnimationCurve() const { |
| std::unique_ptr<TimingFunction> timing_function( |
| static_cast<TimingFunction*>(timing_function_->Clone().release())); |
| std::unique_ptr<ScrollOffsetAnimationCurve> curve_clone = base::WrapUnique( |
| new ScrollOffsetAnimationCurve(target_value_, std::move(timing_function), |
| animation_type_, duration_behavior_)); |
| curve_clone->initial_value_ = initial_value_; |
| curve_clone->total_animation_duration_ = total_animation_duration_; |
| curve_clone->last_retarget_ = last_retarget_; |
| curve_clone->has_set_initial_value_ = has_set_initial_value_; |
| return curve_clone; |
| } |
| |
| void ScrollOffsetAnimationCurve::SetAnimationDurationForTesting( |
| base::TimeDelta duration) { |
| animation_duration_for_testing_ = duration.InSecondsF(); |
| } |
| |
| double ScrollOffsetAnimationCurve::CalculateVelocity(base::TimeDelta t) { |
| base::TimeDelta duration = total_animation_duration_ - last_retarget_; |
| const double slope = |
| timing_function_->Velocity((t - last_retarget_) / duration); |
| |
| gfx::Vector2dF delta = target_value_.DeltaFrom(initial_value_); |
| |
| // TimingFunction::Velocity just gives the slope of the curve. Convert it to |
| // units of pixels per second. |
| return slope * (MaximumDimension(delta) / duration.InSecondsF()); |
| } |
| |
| void ScrollOffsetAnimationCurve::UpdateTarget( |
| base::TimeDelta t, |
| const gfx::ScrollOffset& new_target) { |
| DCHECK_NE(animation_type_, AnimationType::kLinear) |
| << "UpdateTarget is not supported on linear scroll animations."; |
| |
| // UpdateTarget is still called for linear animations occasionally. This is |
| // tracked via crbug.com/1164008. |
| if (animation_type_ == AnimationType::kLinear) |
| return; |
| |
| // If the new UpdateTarget actually happened before the previous one, keep |
| // |t| as the most recent, but reduce the duration of any generated |
| // animation. |
| base::TimeDelta delayed_by = std::max(base::TimeDelta(), last_retarget_ - t); |
| t = std::max(t, last_retarget_); |
| |
| if (animation_type_ == AnimationType::kEaseInOut && |
| std::abs(MaximumDimension(target_value_.DeltaFrom(new_target))) < |
| kEpsilon) { |
| // Don't update the animation if the new target is the same as the old one. |
| // This is done for EaseInOut-style animation curves, since the duration is |
| // inversely proportional to the distance, and it may cause an animation |
| // that is longer than the one currently running. |
| // Specifically avoid doing this for Impulse-style animation curves since |
| // its duration is directly proportional to the distance, and we don't want |
| // to drop user input. |
| target_value_ = new_target; |
| return; |
| } |
| |
| gfx::ScrollOffset current_position = GetValue(t); |
| gfx::Vector2dF new_delta = new_target.DeltaFrom(current_position); |
| |
| // We are already at or very close to the new target. Stop animating. |
| if (std::abs(MaximumDimension(new_delta)) < kEpsilon) { |
| last_retarget_ = t; |
| total_animation_duration_ = t; |
| target_value_ = new_target; |
| return; |
| } |
| |
| // The last segment was of zero duration. |
| base::TimeDelta old_duration = total_animation_duration_ - last_retarget_; |
| if (old_duration.is_zero()) { |
| DCHECK_EQ(t, last_retarget_); |
| total_animation_duration_ = SegmentDuration(new_delta, delayed_by); |
| target_value_ = new_target; |
| return; |
| } |
| |
| const base::TimeDelta new_duration = |
| EaseInOutBoundedSegmentDuration(new_delta, t, delayed_by); |
| if (new_duration.InSecondsF() < kEpsilon) { |
| // The duration is (close to) 0, so stop the animation. |
| target_value_ = new_target; |
| total_animation_duration_ = t; |
| return; |
| } |
| |
| // Adjust the slope of the new animation in order to preserve the velocity of |
| // the old animation. |
| double velocity = CalculateVelocity(t); |
| double new_slope = |
| velocity * (new_duration.InSecondsF() / MaximumDimension(new_delta)); |
| |
| DCHECK(animation_type_ == AnimationType::kImpulse || |
| animation_type_ == AnimationType::kEaseInOut); |
| if (animation_type_ == AnimationType::kImpulse && |
| IsNewTargetInOppositeDirection(current_position, target_value_, |
| new_target)) { |
| // Prevent any rubber-banding by setting the velocity (and subsequently, the |
| // slope) to 0 when moving in the opposite direciton. |
| new_slope = 0; |
| } |
| |
| timing_function_ = EaseInOutWithInitialSlope(new_slope); |
| initial_value_ = current_position; |
| target_value_ = new_target; |
| total_animation_duration_ = t + new_duration; |
| last_retarget_ = t; |
| } |
| |
| const ScrollOffsetAnimationCurve* |
| ScrollOffsetAnimationCurve::ToScrollOffsetAnimationCurve( |
| const AnimationCurve* c) { |
| DCHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type()); |
| return static_cast<const ScrollOffsetAnimationCurve*>(c); |
| } |
| |
| ScrollOffsetAnimationCurve* |
| ScrollOffsetAnimationCurve::ToScrollOffsetAnimationCurve(AnimationCurve* c) { |
| DCHECK_EQ(ScrollOffsetAnimationCurve::SCROLL_OFFSET, c->Type()); |
| return static_cast<ScrollOffsetAnimationCurve*>(c); |
| } |
| |
| } // namespace cc |