animation: add AnimatedVariable and AnimationManager

animation: add concrete CAnimatedVariable implementation
This commit is contained in:
Maximilian Seidler 2024-12-18 10:27:02 +01:00
parent ad3ca45576
commit 5aa92ebc1a
6 changed files with 635 additions and 0 deletions

View file

@ -94,6 +94,14 @@ add_test(
COMMAND hyprutils_filedescriptor "filedescriptor") COMMAND hyprutils_filedescriptor "filedescriptor")
add_dependencies(tests hyprutils_filedescriptor) add_dependencies(tests hyprutils_filedescriptor)
add_executable(hyprutils_animation "tests/animation.cpp")
target_link_libraries(hyprutils_animation PRIVATE hyprutils PkgConfig::deps)
add_test(
NAME "Animation"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_animation "utils")
add_dependencies(tests hyprutils_animation)
# Installation # Installation
install(TARGETS hyprutils) install(TARGETS hyprutils)
install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

View file

@ -0,0 +1,281 @@
#pragma once
#include <functional>
#include <chrono>
namespace Hyprutils {
namespace Animation {
class CAnimationManager;
/*
Structure for animation properties.
Config properties need to have a static lifetime to allow for config reload.
*/
struct SAnimationPropertyConfig {
bool overridden = true;
std::string internalBezier = "";
std::string internalStyle = "";
float internalSpeed = 0.f;
int internalEnabled = -1;
SAnimationPropertyConfig* pValues = nullptr;
SAnimationPropertyConfig* pParentAnimation = nullptr;
};
static const std::string DEFAULTSTYLE = "";
static const std::string DEFAULTBEZIERNAME = "default";
/* A base class for animated variables. */
class CBaseAnimatedVariable {
public:
using CallbackFun = std::function<void(CBaseAnimatedVariable* thisptr)>;
CBaseAnimatedVariable() {
; // m_bDummy = true;
};
void create(CAnimationManager* p, int typeInfo);
void connectToActive();
void disconnectFromActive();
/* Needs to call disconnectFromActive to remove `this` from the active animations */
virtual ~CBaseAnimatedVariable() {
disconnectFromActive();
};
virtual void warp(bool endCallback = true) = 0;
CBaseAnimatedVariable(const CBaseAnimatedVariable&) = delete;
CBaseAnimatedVariable(CBaseAnimatedVariable&&) = delete;
CBaseAnimatedVariable& operator=(const CBaseAnimatedVariable&) = delete;
CBaseAnimatedVariable& operator=(CBaseAnimatedVariable&&) = delete;
void setConfig(SAnimationPropertyConfig* pConfig) {
m_pConfig = pConfig;
}
SAnimationPropertyConfig* getConfig() const {
return m_pConfig;
}
bool enabled() const {
return m_pConfig ? m_pConfig->pValues->internalEnabled : false;
}
const std::string& getBezierName() const {
return m_pConfig ? m_pConfig->pValues->internalBezier : DEFAULTBEZIERNAME;
}
const std::string& getStyle() const {
return m_pConfig ? m_pConfig->pValues->internalStyle : DEFAULTSTYLE;
}
/* returns the spent (completion) % */
float getPercent() const;
/* returns the current curve value */
float getCurveValue() const;
/* checks if an animation is in progress */
bool isBeingAnimated() const {
return m_bIsBeingAnimated;
}
/* calls the update callback */
void onUpdate() {
if (m_fUpdateCallback)
m_fUpdateCallback(this);
}
bool ok() const {
return !m_bDummy && m_pAnimationManager;
}
/* sets a function to be ran when the animation finishes.
if an animation is not running, runs instantly.
if "remove" is set to true, will remove the callback when ran. */
void setCallbackOnEnd(CallbackFun func, bool remove = true) {
m_fEndCallback = std::move(func);
m_bRemoveEndAfterRan = remove;
if (!isBeingAnimated())
onAnimationEnd();
}
/* sets a function to be ran when an animation is started.
if "remove" is set to true, will remove the callback when ran. */
void setCallbackOnBegin(CallbackFun func, bool remove = true) {
m_fBeginCallback = std::move(func);
m_bRemoveBeginAfterRan = remove;
}
/* sets the update callback, called every time the value is animated and a step is done
Warning: calling unregisterVar/registerVar in this handler will cause UB */
void setUpdateCallback(CallbackFun func) {
m_fUpdateCallback = std::move(func);
}
/* resets all callbacks. Does not call any. */
void resetAllCallbacks() {
m_fBeginCallback = nullptr;
m_fEndCallback = nullptr;
m_fUpdateCallback = nullptr;
m_bRemoveBeginAfterRan = false;
m_bRemoveEndAfterRan = false;
}
void onAnimationEnd() {
m_bIsBeingAnimated = false;
/* We do not call disconnectFromActive here. The animation manager will remove it on a call to tickDone. */
if (m_fEndCallback) {
/* loading m_bRemoveEndAfterRan before calling the callback allows the callback to delete this animation safely if it is false. */
auto removeEndCallback = m_bRemoveEndAfterRan;
m_fEndCallback(this);
if (removeEndCallback)
m_fEndCallback = nullptr; // reset
}
}
void onAnimationBegin() {
m_bIsBeingAnimated = true;
animationBegin = std::chrono::steady_clock::now();
connectToActive();
if (m_fBeginCallback) {
m_fBeginCallback(this);
if (m_bRemoveBeginAfterRan)
m_fBeginCallback = nullptr; // reset
}
}
int m_Type = -1;
protected:
friend class CAnimationManager;
bool m_bIsConnectedToActive = false;
bool m_bIsBeingAnimated = false;
private:
SAnimationPropertyConfig* m_pConfig;
std::chrono::steady_clock::time_point animationBegin;
bool m_bDummy = true;
CAnimationManager* m_pAnimationManager;
bool m_bRemoveEndAfterRan = true;
bool m_bRemoveBeginAfterRan = true;
CallbackFun m_fEndCallback;
CallbackFun m_fBeginCallback;
CallbackFun m_fUpdateCallback;
};
/* This concept represents the minimum requirement for a type to be used with CGenericAnimatedVariable */
template <class ValueImpl>
concept AnimatedType = requires(ValueImpl val) {
requires std::is_copy_constructible_v<ValueImpl>;
// requires operator==
{ val == val } -> std::same_as<bool>;
// requires operator=
{ val = val };
};
/*
A generic class for variables.
VarType is the type of the variable to be animated.
AnimationContext is there to attach additional data to the animation.
In Hyprland that struct would contain a reference to window, workspace or layer for example.
*/
template <AnimatedType VarType, class AnimationContext>
class CGenericAnimatedVariable : public CBaseAnimatedVariable {
public:
CGenericAnimatedVariable() = default;
void create(const int typeInfo, const VarType& initialValue, CAnimationManager* pAnimationManager) {
m_Begun = initialValue;
m_Value = initialValue;
m_Goal = initialValue;
CBaseAnimatedVariable::create(pAnimationManager, typeInfo);
}
CGenericAnimatedVariable(const CGenericAnimatedVariable&) = delete;
CGenericAnimatedVariable(CGenericAnimatedVariable&&) = delete;
CGenericAnimatedVariable& operator=(const CGenericAnimatedVariable&) = delete;
CGenericAnimatedVariable& operator=(CGenericAnimatedVariable&&) = delete;
virtual void warp(bool endCallback = true) {
if (!m_bIsBeingAnimated)
return;
m_Value = m_Goal;
m_bIsBeingAnimated = false;
onUpdate();
if (endCallback)
onAnimationEnd();
}
const VarType& value() const {
return m_Value;
}
/* used to update the value each tick via the AnimationManager */
VarType& value() {
return m_Value;
}
const VarType& goal() const {
return m_Goal;
}
const VarType& begun() const {
return m_Begun;
}
CGenericAnimatedVariable& operator=(const VarType& v) {
if (v == m_Goal)
return *this;
m_Goal = v;
m_Begun = m_Value;
onAnimationBegin();
return *this;
}
// Sets the actual stored value, without affecting the goal, but resets the timer
void setValue(const VarType& v) {
if (v == m_Value)
return;
m_Value = v;
m_Begun = m_Value;
onAnimationBegin();
}
// Sets the actual value and goal
void setValueAndWarp(const VarType& v) {
m_Goal = v;
m_bIsBeingAnimated = true;
warp();
}
AnimationContext m_Context;
private:
VarType m_Value{};
VarType m_Goal{};
VarType m_Begun{};
};
}
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "./BezierCurve.hpp"
#include "./AnimatedVariable.hpp"
#include "../math/Vector2D.hpp"
#include "../memory/SharedPtr.hpp"
#include <unordered_map>
#include <vector>
using namespace Hyprutils::Memory;
using namespace Hyprutils::Math;
namespace Hyprutils {
namespace Animation {
/* A class for managing bezier curves and variables that are being animated. */
class CAnimationManager {
public:
CAnimationManager();
void tickDone();
bool shouldTickForNext();
virtual void scheduleTick() = 0;
virtual void onTicked() = 0;
void addBezierWithName(std::string, const Vector2D&, const Vector2D&);
void removeAllBeziers();
bool bezierExists(const std::string&);
CSharedPointer<CBezierCurve> getBezier(const std::string&);
std::unordered_map<std::string, CSharedPointer<CBezierCurve>> getAllBeziers();
std::vector<CBaseAnimatedVariable*> m_vActiveAnimatedVariables;
private:
std::unordered_map<std::string, CSharedPointer<CBezierCurve>> m_mBezierCurves;
bool m_bTickScheduled = false;
};
}
}

View file

@ -0,0 +1,46 @@
#include <hyprutils/animation/AnimatedVariable.hpp>
#include <hyprutils/animation/AnimationManager.hpp>
using namespace Hyprutils::Animation;
void CBaseAnimatedVariable::create(Hyprutils::Animation::CAnimationManager* pAnimationManager, int typeInfo) {
m_pAnimationManager = pAnimationManager;
m_Type = typeInfo;
m_bDummy = false;
}
void CBaseAnimatedVariable::connectToActive() {
if (!m_pAnimationManager || m_bDummy)
return;
m_pAnimationManager->scheduleTick(); // otherwise the animation manager will never pick this up
if (!m_bIsConnectedToActive)
m_pAnimationManager->m_vActiveAnimatedVariables.push_back(this);
m_bIsConnectedToActive = true;
}
void CBaseAnimatedVariable::disconnectFromActive() {
if (!m_pAnimationManager)
return;
std::erase_if(m_pAnimationManager->m_vActiveAnimatedVariables, [&](const auto& other) { return other == this; });
m_bIsConnectedToActive = false;
}
float CBaseAnimatedVariable::getPercent() const {
const auto DURATIONPASSED = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - animationBegin).count();
return std::clamp((DURATIONPASSED / 100.f) / m_pConfig->pValues->internalSpeed, 0.f, 1.f);
}
float CBaseAnimatedVariable::getCurveValue() const {
if (!m_bIsBeingAnimated || !m_pAnimationManager)
return 1.f;
const auto SPENT = getPercent();
if (SPENT >= 1.f)
return 1.f;
return m_pAnimationManager->getBezier(m_pConfig->pValues->internalBezier)->getYForPoint(SPENT);
}

View file

@ -0,0 +1,68 @@
#include <hyprutils/animation/AnimationManager.hpp>
using namespace Hyprutils::Animation;
using namespace Hyprutils::Math;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
const std::array<Vector2D, 2> DEFAULTBEZIERPOINTS = {Vector2D(0.0, 0.75), Vector2D(0.15, 1.0)};
CAnimationManager::CAnimationManager() {
const auto BEZIER = makeShared<CBezierCurve>();
BEZIER->setup(DEFAULTBEZIERPOINTS);
m_mBezierCurves["default"] = BEZIER;
}
void CAnimationManager::removeAllBeziers() {
m_mBezierCurves.clear();
// add the default one
const auto BEZIER = makeShared<CBezierCurve>();
BEZIER->setup(DEFAULTBEZIERPOINTS);
m_mBezierCurves["default"] = BEZIER;
}
void CAnimationManager::addBezierWithName(std::string name, const Vector2D& p1, const Vector2D& p2) {
const auto BEZIER = makeShared<CBezierCurve>();
BEZIER->setup({
p1,
p2,
});
m_mBezierCurves[name] = BEZIER;
}
bool CAnimationManager::shouldTickForNext() {
return !m_vActiveAnimatedVariables.empty();
}
void CAnimationManager::tickDone() {
std::vector<CBaseAnimatedVariable*> active;
for (auto const& av : m_vActiveAnimatedVariables) {
if (av->ok() && av->isBeingAnimated())
active.push_back(av);
else
av->m_bIsConnectedToActive = false;
}
m_vActiveAnimatedVariables = std::move(active);
}
bool CAnimationManager::bezierExists(const std::string& bezier) {
for (auto const& [bc, bz] : m_mBezierCurves) {
if (bc == bezier)
return true;
}
return false;
}
SP<CBezierCurve> CAnimationManager::getBezier(const std::string& name) {
const auto BEZIER = std::find_if(m_mBezierCurves.begin(), m_mBezierCurves.end(), [&](const auto& other) { return other.first == name; });
return BEZIER == m_mBezierCurves.end() ? m_mBezierCurves["default"] : BEZIER->second;
}
std::unordered_map<std::string, SP<CBezierCurve>> CAnimationManager::getAllBeziers() {
return m_mBezierCurves;
}

188
tests/animation.cpp Normal file
View file

@ -0,0 +1,188 @@
#include <hyprutils/animation/AnimationManager.hpp>
#include <hyprutils/animation/AnimatedVariable.hpp>
#include "shared.hpp"
using namespace Hyprutils::Animation;
using namespace Hyprutils::Math;
class EmtpyContext {};
template <typename VarType>
using CAnimatedVariable = CGenericAnimatedVariable<VarType, EmtpyContext>;
enum eAVTypes {
INT = 1,
TEST,
};
struct SomeTestType {
bool done = false;
bool operator==(const SomeTestType& other) const {
return done == other.done;
}
SomeTestType& operator=(const SomeTestType& other) {
done = other.done;
return *this;
}
};
#define INITANIMCFG(name) animationConfig[name] = {}
#define CREATEANIMCFG(name, parent) animationConfig[name] = {false, "", "", 0.f, -1, &animationConfig["global"], &animationConfig[parent]}
std::unordered_map<std::string, SAnimationPropertyConfig> animationConfig;
class CMyAnimationManager : public CAnimationManager {
public:
void tick() {
for (auto const& av : m_vActiveAnimatedVariables) {
if (!av->ok())
continue;
const auto SPENT = av->getPercent();
const auto PBEZIER = getBezier(av->getBezierName());
const auto POINTY = PBEZIER->getYForPoint(SPENT);
if (POINTY >= 1.f || !av->enabled()) {
av->warp();
continue;
}
switch (av->m_Type) {
case eAVTypes::INT: {
auto* avInt = dynamic_cast<CAnimatedVariable<int>*>(av);
if (!avInt)
std::cout << Colors::RED << "Dynamic cast upcast failed" << Colors::RESET;
const auto DELTA = avInt->goal() - avInt->value();
avInt->value() = avInt->begun() + (DELTA * POINTY);
} break;
case eAVTypes::TEST: {
auto* avCustom = dynamic_cast<CAnimatedVariable<SomeTestType>*>(av);
if (!avCustom)
std::cout << Colors::RED << "Dynamic cast upcast failed" << Colors::RESET;
if (SPENT >= 1.f)
avCustom->value().done = true;
} break;
default: {
std::cout << Colors::RED << "What are we even doing?" << Colors::RESET;
} break;
}
av->onUpdate();
}
tickDone();
}
template <typename AnimType>
void addAnimation(const AnimType& v, CAnimatedVariable<AnimType>& av, const std::string& animationConfigName) {
constexpr const eAVTypes EAVTYPE = std::is_same_v<AnimType, int> ? eAVTypes::INT : eAVTypes::TEST;
av.create(EAVTYPE, v, static_cast<CAnimationManager*>(this));
av.setConfig(&animationConfig[animationConfigName]);
}
virtual void scheduleTick() {
;
}
virtual void onTicked() {
;
}
};
CMyAnimationManager gAnimationManager;
class Subject {
public:
Subject(const int& a, const int& b) {
gAnimationManager.addAnimation(a, m_iA, "default");
gAnimationManager.addAnimation(b, m_iB, "default");
gAnimationManager.addAnimation({}, m_iC, "default");
}
CAnimatedVariable<int> m_iA;
CAnimatedVariable<int> m_iB;
CAnimatedVariable<SomeTestType> m_iC;
};
int main(int argc, char** argv, char** envp) {
INITANIMCFG("global");
CREATEANIMCFG("default", "global");
animationConfig["default"].internalBezier = "default";
animationConfig["default"].internalSpeed = 1.0;
animationConfig["default"].internalEnabled = 1;
animationConfig["default"].pValues = &animationConfig["default"];
int ret = 0;
Subject s(0, 0);
EXPECT(s.m_iA.value(), 0);
EXPECT(s.m_iB.value(), 0);
// Test destruction of a CAnimatedVariable
{
Subject s2(10, 10);
// Adds them to active
s2.m_iA = 1;
s2.m_iB = 2;
// We deliberately do not tick here, to make sure the destructor removes active animated variables
}
EXPECT(gAnimationManager.shouldTickForNext(), false);
EXPECT(s.m_iC.value().done, false);
s.m_iA = 10;
s.m_iB = 100;
s.m_iC = SomeTestType(true);
EXPECT(s.m_iC.value().done, false);
while (gAnimationManager.shouldTickForNext()) {
gAnimationManager.tick();
}
EXPECT(s.m_iA.value(), 10);
EXPECT(s.m_iB.value(), 100);
EXPECT(s.m_iC.value().done, true);
s.m_iA.setValue(0);
s.m_iB.setValue(0);
while (gAnimationManager.shouldTickForNext()) {
gAnimationManager.tick();
}
EXPECT(s.m_iA.value(), 10);
EXPECT(s.m_iB.value(), 100);
//
// Test callbacks
//
bool beginCallbackRan = false;
bool updateCallbackRan = false;
bool endCallbackRan = false;
s.m_iA.setCallbackOnBegin([&beginCallbackRan](CBaseAnimatedVariable* av) { beginCallbackRan = true; });
s.m_iA.setUpdateCallback([&updateCallbackRan](CBaseAnimatedVariable* av) { updateCallbackRan = true; });
s.m_iA.setCallbackOnEnd([&endCallbackRan](CBaseAnimatedVariable* av) { endCallbackRan = true; }, false);
s.m_iA.setValueAndWarp(42);
EXPECT(beginCallbackRan, false);
EXPECT(updateCallbackRan, true);
EXPECT(endCallbackRan, true);
beginCallbackRan = false;
updateCallbackRan = false;
endCallbackRan = false;
s.m_iA = 1337;
while (gAnimationManager.shouldTickForNext()) {
gAnimationManager.tick();
}
EXPECT(beginCallbackRan, true);
EXPECT(updateCallbackRan, true);
EXPECT(endCallbackRan, true);
return ret;
}