1 /*
2 * Copyright 2017 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "tools/viewer/SkottieSlide.h"
9
10 #if defined(SK_ENABLE_SKOTTIE)
11
12 #include "include/core/SkCanvas.h"
13 #include "include/core/SkFont.h"
14 #include "include/core/SkTime.h"
15 #include "modules/skottie/include/Skottie.h"
16 #include "modules/skresources/include/SkResources.h"
17 #include "src/utils/SkOSPath.h"
18 #include "tools/timer/TimeUtils.h"
19
20 #include <cmath>
21
22 #include "imgui.h"
23
draw_stats_box(SkCanvas * canvas,const skottie::Animation::Builder::Stats & stats)24 static void draw_stats_box(SkCanvas* canvas, const skottie::Animation::Builder::Stats& stats) {
25 static constexpr SkRect kR = { 10, 10, 280, 120 };
26 static constexpr SkScalar kTextSize = 20;
27
28 SkPaint paint;
29 paint.setAntiAlias(true);
30 paint.setColor(0xffeeeeee);
31
32 SkFont font(nullptr, kTextSize);
33
34 canvas->drawRect(kR, paint);
35
36 paint.setColor(SK_ColorBLACK);
37
38 const auto json_size = SkStringPrintf("Json size: %lu bytes",
39 stats.fJsonSize);
40 canvas->drawString(json_size, kR.x() + 10, kR.y() + kTextSize * 1, font, paint);
41 const auto animator_count = SkStringPrintf("Animator count: %lu",
42 stats.fAnimatorCount);
43 canvas->drawString(animator_count, kR.x() + 10, kR.y() + kTextSize * 2, font, paint);
44 const auto json_parse_time = SkStringPrintf("Json parse time: %.3f ms",
45 stats.fJsonParseTimeMS);
46 canvas->drawString(json_parse_time, kR.x() + 10, kR.y() + kTextSize * 3, font, paint);
47 const auto scene_parse_time = SkStringPrintf("Scene build time: %.3f ms",
48 stats.fSceneParseTimeMS);
49 canvas->drawString(scene_parse_time, kR.x() + 10, kR.y() + kTextSize * 4, font, paint);
50 const auto total_load_time = SkStringPrintf("Total load time: %.3f ms",
51 stats.fTotalLoadTimeMS);
52 canvas->drawString(total_load_time, kR.x() + 10, kR.y() + kTextSize * 5, font, paint);
53
54 paint.setStyle(SkPaint::kStroke_Style);
55 canvas->drawRect(kR, paint);
56 }
57
SkottieSlide(const SkString & name,const SkString & path)58 SkottieSlide::SkottieSlide(const SkString& name, const SkString& path)
59 : fPath(path) {
60 fName = name;
61 }
62
load(SkScalar w,SkScalar h)63 void SkottieSlide::load(SkScalar w, SkScalar h) {
64 class Logger final : public skottie::Logger {
65 public:
66 struct LogEntry {
67 SkString fMessage,
68 fJSON;
69 };
70
71 void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
72 auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
73 log.push_back({ SkString(message), json ? SkString(json) : SkString() });
74 }
75
76 void report() const {
77 SkDebugf("Animation loaded with %lu error%s, %lu warning%s.\n",
78 fErrors.size(), fErrors.size() == 1 ? "" : "s",
79 fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
80
81 const auto& show = [](const LogEntry& log, const char prefix[]) {
82 SkDebugf("%s%s", prefix, log.fMessage.c_str());
83 if (!log.fJSON.isEmpty())
84 SkDebugf(" : %s", log.fJSON.c_str());
85 SkDebugf("\n");
86 };
87
88 for (const auto& err : fErrors) show(err, " !! ");
89 for (const auto& wrn : fWarnings) show(wrn, " ?? ");
90 }
91
92 private:
93 std::vector<LogEntry> fErrors,
94 fWarnings;
95 };
96
97 auto logger = sk_make_sp<Logger>();
98 skottie::Animation::Builder builder;
99
100 fAnimation = builder
101 .setLogger(logger)
102 .setResourceProvider(
103 skresources::DataURIResourceProviderProxy::Make(
104 skresources::FileResourceProvider::Make(SkOSPath::Dirname(fPath.c_str()),
105 /*predecode=*/true),
106 /*predecode=*/true))
107 .makeFromFile(fPath.c_str());
108 fAnimationStats = builder.getStats();
109 fWinSize = SkSize::Make(w, h);
110 fTimeBase = 0; // force a time reset
111
112 if (fAnimation) {
113 fAnimation->seek(0);
114 fFrameTimes.resize(SkScalarCeilToInt(fAnimation->duration() * fAnimation->fps()));
115 SkDebugf("Loaded Bodymovin animation v: %s, size: [%f %f]\n",
116 fAnimation->version().c_str(),
117 fAnimation->size().width(),
118 fAnimation->size().height());
119 logger->report();
120 } else {
121 SkDebugf("failed to load Bodymovin animation: %s\n", fPath.c_str());
122 }
123 }
124
unload()125 void SkottieSlide::unload() {
126 fAnimation.reset();
127 }
128
resize(SkScalar w,SkScalar h)129 void SkottieSlide::resize(SkScalar w, SkScalar h) {
130 fWinSize = { w, h };
131 }
132
getDimensions() const133 SkISize SkottieSlide::getDimensions() const {
134 // We always scale to fill the window.
135 return fWinSize.toCeil();
136 }
137
draw(SkCanvas * canvas)138 void SkottieSlide::draw(SkCanvas* canvas) {
139 if (fAnimation) {
140 SkAutoCanvasRestore acr(canvas, true);
141 const auto dstR = SkRect::MakeSize(fWinSize);
142
143 {
144 const auto t0 = SkTime::GetNSecs();
145 fAnimation->render(canvas, &dstR);
146
147 // TODO: this does not capture GPU flush time!
148 const auto frame_index = static_cast<size_t>(fCurrentFrame);
149 fFrameTimes[frame_index] = static_cast<float>((SkTime::GetNSecs() - t0) * 1e-6);
150 }
151
152 if (fShowAnimationStats) {
153 draw_stats_box(canvas, fAnimationStats);
154 }
155 if (fShowAnimationInval) {
156 const auto t = SkMatrix::MakeRectToRect(SkRect::MakeSize(fAnimation->size()),
157 dstR,
158 SkMatrix::kCenter_ScaleToFit);
159 SkPaint fill, stroke;
160 fill.setAntiAlias(true);
161 fill.setColor(0x40ff0000);
162 stroke.setAntiAlias(true);
163 stroke.setColor(0xffff0000);
164 stroke.setStyle(SkPaint::kStroke_Style);
165
166 for (const auto& r : fInvalController) {
167 SkRect bounds;
168 t.mapRect(&bounds, r);
169 canvas->drawRect(bounds, fill);
170 canvas->drawRect(bounds, stroke);
171 }
172 }
173 if (fShowUI) {
174 this->renderUI();
175 }
176
177 }
178 }
179
animate(double nanos)180 bool SkottieSlide::animate(double nanos) {
181 if (!fTimeBase) {
182 // Reset the animation time.
183 fTimeBase = nanos;
184 }
185
186 if (fAnimation) {
187 fInvalController.reset();
188
189 const auto frame_count = fAnimation->duration() * fAnimation->fps();
190
191 if (!fDraggingProgress) {
192 // Clock-driven progress: update current frame.
193 const double t_sec = (nanos - fTimeBase) * 1e-9;
194 fCurrentFrame = std::fmod(t_sec * fAnimation->fps(), frame_count);
195 } else {
196 // Slider-driven progress: update the time origin.
197 fTimeBase = nanos - fCurrentFrame / fAnimation->fps() * 1e9;
198 }
199
200 // Sanitize and rate-lock the current frame.
201 fCurrentFrame = SkTPin<float>(fCurrentFrame, 0.0f, frame_count - 1);
202 if (fFrameRate > 0) {
203 const auto fps_scale = fFrameRate / fAnimation->fps();
204 fCurrentFrame = std::trunc(fCurrentFrame * fps_scale) / fps_scale;
205 }
206
207 fAnimation->seekFrame(fCurrentFrame);
208 }
209 return true;
210 }
211
onChar(SkUnichar c)212 bool SkottieSlide::onChar(SkUnichar c) {
213 switch (c) {
214 case 'I':
215 fShowAnimationStats = !fShowAnimationStats;
216 break;
217 default:
218 break;
219 }
220
221 return INHERITED::onChar(c);
222 }
223
onMouse(SkScalar x,SkScalar y,skui::InputState state,skui::ModifierKey)224 bool SkottieSlide::onMouse(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey) {
225 switch (state) {
226 case skui::InputState::kUp:
227 fShowAnimationInval = !fShowAnimationInval;
228 fShowAnimationStats = !fShowAnimationStats;
229 break;
230 default:
231 break;
232 }
233
234 fShowUI = this->UIArea().contains(x, y);
235
236 return false;
237 }
238
UIArea() const239 SkRect SkottieSlide::UIArea() const {
240 static constexpr float kUIHeight = 120.0f;
241
242 return SkRect::MakeXYWH(0, fWinSize.height() - kUIHeight, fWinSize.width(), kUIHeight);
243 }
244
renderUI()245 void SkottieSlide::renderUI() {
246 static constexpr auto kUI_opacity = 0.35f,
247 kUI_hist_height = 50.0f,
248 kUI_fps_width = 100.0f;
249
250 auto add_frame_rate_option = [this](const char* label, double rate) {
251 const auto is_selected = (fFrameRate == rate);
252 if (ImGui::Selectable(label, is_selected)) {
253 fFrameRate = rate;
254 fFrameRateLabel = label;
255 }
256 if (is_selected) {
257 ImGui::SetItemDefaultFocus();
258 }
259 };
260
261 ImGui::SetNextWindowBgAlpha(kUI_opacity);
262 if (ImGui::Begin("Skottie Controls", nullptr, ImGuiWindowFlags_NoDecoration |
263 ImGuiWindowFlags_NoResize |
264 ImGuiWindowFlags_NoMove |
265 ImGuiWindowFlags_NoSavedSettings |
266 ImGuiWindowFlags_NoFocusOnAppearing |
267 ImGuiWindowFlags_NoNav)) {
268 const auto ui_area = this->UIArea();
269 ImGui::SetWindowPos(ImVec2(ui_area.x(), ui_area.y()));
270 ImGui::SetWindowSize(ImVec2(ui_area.width(), ui_area.height()));
271
272 ImGui::PushItemWidth(-1);
273 ImGui::PlotHistogram("", fFrameTimes.data(), fFrameTimes.size(),
274 0, nullptr, FLT_MAX, FLT_MAX, ImVec2(0, kUI_hist_height));
275 ImGui::SliderFloat("", &fCurrentFrame, 0, fAnimation->duration() * fAnimation->fps() - 1);
276 fDraggingProgress = ImGui::IsItemActive();
277 ImGui::PopItemWidth();
278
279 ImGui::PushItemWidth(kUI_fps_width);
280 if (ImGui::BeginCombo("FPS", fFrameRateLabel)) {
281 add_frame_rate_option("", 0.0);
282 add_frame_rate_option("Native", fAnimation->fps());
283 add_frame_rate_option( "1", 1.0);
284 add_frame_rate_option("15", 15.0);
285 add_frame_rate_option("24", 24.0);
286 add_frame_rate_option("30", 30.0);
287 add_frame_rate_option("60", 60.0);
288 ImGui::EndCombo();
289 }
290 ImGui::PopItemWidth();
291 }
292 ImGui::End();
293 }
294
295 #endif // SK_ENABLE_SKOTTIE
296