summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-04-29 21:31:06 +0100
committerGitHub <noreply@github.com>2026-04-29 21:31:06 +0100
commit9ee5ff1f7181cc5fa575f1702802ccb78c21ba12 (patch)
treeaab51303b4d045d0633cfe122069456694fdf2e0
parent45ffaee09361110c018e962dbb3d93f7f1ee8e09 (diff)
animations: add springs (#14171)
-rw-r--r--CMakeLists.txt2
-rw-r--r--example/hyprland.lua7
-rw-r--r--flake.lock6
-rw-r--r--src/config/lua/bindings/LuaBindingsConfigRules.cpp154
-rw-r--r--src/config/lua/bindings/LuaBindingsInternal.cpp11
-rw-r--r--src/config/lua/bindings/LuaBindingsInternal.hpp1
-rw-r--r--src/managers/animation/AnimationManager.cpp11
7 files changed, 137 insertions, 55 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d32bc545..c1ffc6b3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,7 +131,7 @@ find_package(glslang CONFIG REQUIRED)
set(AQUAMARINE_MINIMUM_VERSION 0.9.3)
set(HYPRLANG_MINIMUM_VERSION 0.6.7)
set(HYPRCURSOR_MINIMUM_VERSION 0.1.7)
-set(HYPRUTILS_MINIMUM_VERSION 0.11.1)
+set(HYPRUTILS_MINIMUM_VERSION 0.13.0)
set(HYPRGRAPHICS_MINIMUM_VERSION 0.5.1)
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=${AQUAMARINE_MINIMUM_VERSION})
diff --git a/example/hyprland.lua b/example/hyprland.lua
index 16306bb2..dcc15342 100644
--- a/example/hyprland.lua
+++ b/example/hyprland.lua
@@ -139,10 +139,13 @@ hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1}
hl.curve("almostLinear", { type = "bezier", points = { {0.5, 0.5}, {0.75, 1} } })
hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } })
+-- Default springs
+hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 })
+
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
-hl.animation({ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" })
-hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" })
+hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
+hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" })
diff --git a/flake.lock b/flake.lock
index 45c80cd3..8ebb11f8 100644
--- a/flake.lock
+++ b/flake.lock
@@ -261,11 +261,11 @@
]
},
"locked": {
- "lastModified": 1777148223,
- "narHash": "sha256-PTf7kRFFzCW6rIYxLH2fWfVJmj86FSYe3k6L8B+IM9o=",
+ "lastModified": 1777492286,
+ "narHash": "sha256-PwuoEJQcjSKJNP5T55qhfDwIP0tw5zxEhfu8GDfKfeg=",
"owner": "hyprwm",
"repo": "hyprutils",
- "rev": "fa3992be2dfebe4ab06d753c6ca59bea298e798f",
+ "rev": "ec5c0c709706bad5b82f667fd8758eae442577ce",
"type": "github"
},
"original": {
diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
index 52ecaf69..14e17264 100644
--- a/src/config/lua/bindings/LuaBindingsConfigRules.cpp
+++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
@@ -40,9 +40,12 @@
#include "../../../managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp"
#include "../../../managers/permissions/DynamicPermissionManager.hpp"
+#include <hyprutils/utils/ScopeGuard.hpp>
+
using namespace Config;
using namespace Config::Lua;
using namespace Config::Lua::Bindings;
+using namespace Hyprutils::Utils;
namespace {
struct SFieldDesc {
@@ -286,53 +289,101 @@ static int hlCurve(lua_State* L) {
const auto& curveType = typeParser.parsed();
- if (curveType != "bezier")
- return Internal::configError(L, std::format("hl.curve(\"{}\"): unknown curve type \"{}\", expected \"bezier\"", name, curveType));
+ if (curveType == "bezier") {
+ lua_getfield(L, 2, "points");
+ if (!lua_istable(L, -1)) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): missing or invalid \"points\" field, expected a table of two points", name));
+ }
+ int pointsIdx = lua_gettop(L);
- lua_getfield(L, 2, "points");
- if (!lua_istable(L, -1)) {
- lua_pop(L, 1);
- return Internal::configError(L, std::format("hl.curve(\"{}\"): missing or invalid \"points\" field, expected a table of two points", name));
- }
- int pointsIdx = lua_gettop(L);
+ if (luaL_len(L, pointsIdx) != 2) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): \"points\" must contain exactly 2 points, e.g. {{ {{0, 0}}, {{1, 1}} }}", name));
+ }
+
+ float coords[4] = {};
+ for (int pt = 1; pt <= 2; pt++) {
+ lua_rawgeti(L, pointsIdx, pt);
+ if (!lua_istable(L, -1) || luaL_len(L, -1) != 2) {
+ lua_pop(L, 2);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): point {} must be a table of 2 numbers, e.g. {{0.25, 0.1}}", name, pt));
+ }
+ int ptIdx = lua_gettop(L);
+
+ for (int comp = 0; comp < 2; comp++) {
+ lua_rawgeti(L, ptIdx, comp + 1);
+ CLuaConfigFloat coordParser(0.F, -1.F, 2.F);
+ auto coordErr = coordParser.parse(L);
+ lua_pop(L, 1);
+ if (coordErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 2);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): point {}[{}]: {}", name, pt, comp + 1, coordErr.message));
+ }
+ coords[((pt - 1) * 2) + comp] = coordParser.parsed();
+ }
- if (luaL_len(L, pointsIdx) != 2) {
+ lua_pop(L, 1);
+ }
lua_pop(L, 1);
- return Internal::configError(L, std::format("hl.curve(\"{}\"): \"points\" must contain exactly 2 points, e.g. {{ {{0, 0}}, {{1, 1}} }}", name));
- }
- float coords[4] = {};
- for (int pt = 1; pt <= 2; pt++) {
- lua_rawgeti(L, pointsIdx, pt);
- if (!lua_istable(L, -1) || luaL_len(L, -1) != 2) {
- lua_pop(L, 2);
- return Internal::configError(L, std::format("hl.curve(\"{}\"): point {} must be a table of 2 numbers, e.g. {{0.25, 0.1}}", name, pt));
+ g_pAnimationManager->addBezierWithName(name, Vector2D(coords[0], coords[1]), Vector2D(coords[2], coords[3]));
+ } else if (curveType == "spring") {
+
+ Hyprutils::Animation::SSpringCurve curve;
+
+ {
+ CScopeGuard x([L] { lua_pop(L, 1); });
+
+ lua_getfield(L, 2, "stiffness");
+
+ if (!lua_isnumber(L, -1))
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): stiffness expects a number", name));
+
+ curve.stiffness = lua_tonumber(L, -1);
+
+ if (curve.stiffness <= 0.5F)
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): stiffness expects a number >= 0.5", name));
}
- int ptIdx = lua_gettop(L);
- for (int comp = 0; comp < 2; comp++) {
- lua_rawgeti(L, ptIdx, comp + 1);
- CLuaConfigFloat coordParser(0.F, -1.F, 2.F);
- auto coordErr = coordParser.parse(L);
- lua_pop(L, 1);
- if (coordErr.errorCode != PARSE_ERROR_OK) {
- lua_pop(L, 2);
- return Internal::configError(L, std::format("hl.curve(\"{}\"): point {}[{}]: {}", name, pt, comp + 1, coordErr.message));
- }
- coords[((pt - 1) * 2) + comp] = coordParser.parsed();
+ {
+ CScopeGuard x([L] { lua_pop(L, 1); });
+
+ lua_getfield(L, 2, "dampening");
+
+ if (!lua_isnumber(L, -1))
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): dampening expects a number", name));
+
+ curve.damping = lua_tonumber(L, -1);
+
+ if (curve.damping <= 0.5F)
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): dampening expects a number >= 0.5", name));
}
- lua_pop(L, 1);
- }
- lua_pop(L, 1);
+ {
+ CScopeGuard x([L] { lua_pop(L, 1); });
+
+ lua_getfield(L, 2, "mass");
+
+ if (!lua_isnumber(L, -1))
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): mass expects a number", name));
+
+ curve.mass = lua_tonumber(L, -1);
+
+ if (curve.mass <= 0.5F)
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): mass expects a number >= 0.5", name));
+ }
+
+ g_pAnimationManager->addSpringWithName(name, curve);
+ } else
+ return Internal::configError(L, std::format(R"(hl.curve("{}"): unknown curve type "{}", expected "bezier" or "spring")", name, curveType));
- g_pAnimationManager->addBezierWithName(name, Vector2D(coords[0], coords[1]), Vector2D(coords[2], coords[3]));
return 0;
}
static int hlAnimation(lua_State* L) {
if (!lua_istable(L, 1))
- return Internal::configError(L, "hl.animation: expected a table, e.g. { leaf = \"global\", enabled = true, speed = 5, bezier = \"default\" }");
+ return Internal::configError(L, R"(hl.animation: expected a table, e.g. { leaf = "global", enabled = true, speed = 5, bezier = "default" })");
CLuaConfigString leafParser("");
auto leafErr = Internal::parseTableField(L, 1, "leaf", leafParser);
@@ -366,15 +417,34 @@ static int hlAnimation(lua_State* L) {
if (speed <= 0)
return Internal::configError(L, std::format("hl.animation(\"{}\"): speed must be greater than 0", leaf));
- CLuaConfigString bezierParser("");
- auto bezierErr = Internal::parseTableField(L, 1, "bezier", bezierParser);
- if (bezierErr.errorCode != PARSE_ERROR_OK)
- return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, bezierErr.message));
+ std::string curveName;
+
+ if (Internal::hasTableField(L, 1, "bezier")) {
+ CLuaConfigString bezierParser("");
+ auto bezierErr = Internal::parseTableField(L, 1, "bezier", bezierParser);
+ if (bezierErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, bezierErr.message));
+
+ const auto& bezierName = bezierParser.parsed();
+
+ if (!g_pAnimationManager->bezierExists(bezierName))
+ return Internal::configError(L, std::format(R"(hl.animation("{}"): no such bezier "{}")", leaf, bezierName));
+
+ curveName = bezierName;
+ } else if (Internal::hasTableField(L, 1, "spring")) {
+ CLuaConfigString springParser("");
+ auto springErr = Internal::parseTableField(L, 1, "spring", springParser);
+ if (springErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, springErr.message));
+
+ const auto& springName = springParser.parsed();
- const auto& bezierName = bezierParser.parsed();
+ if (!g_pAnimationManager->springExists(springName))
+ return Internal::configError(L, std::format(R"(hl.animation("{}"): no such spring "{}")", leaf, springName));
- if (!g_pAnimationManager->bezierExists(bezierName))
- return Internal::configError(L, std::format("hl.animation(\"{}\"): no such bezier \"{}\"", leaf, bezierName));
+ curveName = "spring:" + springName;
+ } else
+ return Internal::configError(L, std::format(R"(hl.animation("{}"): bezier or spring is required)", leaf));
std::string style;
lua_getfield(L, 1, "style");
@@ -383,7 +453,7 @@ static int hlAnimation(lua_State* L) {
auto styleErr = styleParser.parse(L);
if (styleErr.errorCode != PARSE_ERROR_OK) {
lua_pop(L, 1);
- return Internal::configError(L, std::format("hl.animation(\"{}\"): field \"style\": {}", leaf, styleErr.message));
+ return Internal::configError(L, std::format(R"(hl.animation("{}"): field "style": {})", leaf, styleErr.message));
}
style = styleParser.parsed();
}
@@ -395,7 +465,7 @@ static int hlAnimation(lua_State* L) {
return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, err));
}
- Config::animationTree()->setConfigForNode(leaf, true, speed, bezierName, style);
+ Config::animationTree()->setConfigForNode(leaf, true, speed, curveName, style);
return 0;
}
diff --git a/src/config/lua/bindings/LuaBindingsInternal.cpp b/src/config/lua/bindings/LuaBindingsInternal.cpp
index a6af42f8..e0cef146 100644
--- a/src/config/lua/bindings/LuaBindingsInternal.cpp
+++ b/src/config/lua/bindings/LuaBindingsInternal.cpp
@@ -574,3 +574,14 @@ std::expected<SP<Desktop::Rule::CWindowRule>, int> Internal::buildRuleFromTable(
return rule;
}
+
+bool Internal::hasTableField(lua_State* L, int tableIdx, const char* field) {
+ lua_getfield(L, tableIdx, field);
+ if (lua_isnoneornil(L, -1)) {
+ lua_pop(L, 1);
+ return false;
+ }
+
+ lua_pop(L, 1);
+ return true;
+}
diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp
index 5d4a8d87..0cced6b4 100644
--- a/src/config/lua/bindings/LuaBindingsInternal.hpp
+++ b/src/config/lua/bindings/LuaBindingsInternal.hpp
@@ -196,6 +196,7 @@ namespace Config::Lua::Bindings::Internal {
return err;
}
+ bool hasTableField(lua_State* L, int tableIdx, const char* field);
void registerToplevelBindings(lua_State* L, CConfigManager* mgr);
void registerQueryBindings(lua_State* L);
void registerNotificationBindings(lua_State* L);
diff --git a/src/managers/animation/AnimationManager.cpp b/src/managers/animation/AnimationManager.cpp
index 687f5f2c..c5c3ca54 100644
--- a/src/managers/animation/AnimationManager.cpp
+++ b/src/managers/animation/AnimationManager.cpp
@@ -42,7 +42,7 @@ static void updateVariable(CAnimatedVariable<VarType>& av, const float POINTY, b
av.value() = av.begun() + DELTA * POINTY;
}
-static void updateColorVariable(CAnimatedVariable<CHyprColor>& av, const float POINTY, bool warp) {
+static void updateColorVariable(CAnimatedVariable<CHyprColor>& av, const float POINTY, bool warp = false) {
if (warp || av.value() == av.goal()) {
av.warp(true, false);
return;
@@ -139,15 +139,12 @@ static void handleUpdate(CAnimatedVariable<VarType>& av, bool warp) {
animationsDisabled = animationsDisabled || ls->m_ruleApplicator->noanim().valueOrDefault();
}
- const auto SPENT = av.getPercent();
- const auto PBEZIER = g_pAnimationManager->getBezier(av.getBezierName());
- const auto POINTY = PBEZIER->getYForPoint(SPENT);
- const bool WARP = animationsDisabled || SPENT >= 1.f;
+ const auto STEP = av.getCurveStep();
if constexpr (std::same_as<VarType, CHyprColor>)
- updateColorVariable(av, POINTY, WARP);
+ updateColorVariable(av, STEP.value, STEP.finished);
else
- updateVariable<VarType>(av, POINTY, WARP);
+ updateVariable<VarType>(av, STEP.value, STEP.finished);
av.onUpdate();
}