From 2a61bfc27d6915877d18a09bc440d462a2051857 Mon Sep 17 00:00:00 2001 From: Selene ToyKeeper Date: Wed, 26 Oct 2022 13:49:44 -0600 Subject: [PATCH] add "inertia" mode for mouse keys (#18774) Co-authored-by: Selene ToyKeeper --- docs/feature_mouse_keys.md | 34 ++++++++- quantum/mousekey.c | 147 +++++++++++++++++++++++++++++++++++++ quantum/mousekey.h | 39 +++++++--- 3 files changed, 208 insertions(+), 12 deletions(-) diff --git a/docs/feature_mouse_keys.md b/docs/feature_mouse_keys.md index 8e474c4245d..eed4f4f4aaa 100644 --- a/docs/feature_mouse_keys.md +++ b/docs/feature_mouse_keys.md @@ -48,8 +48,9 @@ Mouse keys supports three different modes to move the cursor: * **Kinetic:** Holding movement keys accelerates the cursor with its speed following a quadratic curve until it reaches its maximum speed. * **Constant:** Holding movement keys moves the cursor at constant speeds. * **Combined:** Holding movement keys accelerates the cursor until it reaches its maximum speed, but holding acceleration and movement keys simultaneously moves the cursor at constant speeds. +* **Inertia:** Cursor accelerates when key held, and decelerates after key release. Tracks X and Y velocity separately for more nuanced movements. Applies to cursor only, not scrolling. -The same principle applies to scrolling. +The same principle applies to scrolling, in most modes. Configuration options that are times, intervals or delays are given in milliseconds. Scroll speed is given as multiples of the default scroll step. For example, a scroll speed of 8 means that each scroll action covers 8 times the length of the default scroll step as defined by your operating system or application. @@ -170,6 +171,37 @@ To use combined speed mode, you must at least define `MK_COMBINED` in your keyma #define MK_COMBINED ``` +### Inertia mode + +This mode provides smooth motion, like sliding on ice. The cursor accelerates +along a quadratic curve while a key is held, then glides to a stop after the +key is released. Vertical and horizontal movements are tracked independently, +so the cursor can move in many directions and make curves. + +Cannot be used at the same time as Kinetic mode, Constant mode, or Combined mode. + +Recommended settings in your keymap’s `config.h` file: + +|Define |Default |Description | +|----------------------------|---------|-----------------------------------------------------------| +|`MOUSEKEY_INERTIA` |undefined|Enable Inertia mode | +|`MOUSEKEY_DELAY` |150 |Delay between pressing a movement key and cursor movement | +|`MOUSEKEY_INTERVAL` |16 |Time between cursor movements in milliseconds (16 = 60fps) | +|`MOUSEKEY_MAX_SPEED` |32 |Maximum cursor speed at which acceleration stops | +|`MOUSEKEY_TIME_TO_MAX` |32 |Number of frames until maximum cursor speed is reached | +|`MOUSEKEY_FRICTION` |24 |How quickly the cursor stops after releasing a key | +|`MOUSEKEY_MOVE_DELTA` |1 |How much to move on first frame (1 strongly recommended) | + +Tips: + +* Set `MOUSEKEY_DELAY` to roughly the same value as your host computer's key repeat delay, in ms. Recommended values are 100 to 300. +* Set `MOUSEKEY_INTERVAL` to a value of 1000 / your monitor's FPS. For 60 FPS, 1000/60 = 16. +* Set `MOUSEKEY_MAX_SPEED` based on your screen resolution and refresh rate, like Width / FPS. For example, 1920 pixels / 60 FPS = 32 pixels per frame. +* Set `MOUSEKEY_TIME_TO_MAX` to a value of approximately FPS / 2, to make it reach full speed in half a second (or so). +* Set `MOUSEKEY_FRICTION` to something between 1 and 255. Lower makes the cursor glide longer. Values from 8 to 40 are the most effective. +* Keep `MOUSEKEY_MOVE_DELTA` at 1. This allows precise movements before the gliding effect starts. +* Mouse wheel options are the same as the default accelerated mode, and do not use inertia. + ## Use with PS/2 Mouse and Pointing Device Mouse keys button state is shared with [PS/2 mouse](feature_ps2_mouse.md) and [pointing device](feature_pointing_device.md) so mouse keys button presses can be used for clicks and drags. diff --git a/quantum/mousekey.c b/quantum/mousekey.c index 25a89bdba7d..b91db80de6b 100644 --- a/quantum/mousekey.c +++ b/quantum/mousekey.c @@ -37,6 +37,13 @@ static void mousekey_debug(void); static uint8_t mousekey_accel = 0; static uint8_t mousekey_repeat = 0; static uint8_t mousekey_wheel_repeat = 0; +#ifdef MOUSEKEY_INERTIA +static uint8_t mousekey_frame = 0; // track whether gesture is inactive, first frame, or repeating +static int8_t mousekey_x_dir = 0; // -1 / 0 / 1 = left / neutral / right +static int8_t mousekey_y_dir = 0; // -1 / 0 / 0 = up / neutral / down +static int8_t mousekey_x_inertia = 0; // current velocity, limit +/- MOUSEKEY_TIME_TO_MAX +static int8_t mousekey_y_inertia = 0; // ... +#endif #ifdef MK_KINETIC_SPEED static uint16_t mouse_timer = 0; #endif @@ -76,6 +83,7 @@ uint8_t mk_wheel_time_to_max = MOUSEKEY_WHEEL_TIME_TO_MAX; # ifndef MK_COMBINED # ifndef MK_KINETIC_SPEED +# ifndef MOUSEKEY_INERTIA /* Default accelerated mode */ @@ -97,6 +105,53 @@ static uint8_t move_unit(void) { return (unit > MOUSEKEY_MOVE_MAX ? MOUSEKEY_MOVE_MAX : (unit == 0 ? 1 : unit)); } +# else // MOUSEKEY_INERTIA mode + +static int8_t move_unit(uint8_t axis) { + int16_t unit; + + // handle X or Y axis + int8_t inertia, dir; + if (axis) { + inertia = mousekey_y_inertia; + dir = mousekey_y_dir; + } else { + inertia = mousekey_x_inertia; + dir = mousekey_x_dir; + } + + if (mousekey_frame < 2) { // first frame(s): initial keypress moves one pixel + mousekey_frame = 1; + unit = dir * MOUSEKEY_MOVE_DELTA; + } else { // acceleration + // linear acceleration (is here for reference, but doesn't feel as good during use) + // unit = (MOUSEKEY_MOVE_DELTA * mk_max_speed * inertia) / mk_time_to_max; + + // x**2 acceleration (quadratic, more precise for short movements) + int16_t percent = (inertia << 8) / mk_time_to_max; + percent = (percent * percent) >> 8; + if (inertia < 0) percent = -percent; + + // unit = sign(inertia) + (percent of max speed) + if (inertia > 0) + unit = 1; + else if (inertia < 0) + unit = -1; + else + unit = 0; + + unit = unit + ((mk_max_speed * percent) >> 8); + } + + if (unit > MOUSEKEY_MOVE_MAX) + unit = MOUSEKEY_MOVE_MAX; + else if (unit < -MOUSEKEY_MOVE_MAX) + unit = -MOUSEKEY_MOVE_MAX; + return unit; +} + +# endif // end MOUSEKEY_INERTIA mode + static uint8_t wheel_unit(void) { uint16_t unit; if (mousekey_accel & (1 << 0)) { @@ -213,6 +268,28 @@ static uint8_t wheel_unit(void) { # endif /* #ifndef MK_COMBINED */ +# ifdef MOUSEKEY_INERTIA + +static int8_t calc_inertia(int8_t direction, int8_t velocity) { + // simulate acceleration and deceleration + + // deceleration + if ((direction > -1) && (velocity < 0)) + velocity = (velocity + 1) * (256 - MOUSEKEY_FRICTION) / 256; + else if ((direction < 1) && (velocity > 0)) + velocity = velocity * (256 - MOUSEKEY_FRICTION) / 256; + + // acceleration + if ((direction > 0) && (velocity < mk_time_to_max)) + velocity++; + else if ((direction < 0) && (velocity > -mk_time_to_max)) + velocity--; + + return velocity; +} + +# endif + void mousekey_task(void) { // report cursor and scroll movement independently report_mouse_t tmpmr = mouse_report; @@ -222,6 +299,32 @@ void mousekey_task(void) { mouse_report.v = 0; mouse_report.h = 0; +# ifdef MOUSEKEY_INERTIA + + // if an animation is in progress and it's time for the next frame + if ((mousekey_frame) && timer_elapsed(last_timer_c) > ((mousekey_frame > 1) ? mk_interval : mk_delay * 10)) { + mousekey_x_inertia = calc_inertia(mousekey_x_dir, mousekey_x_inertia); + mousekey_y_inertia = calc_inertia(mousekey_y_dir, mousekey_y_inertia); + + mouse_report.x = move_unit(0); + mouse_report.y = move_unit(1); + + // prevent sticky "drift" + if ((!mousekey_x_dir) && (!mousekey_x_inertia)) tmpmr.x = 0; + if ((!mousekey_y_dir) && (!mousekey_y_inertia)) tmpmr.y = 0; + + if (mousekey_frame < 2) mousekey_frame++; + } + + // reset if not moving and no movement keys are held + if ((!mousekey_x_dir) && (!mousekey_y_dir) && (!mousekey_x_inertia) && (!mousekey_y_inertia)) { + mousekey_frame = 0; + tmpmr.x = 0; + tmpmr.y = 0; + } + +# else // default acceleration + if ((tmpmr.x || tmpmr.y) && timer_elapsed(last_timer_c) > (mousekey_repeat ? mk_interval : mk_delay * 10)) { if (mousekey_repeat != UINT8_MAX) mousekey_repeat++; if (tmpmr.x != 0) mouse_report.x = move_unit() * ((tmpmr.x > 0) ? 1 : -1); @@ -239,6 +342,9 @@ void mousekey_task(void) { } } } + +# endif // MOUSEKEY_INERTIA or not + if ((tmpmr.v || tmpmr.h) && timer_elapsed(last_timer_w) > (mousekey_wheel_repeat ? mk_wheel_interval : mk_wheel_delay * 10)) { if (mousekey_wheel_repeat != UINT8_MAX) mousekey_wheel_repeat++; if (tmpmr.v != 0) mouse_report.v = wheel_unit() * ((tmpmr.v > 0) ? 1 : -1); @@ -260,6 +366,7 @@ void mousekey_task(void) { if (has_mouse_report_changed(&mouse_report, &tmpmr) || should_mousekey_report_send(&mouse_report)) { mousekey_send(); } + // save the state for later memcpy(&mouse_report, &tmpmr, sizeof(tmpmr)); } @@ -270,6 +377,19 @@ void mousekey_on(uint8_t code) { } # endif /* #ifdef MK_KINETIC_SPEED */ +# ifdef MOUSEKEY_INERTIA + + // initial keypress sets impulse and activates first frame of movement + if ((code == KC_MS_UP) || (code == KC_MS_DOWN)) { + mousekey_y_dir = (code == KC_MS_DOWN) ? 1 : -1; + if (mousekey_frame < 2) mouse_report.y = move_unit(1); + } else if ((code == KC_MS_LEFT) || (code == KC_MS_RIGHT)) { + mousekey_x_dir = (code == KC_MS_RIGHT) ? 1 : -1; + if (mousekey_frame < 2) mouse_report.x = move_unit(0); + } + +# else // no inertia + if (code == KC_MS_UP) mouse_report.y = move_unit() * -1; else if (code == KC_MS_DOWN) @@ -278,6 +398,9 @@ void mousekey_on(uint8_t code) { mouse_report.x = move_unit() * -1; else if (code == KC_MS_RIGHT) mouse_report.x = move_unit(); + +# endif // inertia or not + else if (code == KC_MS_WH_UP) mouse_report.v = wheel_unit(); else if (code == KC_MS_WH_DOWN) @@ -297,6 +420,20 @@ void mousekey_on(uint8_t code) { } void mousekey_off(uint8_t code) { +# ifdef MOUSEKEY_INERTIA + + // key release clears impulse unless opposite direction is held + if ((code == KC_MS_UP) && (mousekey_y_dir < 1)) + mousekey_y_dir = 0; + else if ((code == KC_MS_DOWN) && (mousekey_y_dir > -1)) + mousekey_y_dir = 0; + else if ((code == KC_MS_LEFT) && (mousekey_x_dir < 1)) + mousekey_x_dir = 0; + else if ((code == KC_MS_RIGHT) && (mousekey_x_dir > -1)) + mousekey_x_dir = 0; + +# else // no inertia + if (code == KC_MS_UP && mouse_report.y < 0) mouse_report.y = 0; else if (code == KC_MS_DOWN && mouse_report.y > 0) @@ -305,6 +442,9 @@ void mousekey_off(uint8_t code) { mouse_report.x = 0; else if (code == KC_MS_RIGHT && mouse_report.x > 0) mouse_report.x = 0; + +# endif // inertia or not + else if (code == KC_MS_WH_UP && mouse_report.v > 0) mouse_report.v = 0; else if (code == KC_MS_WH_DOWN && mouse_report.v < 0) @@ -476,6 +616,13 @@ void mousekey_clear(void) { mousekey_repeat = 0; mousekey_wheel_repeat = 0; mousekey_accel = 0; +#ifdef MOUSEKEY_INERTIA + mousekey_frame = 0; + mousekey_x_inertia = 0; + mousekey_y_inertia = 0; + mousekey_x_dir = 0; + mousekey_y_dir = 0; +#endif } static void mousekey_debug(void) { diff --git a/quantum/mousekey.h b/quantum/mousekey.h index da2edb481a4..e968e000c02 100644 --- a/quantum/mousekey.h +++ b/quantum/mousekey.h @@ -36,34 +36,48 @@ along with this program. If not, see . # endif # ifndef MOUSEKEY_MOVE_DELTA -# ifndef MK_KINETIC_SPEED -# define MOUSEKEY_MOVE_DELTA 8 -# else +# if defined(MK_KINETIC_SPEED) # define MOUSEKEY_MOVE_DELTA 16 +# elif defined(MOUSEKEY_INERTIA) +# define MOUSEKEY_MOVE_DELTA 1 +# else +# define MOUSEKEY_MOVE_DELTA 8 # endif # endif # ifndef MOUSEKEY_WHEEL_DELTA # define MOUSEKEY_WHEEL_DELTA 1 # endif # ifndef MOUSEKEY_DELAY -# ifndef MK_KINETIC_SPEED -# define MOUSEKEY_DELAY 10 -# else +# if defined(MK_KINETIC_SPEED) # define MOUSEKEY_DELAY 5 +# elif defined(MOUSEKEY_INERTIA) +# define MOUSEKEY_DELAY 150 // allow single-pixel movements before repeat activates +# else +# define MOUSEKEY_DELAY 10 # endif # endif # ifndef MOUSEKEY_INTERVAL -# ifndef MK_KINETIC_SPEED -# define MOUSEKEY_INTERVAL 20 -# else +# if defined(MK_KINETIC_SPEED) # define MOUSEKEY_INTERVAL 10 +# elif defined(MOUSEKEY_INERTIA) +# define MOUSEKEY_INTERVAL 16 // 60 fps +# else +# define MOUSEKEY_INTERVAL 20 # endif # endif # ifndef MOUSEKEY_MAX_SPEED -# define MOUSEKEY_MAX_SPEED 10 +# if defined(MOUSEKEY_INERTIA) +# define MOUSEKEY_MAX_SPEED 32 +# else +# define MOUSEKEY_MAX_SPEED 10 +# endif # endif # ifndef MOUSEKEY_TIME_TO_MAX -# define MOUSEKEY_TIME_TO_MAX 30 +# if defined(MOUSEKEY_INERTIA) +# define MOUSEKEY_TIME_TO_MAX 32 +# else +# define MOUSEKEY_TIME_TO_MAX 30 +# endif # endif # ifndef MOUSEKEY_WHEEL_DELAY # define MOUSEKEY_WHEEL_DELAY 10 @@ -78,6 +92,9 @@ along with this program. If not, see . # define MOUSEKEY_WHEEL_TIME_TO_MAX 40 # endif +# ifndef MOUSEKEY_FRICTION +# define MOUSEKEY_FRICTION 24 // 0 to 255 +# endif # ifndef MOUSEKEY_INITIAL_SPEED # define MOUSEKEY_INITIAL_SPEED 100 # endif