| /* |
| * Copyright 2017 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "tools/viewer/SkottieSlide.h" |
| |
| #if defined(SK_ENABLE_SKOTTIE) |
| |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkFont.h" |
| #include "include/private/base/SkNoncopyable.h" |
| #include "include/private/base/SkTPin.h" |
| #include "modules/audioplayer/SkAudioPlayer.h" |
| #include "modules/skottie/include/Skottie.h" |
| #include "modules/skottie/include/SkottieProperty.h" |
| #include "modules/skottie/include/SlotManager.h" |
| #include "modules/skottie/utils/SkottieUtils.h" |
| #include "modules/skottie/utils/TextEditor.h" |
| #include "modules/skresources/include/SkResources.h" |
| #include "src/base/SkTime.h" |
| #include "src/core/SkOSFile.h" |
| #include "src/utils/SkOSPath.h" |
| #include "tools/Resources.h" |
| #include "tools/fonts/FontToolUtils.h" |
| #include "tools/timer/TimeUtils.h" |
| |
| #include <cmath> |
| #include <vector> |
| |
| #include "imgui.h" |
| |
| namespace { |
| |
| class Track final : public skresources::ExternalTrackAsset { |
| public: |
| explicit Track(std::unique_ptr<SkAudioPlayer> player) : fPlayer(std::move(player)) {} |
| |
| private: |
| void seek(float t) override { |
| if (fPlayer->isStopped() && t >=0) { |
| fPlayer->play(); |
| } |
| |
| if (fPlayer->isPlaying()) { |
| if (t < 0) { |
| fPlayer->stop(); |
| } else { |
| static constexpr float kTolerance = 0.075f; |
| const auto player_pos = fPlayer->time(); |
| |
| if (std::abs(player_pos - t) > kTolerance) { |
| fPlayer->setTime(t); |
| } |
| } |
| } |
| } |
| |
| const std::unique_ptr<SkAudioPlayer> fPlayer; |
| }; |
| |
| class AudioProviderProxy final : public skresources::ResourceProviderProxyBase { |
| public: |
| explicit AudioProviderProxy(sk_sp<skresources::ResourceProvider> rp) |
| : skresources::ResourceProviderProxyBase(std::move(rp)) {} |
| |
| private: |
| sk_sp<skresources::ExternalTrackAsset> loadAudioAsset(const char path[], |
| const char name[], |
| const char[] /*id*/) override { |
| if (auto data = this->load(path, name)) { |
| if (auto player = SkAudioPlayer::Make(std::move(data))) { |
| return sk_make_sp<Track>(std::move(player)); |
| } |
| } |
| |
| return nullptr; |
| } |
| }; |
| |
| class Decorator : public SkNoncopyable { |
| public: |
| virtual ~Decorator() = default; |
| |
| // We pass in the Matrix and have the Decorator handle using it independently |
| // This is so decorators can keep position on screen after moving. |
| virtual void render(SkCanvas*, double, const SkMatrix) = 0; |
| }; |
| |
| class SimpleMarker final : public Decorator { |
| public: |
| ~SimpleMarker() override = default; |
| |
| static std::unique_ptr<Decorator> Make() { return std::make_unique<SimpleMarker>(); } |
| |
| void render(SkCanvas* canvas, double t, const SkMatrix transform) override { |
| canvas->concat(transform); |
| SkPaint p; |
| p.setAntiAlias(true); |
| |
| p.setColor(SK_ColorGREEN); |
| canvas->drawCircle(0, 0, 5, p); |
| |
| p.setColor(SK_ColorRED); |
| p.setStrokeWidth(1.5f); |
| canvas->drawLine(-10, 0, 10, 0, p); |
| canvas->drawLine(0, -10, 0, 10, p); |
| } |
| }; |
| |
| class TestingResourceProvider : public skresources::ResourceProvider { |
| public: |
| TestingResourceProvider() {} |
| |
| sk_sp<SkData> load(const char resource_path[], const char resource_name[]) const override { |
| auto it = fResources.find(resource_name); |
| if (it != fResources.end()) { |
| return it->second; |
| } else { |
| return GetResourceAsData(SkOSPath::Join(resource_path, resource_name).c_str()); |
| } |
| } |
| |
| sk_sp<skresources::ImageAsset> loadImageAsset(const char resource_path[], |
| const char resource_name[], |
| const char /*resource_id*/[]) const override { |
| auto data = this->load(resource_path, resource_name); |
| // Viewer should have already registered the codecs necessary for MultiFrameImageAsset |
| return skresources::MultiFrameImageAsset::Make(data); |
| } |
| |
| void addPath(const char resource_name[], const SkPath& path) { |
| fResources[resource_name] = path.serialize(); |
| } |
| |
| private: |
| std::unordered_map<std::string, sk_sp<SkData>> fResources; |
| }; |
| |
| static const struct DecoratorRec { |
| const char* fName; |
| std::unique_ptr<Decorator>(*fFactory)(); |
| } kDecorators[] = { |
| { "Simple marker", SimpleMarker::Make }, |
| }; |
| |
| class TextTracker final : public skottie::PropertyObserver { |
| public: |
| explicit TextTracker(sk_sp<PropertyObserver> delegate) : fDelegate(std::move(delegate)) {} |
| |
| std::vector<std::unique_ptr<skottie::TextPropertyHandle>>& props() { |
| return fTextProps; |
| } |
| |
| private: |
| void onTextProperty(const char node_name[], |
| const LazyHandle<skottie::TextPropertyHandle>& lh) override { |
| fTextProps.push_back(lh()); |
| |
| if (fDelegate) { |
| fDelegate->onTextProperty(node_name, lh); |
| } |
| } |
| |
| const sk_sp<PropertyObserver> fDelegate; |
| std::vector<std::unique_ptr<skottie::TextPropertyHandle>> fTextProps; |
| }; |
| |
| } // namespace |
| |
| class SkottieSlide::TransformTracker : public skottie::PropertyObserver { |
| public: |
| void renderUI() { |
| if (ImGui::Begin("Transform Tracker", nullptr)) { |
| if (ImGui::BeginCombo("Transform", fTransformSelect |
| ? std::get<0>(*fTransformSelect).c_str() |
| : nullptr)) { |
| if (ImGui::Selectable("(none)", true)) { |
| fTransformSelect = nullptr; |
| } |
| for (const auto& entry : fTransforms) { |
| const auto* transform_name = std::get<0>(entry).c_str(); |
| if (ImGui::Selectable(transform_name, false)) { |
| if (!fTransformSelect || |
| transform_name != std::get<0>(*fTransformSelect).c_str()) { |
| fTransformSelect = &entry; |
| // Reset the decorator on transform change. |
| fDecorator = fDecoratorSelect->fFactory(); |
| } |
| } |
| } |
| ImGui::EndCombo(); |
| } |
| |
| if (ImGui::BeginCombo("Decoration", fDecoratorSelect->fName)) { |
| for (const auto& dec : kDecorators) { |
| if (ImGui::Selectable(dec.fName, false)) { |
| if (dec.fName != fDecoratorSelect->fName) { |
| fDecoratorSelect = &dec; |
| fDecorator = fDecoratorSelect->fFactory(); |
| } |
| } |
| } |
| ImGui::EndCombo(); |
| } |
| } |
| ImGui::End(); |
| } |
| |
| void renderTracker(SkCanvas* canvas, double time, const SkSize& win_size, const SkSize& anim_size) const { |
| if (!fTransformSelect) { |
| return; |
| } |
| |
| const auto tprop = std::get<1>(*fTransformSelect)->get(); |
| |
| const auto m = SkMatrix::Translate(tprop.fPosition.fX, tprop.fPosition.fY) |
| * SkMatrix::RotateDeg(tprop.fRotation) |
| * SkMatrix::Scale (tprop.fScale.fX*0.01f, tprop.fScale.fY*0.01f) |
| * SkMatrix::Translate(tprop.fAnchorPoint.fX, tprop.fAnchorPoint.fY); |
| |
| const auto viewer_matrix = SkMatrix::RectToRect(SkRect::MakeSize(anim_size), |
| SkRect::MakeSize(win_size), |
| SkMatrix::kCenter_ScaleToFit); |
| |
| SkAutoCanvasRestore acr(canvas, true); |
| canvas->concat(viewer_matrix); |
| |
| SkASSERT(fDecorator); |
| fDecorator->render(canvas, time, m); |
| } |
| |
| private: |
| void onTransformProperty(const char name[], |
| const LazyHandle<skottie::TransformPropertyHandle>& lh) override { |
| |
| fTransforms.push_back(std::make_tuple(SkString(name), lh())); |
| } |
| |
| using TransformT = std::tuple<SkString, std::unique_ptr<skottie::TransformPropertyHandle>>; |
| |
| std::vector<TransformT> fTransforms; |
| std::unique_ptr<Decorator> fDecorator; |
| const TransformT* fTransformSelect = nullptr; |
| const DecoratorRec* fDecoratorSelect = &kDecorators[0]; |
| }; |
| |
| // Holds a pointer to a slot manager and the list of slots for the UI widget to track |
| class SkottieSlide::SlotManagerInterface { |
| public: |
| SlotManagerInterface(sk_sp<skottie::SlotManager> slotManager, sk_sp<skresources::ResourceProvider> rp) |
| : fSlotManager(std::move(slotManager)) |
| , fResourceProvider(std::move(rp)) |
| {} |
| |
| |
| void renderUI() { |
| if (ImGui::Begin("Slot Manager", nullptr)) { |
| ImGui::Text("Color Slots"); |
| for (size_t i = 0; i < fColorSlots.size(); i++) { |
| auto& cSlot = fColorSlots.at(i); |
| ImGui::PushID(i); |
| ImGui::Text("%s", cSlot.first.c_str()); |
| if (ImGui::ColorEdit4("Color", cSlot.second.data())) { |
| this->pushSlots(); |
| } |
| ImGui::PopID(); |
| } |
| ImGui::Text("Scalar Slots"); |
| for (size_t i = 0; i < fScalarSlots.size(); i++) { |
| auto& oSlot = fScalarSlots.at(i); |
| ImGui::PushID(i); |
| ImGui::Text("%s", oSlot.first.c_str()); |
| if (ImGui::InputFloat("Scalar", &(oSlot.second))) { |
| this->pushSlots(); |
| } |
| ImGui::PopID(); |
| } |
| ImGui::Text("Vec2 Slots"); |
| for (size_t i = 0; i < fVec2Slots.size(); i++) { |
| auto& vSlot = fVec2Slots.at(i); |
| ImGui::PushID(i); |
| ImGui::Text("%s", vSlot.first.c_str()); |
| if (ImGui::InputFloat2("x, y", &(vSlot.second.x))) { |
| this->pushSlots(); |
| } |
| ImGui::PopID(); |
| } |
| ImGui::Text("Text Slots"); |
| for (size_t i = 0; i < fTextStringSlots.size(); i++) { |
| auto& tSlot = fTextStringSlots.at(i); |
| ImGui::PushID(i); |
| ImGui::Text("%s", tSlot.first.c_str()); |
| if (ImGui::InputText("Text", tSlot.second.source.data(), |
| tSlot.second.source.size())) { |
| this->pushSlots(); |
| } |
| if (ImGui::BeginCombo("Font", tSlot.second.font.data())) { |
| for (const auto& typeface : fTypefaceList) { |
| if (ImGui::Selectable(typeface, false)) { |
| tSlot.second.font = typeface; |
| this->pushSlots(); |
| } |
| } |
| ImGui::EndCombo(); |
| } |
| ImGui::PopID(); |
| } |
| |
| ImGui::Text("Image Slots"); |
| for (size_t i = 0; i < fImageSlots.size(); i++) { |
| auto& iSlot = fImageSlots.at(i); |
| ImGui::PushID(i); |
| ImGui::Text("%s", iSlot.first.c_str()); |
| if (ImGui::BeginCombo("Resource", iSlot.second.data())) { |
| for (const auto& res : fResList) { |
| if (ImGui::Selectable(res.c_str(), false)) { |
| iSlot.second = res.c_str(); |
| this->pushSlots(); |
| } |
| } |
| ImGui::EndCombo(); |
| } |
| ImGui::PopID(); |
| } |
| } |
| ImGui::End(); |
| } |
| |
| void pushSlots() { |
| for(const auto& s : fColorSlots) { |
| fSlotManager->setColorSlot(s.first, SkColor4f{s.second[0], s.second[1], |
| s.second[2], s.second[3]}.toSkColor()); |
| } |
| for(const auto& s : fScalarSlots) { |
| fSlotManager->setScalarSlot(s.first, s.second); |
| } |
| for(const auto& s : fVec2Slots) { |
| fSlotManager->setVec2Slot(s.first, {s.second.x, s.second.y}); |
| } |
| for(const auto& s : fTextStringSlots) { |
| auto t = fSlotManager->getTextSlot(s.first); |
| t->fText = SkString(s.second.source.data()); |
| t->fTypeface = ToolUtils::TestFontMgr()->matchFamilyStyle(s.second.font.c_str(), |
| SkFontStyle()); |
| fSlotManager->setTextSlot(s.first, *t); |
| } |
| for(const auto& s : fImageSlots) { |
| auto image = fResourceProvider->loadImageAsset("images/", s.second.c_str(), nullptr); |
| fSlotManager->setImageSlot(s.first, image); |
| } |
| } |
| |
| void initializeSlotManagerUI() { |
| prepareImageAssetList(GetResourcePath("skottie/images").c_str()); |
| // only initialize if slots are unpopulated |
| if (fColorSlots.empty() && fScalarSlots.empty() && fTextStringSlots.empty()) { |
| auto slotInfos = fSlotManager->getSlotInfo(); |
| for (const auto &sid : slotInfos.fColorSlotIDs) { |
| addColorSlot(sid); |
| } |
| for (const auto &sid : slotInfos.fScalarSlotIDs) { |
| addScalarSlot(sid); |
| } |
| for (const auto &sid : slotInfos.fVec2SlotIDs) { |
| addVec2Slot(sid); |
| } |
| for (const auto &sid : slotInfos.fImageSlotIDs) { |
| addImageSlot(sid); |
| } |
| for (const auto &sid : slotInfos.fTextSlotIDs) { |
| addTextSlot(sid); |
| } |
| } |
| } |
| |
| private: |
| static constexpr int kBufferLen = 256; |
| |
| sk_sp<skottie::SlotManager> fSlotManager; |
| const sk_sp<skresources::ResourceProvider> fResourceProvider; |
| std::vector<SkString> fResList; |
| static constexpr std::array<const char*, 4> fTypefaceList = {"Arial", |
| "Courier New", |
| "Roboto-Regular", |
| "Georgia"}; |
| |
| using GuiTextBuffer = std::array<char, kBufferLen>; |
| |
| void addColorSlot(SkString slotID) { |
| auto c = fSlotManager->getColorSlot(slotID); |
| SkColor4f color4f = SkColor4f::FromColor(*c); |
| fColorSlots.push_back(std::make_pair(slotID, color4f.array())); |
| } |
| |
| void addScalarSlot(SkString slotID) { |
| fScalarSlots.push_back(std::make_pair(slotID, *fSlotManager->getScalarSlot(slotID))); |
| } |
| |
| void addVec2Slot(SkString slotID) { |
| fVec2Slots.push_back(std::make_pair(slotID, *fSlotManager->getVec2Slot(slotID))); |
| } |
| |
| void addTextSlot(SkString slotID) { |
| std::array<char, kBufferLen> textSource = {'\0'}; |
| SkString s = fSlotManager->getTextSlot(slotID)->fText; |
| std::copy(s.data(), s.data() + s.size(), textSource.data()); |
| TextSlotData data = {textSource, fTypefaceList[0]}; |
| fTextStringSlots.push_back(std::make_pair(slotID, data)); |
| } |
| |
| void addImageSlot(SkString slotID) { |
| fImageSlots.push_back(std::make_pair(slotID, fResList[0].data())); |
| } |
| |
| void prepareImageAssetList(const char* dirname) { |
| fResList.clear(); |
| SkOSFile::Iter iter(dirname, ".png"); |
| for (SkString file; iter.next(&file); ) { |
| fResList.push_back(file); |
| } |
| } |
| |
| struct TextSlotData { |
| GuiTextBuffer source; |
| std::string font; |
| }; |
| |
| std::vector<std::pair<SkString, std::array<float, 4>>> fColorSlots; |
| std::vector<std::pair<SkString, float>> fScalarSlots; |
| std::vector<std::pair<SkString, SkV2>> fVec2Slots; |
| std::vector<std::pair<SkString, TextSlotData>> fTextStringSlots; |
| std::vector<std::pair<SkString, std::string>> fImageSlots; |
| |
| }; |
| |
| static void draw_stats_box(SkCanvas* canvas, const skottie::Animation::Builder::Stats& stats) { |
| static constexpr SkRect kR = { 10, 10, 280, 120 }; |
| static constexpr SkScalar kTextSize = 20; |
| |
| SkPaint paint; |
| paint.setAntiAlias(true); |
| paint.setColor(0xffeeeeee); |
| |
| SkFont font(ToolUtils::DefaultTypeface(), kTextSize); |
| |
| canvas->drawRect(kR, paint); |
| |
| paint.setColor(SK_ColorBLACK); |
| |
| const auto json_size = SkStringPrintf("Json size: %zu bytes", |
| stats.fJsonSize); |
| canvas->drawString(json_size, kR.x() + 10, kR.y() + kTextSize * 1, font, paint); |
| const auto animator_count = SkStringPrintf("Animator count: %zu", |
| stats.fAnimatorCount); |
| canvas->drawString(animator_count, kR.x() + 10, kR.y() + kTextSize * 2, font, paint); |
| const auto json_parse_time = SkStringPrintf("Json parse time: %.3f ms", |
| stats.fJsonParseTimeMS); |
| canvas->drawString(json_parse_time, kR.x() + 10, kR.y() + kTextSize * 3, font, paint); |
| const auto scene_parse_time = SkStringPrintf("Scene build time: %.3f ms", |
| stats.fSceneParseTimeMS); |
| canvas->drawString(scene_parse_time, kR.x() + 10, kR.y() + kTextSize * 4, font, paint); |
| const auto total_load_time = SkStringPrintf("Total load time: %.3f ms", |
| stats.fTotalLoadTimeMS); |
| canvas->drawString(total_load_time, kR.x() + 10, kR.y() + kTextSize * 5, font, paint); |
| |
| paint.setStyle(SkPaint::kStroke_Style); |
| canvas->drawRect(kR, paint); |
| } |
| |
| SkottieSlide::SkottieSlide(const SkString& name, const SkString& path) |
| : fPath(path) { |
| fName = name; |
| } |
| |
| void SkottieSlide::init() { |
| class Logger final : public skottie::Logger { |
| public: |
| struct LogEntry { |
| SkString fMessage, |
| fJSON; |
| }; |
| |
| void log(skottie::Logger::Level lvl, const char message[], const char json[]) override { |
| auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings; |
| log.push_back({ SkString(message), json ? SkString(json) : SkString() }); |
| } |
| |
| void report() const { |
| SkDebugf("Animation loaded with %zu error%s, %zu warning%s.\n", |
| fErrors.size(), fErrors.size() == 1 ? "" : "s", |
| fWarnings.size(), fWarnings.size() == 1 ? "" : "s"); |
| |
| const auto& show = [](const LogEntry& log, const char prefix[]) { |
| SkDebugf("%s%s", prefix, log.fMessage.c_str()); |
| if (!log.fJSON.isEmpty()) |
| SkDebugf(" : %s", log.fJSON.c_str()); |
| SkDebugf("\n"); |
| }; |
| |
| for (const auto& err : fErrors) show(err, " !! "); |
| for (const auto& wrn : fWarnings) show(wrn, " ?? "); |
| } |
| |
| private: |
| std::vector<LogEntry> fErrors, |
| fWarnings; |
| }; |
| |
| auto logger = sk_make_sp<Logger>(); |
| |
| uint32_t flags = 0; |
| if (fPreferGlyphPaths) { |
| flags |= skottie::Animation::Builder::kPreferEmbeddedFonts; |
| } |
| skottie::Animation::Builder builder(flags); |
| |
| // Viewer should have already registered the codecs necessary for DataURIResourceProviderProxy |
| auto predecode = skresources::ImageDecodeStrategy::kPreDecode; |
| auto resource_provider = |
| sk_make_sp<AudioProviderProxy>(skresources::DataURIResourceProviderProxy::Make( |
| skresources::FileResourceProvider::Make(SkOSPath::Dirname(fPath.c_str()), |
| predecode), |
| predecode, |
| ToolUtils::TestFontMgr())); |
| |
| static constexpr char kInterceptPrefix[] = "__"; |
| auto precomp_interceptor = |
| sk_make_sp<skottie_utils::ExternalAnimationPrecompInterceptor>(resource_provider, |
| kInterceptPrefix); |
| |
| fTransformTracker = sk_make_sp<TransformTracker>(); |
| auto text_tracker = sk_make_sp<TextTracker>(fTransformTracker); |
| |
| builder.setLogger(logger) |
| .setFontManager(ToolUtils::TestFontMgr()) |
| .setPrecompInterceptor(std::move(precomp_interceptor)) |
| .setResourceProvider(resource_provider) |
| .setPropertyObserver(text_tracker); |
| |
| fAnimation = builder.makeFromFile(fPath.c_str()); |
| fAnimationStats = builder.getStats(); |
| fTimeBase = 0; // force a time reset |
| |
| if (!fSlotManagerInterface) { |
| fSlotManagerInterface = std::make_unique<SlotManagerInterface>(builder.getSlotManager(), resource_provider); |
| } |
| |
| fSlotManagerInterface->initializeSlotManagerUI(); |
| |
| if (fAnimation) { |
| fAnimation->seek(0); |
| fFrameTimes.resize(SkScalarCeilToInt(fAnimation->duration() * fAnimation->fps())); |
| SkDebugf("Loaded Bodymovin animation v: %s, size: [%f %f]\n", |
| fAnimation->version().c_str(), |
| fAnimation->size().width(), |
| fAnimation->size().height()); |
| logger->report(); |
| |
| if (auto text_props = std::move(text_tracker->props()); !text_props.empty()) { |
| // Attach the editor to the first text layer, and track the rest as dependents. |
| auto editor_target = std::move(text_props[0]); |
| text_props.erase(text_props.cbegin()); |
| fTextEditor = sk_make_sp<skottie_utils::TextEditor>(std::move(editor_target), |
| std::move(text_props)); |
| fTextEditor->setCursorWeight(1.2f); |
| } |
| } else { |
| SkDebugf("failed to load Bodymovin animation: %s\n", fPath.c_str()); |
| } |
| } |
| |
| void SkottieSlide::load(SkScalar w, SkScalar h) { |
| fWinSize = SkSize::Make(w, h); |
| this->init(); |
| } |
| |
| void SkottieSlide::unload() { |
| fAnimation.reset(); |
| } |
| |
| void SkottieSlide::resize(SkScalar w, SkScalar h) { |
| fWinSize = { w, h }; |
| } |
| |
| void SkottieSlide::draw(SkCanvas* canvas) { |
| if (fAnimation) { |
| SkAutoCanvasRestore acr(canvas, true); |
| const auto dstR = SkRect::MakeSize(fWinSize); |
| |
| { |
| const auto t0 = SkTime::GetNSecs(); |
| fAnimation->render(canvas, &dstR); |
| |
| // TODO: this does not capture GPU flush time! |
| const auto frame_index = static_cast<size_t>(fCurrentFrame); |
| fFrameTimes[frame_index] = static_cast<float>((SkTime::GetNSecs() - t0) * 1e-6); |
| } |
| |
| double fr = 60; |
| if (fFrameRate != 0) { |
| fr = fFrameRate; |
| } |
| fTransformTracker->renderTracker(canvas, fCurrentFrame/fr, fWinSize, fAnimation->size()); |
| |
| if (fShowAnimationStats) { |
| draw_stats_box(canvas, fAnimationStats); |
| } |
| if (fShowAnimationInval) { |
| const auto t = SkMatrix::RectToRect(SkRect::MakeSize(fAnimation->size()), dstR, |
| SkMatrix::kCenter_ScaleToFit); |
| SkPaint fill, stroke; |
| fill.setAntiAlias(true); |
| fill.setColor(0x40ff0000); |
| stroke.setAntiAlias(true); |
| stroke.setColor(0xffff0000); |
| stroke.setStyle(SkPaint::kStroke_Style); |
| |
| for (const auto& r : fInvalController) { |
| SkRect bounds; |
| t.mapRect(&bounds, r); |
| canvas->drawRect(bounds, fill); |
| canvas->drawRect(bounds, stroke); |
| } |
| } |
| if (fShowUI) { |
| this->renderUI(); |
| } |
| if (fShowSlotManager) { |
| // not able to track layers with a PropertyObserver while using SM's PropertyObserver |
| fShowTrackerUI = false; |
| fSlotManagerInterface->renderUI(); |
| } |
| if (fShowTrackerUI) { |
| fTransformTracker->renderUI(); |
| } |
| } |
| } |
| |
| bool SkottieSlide::animate(double nanos) { |
| if (!fTimeBase) { |
| // Reset the animation time. |
| fTimeBase = nanos; |
| } |
| |
| if (fAnimation) { |
| fInvalController.reset(); |
| |
| const auto frame_count = fAnimation->duration() * fAnimation->fps(); |
| |
| if (!fDraggingProgress) { |
| // Clock-driven progress: update current frame. |
| const double t_sec = (nanos - fTimeBase) * 1e-9; |
| fCurrentFrame = std::fmod(t_sec * fAnimation->fps(), frame_count); |
| } else { |
| // Slider-driven progress: update the time origin. |
| fTimeBase = nanos - fCurrentFrame / fAnimation->fps() * 1e9; |
| } |
| |
| // Sanitize and rate-lock the current frame. |
| fCurrentFrame = SkTPin<float>(fCurrentFrame, 0.0f, frame_count - 1); |
| if (fFrameRate > 0) { |
| const auto fps_scale = fFrameRate / fAnimation->fps(); |
| fCurrentFrame = std::trunc(fCurrentFrame * fps_scale) / fps_scale; |
| } |
| |
| fAnimation->seekFrame(fCurrentFrame, fShowAnimationInval ? &fInvalController |
| : nullptr); |
| } |
| return true; |
| } |
| |
| bool SkottieSlide::onChar(SkUnichar c) { |
| if (fTextEditor && fTextEditor->onCharInput(c)) { |
| return true; |
| } |
| |
| switch (c) { |
| case 'I': |
| fShowAnimationStats = !fShowAnimationStats; |
| return true; |
| case 'G': |
| fPreferGlyphPaths = !fPreferGlyphPaths; |
| this->load(fWinSize.width(), fWinSize.height()); |
| return true; |
| case 'T': |
| fShowTrackerUI = !fShowTrackerUI; |
| return true; |
| case 'M': |
| fShowSlotManager = !fShowSlotManager; |
| return true; |
| case 'E': |
| if (fTextEditor) { |
| fTextEditor->toggleEnabled(); |
| } |
| return true; |
| } |
| |
| return Slide::onChar(c); |
| } |
| |
| bool SkottieSlide::onMouse(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey mod) { |
| if (fTextEditor && fTextEditor->onMouseInput(x, y, state, mod)) { |
| return true; |
| } |
| |
| switch (state) { |
| case skui::InputState::kUp: |
| fShowAnimationInval = !fShowAnimationInval; |
| fShowAnimationStats = !fShowAnimationStats; |
| break; |
| default: |
| break; |
| } |
| |
| fShowUI = this->UIArea().contains(x, y); |
| |
| return false; |
| } |
| |
| SkRect SkottieSlide::UIArea() const { |
| static constexpr float kUIHeight = 120.0f; |
| |
| return SkRect::MakeXYWH(0, fWinSize.height() - kUIHeight, fWinSize.width(), kUIHeight); |
| } |
| |
| void SkottieSlide::renderUI() { |
| static constexpr auto kUI_opacity = 0.35f, |
| kUI_hist_height = 50.0f, |
| kUI_fps_width = 100.0f; |
| |
| auto add_frame_rate_option = [this](const char* label, double rate) { |
| const auto is_selected = (fFrameRate == rate); |
| if (ImGui::Selectable(label, is_selected)) { |
| fFrameRate = rate; |
| fFrameRateLabel = label; |
| } |
| if (is_selected) { |
| ImGui::SetItemDefaultFocus(); |
| } |
| }; |
| |
| ImGui::SetNextWindowBgAlpha(kUI_opacity); |
| if (ImGui::Begin("Skottie Controls", nullptr, ImGuiWindowFlags_NoDecoration | |
| ImGuiWindowFlags_NoResize | |
| ImGuiWindowFlags_NoMove | |
| ImGuiWindowFlags_NoSavedSettings | |
| ImGuiWindowFlags_NoFocusOnAppearing | |
| ImGuiWindowFlags_NoNav)) { |
| const auto ui_area = this->UIArea(); |
| ImGui::SetWindowPos(ImVec2(ui_area.x(), ui_area.y())); |
| ImGui::SetWindowSize(ImVec2(ui_area.width(), ui_area.height())); |
| |
| ImGui::PushItemWidth(-1); |
| ImGui::PlotHistogram("", fFrameTimes.data(), fFrameTimes.size(), |
| 0, nullptr, FLT_MAX, FLT_MAX, ImVec2(0, kUI_hist_height)); |
| ImGui::SliderFloat("", &fCurrentFrame, 0, fAnimation->duration() * fAnimation->fps() - 1); |
| fDraggingProgress = ImGui::IsItemActive(); |
| ImGui::PopItemWidth(); |
| |
| ImGui::PushItemWidth(kUI_fps_width); |
| if (ImGui::BeginCombo("FPS", fFrameRateLabel)) { |
| add_frame_rate_option("", 0.0); |
| add_frame_rate_option("Native", fAnimation->fps()); |
| add_frame_rate_option( "1", 1.0); |
| add_frame_rate_option("15", 15.0); |
| add_frame_rate_option("24", 24.0); |
| add_frame_rate_option("30", 30.0); |
| add_frame_rate_option("60", 60.0); |
| ImGui::EndCombo(); |
| } |
| ImGui::PopItemWidth(); |
| } |
| ImGui::End(); |
| } |
| |
| #endif // SK_ENABLE_SKOTTIE |