A closer look at color_t |

Roguelike games have historically used an ASCII or ANSI based terminal to present the layout of the game to the player. The days of ANSI terminals may be gone, but terminal emulators are still alive and well, infact the popular roguelike libraries Understanding HOW color is represented, generated, and stored opens up more possibilities to make a stunning and unique visual RGB values can range from 0 to 255 in decimal notation, with each color R, G, and B having its own 0 - 255 value. The reason being is there In BearLibTerminal, libtcod, and NCURSES, as well as many other terminal/terminal emulator libraries there is a color_t type. In almost every typedef unsigned int color_t; //or typedef uint32_t color_t; //(these are the same)The reason being that a 32 bit unsigned integer is 4 x 8bit = 32bits PERFECT for representing our four channels in hexadecimal format as 0xAARRGGBB 0xAA for the alpha channel 0xRR for the red channel 0xGG for the green channel and 0xBB for the blue channel.In this way we can use simple bitwise operations to create and blend any color or colors that we wish. Bitwise operations can be used to do math "cheaply" by being able to change the individual bits representing a number in our computers memory. In most C like languages (C++, perl, go, etc but strangely, Java's are slightly different). The bitwise operators themselves are & (AND), | (OR), ^ (XOR), ~ (NOT). These operators are used in comparing, or relating two numbers. There is also the bit shift operators << (shift left) and >> (shift right). These are used to "shift" the position of each bit in the number. Bitwise and bitshift operators are described below: & - bitwise AND when comparing the bit position of two numbers, if the bit in both numbers is '1' then the value is '1', if they are both '0' the value is '1', and if one is bit is '1' and the other bit is '0', the number is zero. ex: 1101 & 1001 ------- 1011 | - (OR) if either bit is 1, the value is '1' ex: 10101 | 10110 ------- 10111 ^ - (XOR) the bits must be different to be '1' 1100 ^ 1101 -------- 0001 ~ - (NOT) simple negation. can be used for changing positive numbers to negative numbers, etc. ~0001 becomes 1110 << - shift bits left 'x' places 01011 << 2 becomes 01100 >> - shift bits right 'x' places 01011 >> 2 becomes 00010 Needless to say, when used properly these "low level" operations are both extremely powerful and useful. There is also one more bitwise operation that Lets say we have the color violet at full brightness: typedef unsigned int color_t; color_t violet = 0xFF7F00FF; //Alpha: 0xFF = 255 //Red: 0x7F = 127 //Green: 0x00 = 0 //Blue: 0xFF = 255; This is where the bitwise operations come in, and i'll show you why. We know the decimal and hex values of each color and they need to be combined to blend into color_t violet = 0xFF7F00FF; unsigned int alpha = 0xFF; unsigned int red = 0x7F; unsigned int green = 0x00; unsigned int blue = 0xFF; color_t violet = alpha + red + green + blue;Lets see how our Violet turned out:Violet. Uh oh. Clearly that wast going to work because what we need isnt addition. Yes, we're combining the colors, but were not adding the values in the traditional sense like you would with paint. What we need to do is combine the values by "stringing" them together in a one after the other. //when we type 255 as 0xFF unsigned int alpha = 0xFF; //this is shorthand, the actual value is unsigned int alpha = 0xFF000000; //the same goes for the other values unsigned int red = 0x7F; unsigned int red = 0x7F000000;We saw above that we can shift the place of bits using the << operator. And we know that every 2 digits represents 8 bits of our 32 bit value, so following the convention of 0xAARRGGBB we know we want the alpha channel to be the first 8 bits, and the red channel to be the 8 bits immeditely after that, which means that technically the alpha channel doesnt need to be shifted at all, to start we can simply to the following: typedef unsigned int color_t; color_t violet; unsigned int alpha = 0xFF; color_t violet = alpha; unsigned int red = 0x7F;But how to we put the red next to the alpha, without changing the value of the alpha position? we already know that addition wont work. The answer, is the | (OR) operator: color_t violet = 0xFF; unsigned int alpha = 0xFF; unsigned int red = 0x7F; color_t violet = violet | red << 16; // this now makes violet becomes color_t violet = 0xFF7F;Now we're getting somewhere. By combining the bitwise OR operator with the shift left operator we can construct our entire 32 bit unsigned int representation of 0xAARRGGBB: typedef unsigned int color_t; unsigned int alpha = 0xFF; unsigned int red = 0x7F; unsigned int green = 0x00; unsigned int blue = 0xFF; color_t violet = alpha << 24 | red << 16 | green << 8 | blue;Lets try our Violet color now: Violet success! Now that we can put colors together, lets work on taking colors apart from there whole representation in to their respective ARGB values.
In order to do this, we will again make use of bit shifting to obtain the bits typedef unsigned int color_t; color_t violet = 0xFF7F00FF; unsigned int alpha = (violet >> 24); //take the left most 8 bits unsigned int red = (violet >> 16) & 255; // then the next unsigned int green = (violet >> 8) & 255; // and then next unsigned int blue = (violet) & 255; // and finally the last 8 bits So why would we want to seperate the a color into its individual channels? Well, this way we have a very high level of precision over the color we are using, but more importantly // create a struct to split the color into its individual channels struct aRGB { public: unsigned int a,r,g,b; aRGB(unsigned int col) { a = (col >> 24); r = (col >> 16) & 255; g = (col >> 8) & 255; b = (col) & 255; } }; //this function blends the individual channels of two colors and recombines them //into a new color color_t fadeColors(color_t first, color_t second, float mix) { aRGB color_one = aRGB(first); aRGB color_two = aRGB(second); color_t ret_alpha = color_one.a + color_two.a / 2; color_t ret_r = (unsigned int)(color_one.r*(1-mix) + color_two.r*(mix)); color_t ret_g = (unsigned int)(color_one.g*(1-mix) + color_two.g*(mix)); color_t ret_b = (unsigned int)(color_one.b*(1-mix) + color_two.b*(mix)); color_t ret_argb = ret_alpha << 24 | ret_r << 16 | ret_g << 8 | ret_b << 0; return ret_argb; } We use a struct to hold the individual channels of each color, the value of 'mix' controls the amount of blending. The lower the value of mix, the more of the first color is represented, void blended_text(std::string str) { int i; float mix; color_t result; color_t green = 0xFF00FF00; color_t red = 0xFFFF0000; for (i = 0; i < str.length(); i++) { mix =(float)i/str.length(); result = fadeColors(green, red, mix); terminal_color(result); terminal_put(2+i,5,str.at(i)); } }Now that you have a better understanding of how colors can be stored, represented, and manipulated as 32bit unsigned integers, a whole host of color related fun awaits. And the bitwise operations presented here are just the tip of the ice berg when it comes to what those can do, but that's a topic for another article(or maybe a book...).
As always, the code presented here is available at my Github. That's it for now! -max. |

(c) 2020 Max Goren MaxCodes.info maxgoren@icloud.com |