diff --git a/hw-protocols/hyprpaper_core.xml b/hw-protocols/hyprpaper_core.xml index 3d26a10..a1df362 100644 --- a/hw-protocols/hyprpaper_core.xml +++ b/hw-protocols/hyprpaper_core.xml @@ -88,7 +88,7 @@ - + This is an object describing a wallpaper @@ -147,6 +147,14 @@ Destroys this object. + + + + Set a custom fragment shader used for the wallpaper transition animation. + If unset, the default transition shader is used. + + + diff --git a/shaders/circle-reveal.frag b/shaders/circle-reveal.frag new file mode 100644 index 0000000..4b9640e --- /dev/null +++ b/shaders/circle-reveal.frag @@ -0,0 +1,54 @@ +#version 300 es + +precision highp float; + +in vec2 v_texcoord; + +uniform sampler2D tex1; // Start frame (fit pre-resolved in C++) +uniform sampler2D tex2; // End frame (fit pre-resolved in C++) +uniform float progress; // 0.0 = fully start, 1.0 = fully end +uniform float alpha; + +uniform vec2 fullSize; // Output size in pixels +uniform vec2 randomPixel; // Circle center (0-1 normalized) +uniform float u_duration; // Host transition duration in seconds (from hyprland config) + +// Shader-defined visual duration override: the circle effect completes in 0.5s +// regardless of the host's transition duration. The host's u_duration is used +// to remap progress so the effect fits within this window. +const float SHADER_DURATION = 0.5; + +layout(location = 0) out vec4 fragColor; + +void main() { + // Remap progress so the effect completes in SHADER_DURATION seconds. + float p = (u_duration > 0.0) + ? clamp(progress * (u_duration / SHADER_DURATION), 0.0, 1.0) + : progress; + + vec4 startColor = texture(tex1, v_texcoord); + vec4 endColor = texture(tex2, v_texcoord); + + // Circle reveal: a radial mask expanding from randomPixel. + vec2 pixelPos = v_texcoord * fullSize; + vec2 centerPixel = randomPixel * fullSize; + + float dist = distance(pixelPos, centerPixel); + + // Farthest corner from the center, in pixel space (aspect-ratio correct). + float maxDist = 0.0; + maxDist = max(maxDist, distance(vec2(0.0, 0.0), centerPixel)); + maxDist = max(maxDist, distance(vec2(fullSize.x, 0.0), centerPixel)); + maxDist = max(maxDist, distance(vec2(0.0, fullSize.y), centerPixel)); + maxDist = max(maxDist, distance(fullSize, centerPixel)); + + float normalizedDist = dist / maxDist; + + // circleMask: 0 = show start, 1 = show end. + float edgeWidth = 0.02; + float circleMask = smoothstep(p - edgeWidth, p + edgeWidth, normalizedDist); + + vec4 blended = mix(endColor, startColor, circleMask); + + fragColor = blended * alpha; +} diff --git a/shaders/diagonal-wipe.frag b/shaders/diagonal-wipe.frag new file mode 100644 index 0000000..0fed6b3 --- /dev/null +++ b/shaders/diagonal-wipe.frag @@ -0,0 +1,28 @@ +#version 300 es + +precision highp float; + +in vec2 v_texcoord; + +uniform sampler2D tex1; // Start frame (fit pre-resolved in C++) +uniform sampler2D tex2; // End frame (fit pre-resolved in C++) +uniform float progress; // 0.0 = fully start, 1.0 = fully end +uniform float alpha; + +layout(location = 0) out vec4 fragColor; + +void main() { + vec4 startColor = texture(tex1, v_texcoord); + vec4 endColor = texture(tex2, v_texcoord); + + // Diagonal wipe expanding from the top-left corner as progress increases. + float diagonal = (v_texcoord.x + v_texcoord.y) * 0.5; + float edgeWidth = 0.05; + + // wipeMask: 0 = show end, 1 = show start. + float wipeMask = smoothstep(progress - edgeWidth, progress + edgeWidth, diagonal); + + vec4 blended = mix(endColor, startColor, wipeMask); + + fragColor = blended * alpha; +} diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index eefd0f3..aefc322 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -3,12 +3,14 @@ #include #include #include +#include #include #include #include #include #include #include "../helpers/Logger.hpp" +#include "../ipc/HyprlandSocket.hpp" #include "WallpaperMatcher.hpp" #include @@ -290,3 +292,57 @@ static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALU return result; } + +CConfigManager::SAnimationConfig CConfigManager::getAnimationConfig(const std::string& name) { + if (m_animationCache.contains(name)) + return m_animationCache[name]; + + SAnimationConfig config; + + // Query hyprland's live animation config over its IPC socket — the same channel + // hyprctl uses — rather than parsing hyprland.conf (deprecated, and blind to + // runtime `keyword` overrides). The reply lists one block per animation: + // name: fadeLayersIn + // overriden: 0 + // bezier: almostLinear + // enabled: 1 + // speed: 1.79 + // style: popin + const auto REPLY = HyprlandSocket::getFromSocket("animations"); + if (!REPLY) { + g_logger->log(LOG_WARN, "Could not query hyprland animations over IPC: {}", REPLY.error()); + m_animationCache[name] = config; + return config; + } + + try { + std::istringstream stream(*REPLY); + std::string line; + bool inBlock = false; + + while (std::getline(stream, line)) { + const auto TRIMMED = Hyprutils::String::trim(line); + + if (TRIMMED.starts_with("beziers:")) + break; + + if (TRIMMED.starts_with("name:")) { + inBlock = Hyprutils::String::trim(TRIMMED.substr(5)) == name; + continue; + } + + if (!inBlock) + continue; + + if (TRIMMED.starts_with("enabled:")) + config.enabled = std::stoi(Hyprutils::String::trim(TRIMMED.substr(8))) != 0; + else if (TRIMMED.starts_with("speed:")) + config.duration = std::stof(Hyprutils::String::trim(TRIMMED.substr(6))); + } + } catch (const std::exception& e) { + g_logger->log(LOG_ERR, "Error parsing hyprland animation config: {}", e.what()); + } + + m_animationCache[name] = config; + return config; +} diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index 6fd336e..7c537d7 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -3,6 +3,8 @@ #include "../helpers/Memory.hpp" #include #include +#include +#include class CConfigManager { public: @@ -19,6 +21,12 @@ class CConfigManager { std::string order = "default"; int timeout = 0; uint32_t id = 0; + std::string shaderPath; + }; + + struct SAnimationConfig { + bool enabled = true; + float duration = 1.0f; }; constexpr static const uint32_t SETTING_INVALID = 0; @@ -30,10 +38,15 @@ class CConfigManager { const std::string& getCurrentConfigPath() const; + // Queries hyprland's live animation settings over its IPC socket. + SAnimationConfig getAnimationConfig(const std::string& name); + private: Hyprlang::CConfig m_config; std::string m_currentConfigPath; + + std::unordered_map m_animationCache; }; inline UP g_config; diff --git a/src/ipc/IPC.cpp b/src/ipc/IPC.cpp index 052b1b5..3e7b087 100644 --- a/src/ipc/IPC.cpp +++ b/src/ipc/IPC.cpp @@ -46,6 +46,13 @@ CWallpaperObject::CWallpaperObject(SP&& obj) : m_obje apply(); }); + + m_object->setShaderPath([this](const char* s) { + if (m_inert) + m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert"); + + m_shaderPath = s; + }); } static std::string fitModeToStr(hyprpaperCoreWallpaperFitMode m) { @@ -87,6 +94,7 @@ void CWallpaperObject::apply() { .monitor = std::move(m_monitor), .fitMode = fitModeToStr(m_fitMode), .paths = std::vector{std::move(m_path)}, + .shaderPath = std::move(m_shaderPath), }); m_object->sendSuccess(); diff --git a/src/ipc/IPC.hpp b/src/ipc/IPC.hpp index 7968e40..b48ae9b 100644 --- a/src/ipc/IPC.hpp +++ b/src/ipc/IPC.hpp @@ -19,6 +19,7 @@ namespace IPC { std::string m_path; hyprpaperCoreWallpaperFitMode m_fitMode = HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER; std::string m_monitor; + std::string m_shaderPath; bool m_inert = false; }; diff --git a/src/ui/UI.cpp b/src/ui/UI.cpp index 71308af..f033c7c 100644 --- a/src/ui/UI.cpp +++ b/src/ui/UI.cpp @@ -7,6 +7,7 @@ #include "../config/WallpaperMatcher.hpp" #include +#include #include #include @@ -143,6 +144,37 @@ void CWallpaperTarget::onRepeatTimer() { IPC::g_IPCSocket->onWallpaperChanged(m_monitorName, m_lastPath); } +void CWallpaperTarget::transitionTo(const std::string& path, + Hyprtoolkit::eImageFitMode fitMode, + float duration, + const std::string& shaderPath) { + m_lastPath = path; + + std::string shaderSource; + if (!shaderPath.empty()) { + std::ifstream file(shaderPath); + if (file.is_open()) + shaderSource = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + else + g_logger->log(LOG_ERR, "Could not open transition shader: {}", shaderPath); + } + + m_image->transitionTo(path, fitMode, duration, shaderSource); +} + +void CWallpaperTarget::replaceImmediate(const std::string& path, + Hyprtoolkit::eImageFitMode fitMode) { + m_lastPath = path; + + m_image->rebuild() + ->path(std::string{path}) + ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, + Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}}) + ->sync(true) + ->fitMode(fitMode) + ->commence(); +} + void CUI::registerOutput(const SP& mon) { g_matcher->registerOutput(mon->port(), pruneDesc(mon->desc())); if (IPC::g_IPCSocket) @@ -237,11 +269,37 @@ void CUI::targetChanged(const SP& mon) { return; } - std::erase_if(m_targets, [&mon](const auto& e) { return e->m_monitorName == mon->port(); }); - - m_targets.emplace_back(makeShared(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().order)); + auto existingTarget = findTarget(mon->port()); + + if (existingTarget) { + auto outConfig = g_config->getAnimationConfig("fadeLayersOut"); + auto inConfig = g_config->getAnimationConfig("fadeLayersIn"); + + if (inConfig.enabled || outConfig.enabled) { + existingTarget->transitionTo( + TARGET->get().paths.front(), + toFitMode(TARGET->get().fitMode), + std::max(outConfig.duration, inConfig.duration), + TARGET->get().shaderPath); + } else { + // Animations disabled, swap without a transition. + existingTarget->replaceImmediate( + TARGET->get().paths.front(), + toFitMode(TARGET->get().fitMode)); + } + } else { + m_targets.emplace_back(makeShared(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout, TARGET->get().order)); + } } const std::vector>& CUI::targets() { return m_targets; } + +SP CUI::findTarget(const std::string& monitorName) { + for (const auto& target : m_targets) { + if (target->m_monitorName == monitorName) + return target; + } + return nullptr; +} diff --git a/src/ui/UI.hpp b/src/ui/UI.hpp index 6ae4bc4..3d699c0 100644 --- a/src/ui/UI.hpp +++ b/src/ui/UI.hpp @@ -26,6 +26,14 @@ class CWallpaperTarget { std::string m_monitorName, m_lastPath; + void transitionTo(const std::string& path, + Hyprtoolkit::eImageFitMode fitMode, + float duration, + const std::string& shaderPath = ""); + + void replaceImmediate(const std::string& path, + Hyprtoolkit::eImageFitMode fitMode); + private: void onRepeatTimer(); @@ -54,6 +62,7 @@ class CUI { void targetChanged(const SP& mon); void targetChanged(const std::string_view& monName); void registerOutput(const SP& mon); + SP findTarget(const std::string& monitorName); SP m_backend;