animations: add spring controlled curves

This commit is contained in:
Vaxry 2026-04-12 18:02:46 -04:00
parent e6caa3d4d1
commit bacb052c56
Signed by: vaxry
GPG key ID: 665806380871D640
5 changed files with 269 additions and 32 deletions

View file

@ -8,6 +8,10 @@
#include <functional>
#include <chrono>
#include <cmath>
#include <concepts>
#include <string_view>
#include <type_traits>
namespace Hyprutils {
namespace Animation {
@ -17,6 +21,11 @@ namespace Hyprutils {
public:
using CallbackFun = std::function<void(Memory::CWeakPointer<CBaseAnimatedVariable> 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<CAnimationManager::SAnimationManagerSignals> m_pSignals;
private:
void resetSpringState(bool preserveVelocity, float velocityScale);
std::string_view springNameFromSpec(const std::string& spec) const;
Memory::CWeakPointer<SAnimationPropertyConfig> 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 <class ValueImpl>
concept AnimableType = AnimatedType<ValueImpl> && requires(ValueImpl val, float pointy) {
{ val - val };
{ val + ((val - val) * pointy) } -> std::convertible_to<ValueImpl>;
};
/*
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<VarType>) {
const float OLDDELTA = static_cast<float>(m_Goal - m_Begun);
const float NEWDELTA = static_cast<float>(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 <class T = VarType>
requires AnimableType<T>
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:

View file

@ -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<CBezierCurve> getBezier(const std::string&);
Memory::CSharedPointer<SSpringCurve> getSpring(const std::string&);
const std::unordered_map<std::string, Memory::CSharedPointer<CBezierCurve>>& getAllBeziers();
const std::unordered_map<std::string, Memory::CSharedPointer<SSpringCurve>>& getAllSprings();
struct SAnimationManagerSignals {
Signal::CSignalT<Memory::CWeakPointer<CBaseAnimatedVariable>> connect;
@ -45,6 +58,7 @@ namespace Hyprutils {
private:
std::unordered_map<std::string, Memory::CSharedPointer<CBezierCurve>> m_mBezierCurves;
std::unordered_map<std::string, Memory::CSharedPointer<SSpringCurve>> m_mSpringCurves;
bool m_bTickScheduled = false;

View file

@ -2,11 +2,15 @@
#include <hyprutils/animation/AnimationManager.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <algorithm>
#include <cmath>
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::milliseconds>(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<float>(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<int>(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());
}

View file

@ -11,11 +11,14 @@ using namespace Hyprutils::Signal;
#define WP CWeakPointer
const std::array<Vector2D, 2> DEFAULTBEZIERPOINTS = {Vector2D(0.0, 0.75), Vector2D(0.15, 1.0)};
const SSpringCurve DEFAULTSPRING = {};
CAnimationManager::CAnimationManager() {
const auto BEZIER = makeShared<CBezierCurve>();
BEZIER->setup(DEFAULTBEZIERPOINTS);
m_mBezierCurves["default"] = BEZIER;
m_mSpringCurves["default"] = makeShared<SSpringCurve>(DEFAULTSPRING);
m_events = makeUnique<SAnimationManagerSignals>();
m_listeners = makeUnique<SAnimVarListeners>();
@ -43,15 +46,21 @@ void CAnimationManager::removeAllBeziers() {
m_mBezierCurves["default"] = BEZIER;
}
void CAnimationManager::removeAllSprings() {
m_mSpringCurves.clear();
m_mSpringCurves["default"] = makeShared<SSpringCurve>(DEFAULTSPRING);
}
void CAnimationManager::addBezierWithName(std::string name, const Vector2D& p1, const Vector2D& p2) {
const auto BEZIER = makeShared<CBezierCurve>();
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<SSpringCurve>(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<CBezierCurve> 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<SSpringCurve> 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<std::string, SP<CBezierCurve>>& CAnimationManager::getAllBeziers() {
return m_mBezierCurves;
}
const std::unordered_map<std::string, SP<SSpringCurve>>& CAnimationManager::getAllSprings() {
return m_mSpringCurves;
}
CWeakPointer<CAnimationManager::SAnimationManagerSignals> CAnimationManager::getSignals() const {
return m_events;
}

View file

@ -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<CAnimatedVariable<int>*>(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<CAnimatedVariable<SomeTestType>*>(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);
}
}
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<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);
}