paint_throbber.cc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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.
  4. #include "ui/gfx/paint_throbber.h"
  5. #include <algorithm>
  6. #include "base/numerics/safe_conversions.h"
  7. #include "base/time/time.h"
  8. #include "cc/paint/paint_flags.h"
  9. #include "third_party/skia/include/core/SkPath.h"
  10. #include "ui/gfx/animation/tween.h"
  11. #include "ui/gfx/canvas.h"
  12. #include "ui/gfx/color_utils.h"
  13. #include "ui/gfx/geometry/rect.h"
  14. #include "ui/gfx/geometry/skia_conversions.h"
  15. namespace gfx {
  16. namespace {
  17. // The maximum size of the "spinning" state arc, in degrees.
  18. constexpr int64_t kMaxArcSize = 270;
  19. // The amount of time it takes to grow the "spinning" arc from 0 to 270 degrees.
  20. constexpr auto kArcTime = base::Seconds(2.0 / 3.0);
  21. // The amount of time it takes for the "spinning" throbber to make a full
  22. // rotation.
  23. constexpr auto kRotationTime = base::Milliseconds(1568);
  24. void PaintArc(Canvas* canvas,
  25. const Rect& bounds,
  26. SkColor color,
  27. SkScalar start_angle,
  28. SkScalar sweep,
  29. absl::optional<SkScalar> stroke_width) {
  30. if (!stroke_width) {
  31. // Stroke width depends on size.
  32. // . For size < 28: 3 - (28 - size) / 16
  33. // . For 28 <= size: (8 + size) / 12
  34. stroke_width = bounds.width() < 28
  35. ? 3.0 - SkIntToScalar(28 - bounds.width()) / 16.0
  36. : SkIntToScalar(bounds.width() + 8) / 12.0;
  37. }
  38. Rect oval = bounds;
  39. // Inset by half the stroke width to make sure the whole arc is inside
  40. // the visible rect.
  41. const int inset = SkScalarCeilToInt(*stroke_width / 2.0);
  42. oval.Inset(inset);
  43. SkPath path;
  44. path.arcTo(RectToSkRect(oval), start_angle, sweep, true);
  45. cc::PaintFlags flags;
  46. flags.setColor(color);
  47. flags.setStrokeCap(cc::PaintFlags::kRound_Cap);
  48. flags.setStrokeWidth(*stroke_width);
  49. flags.setStyle(cc::PaintFlags::kStroke_Style);
  50. flags.setAntiAlias(true);
  51. canvas->DrawPath(path, flags);
  52. }
  53. void CalculateWaitingAngles(const base::TimeDelta& elapsed_time,
  54. int64_t* start_angle,
  55. int64_t* sweep) {
  56. // Calculate start and end points. The angles are counter-clockwise because
  57. // the throbber spins counter-clockwise. The finish angle starts at 12 o'clock
  58. // (90 degrees) and rotates steadily. The start angle trails 180 degrees
  59. // behind, except for the first half revolution, when it stays at 12 o'clock.
  60. constexpr auto kRevolutionTime = base::Milliseconds(1320);
  61. int64_t twelve_oclock = 90;
  62. int64_t finish_angle_cc =
  63. twelve_oclock +
  64. base::ClampRound<int64_t>(elapsed_time / kRevolutionTime * 360);
  65. int64_t start_angle_cc = std::max(finish_angle_cc - 180, twelve_oclock);
  66. // Negate the angles to convert to the clockwise numbers Skia expects.
  67. if (start_angle)
  68. *start_angle = -finish_angle_cc;
  69. if (sweep)
  70. *sweep = finish_angle_cc - start_angle_cc;
  71. }
  72. // This is a Skia port of the MD spinner SVG. The |start_angle| rotation
  73. // here corresponds to the 'rotate' animation.
  74. ThrobberSpinningState CalculateThrobberSpinningStateWithStartAngle(
  75. base::TimeDelta elapsed_time,
  76. int64_t start_angle) {
  77. // The sweep angle ranges from -270 to 270 over 1333ms. CSS
  78. // animation timing functions apply in between key frames, so we have to
  79. // break up the 1333ms into two keyframes (-270 to 0, then 0 to 270).
  80. const double elapsed_ratio = elapsed_time / kArcTime;
  81. const int64_t sweep_frame = base::ClampFloor<int64_t>(elapsed_ratio);
  82. const double arc_progress = elapsed_ratio - sweep_frame;
  83. // This tween is equivalent to cubic-bezier(0.4, 0.0, 0.2, 1).
  84. double sweep = kMaxArcSize *
  85. Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN, arc_progress);
  86. if (sweep_frame % 2 == 0)
  87. sweep -= kMaxArcSize;
  88. // This part makes sure the sweep is at least 5 degrees long. Roughly
  89. // equivalent to the "magic constants" in SVG's fillunfill animation.
  90. constexpr double kMinSweepLength = 5.0;
  91. if (sweep >= 0.0 && sweep < kMinSweepLength) {
  92. start_angle -= (kMinSweepLength - sweep);
  93. sweep = kMinSweepLength;
  94. } else if (sweep <= 0.0 && sweep > -kMinSweepLength) {
  95. start_angle += (-kMinSweepLength - sweep);
  96. sweep = -kMinSweepLength;
  97. }
  98. // To keep the sweep smooth, we have an additional rotation after each
  99. // arc period has elapsed. See SVG's 'rot' animation.
  100. const int64_t rot_keyframe = (sweep_frame / 2) % 4;
  101. start_angle = start_angle + rot_keyframe * kMaxArcSize;
  102. return ThrobberSpinningState{
  103. .start_angle = static_cast<SkScalar>(start_angle),
  104. .sweep_angle = static_cast<SkScalar>(sweep)};
  105. }
  106. void PaintThrobberSpinningWithState(Canvas* canvas,
  107. const Rect& bounds,
  108. SkColor color,
  109. const ThrobberSpinningState& state,
  110. absl::optional<SkScalar> stroke_width) {
  111. PaintArc(canvas, bounds, color, state.start_angle, state.sweep_angle,
  112. stroke_width);
  113. }
  114. void PaintThrobberSpinningWithStartAngle(
  115. Canvas* canvas,
  116. const Rect& bounds,
  117. SkColor color,
  118. const base::TimeDelta& elapsed_time,
  119. int64_t start_angle,
  120. absl::optional<SkScalar> stroke_width) {
  121. const ThrobberSpinningState state =
  122. CalculateThrobberSpinningStateWithStartAngle(elapsed_time, start_angle);
  123. PaintThrobberSpinningWithState(canvas, bounds, color, state, stroke_width);
  124. }
  125. } // namespace
  126. ThrobberSpinningState CalculateThrobberSpinningState(
  127. base::TimeDelta elapsed_time) {
  128. const int64_t start_angle =
  129. 270 + base::ClampRound<int64_t>(elapsed_time / kRotationTime * 360);
  130. return CalculateThrobberSpinningStateWithStartAngle(elapsed_time,
  131. start_angle);
  132. }
  133. void PaintThrobberSpinning(Canvas* canvas,
  134. const Rect& bounds,
  135. SkColor color,
  136. const base::TimeDelta& elapsed_time,
  137. absl::optional<SkScalar> stroke_width) {
  138. const ThrobberSpinningState state =
  139. CalculateThrobberSpinningState(elapsed_time);
  140. PaintThrobberSpinningWithState(canvas, bounds, color, state, stroke_width);
  141. }
  142. void PaintThrobberWaiting(Canvas* canvas,
  143. const Rect& bounds,
  144. SkColor color,
  145. const base::TimeDelta& elapsed_time,
  146. absl::optional<SkScalar> stroke_width) {
  147. int64_t start_angle = 0, sweep = 0;
  148. CalculateWaitingAngles(elapsed_time, &start_angle, &sweep);
  149. PaintArc(canvas, bounds, color, start_angle, sweep, stroke_width);
  150. }
  151. void PaintThrobberSpinningAfterWaiting(Canvas* canvas,
  152. const Rect& bounds,
  153. SkColor color,
  154. const base::TimeDelta& elapsed_time,
  155. ThrobberWaitingState* waiting_state,
  156. absl::optional<SkScalar> stroke_width) {
  157. int64_t waiting_start_angle = 0, waiting_sweep = 0;
  158. CalculateWaitingAngles(waiting_state->elapsed_time, &waiting_start_angle,
  159. &waiting_sweep);
  160. // |arc_time_offset| is the effective amount of time one would have to wait
  161. // for the "spinning" sweep to match |waiting_sweep|. Brute force calculation.
  162. if (waiting_state->arc_time_offset.is_zero()) {
  163. for (int64_t arc_ms = 0; arc_ms <= kArcTime.InMillisecondsRoundedUp();
  164. ++arc_ms) {
  165. const base::TimeDelta arc_time =
  166. std::min(base::Milliseconds(arc_ms), kArcTime);
  167. if (kMaxArcSize * Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN,
  168. arc_time / kArcTime) >=
  169. waiting_sweep) {
  170. // Add kArcTime to sidestep the |sweep_keyframe == 0| offset below.
  171. waiting_state->arc_time_offset = kArcTime + arc_time;
  172. break;
  173. }
  174. }
  175. }
  176. // Blend the color between "waiting" and "spinning" states.
  177. constexpr auto kColorFadeTime = base::Milliseconds(900);
  178. const float color_progress = static_cast<float>(Tween::CalculateValue(
  179. Tween::LINEAR_OUT_SLOW_IN, std::min(elapsed_time / kColorFadeTime, 1.0)));
  180. const SkColor blend_color =
  181. color_utils::AlphaBlend(color, waiting_state->color, color_progress);
  182. const int64_t start_angle =
  183. waiting_start_angle +
  184. base::ClampRound<int64_t>(elapsed_time / kRotationTime * 360);
  185. const base::TimeDelta effective_elapsed_time =
  186. elapsed_time + waiting_state->arc_time_offset;
  187. PaintThrobberSpinningWithStartAngle(canvas, bounds, blend_color,
  188. effective_elapsed_time, start_angle,
  189. stroke_width);
  190. }
  191. GFX_EXPORT void PaintNewThrobberWaiting(Canvas* canvas,
  192. const RectF& throbber_container_bounds,
  193. SkColor color,
  194. const base::TimeDelta& elapsed_time) {
  195. // Cycle time for the waiting throbber.
  196. constexpr auto kNewThrobberWaitingCycleTime = base::Seconds(1);
  197. // The throbber bounces back and forth. We map the elapsed time to 0->2. Time
  198. // 0->1 represents when the throbber moves left to right, time 1->2 represents
  199. // right to left.
  200. float time = 2.0f * (elapsed_time % kNewThrobberWaitingCycleTime) /
  201. kNewThrobberWaitingCycleTime;
  202. // 1 -> 2 values mirror back to 1 -> 0 values to represent right-to-left.
  203. const bool going_back = time > 1.0f;
  204. if (going_back)
  205. time = 2.0f - time;
  206. // This animation should be fast in the middle and slow at the edges.
  207. time = Tween::CalculateValue(Tween::EASE_IN_OUT, time);
  208. const float min_width = throbber_container_bounds.height();
  209. // The throbber animation stretches longer when moving in (left to right) than
  210. // when going back.
  211. const float throbber_width =
  212. (going_back ? 0.75f : 1.0f) * throbber_container_bounds.width();
  213. // These bounds keep at least |min_width| of the throbber visible (inside the
  214. // throbber bounds).
  215. const float min_x =
  216. throbber_container_bounds.x() - throbber_width + min_width;
  217. const float max_x = throbber_container_bounds.right() - min_width;
  218. RectF bounds = throbber_container_bounds;
  219. // Linear interpolation between |min_x| and |max_x|.
  220. bounds.set_x(time * (max_x - min_x) + min_x);
  221. bounds.set_width(throbber_width);
  222. // The throbber is designed to go out of bounds, but it should not be rendered
  223. // outside |throbber_container_bounds|. This clips the throbber to the edges,
  224. // which gives a smooth bouncing effect.
  225. bounds.Intersect(throbber_container_bounds);
  226. cc::PaintFlags flags;
  227. flags.setColor(color);
  228. flags.setStyle(cc::PaintFlags::kFill_Style);
  229. // Draw with circular end caps.
  230. canvas->DrawRoundRect(bounds, bounds.height() / 2, flags);
  231. }
  232. } // namespace gfx