From 8af7e4b9ded8b07a2293032aa494efb7c0eab3c5 Mon Sep 17 00:00:00 2001 From: Maximilian Seidler <78690852+PaideiaDilemma@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:26:01 +0000 Subject: [PATCH] animation: add BezierCurve, AnimationManager and AnimatedVariable (#27) --- CMakeLists.txt | 8 + .../hyprutils/animation/AnimatedVariable.hpp | 230 ++++++++++++++++++ .../hyprutils/animation/AnimationManager.hpp | 40 +++ include/hyprutils/animation/BezierCurve.hpp | 30 +++ src/animation/AnimatedVariable.cpp | 158 ++++++++++++ src/animation/AnimationManager.cpp | 73 ++++++ src/animation/BezierCurve.cpp | 78 ++++++ tests/animation.cpp | 224 +++++++++++++++++ 8 files changed, 841 insertions(+) create mode 100644 include/hyprutils/animation/AnimatedVariable.hpp create mode 100644 include/hyprutils/animation/AnimationManager.hpp create mode 100644 include/hyprutils/animation/BezierCurve.hpp create mode 100644 src/animation/AnimatedVariable.cpp create mode 100644 src/animation/AnimationManager.cpp create mode 100644 src/animation/BezierCurve.cpp create mode 100644 tests/animation.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a337bcb..3727e32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,14 @@ add_test( COMMAND hyprutils_filedescriptor "filedescriptor") add_dependencies(tests hyprutils_filedescriptor) +add_executable(hyprutils_animation "tests/animation.cpp") +target_link_libraries(hyprutils_animation PRIVATE hyprutils PkgConfig::deps) +add_test( + NAME "Animation" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND hyprutils_animation "utils") +add_dependencies(tests hyprutils_animation) + # Installation install(TARGETS hyprutils) install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/include/hyprutils/animation/AnimatedVariable.hpp b/include/hyprutils/animation/AnimatedVariable.hpp new file mode 100644 index 0000000..6b44b1a --- /dev/null +++ b/include/hyprutils/animation/AnimatedVariable.hpp @@ -0,0 +1,230 @@ +#pragma once + +#include "../memory/WeakPtr.hpp" +#include "hyprutils/memory/SharedPtr.hpp" + +#include +#include + +namespace Hyprutils { + namespace Animation { + class CAnimationManager; + + /* + Structure for animation properties. + Config properties need to have a static lifetime to allow for config reload. + */ + struct SAnimationPropertyConfig { + bool overridden = true; + + std::string internalBezier = ""; + std::string internalStyle = ""; + float internalSpeed = 0.f; + int internalEnabled = -1; + + Memory::CWeakPointer pValues; + Memory::CWeakPointer pParentAnimation; + }; + + /* A base class for animated variables. */ + class CBaseAnimatedVariable { + public: + using CallbackFun = std::function thisptr)>; + + CBaseAnimatedVariable() { + ; // m_bDummy = true; + }; + + void create(CAnimationManager*, int, Memory::CSharedPointer); + void connectToActive(); + void disconnectFromActive(); + + /* Needs to call disconnectFromActive to remove `m_pSelf` from the active animation list */ + virtual ~CBaseAnimatedVariable() { + disconnectFromActive(); + }; + + virtual void warp(bool endCallback = true) = 0; + + CBaseAnimatedVariable(const CBaseAnimatedVariable&) = delete; + CBaseAnimatedVariable(CBaseAnimatedVariable&&) = delete; + CBaseAnimatedVariable& operator=(const CBaseAnimatedVariable&) = delete; + CBaseAnimatedVariable& operator=(CBaseAnimatedVariable&&) = delete; + + void setConfig(Memory::CSharedPointer pConfig) { + m_pConfig = pConfig; + } + + Memory::CWeakPointer getConfig() const { + return m_pConfig; + } + + bool enabled() const; + const std::string& getBezierName() const; + const std::string& getStyle() const; + + /* returns the spent (completion) % */ + float getPercent() const; + + /* returns the current curve value */ + float getCurveValue() const; + + /* checks if an animation is in progress */ + bool isBeingAnimated() const { + return m_bIsBeingAnimated; + } + + /* checks m_bDummy and m_pAnimationManager */ + bool ok() const; + + /* calls the update callback */ + void onUpdate(); + + /* sets a function to be ran when an animation ended. + if "remove" is set to true, it will remove the callback when ran. */ + void setCallbackOnEnd(CallbackFun func, bool remove = true); + + /* sets a function to be ran when an animation is started. + if "remove" is set to true, it will remove the callback when ran. */ + void setCallbackOnBegin(CallbackFun func, bool remove = true); + + /* sets the update callback, called every time the value is animated and a step is done + Warning: calling unregisterVar/registerVar in this handler will cause UB */ + void setUpdateCallback(CallbackFun func); + + /* resets all callbacks. Does not call any. */ + void resetAllCallbacks(); + + void onAnimationEnd(); + void onAnimationBegin(); + + int m_Type = -1; + + protected: + friend class CAnimationManager; + + bool m_bIsConnectedToActive = false; + bool m_bIsBeingAnimated = false; + + Memory::CWeakPointer m_pSelf; + + private: + Memory::CWeakPointer m_pConfig; + + std::chrono::steady_clock::time_point animationBegin; + + bool m_bDummy = true; + + CAnimationManager* m_pAnimationManager = nullptr; + bool m_bRemoveEndAfterRan = true; + bool m_bRemoveBeginAfterRan = true; + + CallbackFun m_fEndCallback; + CallbackFun m_fBeginCallback; + CallbackFun m_fUpdateCallback; + }; + + /* This concept represents the minimum requirement for a type to be used with CGenericAnimatedVariable */ + template + concept AnimatedType = requires(ValueImpl val) { + requires std::is_copy_constructible_v; + { val == val } -> std::same_as; // requires operator== + { val = val }; // requires operator= + }; + + /* + A generic class for variables. + VarType is the type of the variable to be animated. + AnimationContext is there to attach additional data to the animation. + In Hyprland that struct would contain a reference to window, workspace or layer for example. + */ + template + class CGenericAnimatedVariable : public CBaseAnimatedVariable { + public: + CGenericAnimatedVariable() = default; + + void create(const int typeInfo, CAnimationManager* pAnimationManager, Memory::CSharedPointer> pSelf, + const VarType& initialValue) { + m_Begun = initialValue; + m_Value = initialValue; + m_Goal = initialValue; + + CBaseAnimatedVariable::create(pAnimationManager, typeInfo, pSelf); + } + + CGenericAnimatedVariable(const CGenericAnimatedVariable&) = delete; + CGenericAnimatedVariable(CGenericAnimatedVariable&&) = delete; + CGenericAnimatedVariable& operator=(const CGenericAnimatedVariable&) = delete; + CGenericAnimatedVariable& operator=(CGenericAnimatedVariable&&) = delete; + + virtual void warp(bool endCallback = true) { + if (!m_bIsBeingAnimated) + return; + + m_Value = m_Goal; + + m_bIsBeingAnimated = false; + + onUpdate(); + + if (endCallback) + onAnimationEnd(); + } + + const VarType& value() const { + return m_Value; + } + + /* used to update the value each tick via the AnimationManager */ + VarType& value() { + return m_Value; + } + + const VarType& goal() const { + return m_Goal; + } + + const VarType& begun() const { + return m_Begun; + } + + CGenericAnimatedVariable& operator=(const VarType& v) { + if (v == m_Goal) + return *this; + + m_Goal = v; + m_Begun = m_Value; + + onAnimationBegin(); + + return *this; + } + + /* Sets the actual stored value, without affecting the goal, but resets the timer*/ + void setValue(const VarType& v) { + if (v == m_Value) + return; + + m_Value = v; + m_Begun = m_Value; + + onAnimationBegin(); + } + + /* Sets the actual value and goal*/ + void setValueAndWarp(const VarType& v) { + m_Goal = v; + m_bIsBeingAnimated = true; + + warp(); + } + + AnimationContext m_Context; + + private: + VarType m_Value{}; + VarType m_Goal{}; + VarType m_Begun{}; + }; + } +} diff --git a/include/hyprutils/animation/AnimationManager.hpp b/include/hyprutils/animation/AnimationManager.hpp new file mode 100644 index 0000000..9cb7e65 --- /dev/null +++ b/include/hyprutils/animation/AnimationManager.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "./BezierCurve.hpp" +#include "./AnimatedVariable.hpp" +#include "../math/Vector2D.hpp" +#include "../memory/WeakPtr.hpp" + +#include +#include + +namespace Hyprutils { + namespace Animation { + /* A class for managing bezier curves and variables that are being animated. */ + class CAnimationManager { + public: + CAnimationManager(); + + void tickDone(); + bool shouldTickForNext(); + + virtual void scheduleTick() = 0; + virtual void onTicked() = 0; + + void addBezierWithName(std::string, const Math::Vector2D&, const Math::Vector2D&); + void removeAllBeziers(); + + bool bezierExists(const std::string&); + Memory::CSharedPointer getBezier(const std::string&); + + const std::unordered_map>& getAllBeziers(); + + std::vector> m_vActiveAnimatedVariables; + + private: + std::unordered_map> m_mBezierCurves; + + bool m_bTickScheduled = false; + }; + } +} diff --git a/include/hyprutils/animation/BezierCurve.hpp b/include/hyprutils/animation/BezierCurve.hpp new file mode 100644 index 0000000..b1647df --- /dev/null +++ b/include/hyprutils/animation/BezierCurve.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "../math/Vector2D.hpp" + +namespace Hyprutils { + namespace Animation { + constexpr int BAKEDPOINTS = 255; + constexpr float INVBAKEDPOINTS = 1.f / BAKEDPOINTS; + + /* An implementation of a cubic bezier curve. */ + class CBezierCurve { + public: + /* Calculates a cubic bezier curve based on 2 control points (EXCLUDES the 0,0 and 1,1 points). */ + void setup(const std::array& points); + + float getYForT(float const& t) const; + float getXForT(float const& t) const; + float getYForPoint(float const& x) const; + + private: + /* this INCLUDES the 0,0 and 1,1 points. */ + std::vector m_vPoints; + + std::array m_aPointsBaked; + }; + } +} diff --git a/src/animation/AnimatedVariable.cpp b/src/animation/AnimatedVariable.cpp new file mode 100644 index 0000000..eee80e2 --- /dev/null +++ b/src/animation/AnimatedVariable.cpp @@ -0,0 +1,158 @@ +#include +#include +#include + +using namespace Hyprutils::Animation; +using namespace Hyprutils::Memory; + +#define SP CSharedPointer +#define WP CWeakPointer + +void CBaseAnimatedVariable::create(Hyprutils::Animation::CAnimationManager* pAnimationManager, int typeInfo, SP pSelf) { + m_pAnimationManager = pAnimationManager; + m_Type = typeInfo; + m_pSelf = pSelf; + + m_bDummy = false; +} + +void CBaseAnimatedVariable::connectToActive() { + if (!m_pAnimationManager || m_bDummy) + return; + + m_pAnimationManager->scheduleTick(); // otherwise the animation manager will never pick this up + if (!m_bIsConnectedToActive) + m_pAnimationManager->m_vActiveAnimatedVariables.push_back(m_pSelf); + m_bIsConnectedToActive = true; +} + +void CBaseAnimatedVariable::disconnectFromActive() { + if (!m_pAnimationManager) + return; + + std::erase_if(m_pAnimationManager->m_vActiveAnimatedVariables, [&](const auto& other) { return other == m_pSelf; }); + m_bIsConnectedToActive = false; +} + +bool Hyprutils::Animation::CBaseAnimatedVariable::enabled() const { + if (const auto PCONFIG = m_pConfig.lock()) { + const auto PVALUES = PCONFIG->pValues.lock(); + return PVALUES ? PVALUES->internalEnabled : false; + } + + return false; +} + +const std::string& CBaseAnimatedVariable::getBezierName() const { + static constexpr const std::string DEFAULTBEZIERNAME = "default"; + + if (const auto PCONFIG = m_pConfig.lock()) { + const auto PVALUES = PCONFIG->pValues.lock(); + return PVALUES ? PVALUES->internalBezier : DEFAULTBEZIERNAME; + } + + return DEFAULTBEZIERNAME; +} + +const std::string& CBaseAnimatedVariable::getStyle() const { + static constexpr const std::string DEFAULTSTYLE = ""; + + if (const auto PCONFIG = m_pConfig.lock()) { + const auto PVALUES = PCONFIG->pValues.lock(); + return PVALUES ? PVALUES->internalStyle : DEFAULTSTYLE; + } + + return DEFAULTSTYLE; +} + +float CBaseAnimatedVariable::getPercent() const { + const auto DURATIONPASSED = std::chrono::duration_cast(std::chrono::steady_clock::now() - animationBegin).count(); + + if (const auto PCONFIG = m_pConfig.lock()) { + const auto PVALUES = PCONFIG->pValues.lock(); + return PVALUES ? std::clamp((DURATIONPASSED / 100.f) / PVALUES->internalSpeed, 0.f, 1.f) : 1.f; + } + + return 1.f; +} + +float CBaseAnimatedVariable::getCurveValue() const { + if (!m_bIsBeingAnimated || !m_pAnimationManager) + return 1.f; + + std::string bezierName = ""; + if (const auto PCONFIG = m_pConfig.lock()) { + const auto PVALUES = PCONFIG->pValues.lock(); + if (PVALUES) + bezierName = PVALUES->internalBezier; + } + + const auto BEZIER = m_pAnimationManager->getBezier(bezierName); + if (!BEZIER) + return 1.f; + + const auto SPENT = getPercent(); + if (SPENT >= 1.f) + return 1.f; + + return BEZIER->getYForPoint(SPENT); +} + +bool CBaseAnimatedVariable::ok() const { + return m_pConfig && m_pAnimationManager; +} + +void CBaseAnimatedVariable::onUpdate() { + if (m_fUpdateCallback) + m_fUpdateCallback(m_pSelf); +} + +void CBaseAnimatedVariable::setCallbackOnEnd(CallbackFun func, bool remove) { + m_fEndCallback = std::move(func); + m_bRemoveEndAfterRan = remove; + + if (!isBeingAnimated()) + onAnimationEnd(); +} + +void CBaseAnimatedVariable::setCallbackOnBegin(CallbackFun func, bool remove) { + m_fBeginCallback = std::move(func); + m_bRemoveBeginAfterRan = remove; +} + +void CBaseAnimatedVariable::setUpdateCallback(CallbackFun func) { + m_fUpdateCallback = std::move(func); +} + +void CBaseAnimatedVariable::resetAllCallbacks() { + m_fBeginCallback = nullptr; + m_fEndCallback = nullptr; + m_fUpdateCallback = nullptr; + m_bRemoveBeginAfterRan = false; + m_bRemoveEndAfterRan = false; +} + +void CBaseAnimatedVariable::onAnimationEnd() { + m_bIsBeingAnimated = false; + /* We do not call disconnectFromActive here. The animation manager will remove it on a call to tickDone. */ + + if (m_fEndCallback) { + /* loading m_bRemoveEndAfterRan before calling the callback allows the callback to delete this animation safely if it is false. */ + auto removeEndCallback = m_bRemoveEndAfterRan; + m_fEndCallback(m_pSelf); + if (removeEndCallback) + m_fEndCallback = nullptr; // reset + } +} + +void CBaseAnimatedVariable::onAnimationBegin() { + m_bIsBeingAnimated = true; + animationBegin = std::chrono::steady_clock::now(); + connectToActive(); + + if (m_fBeginCallback) { + m_fBeginCallback(m_pSelf); + if (m_bRemoveBeginAfterRan) + m_fBeginCallback = nullptr; // reset + } +} diff --git a/src/animation/AnimationManager.cpp b/src/animation/AnimationManager.cpp new file mode 100644 index 0000000..b020dda --- /dev/null +++ b/src/animation/AnimationManager.cpp @@ -0,0 +1,73 @@ +#include + +using namespace Hyprutils::Animation; +using namespace Hyprutils::Math; +using namespace Hyprutils::Memory; + +#define SP CSharedPointer + +const std::array DEFAULTBEZIERPOINTS = {Vector2D(0.0, 0.75), Vector2D(0.15, 1.0)}; + +CAnimationManager::CAnimationManager() { + const auto BEZIER = makeShared(); + BEZIER->setup(DEFAULTBEZIERPOINTS); + m_mBezierCurves["default"] = BEZIER; +} + +void CAnimationManager::removeAllBeziers() { + m_mBezierCurves.clear(); + + // add the default one + const auto BEZIER = makeShared(); + BEZIER->setup(DEFAULTBEZIERPOINTS); + m_mBezierCurves["default"] = BEZIER; +} + +void CAnimationManager::addBezierWithName(std::string name, const Vector2D& p1, const Vector2D& p2) { + const auto BEZIER = makeShared(); + BEZIER->setup({ + p1, + p2, + }); + m_mBezierCurves[name] = BEZIER; +} + +bool CAnimationManager::shouldTickForNext() { + return !m_vActiveAnimatedVariables.empty(); +} + +void CAnimationManager::tickDone() { + std::vector> active; + active.reserve(m_vActiveAnimatedVariables.size()); // avoid reallocations + for (auto const& av : m_vActiveAnimatedVariables) { + const auto PAV = av.lock(); + if (!PAV) + continue; + + if (PAV->ok() && PAV->isBeingAnimated()) + active.emplace_back(av); + else + PAV->m_bIsConnectedToActive = false; + } + + m_vActiveAnimatedVariables = std::move(active); +} + +bool CAnimationManager::bezierExists(const std::string& bezier) { + for (auto const& [bc, bz] : m_mBezierCurves) { + if (bc == bezier) + return true; + } + + return false; +} + +SP CAnimationManager::getBezier(const std::string& name) { + const auto BEZIER = std::find_if(m_mBezierCurves.begin(), m_mBezierCurves.end(), [&](const auto& other) { return other.first == name; }); + + return BEZIER == m_mBezierCurves.end() ? m_mBezierCurves["default"] : BEZIER->second; +} + +const std::unordered_map>& CAnimationManager::getAllBeziers() { + return m_mBezierCurves; +} diff --git a/src/animation/BezierCurve.cpp b/src/animation/BezierCurve.cpp new file mode 100644 index 0000000..ac37a01 --- /dev/null +++ b/src/animation/BezierCurve.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +using namespace Hyprutils::Animation; +using namespace Hyprutils::Math; + +void CBezierCurve::setup(const std::array& pVec) { + // Avoid reallocations by reserving enough memory upfront + m_vPoints.resize(pVec.size() + 2); + m_vPoints = { + Vector2D(0, 0), // Start point + pVec[0], pVec[1], // Control points + Vector2D(1, 1) // End point + }; + + if (m_vPoints.size() != 4) + std::abort(); + + // bake BAKEDPOINTS points for faster lookups + // T -> X ( / BAKEDPOINTS ) + for (int i = 0; i < BAKEDPOINTS; ++i) { + float const t = (i + 1) / (float)BAKEDPOINTS; + m_aPointsBaked[i] = Vector2D(getXForT(t), getYForT(t)); + } + + for (int j = 1; j < 10; ++j) { + float i = j / 10.0f; + getYForPoint(i); + } +} + +float CBezierCurve::getXForT(float const& t) const { + float t2 = t * t; + float t3 = t2 * t; + + return 3 * t * (1 - t) * (1 - t) * m_vPoints[1].x + 3 * t2 * (1 - t) * m_vPoints[2].x + t3 * m_vPoints[3].x; +} + +float CBezierCurve::getYForT(float const& t) const { + float t2 = t * t; + float t3 = t2 * t; + + return 3 * t * (1 - t) * (1 - t) * m_vPoints[1].y + 3 * t2 * (1 - t) * m_vPoints[2].y + t3 * m_vPoints[3].y; +} + +// Todo: this probably can be done better and faster +float CBezierCurve::getYForPoint(float const& x) const { + if (x >= 1.f) + return 1.f; + if (x <= 0.f) + return 0.f; + + int index = 0; + bool below = true; + for (int step = (BAKEDPOINTS + 1) / 2; step > 0; step /= 2) { + if (below) + index += step; + else + index -= step; + + below = m_aPointsBaked[index].x < x; + } + + int lowerIndex = index - (!below || index == BAKEDPOINTS - 1); + + // in the name of performance i shall make a hack + const auto LOWERPOINT = &m_aPointsBaked[lowerIndex]; + const auto UPPERPOINT = &m_aPointsBaked[lowerIndex + 1]; + + const auto PERCINDELTA = (x - LOWERPOINT->x) / (UPPERPOINT->x - LOWERPOINT->x); + + if (std::isnan(PERCINDELTA) || std::isinf(PERCINDELTA)) // can sometimes happen for VERY small x + return 0.f; + + return LOWERPOINT->y + (UPPERPOINT->y - LOWERPOINT->y) * PERCINDELTA; +} diff --git a/tests/animation.cpp b/tests/animation.cpp new file mode 100644 index 0000000..04f50d3 --- /dev/null +++ b/tests/animation.cpp @@ -0,0 +1,224 @@ +#include +#include +#include +#include "shared.hpp" + +#define SP CSharedPointer +#define WP CWeakPointer + +using namespace Hyprutils::Animation; +using namespace Hyprutils::Math; +using namespace Hyprutils::Memory; + +class EmtpyContext {}; + +template +using CAnimatedVariable = CGenericAnimatedVariable; + +template +using PANIMVAR = SP>; + +template +using PANIMVARREF = WP>; + +enum eAVTypes { + INT = 1, + TEST, +}; + +struct SomeTestType { + bool done = false; + bool operator==(const SomeTestType& other) const { + return done == other.done; + } + SomeTestType& operator=(const SomeTestType& other) { + done = other.done; + return *this; + } +}; + +std::unordered_map> animationConfig; + +class CMyAnimationManager : public CAnimationManager { + public: + void tick() { + for (auto const& av : m_vActiveAnimatedVariables) { + const auto PAV = av.lock(); + if (!PAV || !PAV->ok()) + continue; + + const auto SPENT = PAV->getPercent(); + const auto PBEZIER = getBezier(PAV->getBezierName()); + const auto POINTY = PBEZIER->getYForPoint(SPENT); + + if (POINTY >= 1.f || !PAV->enabled()) { + PAV->warp(); + continue; + } + + switch (PAV->m_Type) { + case eAVTypes::INT: { + auto avInt = dynamic_cast*>(PAV.get()); + if (!avInt) + std::cout << Colors::RED << "Dynamic cast upcast failed" << Colors::RESET; + + const auto DELTA = avInt->goal() - avInt->value(); + avInt->value() = avInt->begun() + (DELTA * POINTY); + } break; + case eAVTypes::TEST: { + auto avCustom = dynamic_cast*>(PAV.get()); + if (!avCustom) + std::cout << Colors::RED << "Dynamic cast upcast failed" << Colors::RESET; + + if (SPENT >= 1.f) + avCustom->value().done = true; + } break; + default: { + std::cout << Colors::RED << "What are we even doing?" << Colors::RESET; + } break; + } + + av->onUpdate(); + } + + tickDone(); + } + + template + void createAnimation(const VarType& v, PANIMVAR& av, const std::string& animationConfigName) { + constexpr const eAVTypes EAVTYPE = std::is_same_v ? eAVTypes::INT : eAVTypes::TEST; + const auto PAV = makeShared>(); + + PAV->create(EAVTYPE, static_cast(this), PAV, v); + PAV->setConfig(animationConfig[animationConfigName]); + av = std::move(PAV); + } + + virtual void scheduleTick() { + ; + } + + virtual void onTicked() { + ; + } +}; + +CMyAnimationManager gAnimationManager; + +class Subject { + public: + Subject(const int& a, const int& b) { + gAnimationManager.createAnimation(a, m_iA, "default"); + gAnimationManager.createAnimation(b, m_iB, "default"); + gAnimationManager.createAnimation({}, m_iC, "default"); + } + PANIMVAR m_iA; + PANIMVAR m_iB; + PANIMVAR m_iC; +}; + +int main(int argc, char** argv, char** envp) { + animationConfig["default"] = makeShared(); + animationConfig["default"]->internalBezier = "default"; + animationConfig["default"]->internalSpeed = 1.0; + animationConfig["default"]->internalStyle = "asdf"; + animationConfig["default"]->internalEnabled = 1; + animationConfig["default"]->pValues = animationConfig["default"]; + + int ret = 0; + Subject s(0, 0); + + EXPECT(s.m_iA->value(), 0); + EXPECT(s.m_iB->value(), 0); + + // Test destruction of a CAnimatedVariable + { + Subject s2(10, 10); + // Adds them to active + *s2.m_iA = 1; + *s2.m_iB = 2; + // We deliberately do not tick here, to make sure the destructor removes active animated variables + } + + EXPECT(gAnimationManager.shouldTickForNext(), false); + EXPECT(s.m_iC->value().done, false); + + *s.m_iA = 10; + *s.m_iB = 100; + *s.m_iC = SomeTestType(true); + + EXPECT(s.m_iC->value().done, false); + + while (gAnimationManager.shouldTickForNext()) { + gAnimationManager.tick(); + } + + EXPECT(s.m_iA->value(), 10); + EXPECT(s.m_iB->value(), 100); + EXPECT(s.m_iC->value().done, true); + + s.m_iA->setValue(0); + s.m_iB->setValue(0); + + while (gAnimationManager.shouldTickForNext()) { + gAnimationManager.tick(); + } + + EXPECT(s.m_iA->value(), 10); + EXPECT(s.m_iB->value(), 100); + + // Test config stuff + EXPECT(s.m_iA->getBezierName(), "default"); + EXPECT(s.m_iA->getStyle(), "asdf"); + EXPECT(s.m_iA->enabled(), true); + + animationConfig["default"]->internalEnabled = 0; + + EXPECT(s.m_iA->enabled(), false); + + *s.m_iA = 50; + gAnimationManager.tick(); // Expecting a warp + EXPECT(s.m_iA->value(), 50); + + // Test missing pValues + animationConfig["default"]->internalEnabled = 1; + animationConfig["default"]->pValues.reset(); + + EXPECT(s.m_iA->enabled(), false); + EXPECT(s.m_iA->getBezierName(), "default"); + EXPECT(s.m_iA->getStyle(), ""); + EXPECT(s.m_iA->getPercent(), 1.f); + + animationConfig["default"]->pValues = animationConfig["default"]; + + // + // Test callbacks + // + bool beginCallbackRan = false; + bool updateCallbackRan = false; + bool endCallbackRan = false; + s.m_iA->setCallbackOnBegin([&beginCallbackRan](WP pav) { beginCallbackRan = true; }); + s.m_iA->setUpdateCallback([&updateCallbackRan](WP pav) { updateCallbackRan = true; }); + s.m_iA->setCallbackOnEnd([&endCallbackRan](WP pav) { endCallbackRan = true; }, false); + + s.m_iA->setValueAndWarp(42); + + EXPECT(beginCallbackRan, false); + EXPECT(updateCallbackRan, true); + EXPECT(endCallbackRan, true); + + beginCallbackRan = false; + updateCallbackRan = false; + endCallbackRan = false; + + *s.m_iA = 1337; + while (gAnimationManager.shouldTickForNext()) { + gAnimationManager.tick(); + } + + EXPECT(beginCallbackRan, true); + EXPECT(updateCallbackRan, true); + EXPECT(endCallbackRan, true); + + return ret; +}