animations: do not floor spring step dt

This commit is contained in:
Ivan Malison 2026-05-17 00:17:57 -07:00
parent 8597dd96de
commit fd23d767b0
3 changed files with 108 additions and 122 deletions

View file

@ -1,6 +1,7 @@
#include <hyprutils/animation/AnimatedVariable.hpp>
#include <hyprutils/animation/AnimationManager.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include "animation/Spring.hpp"
#include <algorithm>
#include <cmath>
@ -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<CBaseAnimatedVariable> 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<float>(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) {

61
src/animation/Spring.hpp Normal file
View file

@ -0,0 +1,61 @@
#pragma once
#include <hyprutils/animation/AnimationManager.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
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<float>(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);
}
}

View file

@ -6,9 +6,9 @@
#include <hyprutils/animation/AnimatedVariable.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/UniquePtr.hpp>
#include "animation/Spring.hpp"
#include <chrono>
#include <thread>
#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<int> 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<int> 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<int> 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);
}