1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
|
// 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
|