| /* | |
| * Copyright 2017 Google LLC | |
| * | |
| * 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/core/SkFontMgr.h" | |
| #include "include/private/SkNoncopyable.h" | |
| #include "include/private/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/include/TextShaper.h" | |
| #include "modules/skottie/utils/SkottieUtils.h" | |
| #include "modules/skottie/utils/TextEditor.h" | |
| #include "modules/skresources/include/SkResources.h" | |
| #include "modules/skshaper/utils/FactoryHelpers.h" | |
| #include "src/core/SkOSFile.h" | |
| #include "src/core/SkTime.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; | |
| }; | |
| sk_sp<SkShapers::Factory> make_shapers_factory(bool prefer_coretext) { | |
| #if defined(SK_SHAPER_CORETEXT_AVAILABLE) | |
| if (prefer_coretext) { | |
| return sk_make_sp<SkShapers::CoreTextFactory>(SkShapers::CT::LineBreakMode::kStrict); | |
| } | |
| #endif | |
| #if defined(SK_SHAPER_UNICODE_AVAILABLE) | |
| return sk_make_sp<SkShapers::HarfbuzzFactory>( | |
| skottie::MakeStrictLinebreakUnicode( | |
| SkShapers::BestAvailableUnicode())); | |
| #else | |
| return nullptr; | |
| #endif | |
| } | |
| } // 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::RectToRectOrIdentity(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) | |
| .setTextShapingFactory(make_shapers_factory(fPreferCoretext)); | |
| fAnimation = builder.makeFromFile(fPath.c_str()); | |
| fAnimationStats = builder.getStats(); | |
| fTimeBase = 0; // force a time reset | |
| if (fAnimation) { | |
| if (!fSlotManagerInterface) { | |
| fSlotManagerInterface = | |
| std::make_unique<SlotManagerInterface>(builder.getSlotManager(), resource_provider); | |
| } | |
| auto li = builder.getLayerInfo(); | |
| for (const auto& layer : li) { | |
| SkDebugf( | |
| "Layer: Name: \"%s\" | Size: %.2fx%.2f | In/Out: [%.2f, %.2f]\n", | |
| layer.fName.c_str(), | |
| layer.fSize.width(), | |
| layer.fSize.height(), | |
| layer.fInPoint, | |
| layer.fOutPoint | |
| ); | |
| } | |
| fSlotManagerInterface->initializeSlotManagerUI(); | |
| 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::RectToRectOrIdentity(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 'C': | |
| fPreferCoretext = !fPreferCoretext; | |
| this->load(fWinSize.width(), fWinSize.height()); | |
| return true; | |
| 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 |