blob: 18122bfaf1961a4943c13749ee0b2c8f3289ffc3 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/events/gestures/physics_based_fling_curve.h"
#include <algorithm>
#include <cmath>
#include "base/check.h"
namespace {
// These constants are defined based on UX experiment.
const float kDefaultP1X = 0.2f;
const float kDefaultP1Y = 1.0f;
const float kDefaultP2X = 0.55f;
const float kDefaultP2Y = 1.0f;
const float kDefaultPixelDeceleration = 2300.0f;
const float kMaxCurveDurationForFling = 2.0f;
const float kDefaultPhysicalDeceleration = 2.7559e-5f; // inch/ms^2.
inline gfx::Vector2dF GetPositionAtValue(const gfx::Vector2dF& end_point,
double progress) {
return gfx::ScaleVector2d(end_point, progress);
}
inline gfx::Vector2dF GetVelocityAtTime(const gfx::Vector2dF& current_offset,
const gfx::Vector2dF& prev_offset,
base::TimeDelta delta) {
return gfx::ScaleVector2d(current_offset - prev_offset, delta.ToHz());
}
float GetOffset(float velocity, float deceleration, float duration) {
const float position =
(std::abs(velocity) - deceleration * duration * 0.5) * duration;
return std::copysign(position, velocity);
}
gfx::Vector2dF GetDecelerationInPixelsPerMs2(
const gfx::Vector2dF& pixels_per_inch) {
return gfx::ScaleVector2d(pixels_per_inch, kDefaultPhysicalDeceleration);
}
gfx::Vector2dF GetDuration(const gfx::Vector2dF& velocity,
const gfx::Vector2dF& deceleration) {
return gfx::Vector2dF(fabs(velocity.x() / deceleration.x()),
fabs(velocity.y() / deceleration.y()));
}
// Calculates the |distance_| based on the fling velocity. It utilizes
// following equation for |distance_| calculation. d = v*t - 0.5*a*t^2 where d
// = distance, v = initial velocity, a = acceleration and time = duration before
// it reaches zero velocity. time = final velocity(0) - initial velocity(v) /
// acceleration (a) This |distance_| is later used by PhysicsBasedFlingCurve to
// generate fling animation curve.
gfx::Vector2dF CalculateEndPoint(const gfx::Vector2dF& pixels_per_inch,
const gfx::Vector2dF& velocity_pixels_per_ms,
const gfx::Size& bounding_size) {
// deceleration is in pixels/ ms^2.
gfx::Vector2dF deceleration = GetDecelerationInPixelsPerMs2(pixels_per_inch);
// duration is in ms
gfx::Vector2dF duration = GetDuration(velocity_pixels_per_ms, deceleration);
// distance offset in screen coordinate
gfx::Vector2dF offset_in_screen_coord_space = gfx::Vector2dF(
GetOffset(velocity_pixels_per_ms.x(), deceleration.x(), duration.x()),
GetOffset(velocity_pixels_per_ms.y(), deceleration.y(), duration.y()));
if (std::abs(offset_in_screen_coord_space.x()) > bounding_size.width()) {
float sign = offset_in_screen_coord_space.x() > 0 ? 1 : -1;
offset_in_screen_coord_space.set_x(bounding_size.width() * sign);
}
if (std::abs(offset_in_screen_coord_space.y()) > bounding_size.height()) {
float sign = offset_in_screen_coord_space.y() > 0 ? 1 : -1;
offset_in_screen_coord_space.set_y(bounding_size.height() * sign);
}
return offset_in_screen_coord_space;
}
} // namespace
namespace ui {
PhysicsBasedFlingCurve::PhysicsBasedFlingCurve(
const gfx::Vector2dF& velocity,
base::TimeTicks start_timestamp,
const gfx::Vector2dF& pixels_per_inch,
const float boost_multiplier,
const gfx::Size& bounding_size)
: start_timestamp_(start_timestamp),
p1_(gfx::PointF(kDefaultP1X, kDefaultP1Y)),
p2_(gfx::PointF(kDefaultP2X, kDefaultP2Y)),
distance_(CalculateEndPoint(
pixels_per_inch,
gfx::ScaleVector2d(velocity, 1 / 1000.0f),
ScaleToFlooredSize(bounding_size,
boost_multiplier * kDefaultBoundsMultiplier))),
curve_duration_(CalculateDurationAndConfigureControlPoints(velocity)),
bezier_(p1_.x(), p1_.y(), p2_.x(), p2_.y()),
previous_time_delta_(base::TimeDelta()) {
DCHECK(!velocity.IsZero());
DCHECK(!std::isnan(velocity.x()));
DCHECK(!std::isnan(velocity.y()));
}
PhysicsBasedFlingCurve::~PhysicsBasedFlingCurve() = default;
bool PhysicsBasedFlingCurve::ComputeScrollOffset(base::TimeTicks time,
gfx::Vector2dF* offset,
gfx::Vector2dF* velocity) {
DCHECK(offset);
DCHECK(velocity);
const base::TimeDelta elapsed_time = time - start_timestamp_;
if (elapsed_time.is_negative()) {
*offset = gfx::Vector2dF();
*velocity = gfx::Vector2dF();
return true;
}
const double x = elapsed_time / curve_duration_;
const bool still_active = x < 1.0f;
if (still_active) {
const double progress = bezier_.Solve(x);
*offset = GetPositionAtValue(distance_, progress);
*velocity = GetVelocityAtTime(*offset, prev_offset_,
elapsed_time - previous_time_delta_);
prev_offset_ = *offset;
previous_time_delta_ = elapsed_time;
} else {
// At the end of animation, we should have traveled a distance equal to
// |distance_|.
*offset = distance_;
*velocity = gfx::Vector2dF();
}
return still_active;
}
base::TimeDelta
PhysicsBasedFlingCurve::CalculateDurationAndConfigureControlPoints(
const gfx::Vector2dF& velocity) {
float fling_velocity = std::max(fabs(velocity.x()), fabs(velocity.y()));
float duration = std::min(kMaxCurveDurationForFling,
(fling_velocity / kDefaultPixelDeceleration));
float slope = fabs(velocity.y()) == fling_velocity
? fabs(velocity.y() * duration / distance_.y())
: fabs(velocity.x() * duration / distance_.x());
// Configure cubic bezier control points based on initial fling velocity.
// When matching the initial fling velocity for a Cubic Bezier Curve, scale
// |p1_.y| up until it reaches 1 After it reaches 1, move |p1_.x| to the left.
if (slope * p1_.x() < 1.0f) {
p1_.set_y(p1_.x() * slope);
} else {
p1_.set_x(p1_.y() / slope);
}
return base::Seconds(duration);
}
} // namespace ui