diff --git a/include/hyprutils/animation/AnimatedVariable.hpp b/include/hyprutils/animation/AnimatedVariable.hpp index 3909cb5..87e8302 100644 --- a/include/hyprutils/animation/AnimatedVariable.hpp +++ b/include/hyprutils/animation/AnimatedVariable.hpp @@ -8,6 +8,10 @@ #include #include +#include +#include +#include +#include namespace Hyprutils { namespace Animation { @@ -17,6 +21,11 @@ namespace Hyprutils { public: using CallbackFun = std::function thisptr)>; + struct SCurveStepResult { + float value = 1.f; + bool finished = true; + }; + CBaseAnimatedVariable() { ; // m_bDummy = true; }; @@ -57,6 +66,11 @@ namespace Hyprutils { /* returns the current curve value. */ float getCurveValue() const; + /* steps the current curve by one frame. */ + SCurveStepResult getCurveStep(); + + bool isSpringCurve() const; + /* checks if an animation is in progress */ bool isBeingAnimated() const { return m_bIsBeingAnimated; @@ -84,7 +98,7 @@ namespace Hyprutils { void resetAllCallbacks(); void onAnimationEnd(); - void onAnimationBegin(); + void onAnimationBegin(bool preserveCurveState = false, float springVelocityScale = 1.F); /* returns whether the parent CAnimationManager is dead */ bool isAnimationManagerDead() const; @@ -104,12 +118,19 @@ namespace Hyprutils { Memory::CWeakPointer m_pSignals; private: + void resetSpringState(bool preserveVelocity, float velocityScale); + std::string_view springNameFromSpec(const std::string& spec) const; + Memory::CWeakPointer m_pConfig; std::chrono::steady_clock::time_point animationBegin; + std::chrono::steady_clock::time_point springLastStep; bool m_bDummy = true; + float m_fSpringValue = 1.F; + float m_fSpringVelocity = 0.F; + bool m_bRemoveEndAfterRan = true; bool m_bRemoveBeginAfterRan = true; @@ -126,6 +147,12 @@ namespace Hyprutils { { val = val }; // requires operator= }; + template + concept AnimableType = AnimatedType && requires(ValueImpl val, float pointy) { + { val - val }; + { val + ((val - val) * pointy) } -> std::convertible_to; + }; + /* A generic class for variables. VarType is the type of the variable to be animated. @@ -200,10 +227,25 @@ namespace Hyprutils { if (v == m_Goal) return *this; + const bool WASANIMATING = m_bIsBeingAnimated; + float SPRINGVELOCITYSCALE = 1.f; + + if (WASANIMATING && isSpringCurve()) { + if constexpr (std::is_arithmetic_v) { + const float OLDDELTA = static_cast(m_Goal - m_Begun); + const float NEWDELTA = static_cast(v - m_Value); + + if (std::abs(NEWDELTA) > 1e-6f) + SPRINGVELOCITYSCALE = OLDDELTA / NEWDELTA; + else + SPRINGVELOCITYSCALE = 0.f; + } + } + m_Goal = v; m_Begun = m_Value; - onAnimationBegin(); + onAnimationBegin(WASANIMATING && isSpringCurve(), SPRINGVELOCITYSCALE); return *this; } @@ -227,6 +269,26 @@ namespace Hyprutils { warp(); } + template + requires AnimableType + void update(bool warpNow = false) { + if (warpNow || m_Value == m_Goal || !enabled()) { + warp(true, false); + return; + } + + const auto STEP = getCurveStep(); + if (STEP.finished) { + warp(true, false); + return; + } + + const auto DELTA = m_Goal - m_Begun; + m_Value = m_Begun + (DELTA * STEP.value); + + onUpdate(); + } + AnimationContext m_Context; private: diff --git a/include/hyprutils/animation/AnimationManager.hpp b/include/hyprutils/animation/AnimationManager.hpp index 9179968..8124ba8 100644 --- a/include/hyprutils/animation/AnimationManager.hpp +++ b/include/hyprutils/animation/AnimationManager.hpp @@ -13,6 +13,14 @@ namespace Hyprutils { namespace Animation { class CBaseAnimatedVariable; + struct SSpringCurve { + float stiffness = 250.F; + float damping = 25.F; + float mass = 1.F; + float valueEpsilon = 0.001F; + float velocityEpsilon = 0.001F; + }; + /* A class for managing bezier curves and variables that are being animated. */ class CAnimationManager { public: @@ -28,11 +36,16 @@ namespace Hyprutils { void addBezierWithName(std::string, const Math::Vector2D&, const Math::Vector2D&); void removeAllBeziers(); + void addSpringWithName(std::string, const SSpringCurve&); + void removeAllSprings(); bool bezierExists(const std::string&); + bool springExists(const std::string&); Memory::CSharedPointer getBezier(const std::string&); + Memory::CSharedPointer getSpring(const std::string&); const std::unordered_map>& getAllBeziers(); + const std::unordered_map>& getAllSprings(); struct SAnimationManagerSignals { Signal::CSignalT> connect; @@ -45,6 +58,7 @@ namespace Hyprutils { private: std::unordered_map> m_mBezierCurves; + std::unordered_map> m_mSpringCurves; bool m_bTickScheduled = false; diff --git a/src/animation/AnimatedVariable.cpp b/src/animation/AnimatedVariable.cpp index 81da415..8a1ff05 100644 --- a/src/animation/AnimatedVariable.cpp +++ b/src/animation/AnimatedVariable.cpp @@ -2,11 +2,15 @@ #include #include +#include +#include + using namespace Hyprutils::Animation; using namespace Hyprutils::Memory; -static const std::string DEFAULTBEZIERNAME = "default"; -static const std::string DEFAULTSTYLE = ""; +static const std::string DEFAULTBEZIERNAME = "default"; +static const std::string DEFAULTSTYLE = ""; +static constexpr std::string_view SPRINGPREFIX = "spring:"; #define SP CSharedPointer #define WP CWeakPointer @@ -70,26 +74,91 @@ float CBaseAnimatedVariable::getPercent() const { const auto DURATIONPASSED = std::chrono::duration_cast(std::chrono::steady_clock::now() - animationBegin).count(); if (m_pConfig && m_pConfig->pValues) - return std::clamp((DURATIONPASSED / 100.f) / m_pConfig->pValues->internalSpeed, 0.f, 1.f); + return std::clamp((DURATIONPASSED / 100.F) / m_pConfig->pValues->internalSpeed, 0.f, 1.f); - return 1.f; + return 1.F; } float CBaseAnimatedVariable::getCurveValue() const { if (!m_bIsBeingAnimated || isAnimationManagerDead()) - return 1.f; + return 1.F; + + if (isSpringCurve()) + return m_fSpringValue; const auto BEZIER = m_pAnimationManager->getBezier(getBezierName()); if (!BEZIER) - return 1.f; + return 1.F; const auto SPENT = getPercent(); - if (SPENT >= 1.f) - return 1.f; + if (SPENT >= 1.F) + return 1.F; return BEZIER->getYForPoint(SPENT); } +CBaseAnimatedVariable::SCurveStepResult CBaseAnimatedVariable::getCurveStep() { + if (!m_bIsBeingAnimated || isAnimationManagerDead()) + return {}; + + if (!isSpringCurve()) { + const auto SPENT = getPercent(); + if (SPENT >= 1.f) + return {.value = 1.F, .finished = true}; + + const auto BEZIER = m_pAnimationManager->getBezier(getBezierName()); + if (!BEZIER) + return {.value = 1.F, .finished = true}; + + return { + .value = BEZIER->getYForPoint(SPENT), + .finished = false, + }; + } + + const auto SPRINGNAME = springNameFromSpec(getBezierName()); + const auto SPRING = m_pAnimationManager->getSpring(std::string{SPRINGNAME}); + if (!SPRING) + return {.value = 1.F, .finished = true}; + + const auto NOW = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(NOW - springLastStep).count(); + springLastStep = NOW; + + constexpr float MINDELTA = 1.F / 240.F; + if (dt <= 0.F) + dt = MINDELTA; + else + dt = std::clamp(dt, MINDELTA, 0.05F); + + if (dt > 0.F) { + constexpr const float FIXEDSTEP = 1.F / 240.F; + const int SUBSTEPS = std::clamp(static_cast(std::ceil(dt / FIXEDSTEP)), 1, 16); + const float STEPTIME = dt / SUBSTEPS; + const float MASS = std::max(SPRING->mass, 0.0001f); + + for (int i = 0; i < SUBSTEPS; ++i) { + const float displacement = m_fSpringValue - 1.f; + const float acceleration = ((-SPRING->stiffness * displacement) - (SPRING->damping * m_fSpringVelocity)) / MASS; + + m_fSpringVelocity += acceleration * STEPTIME; + m_fSpringValue += m_fSpringVelocity * STEPTIME; + } + } + + const bool FINISHED = std::abs(1.F - m_fSpringValue) <= SPRING->valueEpsilon && std::abs(m_fSpringVelocity) <= SPRING->velocityEpsilon; + if (FINISHED) { + m_fSpringValue = 1.F; + m_fSpringVelocity = 0.F; + } + + return {.value = m_fSpringValue, .finished = FINISHED}; +} + +bool CBaseAnimatedVariable::isSpringCurve() const { + return !springNameFromSpec(getBezierName()).empty(); +} + bool CBaseAnimatedVariable::ok() const { return m_pConfig && !m_bDummy && !isAnimationManagerDead(); } @@ -138,7 +207,10 @@ void CBaseAnimatedVariable::onAnimationEnd() { } } -void CBaseAnimatedVariable::onAnimationBegin() { +void CBaseAnimatedVariable::onAnimationBegin(bool preserveCurveState, float springVelocityScale) { + if (isSpringCurve()) + resetSpringState(preserveCurveState, springVelocityScale); + m_bIsBeingAnimated = true; animationBegin = std::chrono::steady_clock::now(); connectToActive(); @@ -153,3 +225,20 @@ void CBaseAnimatedVariable::onAnimationBegin() { bool CBaseAnimatedVariable::isAnimationManagerDead() const { return m_pSignals.expired(); } + +void CBaseAnimatedVariable::resetSpringState(bool preserveVelocity, float velocityScale) { + m_fSpringValue = 0.f; + if (!preserveVelocity) + m_fSpringVelocity = 0.f; + else + m_fSpringVelocity *= velocityScale; + + springLastStep = std::chrono::steady_clock::now(); +} + +std::string_view CBaseAnimatedVariable::springNameFromSpec(const std::string& spec) const { + if (!spec.starts_with(SPRINGPREFIX) || spec.size() <= SPRINGPREFIX.size()) + return {}; + + return std::string_view(spec).substr(SPRINGPREFIX.size()); +} diff --git a/src/animation/AnimationManager.cpp b/src/animation/AnimationManager.cpp index 2910549..b907fd3 100644 --- a/src/animation/AnimationManager.cpp +++ b/src/animation/AnimationManager.cpp @@ -11,11 +11,14 @@ using namespace Hyprutils::Signal; #define WP CWeakPointer const std::array DEFAULTBEZIERPOINTS = {Vector2D(0.0, 0.75), Vector2D(0.15, 1.0)}; +const SSpringCurve DEFAULTSPRING = {}; CAnimationManager::CAnimationManager() { const auto BEZIER = makeShared(); BEZIER->setup(DEFAULTBEZIERPOINTS); + m_mBezierCurves["default"] = BEZIER; + m_mSpringCurves["default"] = makeShared(DEFAULTSPRING); m_events = makeUnique(); m_listeners = makeUnique(); @@ -43,15 +46,21 @@ void CAnimationManager::removeAllBeziers() { m_mBezierCurves["default"] = BEZIER; } +void CAnimationManager::removeAllSprings() { + m_mSpringCurves.clear(); + m_mSpringCurves["default"] = makeShared(DEFAULTSPRING); +} + void CAnimationManager::addBezierWithName(std::string name, const Vector2D& p1, const Vector2D& p2) { const auto BEZIER = makeShared(); - BEZIER->setup({ - p1, - p2, - }); + BEZIER->setup({p1, p2}); m_mBezierCurves[name] = BEZIER; } +void CAnimationManager::addSpringWithName(std::string name, const SSpringCurve& spring) { + m_mSpringCurves[name] = makeShared(spring); +} + bool CAnimationManager::shouldTickForNext() { return !m_vActiveAnimatedVariables.empty(); } @@ -85,16 +94,35 @@ bool CAnimationManager::bezierExists(const std::string& bezier) { return false; } +bool CAnimationManager::springExists(const std::string& spring) { + for (auto const& [sc, cfg] : m_mSpringCurves) { + if (sc == spring) + return true; + } + + return false; +} + SP CAnimationManager::getBezier(const std::string& name) { const auto BEZIER = std::ranges::find_if(m_mBezierCurves, [&](const auto& other) { return other.first == name; }); return BEZIER == m_mBezierCurves.end() ? m_mBezierCurves["default"] : BEZIER->second; } +SP CAnimationManager::getSpring(const std::string& name) { + const auto SPRING = std::ranges::find_if(m_mSpringCurves, [&](const auto& other) { return other.first == name; }); + + return SPRING == m_mSpringCurves.end() ? m_mSpringCurves["default"] : SPRING->second; +} + const std::unordered_map>& CAnimationManager::getAllBeziers() { return m_mBezierCurves; } +const std::unordered_map>& CAnimationManager::getAllSprings() { + return m_mSpringCurves; +} + CWeakPointer CAnimationManager::getSignals() const { return m_events; } diff --git a/tests/animation/Animation.cpp b/tests/animation/Animation.cpp index 7a2b639..6c471b4 100644 --- a/tests/animation/Animation.cpp +++ b/tests/animation/Animation.cpp @@ -51,39 +51,36 @@ class CMyAnimationManager : public CAnimationManager { if (!PAV || !PAV->ok() || !PAV->isBeingAnimated()) continue; - const auto SPENT = PAV->getPercent(); - const auto PBEZIER = getBezier(PAV->getBezierName()); - - if (SPENT >= 1.f || !PAV->enabled()) { - PAV->warp(true, false); - continue; - } - - const auto POINTY = PBEZIER->getYForPoint(SPENT); - switch (PAV->m_Type) { case eAVTypes::INT: { auto avInt = dc*>(PAV.get()); if (!avInt) std::cout << "Dynamic cast upcast failed\n"; - const auto DELTA = avInt->goal() - avInt->begun(); - avInt->value() = avInt->begun() + (DELTA * POINTY); + avInt->update(); } break; case eAVTypes::TEST: { auto avCustom = dc*>(PAV.get()); if (!avCustom) std::cout << "Dynamic cast upcast failed\n"; - if (SPENT >= 1.f) - avCustom->value().done = true; + if (!PAV->enabled()) { + PAV->warp(true, false); + continue; + } + + const auto STEP = PAV->getCurveStep(); + if (STEP.finished) { + PAV->warp(true, false); + continue; + } + + PAV->onUpdate(); } break; default: { std::cout << "What are we even doing?\n"; } break; } - - PAV->onUpdate(); } tickDone(); @@ -392,4 +389,51 @@ TEST(Animation, animation) { } // a gets destroyed EXPECT_EQ(pAnimationManager.get(), nullptr); -} \ No newline at end of file +} + +TEST(Animation, springRetargetPreservesVelocity) { + config(); + + pAnimationManager->addSpringWithName("momentum", + { + .stiffness = 120.f, + .damping = 8.f, + .mass = 1.f, + .valueEpsilon = 0.001f, + .velocityEpsilon = 0.001f, + }); + + animationTree.createNode("spring_global"); + animationTree.createNode("spring_velocity", "spring_global"); + animationTree.setConfigForNode("spring_global", 1, 1.f, "spring:momentum"); + + PANIMVAR av; + pAnimationManager->createAnimation(0, av, "spring_velocity"); + + *av = 100; + + int ticks = 0; + while (av->value() < 40 && ticks++ < 300) { + pAnimationManager->tick(); + } + + const auto RETARGETPOINT = av->value(); + EXPECT_GT(RETARGETPOINT, 20); + + PANIMVAR fromRest; + pAnimationManager->createAnimation(RETARGETPOINT, fromRest, "spring_velocity"); + + *av = 0; + *fromRest = 0; + EXPECT_EQ(av->value(), RETARGETPOINT); + + pAnimationManager->tick(); + EXPECT_GT(av->value(), fromRest->value()); + + while (pAnimationManager->shouldTickForNext() && ticks++ < 2000) { + pAnimationManager->tick(); + } + + EXPECT_EQ(av->value(), 0); + EXPECT_EQ(fromRest->value(), 0); +}