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