mirror of
https://github.com/hyprwm/hyprutils.git
synced 2026-05-06 20:08:02 +02:00
animations: add spring controlled curves
This commit is contained in:
parent
e6caa3d4d1
commit
bacb052c56
5 changed files with 269 additions and 32 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue