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.

Comments

  1. Trentin Quarantino says:

    Hi there, I’m appy to find this tutorial! The biggest advantage from a beginners’ perspective is that you keep things simpler compared to pleasuretek. Can I ask you for a suggestion of approaching even a simpler ploblem than you have described. I also want to have LEDs only for indication of currently active (topmost) layer. I’m working with a (Mass)Drop CTRL leyboard and I’m OK with keeping the default rainbow scroll for all layers, just want all LEDs except for the one under ESC to be off for layer 0. Similarly only led under F1 would be active for layer 1, and so on… until layer 12 and F12. For layer 13 I would like to turn on all LEDs in the first row (ESC + F1..F12) as this is the “layer selection layer” from which any layer can be switched on by pressing one of keys in range ESC + F1..F12…

    This seems to be quite a simple thing for a starter to get one’s head around the complexities of LED control (its probably as basic as anything, that is functional, can be: to have all LEDs off except for the one LED that indicates the current layer number… and to keep the default rainbow scroll on). Despite this task being simple in theory, the machinery surrounding LED control is quite overwhelming form me now…

    1. Brad says:

      Sorry to get back to you so late! Between all of the everything that has happened in the last six months or so, I’ve not even had time to think about this blog. Finally getting back on my feet now.

      I hope you’ve figured out a solution to your problem already, but if not, I think the effect you’re looking for is definitely doable. It’s kind of tricky though:

      You can’t have two effects running against a single led at once. If you do then, at best, the second one will override the first one. At worst it’ll flicker between the two and be very distracting. But it is doable.

      You need to keep a map of what keys have been assigned each time you call the run() function of your effect. The way I did this was by declaring a bool array with a size of LED_COUNT. That basically creates a boolean for each led.

      Each time my run function starts, I set all of the values to False. Then each time I set an LED’s color I check to see if the value is False, and if it is, I also set the corresponding array index to True.

      I then just toss multiple effects into the same run() and then order them by which ones I want to see. In this case, I would set the layer key and then I would set all the other keys.

      My function for setting an led looks like this:

      static void setRGB(int x, Color rgb) {
      int flag = rgb_matrix_get_flags();
      bool on = false;
      // LED_FLAG_ALL
      if (flag == 255) {
      on = true;
      }
      // LED_FLAG_UNDERGLOW
      else if (flag == 2 && x > LAST_KEY) {
      on = true;
      }
      // LED_FLAG_KEYLIGHT
      else if (flag == 4 && x <= LAST_KEY) { on = true; } if (!set[x] && on) { rgb_matrix_set_color(x, rgb.r, rgb.g, rgb.b); set[x] = true; } } The "set" array is the boolean array I mentioned earlier. As you can see, we don't call rgb_matrix_set_color() if we've already defined it as true for that led. Hope that helps!

  2. Jose says:

    Hi There, thank you so much for sharing this. I’m a bit of a beginner on the whole QMK. Would you mind sharing your bin file for the Alt, so I can flash it? Because I have no idea on how to create the bin once I’m done modding the files.

Leave a Reply

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