| // Copyright 2016 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 "ash/fast_ink/fast_ink_points.h" |
| |
| #include <algorithm> |
| #include <array> |
| #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 { |
| |
| constexpr SkColor kDefaultPointColor = SkColorSetRGB(0x42, 0x85, 0xF4); |
| constexpr int kDefaultOpacity = 0xCC; |
| |
| } // namespace |
| |
| namespace fast_ink { |
| |
| 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() { |
| // Not doing anything special regarding prediction, as in real usage there |
| // will be a gap in timestamps, and the prediction algorithm will reject the |
| // points that are too old. |
| 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()) { |
| // Remove obsolete points. |
| const base::TimeTicks expiration = latest_time - life_duration_; |
| auto first_alive_point = std::find_if( |
| points_.begin(), points_.end(), |
| [expiration](const FastInkPoint& p) { return p.time > expiration; }); |
| 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; |
| // Skip the last gap to delete until the penultimate gap. |
| 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()); |
| |
| // Create a new set of predicted points based on the last four points added. |
| // We add enough predicted points to fill the time between the new point and |
| // the expected presentation time. Note that estimated presentation time is |
| // based on current time and inefficient rendering of points can result in an |
| // actual presentation time that is later. |
| |
| // TODO(reveman): Determine interval based on history when event time stamps |
| // are accurate. b/36137953 |
| 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()); |
| |
| // Use the last four points for prediction. |
| using PositionArray = std::array<gfx::PointF, 4>; |
| PositionArray position; |
| PositionArray::iterator it = position.begin(); |
| for (const auto& point : base::Reversed(real_points.points())) { |
| // Stop adding positions if interval between points is too large to provide |
| // an accurate history for prediction. |
| 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; |
| |
| // Stop when no more positions are needed. |
| if (it == position.end()) |
| break; |
| } |
| |
| const size_t valid_positions = it - position.begin(); |
| if (valid_positions < 2) // Not enough reliable data, bail out. |
| return; |
| |
| // Note: Currently there's no need to divide by the time delta between |
| // points as we assume a constant delta between points that matches the |
| // prediction point interval. |
| gfx::Vector2dF velocity[3]; |
| for (size_t i = 0; i < valid_positions - 1; ++i) |
| velocity[i] = position[i] - position[i + 1]; |
| // velocity[0] is always valid, since |valid_positions| >=2 |
| |
| gfx::Vector2dF acceleration[2]; |
| for (size_t i = 0; i < valid_positions - 2; ++i) |
| acceleration[i] = velocity[i] - velocity[i + 1]; |
| // acceleration[0] is always valid (zero if |valid_positions| < 3). |
| |
| gfx::Vector2dF jerk; |
| if (valid_positions > 3) |
| jerk = acceleration[0] - acceleration[1]; |
| // |jerk| is aways valid (zero if |valid_positions| < 4). |
| |
| // Adjust max prediction time based on speed as prediction data is not great |
| // at lower speeds. |
| 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); |
| |
| // Add predicted points until we reach the max prediction time. |
| gfx::PointF location = position[0]; |
| for (base::TimeTicks time = newest_real_point.time + prediction_interval; |
| time < max_prediction_time; time += prediction_interval) { |
| // Note: Currently there's no need to multiply by the prediction interval |
| // as the velocity is calculated based on a time delta between points that |
| // is the same as the 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); |
| |
| // Always stop at three predicted points as a four point history doesn't |
| // provide accurate prediction of more points. |
| if (GetNumberOfPoints() == 3) |
| break; |
| } |
| } |
| |
| } // namespace fast_ink |