xref: /aosp_15_r20/external/skia/tools/viewer/SkottieSlide.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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/SkFontMgr.h"
15 #include "include/private/base/SkNoncopyable.h"
16 #include "include/private/base/SkTPin.h"
17 #include "modules/audioplayer/SkAudioPlayer.h"
18 #include "modules/skottie/include/Skottie.h"
19 #include "modules/skottie/include/SkottieProperty.h"
20 #include "modules/skottie/include/SlotManager.h"
21 #include "modules/skottie/utils/SkottieUtils.h"
22 #include "modules/skottie/utils/TextEditor.h"
23 #include "modules/skresources/include/SkResources.h"
24 #include "src/base/SkTime.h"
25 #include "src/core/SkOSFile.h"
26 #include "src/utils/SkOSPath.h"
27 #include "tools/Resources.h"
28 #include "tools/fonts/FontToolUtils.h"
29 #include "tools/timer/TimeUtils.h"
30 
31 #include <cmath>
32 #include <vector>
33 
34 #include "imgui.h"
35 
36 namespace {
37 
38 class Track final : public skresources::ExternalTrackAsset {
39 public:
Track(std::unique_ptr<SkAudioPlayer> player)40     explicit Track(std::unique_ptr<SkAudioPlayer> player) : fPlayer(std::move(player)) {}
41 
42 private:
seek(float t)43     void seek(float t) override {
44         if (fPlayer->isStopped() && t >=0) {
45             fPlayer->play();
46         }
47 
48         if (fPlayer->isPlaying()) {
49             if (t < 0) {
50                 fPlayer->stop();
51             } else {
52                 static constexpr float kTolerance = 0.075f;
53                 const auto player_pos = fPlayer->time();
54 
55                 if (std::abs(player_pos - t) > kTolerance) {
56                     fPlayer->setTime(t);
57                 }
58             }
59         }
60     }
61 
62     const std::unique_ptr<SkAudioPlayer> fPlayer;
63 };
64 
65 class AudioProviderProxy final : public skresources::ResourceProviderProxyBase {
66 public:
AudioProviderProxy(sk_sp<skresources::ResourceProvider> rp)67     explicit AudioProviderProxy(sk_sp<skresources::ResourceProvider> rp)
68         : skresources::ResourceProviderProxyBase(std::move(rp)) {}
69 
70 private:
loadAudioAsset(const char path[],const char name[],const char[])71     sk_sp<skresources::ExternalTrackAsset> loadAudioAsset(const char path[],
72                                                           const char name[],
73                                                           const char[] /*id*/) override {
74         if (auto data = this->load(path, name)) {
75             if (auto player = SkAudioPlayer::Make(std::move(data))) {
76                 return sk_make_sp<Track>(std::move(player));
77             }
78         }
79 
80         return nullptr;
81     }
82 };
83 
84 class Decorator : public SkNoncopyable {
85 public:
86     virtual ~Decorator() = default;
87 
88     // We pass in the Matrix and have the Decorator handle using it independently
89     // This is so decorators can keep position on screen after moving.
90     virtual void render(SkCanvas*, double, const SkMatrix) = 0;
91 };
92 
93 class SimpleMarker final : public Decorator {
94 public:
95     ~SimpleMarker() override = default;
96 
Make()97     static std::unique_ptr<Decorator> Make() { return std::make_unique<SimpleMarker>(); }
98 
render(SkCanvas * canvas,double t,const SkMatrix transform)99     void render(SkCanvas* canvas, double t, const SkMatrix transform) override {
100         canvas->concat(transform);
101         SkPaint p;
102         p.setAntiAlias(true);
103 
104         p.setColor(SK_ColorGREEN);
105         canvas->drawCircle(0, 0, 5, p);
106 
107         p.setColor(SK_ColorRED);
108         p.setStrokeWidth(1.5f);
109         canvas->drawLine(-10, 0, 10, 0, p);
110         canvas->drawLine(0, -10, 0, 10, p);
111     }
112 };
113 
114 class TestingResourceProvider : public skresources::ResourceProvider {
115 public:
TestingResourceProvider()116     TestingResourceProvider() {}
117 
load(const char resource_path[],const char resource_name[]) const118     sk_sp<SkData> load(const char resource_path[], const char resource_name[]) const override {
119         auto it = fResources.find(resource_name);
120         if (it != fResources.end()) {
121             return it->second;
122         } else {
123             return GetResourceAsData(SkOSPath::Join(resource_path, resource_name).c_str());
124         }
125     }
126 
loadImageAsset(const char resource_path[],const char resource_name[],const char[]) const127     sk_sp<skresources::ImageAsset> loadImageAsset(const char resource_path[],
128                                                   const char resource_name[],
129                                                   const char /*resource_id*/[]) const override {
130         auto data = this->load(resource_path, resource_name);
131         // Viewer should have already registered the codecs necessary for MultiFrameImageAsset
132         return skresources::MultiFrameImageAsset::Make(data);
133     }
134 
addPath(const char resource_name[],const SkPath & path)135     void addPath(const char resource_name[], const SkPath& path) {
136         fResources[resource_name] = path.serialize();
137     }
138 
139 private:
140     std::unordered_map<std::string, sk_sp<SkData>> fResources;
141 };
142 
143 static const struct DecoratorRec {
144     const char* fName;
145     std::unique_ptr<Decorator>(*fFactory)();
146 } kDecorators[] = {
147     { "Simple marker",       SimpleMarker::Make },
148 };
149 
150 class TextTracker final : public skottie::PropertyObserver {
151 public:
TextTracker(sk_sp<PropertyObserver> delegate)152     explicit TextTracker(sk_sp<PropertyObserver> delegate) : fDelegate(std::move(delegate)) {}
153 
props()154     std::vector<std::unique_ptr<skottie::TextPropertyHandle>>& props() {
155         return fTextProps;
156     }
157 
158 private:
onTextProperty(const char node_name[],const LazyHandle<skottie::TextPropertyHandle> & lh)159     void onTextProperty(const char node_name[],
160                         const LazyHandle<skottie::TextPropertyHandle>& lh) override {
161         fTextProps.push_back(lh());
162 
163         if (fDelegate) {
164             fDelegate->onTextProperty(node_name, lh);
165         }
166     }
167 
168     const sk_sp<PropertyObserver>                             fDelegate;
169     std::vector<std::unique_ptr<skottie::TextPropertyHandle>> fTextProps;
170 };
171 
172 } // namespace
173 
174 class SkottieSlide::TransformTracker : public skottie::PropertyObserver {
175 public:
renderUI()176     void renderUI() {
177         if (ImGui::Begin("Transform Tracker", nullptr)) {
178             if (ImGui::BeginCombo("Transform", fTransformSelect
179                                        ? std::get<0>(*fTransformSelect).c_str()
180                                        : nullptr)) {
181                 if (ImGui::Selectable("(none)", true)) {
182                     fTransformSelect = nullptr;
183                 }
184                 for (const auto& entry : fTransforms) {
185                     const auto* transform_name = std::get<0>(entry).c_str();
186                     if (ImGui::Selectable(transform_name, false)) {
187                         if (!fTransformSelect ||
188                             transform_name != std::get<0>(*fTransformSelect).c_str()) {
189                             fTransformSelect = &entry;
190                             // Reset the decorator on transform change.
191                             fDecorator = fDecoratorSelect->fFactory();
192                         }
193                     }
194                 }
195                 ImGui::EndCombo();
196             }
197 
198             if (ImGui::BeginCombo("Decoration", fDecoratorSelect->fName)) {
199                 for (const auto& dec : kDecorators) {
200                     if (ImGui::Selectable(dec.fName, false)) {
201                         if (dec.fName != fDecoratorSelect->fName) {
202                             fDecoratorSelect = &dec;
203                             fDecorator = fDecoratorSelect->fFactory();
204                         }
205                     }
206                 }
207                 ImGui::EndCombo();
208             }
209         }
210         ImGui::End();
211     }
212 
renderTracker(SkCanvas * canvas,double time,const SkSize & win_size,const SkSize & anim_size) const213     void renderTracker(SkCanvas* canvas, double time, const SkSize& win_size, const SkSize& anim_size) const {
214         if (!fTransformSelect) {
215             return;
216         }
217 
218         const auto tprop = std::get<1>(*fTransformSelect)->get();
219 
220         const auto m = SkMatrix::Translate(tprop.fPosition.fX, tprop.fPosition.fY)
221                      * SkMatrix::RotateDeg(tprop.fRotation)
222                      * SkMatrix::Scale    (tprop.fScale.fX*0.01f, tprop.fScale.fY*0.01f)
223                      * SkMatrix::Translate(tprop.fAnchorPoint.fX, tprop.fAnchorPoint.fY);
224 
225         const auto viewer_matrix = SkMatrix::RectToRect(SkRect::MakeSize(anim_size),
226                                                         SkRect::MakeSize(win_size),
227                                                         SkMatrix::kCenter_ScaleToFit);
228 
229         SkAutoCanvasRestore acr(canvas, true);
230         canvas->concat(viewer_matrix);
231 
232         SkASSERT(fDecorator);
233         fDecorator->render(canvas, time, m);
234     }
235 
236 private:
onTransformProperty(const char name[],const LazyHandle<skottie::TransformPropertyHandle> & lh)237     void onTransformProperty(const char name[],
238                              const LazyHandle<skottie::TransformPropertyHandle>& lh) override {
239 
240         fTransforms.push_back(std::make_tuple(SkString(name), lh()));
241     }
242 
243     using TransformT = std::tuple<SkString, std::unique_ptr<skottie::TransformPropertyHandle>>;
244 
245     std::vector<TransformT>    fTransforms;
246     std::unique_ptr<Decorator> fDecorator;
247     const TransformT*          fTransformSelect = nullptr;
248     const DecoratorRec*        fDecoratorSelect = &kDecorators[0];
249 };
250 
251 // Holds a pointer to a slot manager and the list of slots for the UI widget to track
252 class SkottieSlide::SlotManagerInterface {
253 public:
SlotManagerInterface(sk_sp<skottie::SlotManager> slotManager,sk_sp<skresources::ResourceProvider> rp)254     SlotManagerInterface(sk_sp<skottie::SlotManager> slotManager, sk_sp<skresources::ResourceProvider> rp)
255         : fSlotManager(std::move(slotManager))
256         , fResourceProvider(std::move(rp))
257     {}
258 
259 
renderUI()260     void renderUI() {
261         if (ImGui::Begin("Slot Manager", nullptr)) {
262             ImGui::Text("Color Slots");
263             for (size_t i = 0; i < fColorSlots.size(); i++) {
264                 auto& cSlot = fColorSlots.at(i);
265                 ImGui::PushID(i);
266                 ImGui::Text("%s", cSlot.first.c_str());
267                 if (ImGui::ColorEdit4("Color", cSlot.second.data())) {
268                     this->pushSlots();
269                 }
270                 ImGui::PopID();
271             }
272             ImGui::Text("Scalar Slots");
273             for (size_t i = 0; i < fScalarSlots.size(); i++) {
274                 auto& oSlot = fScalarSlots.at(i);
275                 ImGui::PushID(i);
276                 ImGui::Text("%s", oSlot.first.c_str());
277                 if (ImGui::InputFloat("Scalar", &(oSlot.second))) {
278                     this->pushSlots();
279                 }
280                 ImGui::PopID();
281             }
282             ImGui::Text("Vec2 Slots");
283             for (size_t i = 0; i < fVec2Slots.size(); i++) {
284                 auto& vSlot = fVec2Slots.at(i);
285                 ImGui::PushID(i);
286                 ImGui::Text("%s", vSlot.first.c_str());
287                 if (ImGui::InputFloat2("x, y", &(vSlot.second.x))) {
288                     this->pushSlots();
289                 }
290                 ImGui::PopID();
291             }
292             ImGui::Text("Text Slots");
293             for (size_t i = 0; i < fTextStringSlots.size(); i++) {
294                 auto& tSlot = fTextStringSlots.at(i);
295                 ImGui::PushID(i);
296                 ImGui::Text("%s", tSlot.first.c_str());
297                 if (ImGui::InputText("Text", tSlot.second.source.data(),
298                                              tSlot.second.source.size())) {
299                     this->pushSlots();
300                 }
301                 if (ImGui::BeginCombo("Font", tSlot.second.font.data())) {
302                     for (const auto& typeface : fTypefaceList) {
303                         if (ImGui::Selectable(typeface, false)) {
304                             tSlot.second.font = typeface;
305                             this->pushSlots();
306                         }
307                     }
308                     ImGui::EndCombo();
309                 }
310                 ImGui::PopID();
311             }
312 
313             ImGui::Text("Image Slots");
314             for (size_t i = 0; i < fImageSlots.size(); i++) {
315                 auto& iSlot = fImageSlots.at(i);
316                 ImGui::PushID(i);
317                 ImGui::Text("%s", iSlot.first.c_str());
318                 if (ImGui::BeginCombo("Resource", iSlot.second.data())) {
319                     for (const auto& res : fResList) {
320                         if (ImGui::Selectable(res.c_str(), false)) {
321                             iSlot.second = res.c_str();
322                             this->pushSlots();
323                         }
324                     }
325                     ImGui::EndCombo();
326                 }
327                 ImGui::PopID();
328             }
329         }
330         ImGui::End();
331     }
332 
pushSlots()333     void pushSlots() {
334         for(const auto& s : fColorSlots) {
335             fSlotManager->setColorSlot(s.first, SkColor4f{s.second[0], s.second[1],
336                                                           s.second[2], s.second[3]}.toSkColor());
337         }
338         for(const auto& s : fScalarSlots) {
339             fSlotManager->setScalarSlot(s.first, s.second);
340         }
341         for(const auto& s : fVec2Slots) {
342             fSlotManager->setVec2Slot(s.first, {s.second.x, s.second.y});
343         }
344         for(const auto& s : fTextStringSlots) {
345             auto t = fSlotManager->getTextSlot(s.first);
346             t->fText = SkString(s.second.source.data());
347             t->fTypeface = ToolUtils::TestFontMgr()->matchFamilyStyle(s.second.font.c_str(),
348                                                                       SkFontStyle());
349             fSlotManager->setTextSlot(s.first, *t);
350         }
351         for(const auto& s : fImageSlots) {
352             auto image = fResourceProvider->loadImageAsset("images/", s.second.c_str(), nullptr);
353             fSlotManager->setImageSlot(s.first, image);
354         }
355     }
356 
initializeSlotManagerUI()357     void initializeSlotManagerUI() {
358         prepareImageAssetList(GetResourcePath("skottie/images").c_str());
359         // only initialize if slots are unpopulated
360         if (fColorSlots.empty() && fScalarSlots.empty() && fTextStringSlots.empty()) {
361             auto slotInfos = fSlotManager->getSlotInfo();
362             for (const auto &sid : slotInfos.fColorSlotIDs) {
363                 addColorSlot(sid);
364             }
365             for (const auto &sid : slotInfos.fScalarSlotIDs) {
366                 addScalarSlot(sid);
367             }
368             for (const auto &sid : slotInfos.fVec2SlotIDs) {
369                 addVec2Slot(sid);
370             }
371             for (const auto &sid : slotInfos.fImageSlotIDs) {
372                 addImageSlot(sid);
373             }
374             for (const auto &sid : slotInfos.fTextSlotIDs) {
375                 addTextSlot(sid);
376             }
377         }
378     }
379 
380 private:
381     static constexpr int kBufferLen = 256;
382 
383     sk_sp<skottie::SlotManager> fSlotManager;
384     const sk_sp<skresources::ResourceProvider> fResourceProvider;
385     std::vector<SkString> fResList;
386     static constexpr std::array<const char*, 4> fTypefaceList = {"Arial",
387                                                                  "Courier New",
388                                                                  "Roboto-Regular",
389                                                                  "Georgia"};
390 
391     using GuiTextBuffer = std::array<char, kBufferLen>;
392 
addColorSlot(SkString slotID)393     void addColorSlot(SkString slotID) {
394         auto c = fSlotManager->getColorSlot(slotID);
395         SkColor4f color4f = SkColor4f::FromColor(*c);
396         fColorSlots.push_back(std::make_pair(slotID, color4f.array()));
397     }
398 
addScalarSlot(SkString slotID)399     void addScalarSlot(SkString slotID) {
400         fScalarSlots.push_back(std::make_pair(slotID, *fSlotManager->getScalarSlot(slotID)));
401     }
402 
addVec2Slot(SkString slotID)403     void addVec2Slot(SkString slotID) {
404         fVec2Slots.push_back(std::make_pair(slotID, *fSlotManager->getVec2Slot(slotID)));
405     }
406 
addTextSlot(SkString slotID)407     void addTextSlot(SkString slotID) {
408         std::array<char, kBufferLen> textSource = {'\0'};
409         SkString s = fSlotManager->getTextSlot(slotID)->fText;
410         std::copy(s.data(), s.data() + s.size(), textSource.data());
411         TextSlotData data = {textSource, fTypefaceList[0]};
412         fTextStringSlots.push_back(std::make_pair(slotID, data));
413     }
414 
addImageSlot(SkString slotID)415     void addImageSlot(SkString slotID) {
416         fImageSlots.push_back(std::make_pair(slotID, fResList[0].data()));
417     }
418 
prepareImageAssetList(const char * dirname)419     void prepareImageAssetList(const char* dirname) {
420         fResList.clear();
421         SkOSFile::Iter iter(dirname, ".png");
422         for (SkString file; iter.next(&file); ) {
423             fResList.push_back(file);
424         }
425     }
426 
427     struct TextSlotData {
428         GuiTextBuffer source;
429         std::string   font;
430     };
431 
432     std::vector<std::pair<SkString, std::array<float, 4>>> fColorSlots;
433     std::vector<std::pair<SkString, float>>                fScalarSlots;
434     std::vector<std::pair<SkString, SkV2>>                 fVec2Slots;
435     std::vector<std::pair<SkString, TextSlotData>>         fTextStringSlots;
436     std::vector<std::pair<SkString, std::string>>          fImageSlots;
437 
438 };
439 
draw_stats_box(SkCanvas * canvas,const skottie::Animation::Builder::Stats & stats)440 static void draw_stats_box(SkCanvas* canvas, const skottie::Animation::Builder::Stats& stats) {
441     static constexpr SkRect kR = { 10, 10, 280, 120 };
442     static constexpr SkScalar kTextSize = 20;
443 
444     SkPaint paint;
445     paint.setAntiAlias(true);
446     paint.setColor(0xffeeeeee);
447 
448     SkFont font(ToolUtils::DefaultTypeface(), kTextSize);
449 
450     canvas->drawRect(kR, paint);
451 
452     paint.setColor(SK_ColorBLACK);
453 
454     const auto json_size = SkStringPrintf("Json size: %zu bytes",
455                                           stats.fJsonSize);
456     canvas->drawString(json_size, kR.x() + 10, kR.y() + kTextSize * 1, font, paint);
457     const auto animator_count = SkStringPrintf("Animator count: %zu",
458                                                stats.fAnimatorCount);
459     canvas->drawString(animator_count, kR.x() + 10, kR.y() + kTextSize * 2, font, paint);
460     const auto json_parse_time = SkStringPrintf("Json parse time: %.3f ms",
461                                                 stats.fJsonParseTimeMS);
462     canvas->drawString(json_parse_time, kR.x() + 10, kR.y() + kTextSize * 3, font, paint);
463     const auto scene_parse_time = SkStringPrintf("Scene build time: %.3f ms",
464                                                  stats.fSceneParseTimeMS);
465     canvas->drawString(scene_parse_time, kR.x() + 10, kR.y() + kTextSize * 4, font, paint);
466     const auto total_load_time = SkStringPrintf("Total load time: %.3f ms",
467                                                 stats.fTotalLoadTimeMS);
468     canvas->drawString(total_load_time, kR.x() + 10, kR.y() + kTextSize * 5, font, paint);
469 
470     paint.setStyle(SkPaint::kStroke_Style);
471     canvas->drawRect(kR, paint);
472 }
473 
SkottieSlide(const SkString & name,const SkString & path)474 SkottieSlide::SkottieSlide(const SkString& name, const SkString& path)
475     : fPath(path) {
476     fName = name;
477 }
478 
init()479 void SkottieSlide::init() {
480     class Logger final : public skottie::Logger {
481     public:
482         struct LogEntry {
483             SkString fMessage,
484                      fJSON;
485         };
486 
487         void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
488             auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
489             log.push_back({ SkString(message), json ? SkString(json) : SkString() });
490         }
491 
492         void report() const {
493             SkDebugf("Animation loaded with %zu error%s, %zu warning%s.\n",
494                      fErrors.size(), fErrors.size() == 1 ? "" : "s",
495                      fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
496 
497             const auto& show = [](const LogEntry& log, const char prefix[]) {
498                 SkDebugf("%s%s", prefix, log.fMessage.c_str());
499                 if (!log.fJSON.isEmpty())
500                     SkDebugf(" : %s", log.fJSON.c_str());
501                 SkDebugf("\n");
502             };
503 
504             for (const auto& err : fErrors)   show(err, "  !! ");
505             for (const auto& wrn : fWarnings) show(wrn, "  ?? ");
506         }
507 
508     private:
509         std::vector<LogEntry> fErrors,
510                               fWarnings;
511     };
512 
513     auto logger = sk_make_sp<Logger>();
514 
515     uint32_t flags = 0;
516     if (fPreferGlyphPaths) {
517         flags |= skottie::Animation::Builder::kPreferEmbeddedFonts;
518     }
519     skottie::Animation::Builder builder(flags);
520 
521     // Viewer should have already registered the codecs necessary for DataURIResourceProviderProxy
522     auto predecode = skresources::ImageDecodeStrategy::kPreDecode;
523     auto resource_provider =
524             sk_make_sp<AudioProviderProxy>(skresources::DataURIResourceProviderProxy::Make(
525                     skresources::FileResourceProvider::Make(SkOSPath::Dirname(fPath.c_str()),
526                                                             predecode),
527                     predecode,
528                     ToolUtils::TestFontMgr()));
529 
530     static constexpr char kInterceptPrefix[] = "__";
531     auto precomp_interceptor =
532             sk_make_sp<skottie_utils::ExternalAnimationPrecompInterceptor>(resource_provider,
533                                                                            kInterceptPrefix);
534 
535     fTransformTracker = sk_make_sp<TransformTracker>();
536     auto text_tracker = sk_make_sp<TextTracker>(fTransformTracker);
537 
538     builder.setLogger(logger)
539            .setFontManager(ToolUtils::TestFontMgr())
540            .setPrecompInterceptor(std::move(precomp_interceptor))
541            .setResourceProvider(resource_provider)
542            .setPropertyObserver(text_tracker);
543 
544     fAnimation = builder.makeFromFile(fPath.c_str());
545     fAnimationStats = builder.getStats();
546     fTimeBase       = 0; // force a time reset
547 
548     if (fAnimation) {
549         if (!fSlotManagerInterface) {
550             fSlotManagerInterface =
551                 std::make_unique<SlotManagerInterface>(builder.getSlotManager(), resource_provider);
552         }
553 
554         fSlotManagerInterface->initializeSlotManagerUI();
555 
556         fAnimation->seek(0);
557         fFrameTimes.resize(SkScalarCeilToInt(fAnimation->duration() * fAnimation->fps()));
558         SkDebugf("Loaded Bodymovin animation v: %s, size: [%f %f]\n",
559                  fAnimation->version().c_str(),
560                  fAnimation->size().width(),
561                  fAnimation->size().height());
562         logger->report();
563 
564         if (auto text_props = std::move(text_tracker->props()); !text_props.empty()) {
565             // Attach the editor to the first text layer, and track the rest as dependents.
566             auto editor_target = std::move(text_props[0]);
567             text_props.erase(text_props.cbegin());
568             fTextEditor = sk_make_sp<skottie_utils::TextEditor>(std::move(editor_target),
569                                                                 std::move(text_props));
570             fTextEditor->setCursorWeight(1.2f);
571         }
572     } else {
573         SkDebugf("failed to load Bodymovin animation: %s\n", fPath.c_str());
574     }
575 }
576 
load(SkScalar w,SkScalar h)577 void SkottieSlide::load(SkScalar w, SkScalar h) {
578     fWinSize = SkSize::Make(w, h);
579     this->init();
580 }
581 
unload()582 void SkottieSlide::unload() {
583     fAnimation.reset();
584 }
585 
resize(SkScalar w,SkScalar h)586 void SkottieSlide::resize(SkScalar w, SkScalar h) {
587     fWinSize = { w, h };
588 }
589 
draw(SkCanvas * canvas)590 void SkottieSlide::draw(SkCanvas* canvas) {
591     if (fAnimation) {
592         SkAutoCanvasRestore acr(canvas, true);
593         const auto dstR = SkRect::MakeSize(fWinSize);
594 
595         {
596             const auto t0 = SkTime::GetNSecs();
597             fAnimation->render(canvas, &dstR);
598 
599             // TODO: this does not capture GPU flush time!
600             const auto  frame_index  = static_cast<size_t>(fCurrentFrame);
601             fFrameTimes[frame_index] = static_cast<float>((SkTime::GetNSecs() - t0) * 1e-6);
602         }
603 
604         double fr = 60;
605         if (fFrameRate != 0) {
606             fr = fFrameRate;
607         }
608         fTransformTracker->renderTracker(canvas, fCurrentFrame/fr, fWinSize, fAnimation->size());
609 
610         if (fShowAnimationStats) {
611             draw_stats_box(canvas, fAnimationStats);
612         }
613         if (fShowAnimationInval) {
614             const auto t = SkMatrix::RectToRect(SkRect::MakeSize(fAnimation->size()), dstR,
615                                                 SkMatrix::kCenter_ScaleToFit);
616             SkPaint fill, stroke;
617             fill.setAntiAlias(true);
618             fill.setColor(0x40ff0000);
619             stroke.setAntiAlias(true);
620             stroke.setColor(0xffff0000);
621             stroke.setStyle(SkPaint::kStroke_Style);
622 
623             for (const auto& r : fInvalController) {
624                 SkRect bounds;
625                 t.mapRect(&bounds, r);
626                 canvas->drawRect(bounds, fill);
627                 canvas->drawRect(bounds, stroke);
628             }
629         }
630         if (fShowUI) {
631             this->renderUI();
632         }
633         if (fShowSlotManager) {
634             // not able to track layers with a PropertyObserver while using SM's PropertyObserver
635             fShowTrackerUI = false;
636             fSlotManagerInterface->renderUI();
637         }
638         if (fShowTrackerUI) {
639             fTransformTracker->renderUI();
640         }
641     }
642 }
643 
animate(double nanos)644 bool SkottieSlide::animate(double nanos) {
645     if (!fTimeBase) {
646         // Reset the animation time.
647         fTimeBase = nanos;
648     }
649 
650     if (fAnimation) {
651         fInvalController.reset();
652 
653         const auto frame_count = fAnimation->duration() * fAnimation->fps();
654 
655         if (!fDraggingProgress) {
656             // Clock-driven progress: update current frame.
657             const double t_sec = (nanos - fTimeBase) * 1e-9;
658             fCurrentFrame = std::fmod(t_sec * fAnimation->fps(), frame_count);
659         } else {
660             // Slider-driven progress: update the time origin.
661             fTimeBase = nanos - fCurrentFrame / fAnimation->fps() * 1e9;
662         }
663 
664         // Sanitize and rate-lock the current frame.
665         fCurrentFrame = SkTPin<float>(fCurrentFrame, 0.0f, frame_count - 1);
666         if (fFrameRate > 0) {
667             const auto fps_scale = fFrameRate / fAnimation->fps();
668             fCurrentFrame = std::trunc(fCurrentFrame * fps_scale) / fps_scale;
669         }
670 
671         fAnimation->seekFrame(fCurrentFrame, fShowAnimationInval ? &fInvalController
672                                                                  : nullptr);
673     }
674     return true;
675 }
676 
onChar(SkUnichar c)677 bool SkottieSlide::onChar(SkUnichar c) {
678     if (fTextEditor && fTextEditor->onCharInput(c)) {
679         return true;
680     }
681 
682     switch (c) {
683     case 'I':
684         fShowAnimationStats = !fShowAnimationStats;
685         return true;
686     case 'G':
687         fPreferGlyphPaths = !fPreferGlyphPaths;
688         this->load(fWinSize.width(), fWinSize.height());
689         return true;
690     case 'T':
691         fShowTrackerUI = !fShowTrackerUI;
692         return true;
693     case 'M':
694         fShowSlotManager = !fShowSlotManager;
695         return true;
696     case 'E':
697         if (fTextEditor) {
698             fTextEditor->toggleEnabled();
699         }
700         return true;
701     }
702 
703     return Slide::onChar(c);
704 }
705 
onMouse(SkScalar x,SkScalar y,skui::InputState state,skui::ModifierKey mod)706 bool SkottieSlide::onMouse(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey mod) {
707     if (fTextEditor && fTextEditor->onMouseInput(x, y, state, mod)) {
708         return true;
709     }
710 
711     switch (state) {
712     case skui::InputState::kUp:
713         fShowAnimationInval = !fShowAnimationInval;
714         fShowAnimationStats = !fShowAnimationStats;
715         break;
716     default:
717         break;
718     }
719 
720     fShowUI = this->UIArea().contains(x, y);
721 
722     return false;
723 }
724 
UIArea() const725 SkRect SkottieSlide::UIArea() const {
726     static constexpr float kUIHeight = 120.0f;
727 
728     return SkRect::MakeXYWH(0, fWinSize.height() - kUIHeight, fWinSize.width(), kUIHeight);
729 }
730 
renderUI()731 void SkottieSlide::renderUI() {
732     static constexpr auto kUI_opacity     = 0.35f,
733                           kUI_hist_height = 50.0f,
734                           kUI_fps_width   = 100.0f;
735 
736     auto add_frame_rate_option = [this](const char* label, double rate) {
737         const auto is_selected = (fFrameRate == rate);
738         if (ImGui::Selectable(label, is_selected)) {
739             fFrameRate      = rate;
740             fFrameRateLabel = label;
741         }
742         if (is_selected) {
743             ImGui::SetItemDefaultFocus();
744         }
745     };
746 
747     ImGui::SetNextWindowBgAlpha(kUI_opacity);
748     if (ImGui::Begin("Skottie Controls", nullptr, ImGuiWindowFlags_NoDecoration |
749                                                   ImGuiWindowFlags_NoResize |
750                                                   ImGuiWindowFlags_NoMove |
751                                                   ImGuiWindowFlags_NoSavedSettings |
752                                                   ImGuiWindowFlags_NoFocusOnAppearing |
753                                                   ImGuiWindowFlags_NoNav)) {
754         const auto ui_area = this->UIArea();
755         ImGui::SetWindowPos(ImVec2(ui_area.x(), ui_area.y()));
756         ImGui::SetWindowSize(ImVec2(ui_area.width(), ui_area.height()));
757 
758         ImGui::PushItemWidth(-1);
759         ImGui::PlotHistogram("", fFrameTimes.data(), fFrameTimes.size(),
760                                  0, nullptr, FLT_MAX, FLT_MAX, ImVec2(0, kUI_hist_height));
761         ImGui::SliderFloat("", &fCurrentFrame, 0, fAnimation->duration() * fAnimation->fps() - 1);
762         fDraggingProgress = ImGui::IsItemActive();
763         ImGui::PopItemWidth();
764 
765         ImGui::PushItemWidth(kUI_fps_width);
766         if (ImGui::BeginCombo("FPS", fFrameRateLabel)) {
767             add_frame_rate_option("", 0.0);
768             add_frame_rate_option("Native", fAnimation->fps());
769             add_frame_rate_option( "1",  1.0);
770             add_frame_rate_option("15", 15.0);
771             add_frame_rate_option("24", 24.0);
772             add_frame_rate_option("30", 30.0);
773             add_frame_rate_option("60", 60.0);
774             ImGui::EndCombo();
775         }
776         ImGui::PopItemWidth();
777     }
778     ImGui::End();
779 }
780 
781 #endif // SK_ENABLE_SKOTTIE
782