| // Copyright 2013 The Chromium Authors |
| // 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 "cc/base/features.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; |
| |
| struct CubicBezierPoints { |
| double x1; |
| double y1; |
| double x2; |
| double y2; |
| }; |
| |
| // See `ui/gfx/animation/keyframe/timing_function.cc` |
| static constexpr CubicBezierPoints kEaseInOutControlPoints{ |
| .x1 = 0.42, |
| .y1 = 0, |
| .x2 = 0.58, |
| .y2 = 1, |
| }; |
| |
| CubicBezierPoints GetCubicBezierPointsForProgrammaticScroll() { |
| return { |
| .x1 = features::kCubicBezierX1.Get(), |
| .y1 = features::kCubicBezierY1.Get(), |
| .x2 = features::kCubicBezierX2.Get(), |
| .y2 = features::kCubicBezierY2.Get(), |
| }; |
| } |
| |
| 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 { |
| |
| 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(); |
| } |
| |
| std::unique_ptr<TimingFunction> EaseInOutWithInitialSlope( |
| const CubicBezierPoints& control_points, |
| double slope) { |
| // Clamp slope to a sane value. |
| slope = std::clamp(slope, -1000.0, 1000.0); |
| // Scale the first control point with `slope`. |
| return CubicBezierTimingFunction::Create( |
| control_points.x1, control_points.x1 * slope, control_points.x2, |
| control_points.y2); |
| } |
| |
| 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::Seconds(bound); |
| } |
| |
| } // namespace |
| |
| std::optional<double> |
| ScrollOffsetAnimationCurve::animation_duration_for_testing_; |
| |
| ScrollOffsetAnimationCurve::ScrollOffsetAnimationCurve( |
| const gfx::PointF& target_value, |
| AnimationType animation_type, |
| ScrollType scroll_type, |
| std::optional<DurationBehavior> duration_behavior) |
| : target_value_(target_value), |
| animation_type_(animation_type), |
| scroll_type_(scroll_type), |
| duration_behavior_(duration_behavior), |
| has_set_initial_value_(false) { |
| DCHECK_EQ(animation_type == AnimationType::kEaseInOut, |
| duration_behavior.has_value()); |
| switch (animation_type) { |
| case AnimationType::kEaseInOut: |
| timing_function_ = GetEasingFunction(/*slope=*/std::nullopt); |
| break; |
| case AnimationType::kLinear: |
| timing_function_ = LinearTimingFunction::Create(); |
| break; |
| } |
| } |
| |
| ScrollOffsetAnimationCurve::ScrollOffsetAnimationCurve( |
| const gfx::PointF& target_value, |
| std::unique_ptr<TimingFunction> timing_function, |
| AnimationType animation_type, |
| ScrollType scroll_type, |
| std::optional<DurationBehavior> duration_behavior) |
| : target_value_(target_value), |
| timing_function_(std::move(timing_function)), |
| animation_type_(animation_type), |
| scroll_type_(scroll_type), |
| duration_behavior_(duration_behavior), |
| has_set_initial_value_(false) { |
| DCHECK_EQ(animation_type == AnimationType::kEaseInOut, |
| duration_behavior.has_value()); |
| } |
| |
| ScrollOffsetAnimationCurve::~ScrollOffsetAnimationCurve() = default; |
| |
| 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::kConstant: |
| duration = kConstantDuration; |
| break; |
| case DurationBehavior::kDeltaBased: { |
| CHECK_EQ(scroll_type_, ScrollType::kProgrammatic); |
| duration = std::min<double>( |
| std::sqrt(std::abs(MaximumDimension(delta))), |
| features::kMaxAnimationDuration.Get().InSecondsF() * |
| kDurationDivisor); |
| break; |
| } |
| case DurationBehavior::kInverseDelta: |
| duration = kInverseDeltaOffset + |
| std::abs(MaximumDimension(delta)) * kInverseDeltaSlope; |
| duration = std::clamp(duration, kInverseDeltaMinDuration, |
| kInverseDeltaMaxDuration); |
| break; |
| } |
| duration /= kDurationDivisor; |
| } else { |
| duration = animation_duration_for_testing_.value(); |
| } |
| |
| base::TimeDelta delay_adjusted_duration = |
| base::Seconds(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_ - 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, |
| std::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()); |
| } |
| } |
| |
| // 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::Seconds(duration_in_seconds) - delayed_by; |
| return (delay_adjusted_duration >= base::TimeDelta()) |
| ? delay_adjusted_duration |
| : base::TimeDelta(); |
| } |
| |
| void ScrollOffsetAnimationCurve::SetInitialValue( |
| const gfx::PointF& initial_value, |
| base::TimeDelta delayed_by, |
| float velocity) { |
| initial_value_ = initial_value; |
| has_set_initial_value_ = true; |
| |
| gfx::Vector2dF delta = target_value_ - 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_ = initial_value_ + adjustment; |
| target_value_ = target_value_ + adjustment; |
| } |
| |
| gfx::PointF 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, TimingFunction::LimitDirection::RIGHT); |
| return gfx::PointF(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, |
| gfx::TimingFunction::LimitDirection unused) 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_, |
| scroll_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_ - 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()); |
| } |
| |
| std::unique_ptr<TimingFunction> ScrollOffsetAnimationCurve::GetEasingFunction( |
| std::optional<double> slope) { |
| CubicBezierPoints control_points = kEaseInOutControlPoints; |
| if (scroll_type_ == ScrollType::kProgrammatic) { |
| control_points = GetCubicBezierPointsForProgrammaticScroll(); |
| } |
| if (slope) { |
| return EaseInOutWithInitialSlope(control_points, *slope); |
| } |
| return CubicBezierTimingFunction::Create(control_points.x1, control_points.y1, |
| control_points.x2, |
| control_points.y2); |
| } |
| |
| void ScrollOffsetAnimationCurve::UpdateTarget(base::TimeDelta t, |
| const gfx::PointF& 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_ - 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::PointF current_position = GetValue(t); |
| gfx::Vector2dF new_delta = new_target - 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)); |
| |
| timing_function_ = GetEasingFunction(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 |