From fd23d767b0f324337eca9ec4ec6056b85e1fcd70 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Sun, 17 May 2026 00:17:57 -0700 Subject: [PATCH] animations: do not floor spring step dt --- src/animation/AnimatedVariable.cpp | 59 +--------------- src/animation/Spring.hpp | 61 ++++++++++++++++ tests/animation/Animation.cpp | 110 ++++++++++++----------------- 3 files changed, 108 insertions(+), 122 deletions(-) create mode 100644 src/animation/Spring.hpp diff --git a/src/animation/AnimatedVariable.cpp b/src/animation/AnimatedVariable.cpp index 74e292b..8c94366 100644 --- a/src/animation/AnimatedVariable.cpp +++ b/src/animation/AnimatedVariable.cpp @@ -1,6 +1,7 @@ #include #include #include +#include "animation/Spring.hpp" #include #include @@ -15,54 +16,6 @@ static constexpr std::string_view SPRINGPREFIX = "spring:"; #define SP CSharedPointer #define WP CWeakPointer -static void advanceSpring(float& value, float& velocity, const SSpringCurve& spring, float dt) { - if (dt <= 0.F) - return; - - const float MASS = std::max(spring.mass, 0.0001F); - const float STIFFNESS = std::max(spring.stiffness, 0.0001F); - const float DAMPING = std::max(spring.damping, 0.F); - - const float DISPLACEMENT = value - 1.F; - const float OMEGA0 = std::sqrt(STIFFNESS / MASS); - const float GAMMA = DAMPING / (2.F * MASS); - - if (GAMMA < OMEGA0) { - const float OMEGAD = std::sqrt((OMEGA0 * OMEGA0) - (GAMMA * GAMMA)); - const float EXP = std::exp(-GAMMA * dt); - const float SIN = std::sin(OMEGAD * dt); - const float COS = std::cos(OMEGAD * dt); - - const float NEW_DISPLACEMENT = EXP * ((DISPLACEMENT * COS) + (((velocity + (GAMMA * DISPLACEMENT)) / OMEGAD) * SIN)); - const float NEW_VELOCITY = EXP * ((velocity * COS) - (((GAMMA * velocity) + (OMEGA0 * OMEGA0 * DISPLACEMENT)) / OMEGAD) * SIN); - - value = 1.F + NEW_DISPLACEMENT; - velocity = NEW_VELOCITY; - return; - } - - const float CRITICAL_EPSILON = std::max(OMEGA0, 1.F) * 0.0001F; - if (std::abs(GAMMA - OMEGA0) <= CRITICAL_EPSILON) { - const float EXP = std::exp(-GAMMA * dt); - const float B = velocity + (GAMMA * DISPLACEMENT); - - value = 1.F + (EXP * (DISPLACEMENT + (B * dt))); - velocity = EXP * (velocity - (GAMMA * B * dt)); - return; - } - - const float ROOT = std::sqrt((GAMMA * GAMMA) - (OMEGA0 * OMEGA0)); - const float R1 = -GAMMA + ROOT; - const float R2 = -GAMMA - ROOT; - const float A = (velocity - (R2 * DISPLACEMENT)) / (R1 - R2); - const float B = DISPLACEMENT - A; - const float E1 = std::exp(R1 * dt); - const float E2 = std::exp(R2 * dt); - - value = 1.F + (A * E1) + (B * E2); - velocity = (A * R1 * E1) + (B * R2 * E2); -} - void CBaseAnimatedVariable::create(CAnimationManager* pManager, int typeInfo, SP pSelf) { m_Type = typeInfo; m_pSelf = std::move(pSelf); @@ -170,16 +123,10 @@ CBaseAnimatedVariable::SCurveStepResult CBaseAnimatedVariable::getCurveStep() { return {.value = 1.F, .finished = true}; const auto NOW = std::chrono::steady_clock::now(); - float dt = std::chrono::duration(NOW - springLastStep).count(); + float dt = Details::springDeltaTime(NOW, springLastStep); springLastStep = NOW; - constexpr float MINDELTA = 1.F / 240.F; - if (dt <= 0.F) - dt = MINDELTA; - else - dt = std::max(dt, MINDELTA); - - advanceSpring(m_fSpringValue, m_fSpringVelocity, *SPRING, dt); + Details::advanceSpring(m_fSpringValue, m_fSpringVelocity, *SPRING, dt); const bool FINISHED = std::abs(1.F - m_fSpringValue) <= SPRING->valueEpsilon && std::abs(m_fSpringVelocity) <= SPRING->velocityEpsilon; if (FINISHED) { diff --git a/src/animation/Spring.hpp b/src/animation/Spring.hpp new file mode 100644 index 0000000..b9f1716 --- /dev/null +++ b/src/animation/Spring.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include +#include +#include + +namespace Hyprutils::Animation::Details { + inline float springDeltaTime(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point last) { + return std::max(std::chrono::duration(now - last).count(), 0.F); + } + + inline void advanceSpring(float& value, float& velocity, const SSpringCurve& spring, float dt) { + if (dt <= 0.F) + return; + + const float MASS = std::max(spring.mass, 0.0001F); + const float STIFFNESS = std::max(spring.stiffness, 0.0001F); + const float DAMPING = std::max(spring.damping, 0.F); + + const float DISPLACEMENT = value - 1.F; + const float OMEGA0 = std::sqrt(STIFFNESS / MASS); + const float GAMMA = DAMPING / (2.F * MASS); + + if (GAMMA < OMEGA0) { + const float OMEGAD = std::sqrt((OMEGA0 * OMEGA0) - (GAMMA * GAMMA)); + const float EXP = std::exp(-GAMMA * dt); + const float SIN = std::sin(OMEGAD * dt); + const float COS = std::cos(OMEGAD * dt); + + const float NEW_DISPLACEMENT = EXP * ((DISPLACEMENT * COS) + (((velocity + (GAMMA * DISPLACEMENT)) / OMEGAD) * SIN)); + const float NEW_VELOCITY = EXP * ((velocity * COS) - (((GAMMA * velocity) + (OMEGA0 * OMEGA0 * DISPLACEMENT)) / OMEGAD) * SIN); + + value = 1.F + NEW_DISPLACEMENT; + velocity = NEW_VELOCITY; + return; + } + + const float CRITICAL_EPSILON = std::max(OMEGA0, 1.F) * 0.0001F; + if (std::abs(GAMMA - OMEGA0) <= CRITICAL_EPSILON) { + const float EXP = std::exp(-GAMMA * dt); + const float B = velocity + (GAMMA * DISPLACEMENT); + + value = 1.F + (EXP * (DISPLACEMENT + (B * dt))); + velocity = EXP * (velocity - (GAMMA * B * dt)); + return; + } + + const float ROOT = std::sqrt((GAMMA * GAMMA) - (OMEGA0 * OMEGA0)); + const float R1 = -GAMMA + ROOT; + const float R2 = -GAMMA - ROOT; + const float A = (velocity - (R2 * DISPLACEMENT)) / (R1 - R2); + const float B = DISPLACEMENT - A; + const float E1 = std::exp(R1 * dt); + const float E2 = std::exp(R2 * dt); + + value = 1.F + (A * E1) + (B * E2); + velocity = (A * R1 * E1) + (B * R2 * E2); + } +} diff --git a/tests/animation/Animation.cpp b/tests/animation/Animation.cpp index f9bfad1..fa89111 100644 --- a/tests/animation/Animation.cpp +++ b/tests/animation/Animation.cpp @@ -6,9 +6,9 @@ #include #include #include +#include "animation/Spring.hpp" #include -#include #define SP CSharedPointer #define WP CWeakPointer @@ -394,76 +394,54 @@ TEST(Animation, animation) { EXPECT_EQ(pAnimationManager.get(), nullptr); } -TEST(Animation, springRetargetPreservesVelocity) { - config(); +TEST(Animation, springDeltaUsesElapsedTime) { + const auto START = std::chrono::steady_clock::time_point{} + std::chrono::seconds(1); - 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); + EXPECT_NEAR(Details::springDeltaTime(START + std::chrono::milliseconds(1), START), 0.001F, 0.000001F); + EXPECT_NEAR(Details::springDeltaTime(START + std::chrono::milliseconds(120), START), 0.120F, 0.000001F); + EXPECT_EQ(Details::springDeltaTime(START, START + std::chrono::milliseconds(1)), 0.F); } TEST(Animation, springAdvancesAcrossLateTicks) { - config(); + const SSpringCurve SPRING = { + .stiffness = 100.f, + .damping = 20.f, + .mass = 1.f, + .valueEpsilon = 0.001f, + .velocityEpsilon = 0.001f, + }; - pAnimationManager->addSpringWithName("late", - { - .stiffness = 100.f, - .damping = 20.f, - .mass = 1.f, - .valueEpsilon = 0.001f, - .velocityEpsilon = 0.001f, - }); + float lateValue = 0.F; + float lateVelocity = 0.F; + float cappedValue = 0.F; + float cappedVelocity = 0.F; - animationTree.createNode("late_tick_global"); - animationTree.createNode("late_tick", "late_tick_global"); - animationTree.setConfigForNode("late_tick_global", 1, 1.f, "spring:late"); + Details::advanceSpring(lateValue, lateVelocity, SPRING, 0.120F); + Details::advanceSpring(cappedValue, cappedVelocity, SPRING, 0.050F); - PANIMVAR av; - pAnimationManager->createAnimation(0, av, "late_tick"); - - *av = 100; - - std::this_thread::sleep_for(std::chrono::milliseconds(120)); - pAnimationManager->tick(); - - EXPECT_GT(av->value(), 25); + EXPECT_GT(lateValue, cappedValue); + EXPECT_GT(lateValue, 0.25F); +} + +TEST(Animation, springDoesNotAdvanceFasterThanElapsedTime) { + const SSpringCurve SPRING = { + .stiffness = 38.f, + .damping = 8.f, + .mass = 2.4f, + .valueEpsilon = 0.001f, + .velocityEpsilon = 0.001f, + }; + + float repeatedValue = 0.F; + float repeatedVelocity = 0.F; + float singleValue = 0.F; + float singleVelocity = 0.F; + + for (size_t i = 0; i < 132; ++i) + Details::advanceSpring(repeatedValue, repeatedVelocity, SPRING, 0.001F); + + Details::advanceSpring(singleValue, singleVelocity, SPRING, 0.132F); + + EXPECT_NEAR(repeatedValue, singleValue, 0.0001F); + EXPECT_LT(repeatedValue, 0.5F); }