animations: add springs (#14171)

This commit is contained in:
Vaxry 2026-04-29 21:31:06 +01:00 committed by GitHub
parent 45ffaee093
commit 9ee5ff1f71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 142 additions and 60 deletions

View file

@ -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})

View file

@ -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" })

6
flake.lock generated
View file

@ -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": {

View file

@ -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));
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);
if (curveType == "bezier") {
lua_getfield(L, 2, "points");
if (!lua_istable(L, -1)) {
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();
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));
}
lua_pop(L, 1);
}
lua_pop(L, 1);
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();
}
lua_pop(L, 1);
}
lua_pop(L, 1);
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));
}
{
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));
}
{
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;
const auto& bezierName = bezierParser.parsed();
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));
if (!g_pAnimationManager->bezierExists(bezierName))
return Internal::configError(L, std::format("hl.animation(\"{}\"): no such bezier \"{}\"", leaf, bezierName));
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();
if (!g_pAnimationManager->springExists(springName))
return Internal::configError(L, std::format(R"(hl.animation("{}"): no such spring "{}")", leaf, springName));
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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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();
}