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