feat: cursor shake to find

This commit is contained in:
Virt 2024-06-27 17:21:39 +02:00
commit cde5bf84fb
7 changed files with 134 additions and 50 deletions

View file

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

View file

@ -4,6 +4,7 @@
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <hyprlang.hpp>
#define private public
#include <hyprland/src/managers/PointerManager.hpp>
@ -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<CMonitor> pMonitor, timespec* now, CRegion& damage, std::optional<Vector2D> 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<CMonitor> 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<Hyprlang::INT>("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<CPointerManager::SMonitorPointerState> state, SP<CTexture> 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, SP<CPoint
g_pHyprOpenGL->clear(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, SP<CPointerManager:
if (!P_MONITOR->output->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();

View file

@ -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<Vector2D> samples;
int samples_index = 0;
double calculateShake();
std::vector<Vector2D> shake_samples;
std::vector<double> shake_samples_distance;
int shake_samples_index = 0;
};
inline std::unique_ptr<CDynamicCursors> g_pDynamicCursors;

View file

@ -2,14 +2,19 @@
#include <hyprland/src/plugins/PluginAPI.hpp>
#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;

View file

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

View file

@ -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<CTexture> tex, CBox* pBox, CRegion* damage, float alpha, Vector2D hotspot) {
void renderCursorTextureInternalWithDamage(SP<CTexture> 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<CTexture> 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 {

View file

@ -1,5 +1,5 @@
#include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/helpers/math/Math.hpp>
void renderCursorTextureInternalWithDamage(SP<CTexture> tex, CBox* pBox, CRegion* damage, float alpha, Vector2D hotspot);
void renderCursorTextureInternalWithDamage(SP<CTexture> 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);