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
ash / fast_ink / laser / laser_pointer_view.cc [blame]
// 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