• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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