From cd7cfe36cefc8e2b735dc4d6c99f3332744b9343 Mon Sep 17 00:00:00 2001
From: Leon Styhre <leon@leonstyhre.com>
Date: Sun, 9 Jan 2022 18:17:23 +0100
Subject: [PATCH] Added play direction support to LottieComponent.

Also fixed some bugs and cleaned up the code a bit.
---
 es-core/src/ThemeData.cpp                  |   1 -
 es-core/src/components/LottieComponent.cpp | 113 +++++++++++++++++----
 es-core/src/components/LottieComponent.h   |  21 ++--
 3 files changed, 104 insertions(+), 31 deletions(-)

diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp
index b11fc854e..e9e070a2d 100644
--- a/es-core/src/ThemeData.cpp
+++ b/es-core/src/ThemeData.cpp
@@ -157,7 +157,6 @@ std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>> The
       {"speed", FLOAT},
       {"direction", STRING},
       {"keepAspectRatio", BOOLEAN},
-      {"delay", FLOAT},
       {"visible", BOOLEAN},
       {"zIndex", FLOAT}}},
     {"badges",
diff --git a/es-core/src/components/LottieComponent.cpp b/es-core/src/components/LottieComponent.cpp
index a575833ef..0bbe385af 100644
--- a/es-core/src/components/LottieComponent.cpp
+++ b/es-core/src/components/LottieComponent.cpp
@@ -26,14 +26,17 @@ LottieComponent::LottieComponent(Window* window)
     , mFrameSize{0}
     , mAnimation{nullptr}
     , mSurface{nullptr}
+    , mStartDirection{"normal"}
     , mTotalFrames{0}
     , mFrameNum{0}
     , mFrameRate{0.0}
+    , mSpeedModifier{1.0f}
     , mTargetPacing{0}
     , mTimeAccumulator{0}
+    , mSkippedFrames{0}
     , mHoldFrame{false}
-    , mDroppedFrames{0}
-    , mSpeedModifier{1.0f}
+    , mPause{false}
+    , mAlternate{false}
     , mKeepAspectRatio{true}
 {
     // Get an empty texture for rendering the animation.
@@ -65,8 +68,10 @@ LottieComponent::LottieComponent(Window* window)
 
 LottieComponent::~LottieComponent()
 {
+    // This is required as rlottie could otherwise crash on application shutdown.
     if (mFuture.valid())
         mFuture.get();
+
     mTotalFrameCache -= mCacheSize;
 }
 
@@ -147,10 +152,6 @@ void LottieComponent::setAnimation(const std::string& path)
         return;
     }
 
-    // Render the first frame as a type of preload to decrease the chance of seeing a blank
-    // texture when first entering a view that uses this animation.
-    mFuture = mAnimation->render(mFrameNum, *mSurface, mKeepAspectRatio);
-
     // Some statistics for the file.
     double duration = mAnimation->duration();
     mTotalFrames = mAnimation->totalFrame();
@@ -158,6 +159,11 @@ void LottieComponent::setAnimation(const std::string& path)
     mFrameSize = width * height * 4;
     mTargetPacing = static_cast<int>((1000.0 / mFrameRate) / static_cast<double>(mSpeedModifier));
 
+    mDirection = mStartDirection;
+
+    if (mDirection == "reverse")
+        mFrameNum = mTotalFrames - 1;
+
     if (DEBUG_ANIMATION) {
         LOG(LogDebug) << "LottieComponent::setAnimation(): Rasterized width: " << mSize.x;
         LOG(LogDebug) << "LottieComponent::setAnimation(): Rasterized height: " << mSize.y;
@@ -181,6 +187,15 @@ void LottieComponent::setAnimation(const std::string& path)
     }
 }
 
+void LottieComponent::resetFileAnimation()
+{
+    mTimeAccumulator = 0;
+    mFrameNum = mStartDirection == "reverse" ? mTotalFrames - 1 : 0;
+
+    if (mAnimation != nullptr)
+        mFuture = mAnimation->render(mFrameNum, *mSurface, mKeepAspectRatio);
+}
+
 void LottieComponent::onSizeChanged()
 {
     // Setting the animation again will completely reinitialize it.
@@ -221,6 +236,32 @@ void LottieComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
         mKeepAspectRatio = elem->get<bool>("keepAspectRatio");
     }
 
+    if (elem->has("direction")) {
+        std::string direction = elem->get<std::string>("direction");
+        if (direction == "normal") {
+            mStartDirection = "normal";
+            mAlternate = false;
+        }
+        else if (direction == "reverse") {
+            mStartDirection = "reverse";
+            mAlternate = false;
+        }
+        else if (direction == "alternate") {
+            mStartDirection = "normal";
+            mAlternate = true;
+        }
+        else if (direction == "alternateReverse") {
+            mStartDirection = "reverse";
+            mAlternate = true;
+        }
+        else {
+            LOG(LogWarning) << "LottieComponent: Invalid theme configuration, <direction> set to \""
+                            << direction << "\"";
+            mStartDirection = "normal";
+            mAlternate = false;
+        }
+    }
+
     GuiComponent::applyTheme(theme, view, element, properties);
 
     if (elem->has("path")) {
@@ -255,7 +296,7 @@ void LottieComponent::update(int deltaTime)
     if (mTimeAccumulator > deltaTime * 200)
         mTimeAccumulator = 0;
 
-    // Keep animation speed from going too quickly.
+    // Prevent animation from playing too quickly.
     if (mTimeAccumulator + deltaTime < mTargetPacing) {
         mHoldFrame = true;
         mTimeAccumulator += deltaTime;
@@ -272,8 +313,13 @@ void LottieComponent::update(int deltaTime)
                 << "LottieComponent::update(): Skipped frame, mTimeAccumulator / mTargetPacing: "
                 << mTimeAccumulator - deltaTime << " / " << mTargetPacing;
         }
-        ++mFrameNum;
-        ++mDroppedFrames;
+
+        if (mDirection == "reverse")
+            --mFrameNum;
+        else
+            ++mFrameNum;
+
+        ++mSkippedFrames;
         mTimeAccumulator -= mTargetPacing;
     }
 }
@@ -294,24 +340,48 @@ void LottieComponent::render(const glm::mat4& parentTrans)
                                  static_cast<size_t>(mSize.y));
     }
 
-    // Don't render any new frames if paused or if a menu is open.
-    if (!mPause && mWindow->getGuiStackSize() < 2) {
-        if (mFrameNum >= mTotalFrames) {
+    bool doRender = true;
+
+    // Don't render if a menu is open except if the cached background is getting invalidated.
+    if (mWindow->getGuiStackSize() > 1) {
+        if (mWindow->isInvalidatingCachedBackground())
+            doRender = true;
+        else
+            doRender = false;
+    }
+
+    // Don't render any new frames if paused or if a menu is open (unless invalidating background).
+    if (!mPause && doRender) {
+        if ((mDirection == "normal" && mFrameNum >= mTotalFrames) ||
+            (mDirection == "reverse" && mFrameNum > mTotalFrames)) {
             if (DEBUG_ANIMATION) {
-                LOG(LogError) << "Dropped frames: " << mDroppedFrames;
+                LOG(LogDebug) << "LottieComponent::render(): Skipped frames: " << mSkippedFrames;
                 LOG(LogDebug) << "LottieComponent::render(): Actual duration: "
                               << std::chrono::duration_cast<std::chrono::milliseconds>(
                                      std::chrono::system_clock::now() - mAnimationStartTime)
                                      .count()
                               << " ms";
             }
+
+            if (mAlternate) {
+                if (mDirection == "normal")
+                    mDirection = "reverse";
+                else
+                    mDirection = "normal";
+            }
+
             mTimeAccumulator = 0;
-            mFrameNum = 0;
-            mDroppedFrames = 0;
+            mSkippedFrames = 0;
+
+            if (mDirection == "reverse")
+                mFrameNum = mTotalFrames - 1;
+            else
+                mFrameNum = 0;
         }
 
         if (DEBUG_ANIMATION) {
-            if (mFrameNum == 0)
+            if ((mDirection == "normal" && mFrameNum == 0) ||
+                (mDirection == "reverse" && mFrameNum == mTotalFrames - 1))
                 mAnimationStartTime = std::chrono::system_clock::now();
         }
 
@@ -336,7 +406,10 @@ void LottieComponent::render(const glm::mat4& parentTrans)
                 mTexture->initFromPixels(&mPictureRGBA.at(0), static_cast<size_t>(mSize.x),
                                          static_cast<size_t>(mSize.y));
 
-                ++mFrameNum;
+                if (mDirection == "reverse")
+                    --mFrameNum;
+                else
+                    ++mFrameNum;
 
                 if (mFrameNum == mTotalFrames)
                     renderNextFrame = false;
@@ -350,7 +423,11 @@ void LottieComponent::render(const glm::mat4& parentTrans)
                     mTexture->initFromPixels(&mFrameCache[mFrameNum][0],
                                              static_cast<size_t>(mSize.x),
                                              static_cast<size_t>(mSize.y));
-                    ++mFrameNum;
+
+                    if (mDirection == "reverse")
+                        --mFrameNum;
+                    else
+                        ++mFrameNum;
                 }
             }
             else {
diff --git a/es-core/src/components/LottieComponent.h b/es-core/src/components/LottieComponent.h
index 50a72a208..63092d0a1 100644
--- a/es-core/src/components/LottieComponent.h
+++ b/es-core/src/components/LottieComponent.h
@@ -34,12 +34,7 @@ public:
         mMaxCacheSize = static_cast<size_t>(glm::clamp(value, 0, 1024) * 1024 * 1024);
     }
 
-    void resetFileAnimation() override
-    {
-        mTimeAccumulator = 0;
-        mFrameNum = 0;
-    }
-
+    void resetFileAnimation() override;
     void onSizeChanged() override;
 
     virtual void applyTheme(const std::shared_ptr<ThemeData>& theme,
@@ -62,24 +57,26 @@ private:
     size_t mCacheSize;
     size_t mFrameSize;
 
+    std::chrono::time_point<std::chrono::system_clock> mAnimationStartTime;
     std::unique_ptr<rlottie::Animation> mAnimation;
     std::unique_ptr<rlottie::Surface> mSurface;
     std::future<rlottie::Surface> mFuture;
     std::string mPath;
+    std::string mStartDirection;
+    std::string mDirection;
     size_t mTotalFrames;
     size_t mFrameNum;
+
     double mFrameRate;
+    float mSpeedModifier;
     int mTargetPacing;
     int mTimeAccumulator;
+    int mSkippedFrames;
+
     bool mHoldFrame;
-    int mDroppedFrames;
-
     bool mPause;
-
-    float mSpeedModifier;
+    bool mAlternate;
     bool mKeepAspectRatio;
-
-    std::chrono::time_point<std::chrono::system_clock> mAnimationStartTime;
 };
 
 #endif // ES_CORE_COMPONENTS_LOTTIE_COMPONENT_H