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
|
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gfx/paint_throbber.h"
#include <algorithm>
#include "base/numerics/safe_conversions.h"
#include "base/time/time.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/skia_conversions.h"
namespace gfx {
namespace {
// The maximum size of the "spinning" state arc, in degrees.
constexpr int64_t kMaxArcSize = 270;
// The amount of time it takes to grow the "spinning" arc from 0 to 270 degrees.
constexpr auto kArcTime = base::Seconds(2.0 / 3.0);
// The amount of time it takes for the "spinning" throbber to make a full
// rotation.
constexpr auto kRotationTime = base::Milliseconds(1568);
void PaintArc(Canvas* canvas,
const Rect& bounds,
SkColor color,
SkScalar start_angle,
SkScalar sweep,
std::optional<SkScalar> stroke_width) {
if (!stroke_width) {
// Stroke width depends on size.
// . For size < 28: 3 - (28 - size) / 16
// . For 28 <= size: (8 + size) / 12
stroke_width = bounds.width() < 28
? 3.0 - SkIntToScalar(28 - bounds.width()) / 16.0
: SkIntToScalar(bounds.width() + 8) / 12.0;
}
Rect oval = bounds;
// Inset by half the stroke width to make sure the whole arc is inside
// the visible rect.
const int inset = SkScalarCeilToInt(*stroke_width / 2.0);
oval.Inset(inset);
SkPath path;
path.arcTo(RectToSkRect(oval), start_angle, sweep, true);
cc::PaintFlags flags;
flags.setColor(color);
flags.setStrokeCap(cc::PaintFlags::kRound_Cap);
flags.setStrokeWidth(*stroke_width);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setAntiAlias(true);
canvas->DrawPath(path, flags);
}
// This is a Skia port of the MD spinner SVG. The |start_angle| rotation
// here corresponds to the 'rotate' animation.
ThrobberSpinningState CalculateThrobberSpinningStateWithStartAngle(
base::TimeDelta elapsed_time,
int64_t start_angle,
const int64_t sweep_keyframe_offset = 0) {
// The sweep angle ranges from -270 to 270 over 1333ms. CSS
// animation timing functions apply in between key frames, so we have to
// break up the 1333ms into two keyframes (-270 to 0, then 0 to 270).
const double elapsed_ratio =
(elapsed_time / kArcTime) + sweep_keyframe_offset;
const int64_t sweep_frame = base::ClampFloor<int64_t>(elapsed_ratio);
const double arc_progress = elapsed_ratio - sweep_frame;
// This tween is equivalent to cubic-bezier(0.4, 0.0, 0.2, 1).
double sweep = kMaxArcSize *
Tween::CalculateValue(Tween::FAST_OUT_SLOW_IN, arc_progress);
if (sweep_frame % 2 == 0) {
sweep -= kMaxArcSize;
}
// This part makes sure the sweep is at least 5 degrees long. Roughly
// equivalent to the "magic constants" in SVG's fillunfill animation.
constexpr double kMinSweepLength = 5.0;
if (sweep >= 0.0 && sweep < kMinSweepLength) {
start_angle -= (kMinSweepLength - sweep);
sweep = kMinSweepLength;
} else if (sweep <= 0.0 && sweep > -kMinSweepLength) {
start_angle += (-kMinSweepLength - sweep);
sweep = -kMinSweepLength;
}
// To keep the sweep smooth, we have an additional rotation after each
// arc period has elapsed. See SVG's 'rot' animation.
const int64_t rot_keyframe = (sweep_frame / 2) % 4;
start_angle = start_angle + rot_keyframe * kMaxArcSize;
return ThrobberSpinningState{
.start_angle = static_cast<SkScalar>(start_angle),
.sweep_angle = static_cast<SkScalar>(sweep)};
}
void PaintThrobberSpinningWithState(Canvas* canvas,
const Rect& bounds,
SkColor color,
const ThrobberSpinningState& state,
std::optional<SkScalar> stroke_width) {
PaintArc(canvas, bounds, color, state.start_angle, state.sweep_angle,
stroke_width);
}
} // namespace
ThrobberSpinningState CalculateThrobberSpinningState(
base::TimeDelta elapsed_time,
const int64_t sweep_keyframe_offset) {
const int64_t start_angle =
270 + base::ClampRound<int64_t>(elapsed_time / kRotationTime * 360);
return CalculateThrobberSpinningStateWithStartAngle(elapsed_time, start_angle,
sweep_keyframe_offset);
}
void PaintThrobberSpinning(Canvas* canvas,
const Rect& bounds,
SkColor color,
const base::TimeDelta& elapsed_time,
std::optional<SkScalar> stroke_width) {
const ThrobberSpinningState state =
CalculateThrobberSpinningState(elapsed_time);
PaintThrobberSpinningWithState(canvas, bounds, color, state, stroke_width);
}
void PaintThrobberSpinningWithSweepEasedIn(
Canvas* canvas,
const Rect& bounds,
SkColor color,
const base::TimeDelta& elapsed_time,
std::optional<SkScalar> stroke_width) {
// The second keyframe of the spinning animation is when the arc is
// minimized, compared to the first keyframe, where it is at its maximum.
const ThrobberSpinningState state =
CalculateThrobberSpinningState(elapsed_time, 1);
PaintThrobberSpinningWithState(canvas, bounds, color, state, stroke_width);
}
} // namespace gfx
|