1 // Copyright (c) 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/gfx/paint_throbber.h"
7 #include "base/time/time.h"
8 #include "ui/gfx/animation/tween.h"
9 #include "ui/gfx/canvas.h"
10 #include "ui/gfx/color_utils.h"
11 #include "ui/gfx/geometry/rect.h"
12 #include "ui/gfx/skia_util.h"
18 // The maximum size of the "spinning" state arc, in degrees.
19 const int64_t kMaxArcSize
= 270;
21 // The amount of time it takes to grow the "spinning" arc from 0 to 270 degrees.
22 const int64_t kArcTimeMs
= 666;
24 // The amount of time it takes for the "spinning" throbber to make a full
26 const int64_t kRotationTimeMs
= 1568;
28 void PaintArc(Canvas
* canvas
,
33 // Stroke width depends on size.
34 // . For size < 28: 3 - (28 - size) / 16
35 // . For 28 <= size: (8 + size) / 12
36 SkScalar stroke_width
= bounds
.width() < 28
37 ? 3.0 - SkIntToScalar(28 - bounds
.width()) / 16.0
38 : SkIntToScalar(bounds
.width() + 8) / 12.0;
40 // Inset by half the stroke width to make sure the whole arc is inside
42 int inset
= SkScalarCeilToInt(stroke_width
/ 2.0);
43 oval
.Inset(inset
, inset
);
46 path
.arcTo(RectToSkRect(oval
), start_angle
, sweep
, true);
49 paint
.setColor(color
);
50 paint
.setStrokeCap(SkPaint::kRound_Cap
);
51 paint
.setStrokeWidth(stroke_width
);
52 paint
.setStyle(SkPaint::kStroke_Style
);
53 paint
.setAntiAlias(true);
54 canvas
->DrawPath(path
, paint
);
57 void CalculateWaitingAngles(const base::TimeDelta
& elapsed_time
,
60 // Calculate start and end points. The angles are counter-clockwise because
61 // the throbber spins counter-clockwise. The finish angle starts at 12 o'clock
62 // (90 degrees) and rotates steadily. The start angle trails 180 degrees
63 // behind, except for the first half revolution, when it stays at 12 o'clock.
64 base::TimeDelta revolution_time
= base::TimeDelta::FromMilliseconds(1320);
65 int64_t twelve_oclock
= 90;
66 int64_t finish_angle_cc
=
67 twelve_oclock
+ 360 * elapsed_time
/ revolution_time
;
68 int64_t start_angle_cc
= std::max(finish_angle_cc
- 180, twelve_oclock
);
70 // Negate the angles to convert to the clockwise numbers Skia expects.
72 *start_angle
= -finish_angle_cc
;
74 *sweep
= finish_angle_cc
- start_angle_cc
;
77 // This is a Skia port of the MD spinner SVG. The |start_angle| rotation
78 // here corresponds to the 'rotate' animation.
79 void PaintThrobberSpinningWithStartAngle(Canvas
* canvas
,
82 const base::TimeDelta
& elapsed_time
,
83 int64_t start_angle
) {
84 // The sweep angle ranges from -270 to 270 over 1333ms. CSS
85 // animation timing functions apply in between key frames, so we have to
86 // break up the 1333ms into two keyframes (-270 to 0, then 0 to 270).
87 base::TimeDelta arc_time
= base::TimeDelta::FromMilliseconds(kArcTimeMs
);
88 double arc_size_progress
= static_cast<double>(elapsed_time
.InMicroseconds() %
89 arc_time
.InMicroseconds()) /
90 arc_time
.InMicroseconds();
91 // This tween is equivalent to cubic-bezier(0.4, 0.0, 0.2, 1).
92 double sweep
= kMaxArcSize
* Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN
,
94 int64_t sweep_keyframe
= (elapsed_time
/ arc_time
) % 2;
95 if (sweep_keyframe
== 0)
98 // This part makes sure the sweep is at least 5 degrees long. Roughly
99 // equivalent to the "magic constants" in SVG's fillunfill animation.
100 const double min_sweep_length
= 5.0;
101 if (sweep
>= 0.0 && sweep
< min_sweep_length
) {
102 start_angle
-= (min_sweep_length
- sweep
);
103 sweep
= min_sweep_length
;
104 } else if (sweep
<= 0.0 && sweep
> -min_sweep_length
) {
105 start_angle
+= (-min_sweep_length
- sweep
);
106 sweep
= -min_sweep_length
;
109 // To keep the sweep smooth, we have an additional rotation after each
110 // |arc_time| period has elapsed. See SVG's 'rot' animation.
111 int64_t rot_keyframe
= (elapsed_time
/ (arc_time
* 2)) % 4;
112 PaintArc(canvas
, bounds
, color
, start_angle
+ rot_keyframe
* kMaxArcSize
,
118 void PaintThrobberSpinning(Canvas
* canvas
,
121 const base::TimeDelta
& elapsed_time
) {
122 base::TimeDelta rotation_time
=
123 base::TimeDelta::FromMilliseconds(kRotationTimeMs
);
124 int64_t start_angle
= 270 + 360 * elapsed_time
/ rotation_time
;
125 PaintThrobberSpinningWithStartAngle(canvas
, bounds
, color
, elapsed_time
,
129 void PaintThrobberWaiting(Canvas
* canvas
,
130 const Rect
& bounds
, SkColor color
, const base::TimeDelta
& elapsed_time
) {
131 int64_t start_angle
= 0, sweep
= 0;
132 CalculateWaitingAngles(elapsed_time
, &start_angle
, &sweep
);
133 PaintArc(canvas
, bounds
, color
, start_angle
, sweep
);
136 void PaintThrobberSpinningAfterWaiting(Canvas
* canvas
,
139 const base::TimeDelta
& elapsed_time
,
140 ThrobberWaitingState
* waiting_state
) {
141 int64_t waiting_start_angle
= 0, waiting_sweep
= 0;
142 CalculateWaitingAngles(waiting_state
->elapsed_time
, &waiting_start_angle
,
145 // |arc_time_offset| is the effective amount of time one would have to wait
146 // for the "spinning" sweep to match |waiting_sweep|. Brute force calculation.
147 if (waiting_state
->arc_time_offset
== base::TimeDelta()) {
148 for (int64_t arc_time_it
= 0; arc_time_it
<= kArcTimeMs
; ++arc_time_it
) {
149 double arc_size_progress
= static_cast<double>(arc_time_it
) / kArcTimeMs
;
150 if (kMaxArcSize
* Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN
,
151 arc_size_progress
) >=
153 // Add kArcTimeMs to sidestep the |sweep_keyframe == 0| offset below.
154 waiting_state
->arc_time_offset
=
155 base::TimeDelta::FromMilliseconds(arc_time_it
+ kArcTimeMs
);
161 // Blend the color between "waiting" and "spinning" states.
162 base::TimeDelta color_fade_time
= base::TimeDelta::FromMilliseconds(900);
163 double color_progress
= 1.0;
164 if (elapsed_time
< color_fade_time
) {
165 color_progress
= Tween::CalculateValue(
166 Tween::LINEAR_OUT_SLOW_IN
,
167 static_cast<double>(elapsed_time
.InMicroseconds()) /
168 color_fade_time
.InMicroseconds());
170 SkColor blend_color
= color_utils::AlphaBlend(color
, waiting_state
->color
,
171 color_progress
* 255);
173 int64_t start_angle
=
174 waiting_start_angle
+
175 360 * elapsed_time
/ base::TimeDelta::FromMilliseconds(kRotationTimeMs
);
176 base::TimeDelta effective_elapsed_time
=
177 elapsed_time
+ waiting_state
->arc_time_offset
;
179 PaintThrobberSpinningWithStartAngle(canvas
, bounds
, blend_color
,
180 effective_elapsed_time
, start_angle
);