0
0, 0, 0, 255
by Rupour
originally published: 2024-05-12
last edited: 2024-05-13
TL;DR I made a color picker that linearly blends RGBA color to a gray value that you can specify on a slider. You can also check it out at colorang.org, and the code is available on my GitHub.
Whilst working on my custom game engine, I stumbled into a new way to visualize/select RGBA color
Here's a quick preview of what it looks looks like, but there is a full demo later on in this article!
Since the release of my latest game Hey! Here are some letters back in March, I've been creating a custom game engine "from scratch" using mostly native Windows APIs (D3D11, XAudio2, etc.) because I don't really want to use Unity anymore. But also I wanted to learn how game engines work under the hood. Unity's a great engine, but as I've learned more about lower level programming, I've been wanting to build out my own thing. I'll be discussing that project more in depth at a later time, but for now I'll say that it's been a tough but rewarding learning challenge. And I'm happy that I now have a little game framework that works natively with my brain. It's called DFG, Daniel's Framework for Games.
When developing DFG, I had a goal to make it useful for creating desktop applications as well as games. I had already made a few simple games to test out that side, but I wanted to test the desktop application features of the framework, so I pick the most game-like desktop app I could think of: a pixel art editor!
A naturally immediate-mode project, where real-time interactivity is main way people use it, but it also requires some regular buttons / menus, a simple pixel art editor felt like a perfect first project to see if this framework could work for desktop apps.
I managed to get the drawing set up pretty quickly. However, moving/zooming the camera around was trickier than I expected, but I eventually figured it out.
After my hundreth test of drawing black swiggles to make the sure the drawing was working with zooming, somthing occured to me: it would be great if I could draw with color!
The standard color picker uses Hue, Saturation, and Value (HSV) to select a color.
Here's the HTML5 one:
It's a great system; you figure out what color you want, and then you figure out how dark and saturated you want it. HSV maps pretty well onto how we intuitively think about color, which is probably why it's considered the standard way to select color.
However, it's never been my favorite, as I've always found it difficult to match the exact hue I want when it's a tiny slider, especially if I'm trying to figure out how it would blend with the other colors I'm using. It's also not really how the computer thinks about color. Most of the time, especially on the web, we use the sRGB color space to represent color, where all of the colors are blends of three separate channels; red, green, and blue. We get all the color variation you see via mixing these three channels in different amounts.
But there's a problem, linear blending of RGB channels isn't anywhere near as intuitive as the standard HSV values. It's difficult to know that rgb(123, 66, 158) is a lightish purple unless you've spent a long time working with digital color. But what if there was a way to select color in a way to build intuition for linear RGB channels?
The way the colorang works is we have three 256x256 pixels gradients, of which one blends from blue to red, the second blends from red to green, and the final blends from green back to blue. All of them blend to a gray point at one of the corners, and that gray value can be updated with a slider.
0
0, 0, 0, 255
With the colorang, you can first figure out how dark/light you want your color to be, and then you can figure out the hue. It's a flip of the current model we use for HSV color pickers. We get another benefit from this where updating the gray value also updates the saturation; which means the standard trick of increasing/decreasing the saturation when selecting lighter/darker shades of a color can be done with just one slider! Pretty neat, right?
And that's the basics of the colorang! Linearly blending RGB to a gray value between 0 and 255, so we can see the full sRGB color space (or, you know, as much of it as your monitor can display). There is a little bit of an overlap because I'm actually duplicating a some colors on any given gray value (for example, on a 0 or 255 gray value, on the diagonal I'm using 362 pixels to display only 256 unique colors). But for the most part, I think the colorang shows at least a slightly different way to think about color.
As far as my demo, it has all of the main features you'd want from a color picker, save for being able to change the individual red, green, blue, and alpha channels. There are also a few bugs, namely there is some wonkyness that happens when you select colors at very low alpha values. It seems to be that canvases pre-multiplying the alpha into the RGB channels? Resulting in my beautiful linear blending being destroyed. Couldn't quite figure out how to fix it. However, people generally pick the color with full alpha first, and then change the opacity. The colorang works great for that use case, so I'm not too worried about this bug.
Let's break down a little bit how the colorang works under the hood.
Here is the C struct:
typedef struct {
u8 red_green_bitmap [WIDTH * HEIGHT * CHANNELS_PER_PIXEL];
u8 green_blue_bitmap [WIDTH * HEIGHT * CHANNELS_PER_PIXEL];
u8 blue_red_bitmap [WIDTH * HEIGHT * CHANNELS_PER_PIXEL];
u8 transparent_bitmap [WIDTH * HEIGHT * CHANNELS_PER_PIXEL];
u8 gray_bitmap [WIDTH * GRAY_HEIGHT * CHANNELS_PER_PIXEL];
u8 alpha_bitmap [WIDTH * ALPHA_HEIGHT * CHANNELS_PER_PIXEL];
u8 color_bitmap [WIDTH * COLOR_HEIGHT * CHANNELS_PER_PIXEL];
u8 transparent_color_bitmap [WIDTH * COLOR_HEIGHT * CHANNELS_PER_PIXEL];
u8 gray_value;
u8 alpha_value;
u8 current_color[CHANNELS_PER_PIXEL];
u8 *current_bitmap;
u8 x;
u8 y;
} colorang;
We have arrays of unsigned 8bit integers for the all of the different bitmaps here, as well as the current gray value, alpha value, and color. The "transparent" bitmaps are the dark and light gray checkerboard patterns for when you decrease the transparency of the main bitmaps. Works out to around 1.2 MB in size.
This is how you'd use the C API:
colorang *cr = (colorang*)malloc(sizeof(colorang));
if (!cr) return 1;
colorang_init(cr);
// For when the user selects a new color
colorang_update_current_color_from_pos(cr, 0, 0, cr->red_green_bitmap);
// For when the user updates the gray / alpha value
colorang_update_gray_value(cr, 80);
colorang_update_alpha_value(cr, 100);
// For when the user pastes a new color
color light_purple = rgba(123, 66, 158, 255);
colorang_update_from_color(cr, light_purple);
Because we use the single struct and none of the functions allocate any memory, we only need one allocation for the whole lifetime of the colorang!
The x and y parameters of the colorang_update_current_color_from_pos function correspond to where you are in the pixel grid, so 0,0 is the top left pixel of the bitmap, which for red_green would be rgb(255, 0, 0)
And that's about it! If you want to integrate the colorang into your own system, all you need to do is render the bitmaps in whichever way you prefer, and then use the API to update it's state.
The C code is C99 compatible because it turns out that's the version I like to write the most (no declaring variables within a for loop declaration? Come on C89). It compiles with no warnings on GCC / Clang's -Wall -pedantic, as well as Microsoft CL's /W4.
I originally wrote Colorang in C++ for DFG, which uses a bunch of Windows-specific APIs. However, I wanted to release this so anyone could use it, so I figured I should rewrite it in pure C. Once I did that, I thought, "Hey, it would be fun to showcase this on my blog!"
But then I realized I had to use Javascript.
Initially, I tried skirting around Javascript by compiling my C code to Web Assembly, but it proved to be just a bit too cumbersome to learn Web Assembly and WebGL for this demo. Maybe in the future.
Despite being only about 1000 lines of code, the HTML/JavaScript/CSS model was quite confusing to me at first! Particularly JavaScript and CSS. But I managed to figure it out enough to make the demo, so I'm happy. I can certainly see why web developers like to make Javascript frameworks so much!
I recommend you look at the C code if you want to dig down into how the colorang works. As the JavaScript version ended up cluttered with web-specific intricacies.
Overall, I missed the straightforwardness of C/C++ when I was working on the Javascript code. Although I forsee more Javascript in my future, I wrote a poem to describe my current feelings.
Sung to the tune of O Christmas Tree
O Javascript, O Javascript, At times you bring me so much pain. O Javascript, O Javascript, You mystify and entertain. Dynamic types drive me insane. A try and catch is kinda lame. O Javascript, O Javascript, In C's embrace I will remain.