From cde5bf84fb3f9a7dd37a04c67813c9868f76ef18 Mon Sep 17 00:00:00 2001 From: Virt <41426325+VirtCode@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:21:39 +0200 Subject: [PATCH] feat: cursor shake to find --- README.md | 42 ++++++++++++++++++--- src/cursor.cpp | 96 +++++++++++++++++++++++++++++++++--------------- src/cursor.hpp | 7 ++++ src/globals.hpp | 19 ++++++---- src/main.cpp | 14 ++++--- src/renderer.cpp | 4 +- src/renderer.hpp | 2 +- 7 files changed, 134 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 0ef303c..9e64516 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,26 @@ This plugin makes your cursor more realistic by simulating how it would behave i Why did I implement this again? -## showcase +Inspired by KDE, it also supports shake to find, to enlarge the cursor when it is shaken so it is easier to find it. + +## behaviour modes The plugin supports two different modes, `rotate` and `tilt`. They both are customizable and have a different base behaviour. -#### rotate +### `rotate` In this mode, the cursor is simulated as a stick which is dragged across the screen on one end. This means it will rotate towards the movement direction, and feels really realistic. https://github.com/VirtCode/hypr-dynamic-cursor/assets/41426325/ccd6d742-8e2b-4073-a35e-318c7e19705c -#### tilt -In this mode, the cursor is tilted based on the X direction and speed it is moving at. It was intended to simulate how an object would be affected by air drag, but implemented is only a rough approximation. This mode can also be customized extensively with different activation functions. +### `tilt` +In this mode, the cursor is tilted based on the X direction and speed it is moving at. It was intended to simulate how an object would be affected by air drag, but implemented is only a rough approximation. This mode can also be customized extensively with different activation functions, and is enabled by default. https://github.com/VirtCode/hypr-dynamic-cursors/assets/41426325/ae25415c-e77f-4c85-864c-2eedbfe432e3 +## shake to find +The plugin supports shake to find, akin to how KDE Plasma, MacOS, etc. do it. It is enabled by default. + +INSERT VIDEO HERE + ## state This plugin is still very early in its development. **Currently, only the `-git` version of hyprland is supported**. There are also multiple things which may or may not be implemented in the future: @@ -25,10 +32,12 @@ This plugin is still very early in its development. **Currently, only the `-git` - [X] air drag simulation - [ ] pendulum simulation - [ ] per-shape length and starting angle (if possible) +- [X] cursor shake to find +- [ ] overdue refactoring (wait for aquamarine merge) If anything here sounds interesting to you, don't hesitate to contribute. -Please note that this plugin was created more or less as a joke. I mainly wanted to see how using a rotating or tilted cursor was like. So I will not guarantee any future updates and bugfixes. +Please note that this plugin was created more or less as a joke. I mainly wanted to see how using a rotating or tilted cursor was like. So I will not guarantee any future updates and bugfixes. The only useful features, shake to find, was implemented more or less as an afterthought. ## installation Installation is supported via `hyprpm`. Supported hyprland versions are `v0.42.0` (yet unreleased) and upwards. The main branch generally tries to target `-git`. @@ -51,6 +60,7 @@ plugin:dynamic-cursors { # sets the cursor behaviour, supports these values: # tilt - tilt the cursor based on x-velocity # rotate - rotate the cursor based on movement direction + # none - do not change the cursors behaviour mode = tilt # minimum angle difference in degrees after which the shape is changed @@ -78,6 +88,28 @@ plugin:dynamic-cursors { # negative_quadratic - negative version of the quadratic one, feels more aggressive function = negative_quadratic } + + # enable shake to find + # magnifies the cursor if its is being shaken + shake = true + + # for when shake = true + shake { + + # controls how soon a shake is detected + # lower values mean sooner + threshold = 4.0 + + # controls how fast the cursor gets larger + factor = 1.5 + + # show cursor behaviour `tilt`, `rotate`, etc. while shaking + effects = false + + # use nearest-neighbour (pixelated) scaling when shaking + # may look weird when effects are enabled + nearest = true + } } ``` diff --git a/src/cursor.cpp b/src/cursor.cpp index 7d48301..a8f5f34 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #define private public #include @@ -26,6 +27,7 @@ Reimplements rendering of the software cursor. Is also largely identical to hyprlands impl, but uses our custom rendering to rotate the cursor. */ void CDynamicCursors::renderSoftware(CPointerManager* pointers, SP pMonitor, timespec* now, CRegion& damage, std::optional overridePos) { + static auto* const* PNEAREST = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE_NEAREST)->getDataStaticPtr(); if (!pointers->hasCursor()) return; @@ -50,12 +52,14 @@ void CDynamicCursors::renderSoftware(CPointerManager* pointers, SP pMo return; box.scale(pMonitor->scale); + box.w *= zoom; + box.h *= zoom; // we rotate the cursor by our calculated amount box.rot = this->angle; // now pass the hotspot to rotate around - renderCursorTextureInternalWithDamage(texture, &box, &damage, 1.F, pointers->currentCursorImage.hotspot); + renderCursorTextureInternalWithDamage(texture, &box, &damage, 1.F, pointers->currentCursorImage.hotspot * zoom, zoom > 1 && **PNEAREST); } /* @@ -65,8 +69,8 @@ It is largely identical to hyprlands implementation, but expands the damage reag void CDynamicCursors::damageSoftware(CPointerManager* pointers) { // we damage a 3x3 area around the cursor, to accomodate for all possible hotspots and rotations - Vector2D size = pointers->currentCursorImage.size / pointers->currentCursorImage.scale; - CBox b = CBox{pointers->pointerPos, size * 3}.translate(-(pointers->currentCursorImage.hotspot + size)); + Vector2D size = pointers->currentCursorImage.size / pointers->currentCursorImage.scale * zoom; + CBox b = CBox{pointers->pointerPos, size * 3}.translate(-(pointers->currentCursorImage.hotspot * zoom + size)); static auto PNOHW = CConfigValue("cursor:no_hardware_cursors"); @@ -87,10 +91,11 @@ It is largely copied from hyprland, but adjusted to allow the cursor to be rotat */ wlr_buffer* CDynamicCursors::renderHardware(CPointerManager* pointers, SP state, SP texture) { static auto* const* PHW_DEBUG= (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_HW_DEBUG)->getDataStaticPtr(); + static auto* const* PNEAREST = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE_NEAREST)->getDataStaticPtr(); auto output = state->monitor->output; - auto size = pointers->currentCursorImage.size; + auto size = pointers->currentCursorImage.size * zoom; // we try to allocate a buffer that is thrice as big, see software rendering auto target = size * 3; @@ -151,11 +156,11 @@ wlr_buffer* CDynamicCursors::renderHardware(CPointerManager* pointers, SPclear(CColor{0.F, 0.F, 0.F, 0.F}); // the box should start in the middle portion, rotate by our calculated amount - CBox xbox = {size, Vector2D{pointers->currentCursorImage.size / pointers->currentCursorImage.scale * state->monitor->scale}.round()}; + CBox xbox = {size, Vector2D{pointers->currentCursorImage.size / pointers->currentCursorImage.scale * state->monitor->scale * zoom}.round()}; xbox.rot = this->angle; // use our custom draw function - renderCursorTextureInternalWithDamage(texture, &xbox, &damage, 1.F, pointers->currentCursorImage.hotspot); + renderCursorTextureInternalWithDamage(texture, &xbox, &damage, 1.F, pointers->currentCursorImage.hotspot * zoom, zoom > 1 && **PNEAREST); g_pHyprOpenGL->end(); glFlush(); @@ -178,7 +183,7 @@ bool CDynamicCursors::setHardware(CPointerManager* pointers, SPoutput->cursor_swapchain) return false; // we need to transform the hotspot manually as we need to indent it by the size - const auto HOTSPOT = CBox{(pointers->currentCursorImage.hotspot + pointers->currentCursorImage.size) * P_MONITOR->scale, {0, 0}} + const auto HOTSPOT = CBox{(pointers->currentCursorImage.hotspot + pointers->currentCursorImage.size) * P_MONITOR->scale * zoom, {0, 0}} .transform(wlTransformToHyprutils(wlr_output_transform_invert(P_MONITOR->transform)), P_MONITOR->output->cursor_swapchain->width, P_MONITOR->output->cursor_swapchain->height) .pos(); @@ -226,25 +231,36 @@ Handle cursor tick events. */ void CDynamicCursors::onTick(CPointerManager* pointers) { static auto const* PMODE = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_MODE)->getDataStaticPtr(); + static auto* const* PSHAKE = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE)->getDataStaticPtr(); - if (!strcmp(*PMODE, "tilt")) calculate(); + if (!strcmp(*PMODE, "tilt") || **PSHAKE) calculate(); } void CDynamicCursors::calculate() { static auto const* PMODE = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_MODE)->getDataStaticPtr(); static auto* const* PTHRESHOLD = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_THRESHOLD)->getDataStaticPtr(); + static auto* const* PSHAKE = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE)->getDataStaticPtr(); + static auto* const* PSHAKE_EFFECTS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE_EFFECTS)->getDataStaticPtr(); + + double zoom = 1; + if (**PSHAKE) + zoom = calculateShake(); double angle = 0; if (!strcmp(*PMODE, "rotate")) angle = calculateStick(); else if (!strcmp(*PMODE, "tilt")) angle = calculateAir(); - else + else if (strcmp(*PMODE, "none")) // if not none, print warning Debug::log(WARN, "[dynamic-cursors] unknown mode specified"); + if (zoom > 1 && !**PSHAKE_EFFECTS) + angle = 0; + // we only consider the angle changed if it is larger than 1 degree - if (abs(this->angle - angle) > ((PI / 180) * **PTHRESHOLD)) { + if (abs(this->angle - angle) > ((PI / 180) * **PTHRESHOLD) || abs(this->zoom - zoom) > 0.1) { this->angle = angle; + this->zoom = zoom; // damage software and change hardware cursor shape g_pPointerManager->damageIfSoftware(); @@ -301,32 +317,52 @@ double CDynamicCursors::calculateAir() { samples_index = (samples_index + 1) % max; // increase for next sample int first = samples_index; - /* turns out this is not relevant on my systems (should've checked before implementing lol): - // motion smooting - // fills samples in between with linear approximations - // accomodates for mice with low polling rates and monitors with high fps - int previous = current == 0 ? max - 1 : current - 1; - if (samples[previous] != samples[current]) { - int steps = std::abs(samples_last_change - previous); - Vector2D amount = (samples[current] - samples[previous]) / steps; - - int factor = 1; - for (int i = (samples_last_change + 1) % max; i != current; i = (i + 1) % max) { - samples[i] += amount * factor++; - } - - samples_last_change = current; - } else if (samples_last_change == current) { - samples_last_change = first; // next is the last then - } - */ - // calculate speed and tilt double speed = (samples[current].x - samples[first].x) / 0.1; return airFunction(speed) * (PI / 3); // 120° in both directions } +double CDynamicCursors::calculateShake() { + static auto* const* PTHRESHOLD = (Hyprlang::FLOAT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE_THRESHOLD)->getDataStaticPtr(); + static auto* const* PFACTOR = (Hyprlang::FLOAT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_SHAKE_FACTOR)->getDataStaticPtr(); + + int max = g_pHyprRenderer->m_pMostHzMonitor->refreshRate; // 1s worth of history + shake_samples.resize(max); + shake_samples_distance.resize(max); + + int previous = shake_samples_index == 0 ? max - 1 : shake_samples_index - 1; + shake_samples[shake_samples_index] = Vector2D{g_pPointerManager->pointerPos}; + shake_samples_distance[shake_samples_index] = shake_samples[shake_samples_index].distance(shake_samples[previous]); + shake_samples_index = (shake_samples_index + 1) % max; // increase for next sample + + /* + The idea for this algorith was largely inspired by KDE Plasma + https://invent.kde.org/plasma/kwin/-/blob/master/src/plugins/shakecursor/shakedetector.cpp + */ + + // calculate total distance travelled + double trail = 0; + for (double distance : shake_samples_distance) trail += distance; + + // calculate diagonal of bounding box travelled within + double left = 1e100, right = 0, bottom = 0, top = 1e100; + for (Vector2D position : shake_samples) { + left = std::min(left, position.x); + right = std::max(right, position.x); + top = std::min(top, position.y); + bottom = std::max(bottom, position.y); + } + double diagonal = Vector2D{left, top}.distance(Vector2D(right, bottom)); + + // discard when the diagonal is small, so we don't have issues with inaccuracies + if (diagonal < 100) return 1.0; + + std::cout << trail << " " << diagonal << " " << (trail / diagonal) << "\n"; + + return std::max(1.0, ((trail / diagonal) - **PTHRESHOLD) * **PFACTOR); +} + double CDynamicCursors::calculateStick() { static auto* const* PLENGTH = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_LENGTH)->getDataStaticPtr(); diff --git a/src/cursor.hpp b/src/cursor.hpp index 1a7497f..c166053 100644 --- a/src/cursor.hpp +++ b/src/cursor.hpp @@ -25,6 +25,8 @@ class CDynamicCursors { private: // current angle of the cursor in radiants double angle; + // current zoom value of the cursor + double zoom = 1; // calculates the current angle of the cursor, and changes the cursor shape void calculate(); @@ -39,6 +41,11 @@ class CDynamicCursors { // ring buffer of last position samples std::vector samples; int samples_index = 0; + + double calculateShake(); + std::vector shake_samples; + std::vector shake_samples_distance; + int shake_samples_index = 0; }; inline std::unique_ptr g_pDynamicCursors; diff --git a/src/globals.hpp b/src/globals.hpp index ad74a8c..7cb8ff7 100644 --- a/src/globals.hpp +++ b/src/globals.hpp @@ -2,14 +2,19 @@ #include -#define CONFIG_ENABLED "plugin:dynamic-cursors:enabled" -#define CONFIG_MODE "plugin:dynamic-cursors:mode" -#define CONFIG_THRESHOLD "plugin:dynamic-cursors:threshold" -#define CONFIG_LENGTH "plugin:dynamic-cursors:rotate:length" -#define CONFIG_MASS "plugin:dynamic-cursors:tilt:limit" -#define CONFIG_FUNCTION "plugin:dynamic-cursors:tilt:function" +#define CONFIG_ENABLED "plugin:dynamic-cursors:enabled" +#define CONFIG_MODE "plugin:dynamic-cursors:mode" +#define CONFIG_SHAKE "plugin:dynamic-cursors:shake" +#define CONFIG_THRESHOLD "plugin:dynamic-cursors:threshold" +#define CONFIG_SHAKE_NEAREST "plugin:dynamic-cursors:shake:nearest" +#define CONFIG_SHAKE_THRESHOLD "plugin:dynamic-cursors:shake:threshold" +#define CONFIG_SHAKE_FACTOR "plugin:dynamic-cursors:shake:factor" +#define CONFIG_SHAKE_EFFECTS "plugin:dynamic-cursors:shake:effects" +#define CONFIG_LENGTH "plugin:dynamic-cursors:rotate:length" +#define CONFIG_MASS "plugin:dynamic-cursors:tilt:limit" +#define CONFIG_FUNCTION "plugin:dynamic-cursors:tilt:function" -#define CONFIG_HW_DEBUG "plugin:dynamic-cursors:hw_debug" +#define CONFIG_HW_DEBUG "plugin:dynamic-cursors:hw_debug" inline HANDLE PHANDLE = nullptr; diff --git a/src/main.cpp b/src/main.cpp index 6f7e8e8..cbb6d8a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,12 +55,10 @@ void hkOnCursorMoved(void* thisptr) { else return (*(origOnCursorMoved)g_pOnCursorMovedHook->m_pOriginal)(thisptr); } -void onTick() { - g_pDynamicCursors->onTick(g_pPointerManager.get()); -} - int onTick(void* data) { - g_pDynamicCursors->onTick(g_pPointerManager.get()); + static auto* const* PENABLED = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, CONFIG_ENABLED)->getDataStaticPtr(); + + if (**PENABLED) g_pDynamicCursors->onTick(g_pPointerManager.get()); const int TIMEOUT = g_pHyprRenderer->m_pMostHzMonitor ? 1000.0 / g_pHyprRenderer->m_pMostHzMonitor->refreshRate : 16; wl_event_source_timer_update(tick, TIMEOUT); @@ -111,6 +109,12 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addConfigValue(PHANDLE, CONFIG_MODE, Hyprlang::STRING{"tilt"}); HyprlandAPI::addConfigValue(PHANDLE, CONFIG_THRESHOLD, Hyprlang::INT{2}); + HyprlandAPI::addConfigValue(PHANDLE, CONFIG_SHAKE, Hyprlang::INT{1}); + HyprlandAPI::addConfigValue(PHANDLE, CONFIG_SHAKE_NEAREST, Hyprlang::INT{1}); + HyprlandAPI::addConfigValue(PHANDLE, CONFIG_SHAKE_EFFECTS, Hyprlang::INT{0}); + HyprlandAPI::addConfigValue(PHANDLE, CONFIG_SHAKE_THRESHOLD, Hyprlang::FLOAT{4}); + HyprlandAPI::addConfigValue(PHANDLE, CONFIG_SHAKE_FACTOR, Hyprlang::FLOAT{1.5}); + HyprlandAPI::addConfigValue(PHANDLE, CONFIG_FUNCTION, Hyprlang::STRING{"negative_quadratic"}); HyprlandAPI::addConfigValue(PHANDLE, CONFIG_MASS, Hyprlang::INT{5000}); diff --git a/src/renderer.cpp b/src/renderer.cpp index b7bf352..7dbb304 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -44,7 +44,7 @@ void projectCursorBox(float mat[9], CBox& box, eTransform transform, float rotat /* This renders a texture with damage but rotates the texture around a given hotspot. */ -void renderCursorTextureInternalWithDamage(SP tex, CBox* pBox, CRegion* damage, float alpha, Vector2D hotspot) { +void renderCursorTextureInternalWithDamage(SP tex, CBox* pBox, CRegion* damage, float alpha, Vector2D hotspot, bool nearest) { TRACY_GPU_ZONE("RenderDynamicCursor"); alpha = std::clamp(alpha, 0.f, 1.f); @@ -75,7 +75,7 @@ void renderCursorTextureInternalWithDamage(SP tex, CBox* pBox, CRegion glActiveTexture(GL_TEXTURE0); glBindTexture(tex->m_iTarget, tex->m_iTexID); - if (g_pHyprOpenGL->m_RenderData.useNearestNeighbor) { + if (g_pHyprOpenGL->m_RenderData.useNearestNeighbor || nearest) { glTexParameteri(tex->m_iTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(tex->m_iTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } else { diff --git a/src/renderer.hpp b/src/renderer.hpp index 93730b4..05f26a4 100644 --- a/src/renderer.hpp +++ b/src/renderer.hpp @@ -1,5 +1,5 @@ #include #include -void renderCursorTextureInternalWithDamage(SP tex, CBox* pBox, CRegion* damage, float alpha, Vector2D hotspot); +void renderCursorTextureInternalWithDamage(SP tex, CBox* pBox, CRegion* damage, float alpha, Vector2D hotspot, bool nearest); void projectCursorBox(float mat[9], CBox& box, eTransform transform, float rotation, const float projection[9], Vector2D hotspot);