The Massdrop Alt: Custom Animations and More

I fallen in love with my Alt and put some time into learning how to do animations and effects on it. I thought I would write a bit about it here in case it ever proves useful.

Getting more control

I was convinced to try out using caps lock as control by a post on Reddit. The argument in favor of this key layout (commonly seen on the HHKB) is that it’s more ergonomic to move your pinkie sideways to hit the control than it is dragging it all the way down and equally far to the left. I tried it and fell in love. My bottom left key is now my Fn key, which I still use often, but nowhere near as often as control.

To do this on the Alt, you want to change the layout, but you also want to change the config_led.c so that the caps lock led doesn’t toggle like it currently does for the caps lock function:

// Set to 1 if using the caps lock key as a caps lock, 4 if it is being used as another key.
#define CAPS_LED  4
#define SCROLL_LED 1

(Who knows what SCROLL_LED is doing there)

I also changed KC_CAPS to KC_LCTL in the layout. Physically, I moved the Ctrl key to where the Fn key was, and then the Fn key to where the Ctrl key was. I still haven’t found a r3 1.75 Ctrl that I like that matches my keycaps. I’m pretty sure Tai-Hao made the Alt keyset, but their support of nonstandard keys is limited to 1.75u shift and a stepped caps keycap in a few colors. Maybe I’ll eventually wind up with a GMK set in the future or something.

Everyone loves vim keys, right?

I also mapped Fn+HJKL to be left/down/up/right so I had a method of cursor movement that didn’t require removing my fingers from the home row. That one is a dirty habit I picked up from the Anne Pro 2 I type on at work. I find myself bouncing between using these and using the actual arrow keys in the corner. It’s nice to have dedicated arrow keys, but for a quick typo, it’s also hard to just beat the vim layout.

What about animations

I did say that I was doing some animation stuff. Based on how QMK handles RGB, I think I use the effects pretty differently than most people, but that’s fine. I have hard bound different color schemes to different layers, automatically changing the effect up each time I change the layer, so that I always know what layer I’m on by looking at the keyboard.

Creating some layouts and enabling custom animations

I have six layers, so I want six different animations. I’m also going to need a lot of common code for figuring out what keys are where, and it would be nice to do that by logical grouping rather than numerical value. But first things first, lets set up a custom animation for each layer in rgb_matrix_user.inc:

#include "rgb_matrix_user/common.h"
#include "rgb_matrix_user/layer1.h"
#include "rgb_matrix_user/layer2.h"
#include "rgb_matrix_user/layer3.h"
#include "rgb_matrix_user/layer4.h"
#include "rgb_matrix_user/layer5.h"
#include "rgb_matrix_user/layer6.h"

We should probably also make sure that the rules.mk file expects custom animations:

# Custom RGB matrix handling
RGB_MATRIX_ENABLE = custom
# Custom animations
RGB_MATRIX_CUSTOM_USER = yes

What layer is this?

Now, we need to do is build a map of all the layers and then query which layer we’re on currently:

enum LEDLAYER {
    L0 = 0x1,
    L1 = 0x2,
    L2 = 0x4,
    L3 = 0x8,
    T4 = 0x10,
    T5 = 0x20,
};

L0-L3 are the bitmasks for layers 1-4, while T4-T5 are layers 5-6. I used T for those because they’re temporary layers that only persist if you’re holding the Fn key down.

Next up is the function to set the layer:

uint32_t layer_state_set_user(uint32_t state) {
    if ((state & T5) == T5) {
        rgblight_mode(8);
    }
    else if ((state & T4) == T4) {
        rgblight_mode(7);
    }
    else if ((state & L3) == L3) {
        rgblight_mode(6);
    }
    else if ((state & L2) == L2) {
        rgblight_mode(5);
    }
    else if ((state & L1) == L1) {
        rgblight_mode(4);
    }
    else if ((state & L0) == L0) {
        rgblight_mode(3);
    }
    else {
        rgblight_mode(3);
    }
    return state;
}

The layer_state_set_user() function triggers each time the layer is set, so this is the perfect spot to trigger effects. We need to test the layer state against each of the layers in reverse order of the number to make sure that we always grab the highest layer that’s active at the time. Once we find our layer, we call rgblight_mode() with the animation effect number. Divining the numbers that my animations started at was a surprisingly tricky process. Even after disabling all of the regular animations in config.h, there were still two modes that don’t seem to be removable. Mode 1 is a default red display, and after a lot of beating my head on the wall, I realized that “Mode 2” was actually my common script even though it didn’t have any actual animation in it. I put a simple test pattern in the file just to give it something to load, even though I don’t use it.

What key is this?

Like I said earlier, I’d much rather specify groups of keys by their type rather than remembering that WASD is keys 17, 31, 32, and 33. At this point I started picking out groups that I wanted. I also wanted to differentiate keys from underglow. I knew that the last key was 66, and that the total led count was 106. Anything after 66 was underglow. So I set up a few functions to hold onto those facts for me. I also picked out ranged for WASD, “gamer” keys, arrow keys, home row keys, modifiers, and then anything else printable for that matter. A few examples:

bool isHomeKeys(int x) {
    // asdfjkl;
    if (x >= 31 && x <= 34) 
        return true;
    if (x >= 37 && x <= 40) 
        return true;
    return false;
}
bool isVimKeys(int x) {
    // hjkl
    if (x >= 36 && x <= 39)
        return true;
    return false;
}
bool isPrintable(int x) {
    if (x >= 1 && x <= 12)
        return true;
    if (x >= 16 && x <= 28)
        return true;
    if (x >= 31 && x <= 41)
        return true;
    if (x >= 45 && x <= 54)
        return true;
    return false;
}

I also defined a struct for colors, though I found out later that there’s one already provided. I’m going to remove mine at a future cleanup.

Since I was going to have animations and I was going to be setting colors against the same key potentially multiple times in one tick, I also defined a range of booleans I could flip for a given led to know if it’s been set this cycle already or not:

static bool set[LED_COUNT];

But what about the animations?

At their simplest, we’re checking the number of an led and setting a color based on that if it hasn’t already had one set to it. That’s good, but we should be able to do some fancy stuff with that too. I like the slow fade in/out effect on keys (like they’re twinkling or breathing). I wanted mine to be per key though. I set another array per led with a random number that I used as an offset for how far ahead in the animation the led should be, and then I wrote code to use that when calculating the intensity of the color:

static void doGlitter(int i, Color base) {
    uint16_t time = scale16by8(g_rgb_counters.tick, rgb_matrix_config.speed / 4);
    if (rand() * 50 == 1) {
        if (rand() * 2 == 1) {
            offset[i] += 2;
        }
        else {
            offset[i] -= 2;
        }
    }
    float val = (((float)sin8(time + offset[i]) / 256)/2.1) + .05;
    Color glit;
    glit.r = base.r * val;
    glit.g = base.g * val;
    glit.b = base.b * val;
    setRGB(i, glit);
    //rgb_matrix_set_color(i, newR, newG, newB);
}

I’m actually not terribly proud of how I did that, but it works at the exact speed I want it to, and it’s fast enough, so I don’t care. What we’re doing is getting a base value from time, and we’re converting it down to an 8 bit value. It’s based on rgb_matrix_config.speed, so we can use the rgb controls on the keyboard to mess with the effect at runtime, if we wanted to.

After that, we have a 2% chance for a given key to randomly either speed up a tick or slow down a tick. This will also make sure that over enough time, the keys will not always shift in a consistent frequency between each other.

Now comes the ugly part. We use a sinewave function to get a good fade in/out pattern for our intensity. We pass it our calculated time plus this key’s individual offset, and then we perform a handful of seemingly arbitrary modifications to it. dividing by 256 and then again at 2.1 just perfectly resulted in the speed I wanted. Oddly, I couldn’t simplify the math in this at all. I suspect that there are some weird floating point things going on that prevent making this any less kludgy. I’ll have to dig into it at some point in the future.

We then adjust the base color by the intensity we want to apply to it and then we set the color using the setRGB() function, which will set the color and the flag for the key indicating that it’s already been set this tick.

I added some other effects to the board as well as ripping a few of the neater things out of the default animations like some of the rainbow functions. I wound up with an marquee effect that causes a streak to run through the underglow with a nice fade back to the original color on it as well as a static gradient calculator.

My layers were pretty straightforward with most of the logic extracted to common.h. When the code gets built, these files are all within scope of each other. I suspect this was done in the macro somehow, but I’m not extremely knowledgeable about the innerworkings of QMK. At any rate, this meant that I could easily refer to code in common from any layer. I started with the first layer and set up an init and a run function, and started picking out some colors.

static void layer1_init(effect_params_t* params) {
    makeBasicColors(7);
    initGlitter(0);
    LAYER = 1;
}

static bool layer1_run(effect_params_t* params) {
    RGB_MATRIX_USE_LIMITS(led_min, led_max);
    unsetEntireBoard(0);
    doRainbow(LAYER);
    for(int i=led_min; i<=led_max; i++) {
        if (isModifier(i, TAB)) {
            // light grey-ish purple
            setRGB(i, magenta);
        }
        else if (isModifier(i, CTRL)) {
            // blue
            setRGB(i, blue);
        }
        else if (isModifier(i, SHIFT)) {
            // orange
            setRGB(i, orange);
        }
        else if (isModifier(i, FN)) {
            // blue (as ctrl)
            setRGB(i, blue);
        }
        else if (isModifier(i, META)) {
            // red
            setRGB(i, red);
        }
        else if (isModifier(i, ALT)) {
            // blue (as ctrl)
            setRGB(i, blue);
        }
        else if (isModifier(i, SPACE)) {
            doGlitter(i, grey);
        }
        else if (isModifier(i, ENTER)) {
            // green
            setRGB(i, green);
        }
        else if (isModifier(i, BACKSPACE)) {
            // light grey-ish purple (as tab)
            setRGB(i, magenta);
        }
        else if (isKey(i) && !isHomeKeys(i) && isPrintable(i)) {
            //setRGB(i, alphaNum);
            doGlitter(i, grey);
        }
        else if (isHomeKeys(i)) {
            //doGlitter(i, white);
            setRGB(i, white);
        }
        else if (isArrow(i)) {
            setRGB(i, white);
        }
        else if (isMedia(i)) {
            setRGB(i, pink);
        }
        else if (isGlow(i)) {
            doMarquee(i, LAST_KEY+1, led_max, blue, gold);
        }   
    }
    return led_max < DRIVER_LED_TOTAL;
}

Most of those are functions that I wrote in common.h. The makebasiccolors() and initGlitter() functions have meaningless numbers passed into them because something in the code complains if they don’t all accept some sort of integer as an argument. It’s very strange.

Current code and an example video

I’d like to talk more about what I’m doing in this code, but that’s probably something fit for a future post. Instead I’ll say that you can grab the code at my QMK fork at https://github.com/daed/qmk_firmware/tree/master/keyboards/massdrop/alt/keymaps/daed.

Below is a video of the effects actually running.


Add your Thoughts

Your email address will not be published. Required fields are marked *