The problem, again
In my previous post I highlighted a problem about the limitations of Malison and how I refactored it in order to improve the library’s flexibility.
The problem similar to the one I’m going to go over today, was related to a tight coupling between the library’s core and unnecessary abstraction.
This time the abstraction in question is the
The idea was, again, to introduce a common mapping over some general values, in this case - colors.
The huge problem here was the even more apparent limitation of such type enumeration.
Character structure which represents a text character with foreground and background relies on some kind of color.
I decided to not scale down and implement a full-fledged structure containing the basic RGBA and HSL values for easy color manipulation, transformation and access.
Along with that I have implemented methods for color manipulation and palette generation.
I probably have found some small library for that but I got really into it and decided to just have some fun.
Let me show you what is now possible with new
Quick access to common colors
To limit refactoring to minimum, I have implemented static properties returning certain common colors such as Black, White, Red, Green, Blue and so on.
This is compliant with the syntax of getting for example the
Black value of
TermColor enumeration and the helper methods themselves are pretty useful.
Color transformation methods
To create colors programmatically and quickly create, for example different shades of certain colors I have implemented a number of methods to help users do that.
It’s worth to keep in mind that such methods always return a new
TermColor structure, leaving the source intact. Just like the commonly known
DateTime in C#.
Another tidbit is the usage. I implemented both the instance and structure methods so the user can use whichever he feels more comfortable with.
I’ll be only including the structure methods, hence the instance methods are directly calling them too.
Lightening a color means increasing the L in color’s HSL values.
We clamp the L value between
1.0 so we do not run into any problems with generating the colors.
Darkening is the opposite, here we subtract a certain amount from the source’s L value. In order to do that, we just call the
Lighten method with negative amount supplied.
Saturating changes the S attribute. Saturation is kind of a level of colorfullness in our colors. A grayscaled image is represented by colors, that have saturation equal to zero.
Similarly, we increase the color’s saturation and clamp the values to avoid incorrect input.
Desaturating being the opposite of saturating should be quite straightforward now as you probably get the pattern.
Speaking of saturation and desaturation there is also the
Grayscale method that simply desaturates the color by 1.
You may notice a lack of RGB values here, which are basic and common for color representation and ask yourself, why is that? Why operate on some fancy HSL and not just RGB?
The reason for that is color manipulation and palette generation is very easier to do when operating on HSL. Just like you saw above, making any color Gray scared or lighter/darker is a straightforward operation in HSL, while in RGB it’s quite not intuitive.
Why use RGB then at all? The answer is - because you kind of have to. The displays and all pixels you see are based on RGB, and that’s how the colors are passed in order to render them. If we would keep only the HSL values, we would sooner or later have to convert them to RGB so our rendering framework would understand it.
This is why the
TermColor structure keeps the track of both. This may not be the most optimal approach memory-wise but simplifies the calculations which are only made while creating the color from a set of either RGBA or HSLA values.
I’m not entirely set here and I’m wondering about if and how I should change it, but even if I would remove those, the HSL values would have to be calculated either way to supply easy and rich API for color manipulation.
Still, there is a place where I actually do use RGB values, and that is within the implemented operators.
I have implemented the addition, subtraction and multiplication operators to quickly create new colours. For example, if we add green to red we get yellow, if we add green to blue we get cyan.
Definitely those are a bit less useful but as it’s a library, a rich API is still a good thing.
Speaking of the more interesting API methods I implemented, the palette generation were the ones I had the most fun with.
Let’s start with gradients. Gradient is basically a way to represent a transition from one color to the other.
Gradient method takes a source color, a destination color and the amount of steps.
Given those parameters, the user receives a collection of
TermColor instances that represent the generated gradient.
The code for generating the gradient is based on RGBA values and not HSLA. That’s because the simple stepping for HSLA would not generate an expected result - it would go wildly through the color wheel and throw some unexpected colors in between.
A simple example of a generated gradient usage.
The triad supplies good and distinctive colors, which you will see for yourself in a second. The triad is represented by drawing a triangle on the color wheel, and taking the colors from the triangle’s edges.
To give you an image of how that looks like let’s take a look on the color wheel itself.
Behold, the color wheel.
While it may seem a bit strange at first, the color wheel is quite a handy tool. Whenever we rotate our position on the wheel, we change the hue of the color. Hue basically defines if our color is greenish or reddish or blueish. So, if we set our starting point somewhere on the wheel and then draw a triangle that has an edge in that point, we get a triad.
Triad representation on color wheel.
To generate a triad with as best distributed colors as possible we would draw an equilateral triangle, that means we would have to go 120 degrees in one side, and 120 degrees in the other ( or just 240 degrees in the same way) to get the two other colors.
Example of generated triad. Notice the easily distinctive colors.
Similar to to triad, this time we draw a square. The result is similar but we get 1 more color in the palette.
Tetrad representation on color wheel.
The source code is analogous, this time we move by the intervals of 90 degrees.
Example of generated tetrad. One more color and we can still easily see the difference between them.
Analogous colors are close to each other on the color wheel, but differ in hue. The analogous palette is similar to generating a triad or tetrad but we can generate much more results.
Example of analogous palette with 10 colors.
Example of analogous palette with 30 colors.
We can see that this simple approach, while effective, will start to get more gradient-ish for bigger palette sizes.
To counter that effect we could change the implementation to accept some modifier which would be taken into account to randomly change the L and S parameters to get even more varying colors.
But for now that seems pretty much okay to me, maybe I’ll tackle it in the future
And that’s pretty much the whole result of my refactoring of Malison.
I’m quite satisfied with it as it allows me to do so much more with the library, now that we can support any kind of characters and colors with ease.
I’ll be able to put more time into Praedium now and hopefully I’ll not run into more limitations of my chosen dependencies, which I also maintain.
I have also thought of extracting all the color related code in to some other, very small library. Maybe someone else could use it, without necessarily needing the whole terminal emulation package?
Maybe I’ll give it a go in the next days.
I hope you enjoyed this colorful post and maybe got interested in all the math behind it. The idea of procedural color palette generation is very intriguing so it’s very likely I’ll be expanding this color module in the future and write a post or two about it.
Have a nice day and see you next time!