chromium/ash/fast_ink/laser/laser_pointer_view.cc

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/fast_ink/laser/laser_pointer_view.h"

#include "ash/fast_ink/laser/laser_segment_utils.h"
#include "base/functional/bind.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkTypes.h"
#include "ui/aura/window.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/canvas.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {

// Variables for rendering the laser. Radius in DIP.
const float kPointInitialRadius = 5.0f;
const float kPointFinalRadius = 0.25f;
const int kPointInitialOpacity = 200;
const int kPointFinalOpacity = 10;
const SkColor kPointColor = SkColorSetRGB(255, 0, 0);
// Change this when debugging prediction code.
const SkColor kPredictionPointColor = kPointColor;

float DistanceBetweenPoints(const gfx::PointF& point1,
                            const gfx::PointF& point2) {
  return (point1 - point2).Length();
}

float LinearInterpolate(float initial_value,
                        float final_value,
                        float progress) {
  return initial_value + (final_value - initial_value) * progress;
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////

// The laser segment calcuates the path needed to draw a laser segment. A laser
// segment is used instead of just a regular line segments to avoid overlapping.
// A laser segment looks as follows:
//    _______         _________       _________        _________
//   /       \        \       /      /         /      /         \       |
//   |   A   |       2|.  B  .|1    2|.   C   .|1    2|.   D     \.1    |
//   |       |        |       |      |         |      |          /      |
//    \_____/         /_______\      \_________\      \_________/       |
//
//
// Given a start and end point (represented by the periods in the above
// diagrams), we create each segment by projecting each point along the normal
// to the line segment formed by the start(1) and end(2) points. We then
// create a path using arcs and lines. There are three types of laser segments:
// head(B), regular(C) and tail(D). A typical laser is created by rendering one
// tail(D), zero or more regular segments(C), one head(B) and a circle at the
// end(A). They are meant to fit perfectly with the previous and next segments,
// so that no whitespace/overlap is shown.
// A more detailed version of this is located at:
// https://docs.google.com/document/d/1wqws7g5ra7MCFDaDdMPbTFj7hJ-eq6MLd0podA2y_i0/edit
class LaserSegment {
 public:
  LaserSegment(const std::vector<gfx::PointF>& previous_points,
               const gfx::PointF& start_point,
               const gfx::PointF& end_point,
               float start_radius,
               float end_radius,
               bool is_last_segment) {
    DCHECK(previous_points.empty() || previous_points.size() == 2u);
    bool is_first_segment = previous_points.empty();

    // Calculate the variables for the equation of the lines which pass through
    // the start and end points, and are perpendicular to the line segment
    // between the start and end points.
    float slope, start_y_intercept, end_y_intercept;
    ComputeNormalLineVariables(start_point, end_point, &slope,
                               &start_y_intercept, &end_y_intercept);

    // Project the points along normal line by the given radius.
    gfx::PointF end_first_projection, end_second_projection;
    ComputeProjectedPoints(end_point, slope, end_y_intercept, end_radius,
                           &end_first_projection, &end_second_projection);

    // Create a collection of the points used to create the path and reorder
    // them as needed.
    std::vector<gfx::PointF> ordered_points;
    ordered_points.reserve(4);
    if (!is_first_segment) {
      ordered_points.push_back(previous_points[1]);
      ordered_points.push_back(previous_points[0]);
    } else {
      // We push two of the same point, so that for both cases we have 4 points,
      // and we can use the same indexes when creating the path.
      ordered_points.push_back(start_point);
      ordered_points.push_back(start_point);
    }
    // Push the projected points so that the the smaller angle relative to the
    // line segment between the two data points is first. This will ensure there
    // is always a anticlockwise arc between the last two points, and always a
    // clockwise arc for these two points if and when they are used in the next
    // segment.
    if (IsFirstPointSmallerAngle(start_point, end_point, end_first_projection,
                                 end_second_projection)) {
      ordered_points.push_back(end_first_projection);
      ordered_points.push_back(end_second_projection);
    } else {
      ordered_points.push_back(end_second_projection);
      ordered_points.push_back(end_first_projection);
    }

    // Create the path. The path always goes as follows:
    // 1. Move to point 0.
    // 2. Arc clockwise from point 0 to point 1. This step is skipped if it
    //    is the tail segment.
    // 3. Line from point 1 to point 2.
    // 4. Arc anticlockwise from point 2 to point 3. Arc clockwise if this is
    //    the head segment.
    // 5. Line from point 3 to point 0.
    //      2           1
    //       *---------*                   |
    //      /         /                    |
    //      |         |                    |
    //      |         |                    |
    //      \         \                    |
    //       *--------*
    //      3          0
    DCHECK_EQ(4u, ordered_points.size());
    path_.moveTo(ordered_points[0].x(), ordered_points[0].y());
    if (!is_first_segment) {
      path_.arcTo(start_radius, start_radius, 180.0f, SkPath::kSmall_ArcSize,
                  SkPathDirection::kCW, ordered_points[1].x(),
                  ordered_points[1].y());
    }

    path_.lineTo(ordered_points[2].x(), ordered_points[2].y());
    path_.arcTo(end_radius, end_radius, 180.0f, SkPath::kSmall_ArcSize,
                is_last_segment ? SkPathDirection::kCW : SkPathDirection::kCCW,
                ordered_points[3].x(), ordered_points[3].y());
    path_.lineTo(ordered_points[0].x(), ordered_points[0].y());

    // Store data to be used by the next segment.
    path_points_.push_back(ordered_points[2]);
    path_points_.push_back(ordered_points[3]);
  }

  LaserSegment(const LaserSegment&) = delete;
  LaserSegment& operator=(const LaserSegment&) = delete;

  SkPath path() const { return path_; }
  std::vector<gfx::PointF> path_points() const { return path_points_; }

 private:
  SkPath path_;
  std::vector<gfx::PointF> path_points_;
};

// LaserPointerView
LaserPointerView::LaserPointerView(base::TimeDelta life_duration,
                                   base::TimeDelta presentation_delay,
                                   base::TimeDelta stationary_point_delay)
    : laser_points_(life_duration),
      predicted_laser_points_(life_duration),
      presentation_delay_(presentation_delay),
      stationary_timer_(FROM_HERE,
                        stationary_point_delay,
                        base::BindRepeating(&LaserPointerView::UpdateTime,
                                            base::Unretained(this))) {}

LaserPointerView::~LaserPointerView() = default;

// static
views::UniqueWidgetPtr LaserPointerView::Create(
    base::TimeDelta life_duration,
    base::TimeDelta presentation_delay,
    base::TimeDelta stationary_point_delay,
    aura::Window* container) {
  return FastInkView::CreateWidgetWithContents(
      base::WrapUnique(new LaserPointerView(life_duration, presentation_delay,
                                            stationary_point_delay)),
      container);
}

void LaserPointerView::AddNewPoint(const gfx::PointF& new_point,
                                   const base::TimeTicks& new_time) {
  TRACE_EVENT1("ui", "LaserPointerView::AddNewPoint", "new_point",
               new_point.ToString());
  TRACE_COUNTER1("ui", "LaserPointerPredictionError",
                 predicted_laser_points_.GetNumberOfPoints()
                     ? std::round((new_point -
                                   predicted_laser_points_.GetOldest().location)
                                      .Length())
                     : 0);
  AddPoint(new_point, new_time);
  stationary_point_location_ = new_point;
  stationary_timer_.Reset();
}

void LaserPointerView::FadeOut(base::OnceClosure done) {
  fadeout_done_ = std::move(done);
}

void LaserPointerView::AddPoint(const gfx::PointF& point,
                                const base::TimeTicks& time) {
  laser_points_.AddPoint(point, time, kPointColor);

  // Current time is needed to determine presentation time and the number of
  // predicted points to add.
  base::TimeTicks current_time = ui::EventTimeForNow();
  predicted_laser_points_.Predict(
      laser_points_, current_time, presentation_delay_,
      GetWidget()->GetNativeView()->GetBoundsInScreen().size());

  // Move forward to next presentation time.
  base::TimeTicks next_presentation_time = current_time + presentation_delay_;
  laser_points_.MoveForwardToTime(next_presentation_time);
  predicted_laser_points_.MoveForwardToTime(next_presentation_time);

  ScheduleUpdateBuffer();
}

void LaserPointerView::ScheduleUpdateBuffer() {
  if (pending_update_buffer_)
    return;

  pending_update_buffer_ = true;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&LaserPointerView::UpdateBuffer,
                                weak_ptr_factory_.GetWeakPtr()));
}

void LaserPointerView::UpdateBuffer() {
  DCHECK(pending_update_buffer_);
  pending_update_buffer_ = false;

  gfx::Rect damage_rect = laser_content_rect_;
  laser_content_rect_ = GetBoundingBox();
  damage_rect.Union(laser_content_rect_);

  {
    TRACE_EVENT1("ui", "LaserPointerView::UpdateBuffer::Paint", "damage",
                 damage_rect.ToString());

    auto paint = GetScopedPaint(damage_rect);
    Draw(paint->canvas());
  }

  UpdateSurface(laser_content_rect_, damage_rect, /*auto_refresh=*/true);
}

void LaserPointerView::UpdateTime() {
  if (fadeout_done_.is_null()) {
    // Pointer still active but stationary, repeat the most recent position.
    AddPoint(stationary_point_location_, ui::EventTimeForNow());
    return;
  }

  if (laser_points_.IsEmpty() && predicted_laser_points_.IsEmpty()) {
    // No points left to show, complete the fadeout.
    std::move(fadeout_done_).Run();  // This will delete the LaserPointerView.
    return;
  }

  // Do not add the point but advance the time if the view is in process of
  // fading away.
  base::TimeTicks next_presentation_time =
      ui::EventTimeForNow() + presentation_delay_;
  laser_points_.MoveForwardToTime(next_presentation_time);
  predicted_laser_points_.MoveForwardToTime(next_presentation_time);

  ScheduleUpdateBuffer();
}

gfx::Rect LaserPointerView::GetBoundingBox() {
  // Early out if there are no points.
  if (laser_points_.IsEmpty() && predicted_laser_points_.IsEmpty())
    return gfx::Rect();

  // Merge bounding boxes. Note that this is not a union as the bounding box
  // for a single point is empty.
  gfx::Rect bounding_box;
  if (laser_points_.IsEmpty()) {
    bounding_box = predicted_laser_points_.GetBoundingBox();
  } else if (predicted_laser_points_.IsEmpty()) {
    bounding_box = laser_points_.GetBoundingBox();
  } else {
    gfx::Rect rect = laser_points_.GetBoundingBox();
    gfx::Rect predicted_rect = predicted_laser_points_.GetBoundingBox();
    bounding_box.SetByBounds(std::min(predicted_rect.x(), rect.x()),
                             std::min(predicted_rect.y(), rect.y()),
                             std::max(predicted_rect.right(), rect.right()),
                             std::max(predicted_rect.bottom(), rect.bottom()));
  }

  // Expand the bounding box so that it includes the radius of the points on the
  // edges and antialiasing.
  const int kOutsetForAntialiasing = 1;
  int outset = kPointInitialRadius + kOutsetForAntialiasing;
  bounding_box.Inset(-outset);
  return bounding_box;
}

void LaserPointerView::Draw(gfx::Canvas& canvas) {
  cc::PaintFlags flags;
  flags.setStyle(cc::PaintFlags::kFill_Style);
  flags.setAntiAlias(true);

  int num_points = laser_points_.GetNumberOfPoints() +
                   predicted_laser_points_.GetNumberOfPoints();
  if (!num_points)
    return;

  gfx::PointF previous_point;
  std::vector<gfx::PointF> previous_segment_points;
  float previous_radius;

  for (int i = 0; i < num_points; ++i) {
    gfx::PointF current_point;
    float fadeout_factor;
    if (i < laser_points_.GetNumberOfPoints()) {
      current_point = laser_points_.points()[i].location;
      fadeout_factor = laser_points_.GetFadeoutFactor(i);
    } else {
      int index = i - laser_points_.GetNumberOfPoints();
      current_point = predicted_laser_points_.points()[index].location;
      fadeout_factor = predicted_laser_points_.GetFadeoutFactor(index);
    }

    // Set the radius and opacity based on the age of the point.
    float current_radius = LinearInterpolate(kPointInitialRadius,
                                             kPointFinalRadius, fadeout_factor);
    int current_opacity = static_cast<int>(LinearInterpolate(
        kPointInitialOpacity, kPointFinalOpacity, fadeout_factor));

    if (i < laser_points_.GetNumberOfPoints())
      flags.setColor(SkColorSetA(kPointColor, current_opacity));
    else
      flags.setColor(SkColorSetA(kPredictionPointColor, current_opacity));

    if (i != 0) {
      // If we draw laser_points_ that are within a stroke width of each other,
      // the result will be very jagged, unless we are on the last point, then
      // we draw regardless.
      float distance_threshold = current_radius * 2.0f;
      if (DistanceBetweenPoints(previous_point, current_point) <=
              distance_threshold &&
          i != num_points - 1) {
        continue;
      }

      LaserSegment current_segment(previous_segment_points,
                                   gfx::PointF(previous_point),
                                   gfx::PointF(current_point), previous_radius,
                                   current_radius, i == num_points - 1);
      canvas.DrawPath(current_segment.path(), flags);
      previous_segment_points = current_segment.path_points();
    }

    previous_radius = current_radius;
    previous_point = current_point;
  }

  // Draw the last point as a circle.
  flags.setStyle(cc::PaintFlags::kFill_Style);
  canvas.DrawCircle(previous_point, kPointInitialRadius, flags);
}

}  // namespace ash