#include "ash/fast_ink/fast_ink_points.h"
#include <algorithm>
#include <array>
#include <functional>
#include <limits>
#include "base/containers/adapters.h"
#include "base/containers/circular_deque.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace ash {
namespace {
constexpr SkColor kDefaultPointColor = SkColorSetRGB(0x42, 0x85, 0xF4);
constexpr int kDefaultOpacity = 0xCC;
}
const SkColor FastInkPoints::kDefaultColor =
SkColorSetA(kDefaultPointColor, kDefaultOpacity);
FastInkPoints::FastInkPoints(base::TimeDelta life_duration)
: life_duration_(life_duration) {}
FastInkPoints::~FastInkPoints() = default;
void FastInkPoints::AddPoint(const gfx::PointF& point,
const base::TimeTicks& time) {
FastInkPoint new_point;
new_point.location = point;
new_point.time = time;
points_.push_back(new_point);
}
void FastInkPoints::AddPoint(const gfx::PointF& point,
const base::TimeTicks& time,
SkColor color) {
FastInkPoint new_point;
new_point.location = point;
new_point.time = time;
new_point.color = color;
points_.push_back(new_point);
}
void FastInkPoints::AddGap() {
points_.back().gap_after = true;
}
void FastInkPoints::MoveForwardToTime(const base::TimeTicks& latest_time) {
DCHECK_GE(latest_time, collection_latest_time_);
collection_latest_time_ = latest_time;
if (!points_.empty() && !life_duration_.is_zero()) {
const base::TimeTicks expiration = latest_time - life_duration_;
auto first_alive_point = std::ranges::lower_bound(
points_, expiration, std::ranges::less_equal(), &FastInkPoint::time);
points_.erase(points_.begin(), first_alive_point);
}
}
gfx::Rect FastInkPoints::UndoLastStroke() {
if (points_.empty())
return gfx::Rect();
gfx::PointF min_point = GetNewest().location;
gfx::PointF max_point = min_point;
if (points_.back().gap_after)
points_.pop_back();
while (!points_.empty() && !points_.back().gap_after) {
const gfx::PointF& location = points_.back().location;
min_point.SetToMin(location);
max_point.SetToMax(location);
points_.pop_back();
}
return gfx::ToEnclosingRect(gfx::BoundingRect(min_point, max_point));
}
void FastInkPoints::Clear() {
points_.clear();
}
gfx::Rect FastInkPoints::GetBoundingBox() const {
return gfx::ToEnclosingRect(GetBoundingBoxF());
}
gfx::RectF FastInkPoints::GetBoundingBoxF() const {
if (IsEmpty())
return gfx::RectF();
gfx::PointF min_point = GetOldest().location;
gfx::PointF max_point = min_point;
for (const FastInkPoint& point : points_) {
min_point.SetToMin(point.location);
max_point.SetToMax(point.location);
}
return gfx::BoundingRect(min_point, max_point);
}
FastInkPoints::FastInkPoint FastInkPoints::GetOldest() const {
DCHECK(!IsEmpty());
return points_.front();
}
FastInkPoints::FastInkPoint FastInkPoints::GetNewest() const {
DCHECK(!IsEmpty());
return points_.back();
}
bool FastInkPoints::IsEmpty() const {
return points_.empty();
}
int FastInkPoints::GetNumberOfPoints() const {
return points_.size();
}
const base::circular_deque<FastInkPoints::FastInkPoint>& FastInkPoints::points()
const {
return points_;
}
float FastInkPoints::GetFadeoutFactor(int index) const {
DCHECK(!life_duration_.is_zero());
DCHECK_GE(index, 0);
DCHECK_LT(index, GetNumberOfPoints());
const base::TimeDelta age = collection_latest_time_ - points_[index].time;
return std::min(age / life_duration_, 1.0);
}
void FastInkPoints::Predict(const FastInkPoints& real_points,
const base::TimeTicks& current_time,
base::TimeDelta prediction_duration,
const gfx::Size& screen_size) {
Clear();
if (real_points.IsEmpty() || prediction_duration.is_zero())
return;
gfx::Vector2dF scale(1.0f / screen_size.width(), 1.0f / screen_size.height());
const float kPredictionIntervalMs = 5.0f;
const float kMaxPointIntervalMs = 10.0f;
base::TimeDelta prediction_interval =
base::Milliseconds(kPredictionIntervalMs);
base::TimeDelta max_point_interval = base::Milliseconds(kMaxPointIntervalMs);
const FastInkPoint newest_real_point = real_points.GetNewest();
base::TimeTicks last_point_time = newest_real_point.time;
gfx::PointF last_point_location =
gfx::ScalePoint(newest_real_point.location, scale.x(), scale.y());
using PositionArray = std::array<gfx::PointF, 4>;
PositionArray position;
PositionArray::iterator it = position.begin();
for (const auto& point : base::Reversed(real_points.points())) {
if ((last_point_time - point.time) > max_point_interval)
break;
last_point_time = point.time;
last_point_location = gfx::ScalePoint(point.location, scale.x(), scale.y());
*it++ = last_point_location;
if (it == position.end())
break;
}
const size_t valid_positions = it - position.begin();
if (valid_positions < 2)
return;
std::array<gfx::Vector2dF, 3> velocity = {};
for (size_t i = 0; i < valid_positions - 1; ++i)
velocity[i] = position[i] - position[i + 1];
std::array<gfx::Vector2dF, 2> acceleration = {};
for (size_t i = 0; i < valid_positions - 2; ++i)
acceleration[i] = velocity[i] - velocity[i + 1];
gfx::Vector2dF jerk;
if (valid_positions > 3)
jerk = acceleration[0] - acceleration[1];
const float kMaxPredictionScaleSpeed = 1e-5;
double speed = velocity[0].LengthSquared();
base::TimeTicks max_prediction_time =
current_time +
std::min(prediction_duration * (speed / kMaxPredictionScaleSpeed),
prediction_duration);
gfx::PointF location = position[0];
for (base::TimeTicks time = newest_real_point.time + prediction_interval;
time < max_prediction_time; time += prediction_interval) {
velocity[0] += acceleration[0];
acceleration[0] += jerk;
location += velocity[0];
AddPoint(gfx::ScalePoint(location, 1 / scale.x(), 1 / scale.y()), time,
newest_real_point.color);
if (GetNumberOfPoints() == 3)
break;
}
}
}