Colorex - Library for working with colors.

Colorex is a new elixir library for working with colors.

It has more colorspaces than other libraries I looked at. RGB, HSL, XYZ, LAB/CIELAB, and CMYK. But it also allows you to adjust things without having to worry about what colorspace you need. For example you can change the hue of an RGB color, or the red of an HSL color. But the library abstracts these colorspaces away so you don’t have to think about them, but you can always call a single function to access any colorspace, if you so desire.

It implements the Inspect protocol, so if you have a truecolor terminal, you’ll see the color right there in your terminal, no need to copy paste hex codes back and forth to see what a color looks like.

It also has spectral mixing, which better mimics real life color mixing(i.e. with paint). So yellow and blue actually make green, instead of the typical gray color. It also has regular RGB mixing too if gray is what you’re looking for.

It will preserve the color format. So if you initially pass in a hex string, when you call to_string you’ll get back the color as hex. If you pass in hsl, you’ll get hsl back, etc.

That’s about it.

All this week is launch week, and we’re offering a special introductory price of just $99. Just kidding. It’s free, and MIT licensed.

Hex.pm Link

HexDocs Link

18 Likes

For a longer time I was thinking about working on library like yours, so I have lots of ideas you may be interested in. Colours in practice are very complicated as they have lots of models and variants which is not obvious for everyone …


integer_0_to_255 in many languages it’s called unsigned (bits info) integer i.e. unsigned 8bit integer or u8 in for shorter versions.


At least in CSS (just tried in inspector) Space before , character is valid. Also Firefox removes whitespaces around CSS values. The alpha channel could be specified also using integer and percentage, so for example this value should be valid: rgba( 255 , 0 , 0 , 50% ).

I would recommend to trim leading whitespaces, trim trailing ~r/\s*;?\s*/ regex and split values by ~r/\s*,\s*/ regex. You could use also nimble_parse for this if you don’t like to work with regular expressions.


rgb_percents may be seen as confusing when it returns float. I understand why it’s useful, but by x% I understand 43 percent and not 0.43. Since you have a limited number of cases you can call color_to_percentage/1 and generate it on compile time, so it would be faster with pattern-matching especially that you have behaviour for min and max values - you can define them in module attribute at compile time.

The same compile-time optimisation could be applied for 0 < value < 1 as in most cases people expects up to 2 and sometimes 3 digits after 0. The rest could be done at runtime as there is no sense to work with 0.232151521421412 at compile-time. Maybe you could consider pattern-matching + ceil/floor/round, so you would not have any math logic in runtime except said Float.floor(float, 3) - you would have to confirm that in benchmarks, but I would not be surprised if that would be faster.

Nice one with Colorex.ANSI, but it’s definitely too long. I would expect something like ~a"#f00" or ~ANSI"#f00" and ~ANSI_BG"f00" instead, so for example:

IO.puts(Colorex.ansi_background(Colorex.parse!("#1188FF")) <> "Blue background" <> IO.ANSI.reset())

would become:

import Colorex.ANSI

IO.puts([~ANSI_BG"#1188FF", "Blue background", IO.ANSI.reset()])

Looks like you have duplicated typespec from Colorex in Colorex.Color and Colorex.Utils


It would be nice to link some references in documentation like for a standard page (W3C for example) and MDN where developers may find more information about the color model and color palettes.


Here is a CSV file I have created some time ago. Hope it would be helpful for you …

name;alpha;superset variant;other variants

CMYK;-;16bit per channel (0-65535);8bit per channel (0-255)
HSL;HSLA;hue 0 to 360 degrees|saturation 0 to 100 percent|lightness 0 to 100 percent;-
HSV/HSB;HSVA/HSBA;hue 0 to 360 degrees|saturation 0 to 100 percent|value 0 to 100 percent;-
HWB;HWBa;hue 0 to 360 degrees|whiteness 0 to 100 percent|blackness 0 to 100 percent;-
LAB;LABa;lightness 0 to 100|a negative128 to positive127|b negative128 to positive127;-
LCH;LCHa;lightness 0 to 100|chroma 0 to 230|hue 0 to 360 degrees;-
Linear RGB;Linear RGBA;32bit float per channel;16bit per channel (0-65535)
RGB;RGBA;32bit float per channel;8bit per channel (0-255),16bit per channel (0-65535)
sRGB;sRGBA;16bit per channel (0-65535);8bit per channel (0-255)

Adobe RGB;Adobe RGBA;16bit per channel (0-65535);8bit per channel (0-255)
P3;P3A;16bit per channel (0-65535);-
ProPhoto RGB;ProPhoto RGBA;16bit per channel (0-65535);-

XYZ-D50;-;32bit float CIE XYZ with D50 white point;-
XYZ-D65;-;32bit float CIE XYZ with D65 white point;-

XYZ-D50 and XYZ-D65 requires chromatic adaptation transform and are usually converted through Bradford or CAT02 adaptation matrices as far as I know.

Also different RGB working spaces in many cases require conversion through XYZ space because they use different primaries and white points


In Colorex.Palette.get_palettes/2 it’s not clear when you return 4 or 5 colors in list. From what I can see in your code it could generate from 3 to 5 colours in each list and it’s because of predefined palettes defined in priv directory. You should document why it’s happening or maybe (not sure) change/update said file.


You support only 4 colour palettes. When I was doing research I have found 8 and maybe there are more types/variants of them … The ones I’m aware of are: monochromatic, analogous, complementary, split-complementary, double-complementary, triadic, tetradic and hexadic.


Looks like you made a naming change as Colorex.Support.Guards is in utils directory. Anyway, it would be nice to make it public to make others write functions to manipulate on colours much easier.


You can add more features like accents, shades and tints. Looks like you made already some progress here with darken and lighten functions, but it would be even more amazing to make it easier for others by adding functions that would return for example n shades. In options you can accept min and max changes between original colour and every next shade.


Often developers work on colours from configuration or user input and use it to generate themes based on palette-related functions. Here a useful feature is to detect if the colour is dark and generate it’s light version, so for example:

iex> Colorex.light_dark("#000")
{Colorex.parse!("#fff"), Colorex.parse("#000")}

In other cases you may want to have a passed colour as a references and depending on ligh or dark theme generate n number of shades and tints between 0% and 50% or 50% and 100% of light.

Consider below example:

# colour could have any % of light, so we start with 50%
iex> {tints, shades} = Colorex.lighten_darken(colour, 4)
{
  # 10%, 20%, 30% and 40% of light
  [Colorex.parse!(…), Colorex.parse!(…), Colorex.parse!(…), Colorex.parse!(…)],
  # 60%, 70%, 80% and 90% of light
  [Colorex.parse!(…), Colorex.parse!(…), Colorex.parse!(…), Colorex.parse!(…)],
}

{colours, background_colours} = if MyApp.Account.prefers_dark_theme?(current_user) do
  {tints, shades}
else
  {shades, tints}
end

Of course this is a simple example, but we could pass options with for example a min_step (of light) or max_range (of light) and therefore generate 50% - max_range / min_step number of tints and shades. As you can see there are lots of possible features to simplify many cases.

Of course you may say that some feature should not be a part of your package - just giving a possible features following Elixir’s 10x less code rule. I believe that making as much reasonable features in colorex may help a lot of people.


The number of named colours supported is impressing. What you can do is something like:

parse(named_colour) when named_colour in @named_colours, do: # …
# RGB and others …
parse(named_colour) do
  named_colour
  |> Macro.underscore()
  |> parse()
end

This way said configurable colour would be more human-friendly. Please keep in mind end-users often are not aware or just don’t care about format and the developer is supposed to handle any kind of input, so this way or another we have to support all cases.


You may also consider to add many effects like: blur, brightness, contrast, grayscale, hue rotation, invert, opacity, saturate and sepia, for example:

iex> Colorex.brightness(colour, 0)
Colorex.parse!("#000")

Hope those suggestions are helpful. Good luck with colorex.

4 Likes

Nice, this is what I have been looking for when I started working on fledex. i got kind of what I need, but maybe it’s still worth to merge things together and for me to migrate.

Ley me know whether you think it’s worth it