blob: f2d5475d625785d7d4c6ef9a462d5509b5423500 [file] [log] [blame]
// 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