View Source Colorex (Colorex v1.0.0)

This module contains functions for creating and manipulating colors.

Color representations

Colors are represented internally in any number of colorspaces: RGB, HSL, LAB, XYZ, or CMYK. The Colorex.Color struct represents a general color with an opaque(type opaque) colorspace. It will also attempt to preserve whatever input format you used, but may standardize the format slightly.

iex> "#3355DD" |> Colorex.parse!() |> to_string()
"#3355DD"
iex> "#3355DD43" |> Colorex.parse!() |> to_string()
"#3355DD43"
iex> "rgba(51, 85, 221, 100%)" |> Colorex.parse!() |> to_string()
"rgb(51 85 221)"
iex> "rgba(51, 85, 221, 0.67)" |> Colorex.parse!() |> to_string()
"rgb(51 85 221 / 67%)"
iex> "hsl(228 71% 53% / 100%)" |> Colorex.parse!() |> to_string()
"hsl(228 71% 53%)"
iex> "hsl(228 71% 53% / 67%)" |> Colorex.parse!() |> to_string()
"hsl(228 71% 53% / 67%)"

You can access a specific colorspace by using the functions rgb/1, hsl/1, lab/1, xyz/1, cmyk/1, or to_colorspace/2

In general, most functions in this module will accept any of the following structs: Colorex.Color, Colorex.RGB, Colorex.HSL,Colorex.LAB, Colorex.XYZ, Colorex.CMYK However since the function may use a different colorspace to do its calculation, you may get back a different colorspace, except if you pass in a Colorex.Color struct, the return type will also be a Colorex.Color struct. You can always explicitly cast the colorspace if you need a particular value, casting is a noop if the input is already in that colorspace.

iex> hsl |> Colorex.update(:red, & &1 + 50) |> Colorex.hsl()

All colorspaces contain an alpha channel for opacity.

Parsing and printing

Colorex can parse strings with any valid css color (hex, rgb, rgba, hsl, hsla and named colors).

The Colorex.Color struct implements the Inspect protocol as it's internals are private. The displayed value is code that will evaluate to current struct, and also shows a sample of the color(requires using a terminal with truecolor)

iex> Colorex.parse!("#3355DD")
Colorex.parse!("#3355DD")

Summary

Functions

Averages a list of colors together in the RGB colorspace.

Returns a Colorex.CMYK struct which represents a color in the CMYK colorspace

Makes a color darker.

Adjusts a color to work better against a dark background.

Calculates the distance between two colors.

Caclulate distance using the fast algorithm.

Convenience function for 1 - fast_distance(color, color2)

Flattens any alpha value to 1.0 against the passed in color or white.

Update color format of a Colorex.Color struct

Get any value from any colorspace

Converts a color to grayscale.

Returns true if a color is grayscale

Returns a Colorex.HSL struct which represents a color in the HSL colorspace

Returns a Colorex.LAB struct which represents a color in the LAB colorspace

Makes a color lighter.

Mixes two colors together.

Returns the color in colors that is most similar to color

Parses a string into a Colorex struct

Parses a string into a Colorex struct

Set any value in any colorspace

Returns a Colorex.RGB struct which represents a color in the RGB colorspace

Returns a 4-element tuple {red, green, blue, alpha}.

Returns an integer 0-255 that represents how light/bright a color is.

This is a convenience function for 1 - distance/3.

Color mixing using Kubelka-Munk theory / pigment mixing.

Attempts to find the best color to use for displaying text on top of the given color.

Converts any color/colorspace to the colorspace of the given atom.

Sets the alpha channel to 100% without accounting for background color

Update any attribute in any colorspace

Returns a Colorex.XYZ struct which represents a color in the XYZ colorspace

Types

color()

@type color() :: Colorex.Color.t() | colorspace_color()

color_key()

@type color_key() ::
  :red
  | :green
  | :blue
  | :alpha
  | :hue
  | :saturation
  | :lightness
  | :l
  | :a
  | :b
  | :cyan
  | :magenta
  | :yellow
  | :black
  | :x
  | :y
  | :z

colorspace_color()

@type colorspace_color() ::
  Colorex.RGB.t()
  | Colorex.HSL.t()
  | Colorex.LAB.t()
  | Colorex.XYZ.t()
  | Colorex.CMYK.t()

float_0_to_1()

@type float_0_to_1() :: float()

integer_0_to_255()

@type integer_0_to_255() :: pos_integer()

rgba_tuple()

@type rgba_tuple() ::
  {integer_0_to_255(), integer_0_to_255(), integer_0_to_255(), float_0_to_1()}

Functions

average(colors)

@spec average([color()]) :: color()

Averages a list of colors together in the RGB colorspace.

Examples:

  iex> Colorex.average([Colorex.parse!("#3355FF"), Colorex.parse!("#FF5533")])
  Colorex.parse!("#B855B8")

cmyk(color)

@spec cmyk(color()) :: Colorex.CMYK.t()

Returns a Colorex.CMYK struct which represents a color in the CMYK colorspace

darken(color, amount)

@spec darken(color :: color(), amount :: number()) :: color()

Makes a color darker.

Takes a color and a number between 0 and 1, and returns a color with the lightness decreased by that amount.

The given amount is a percent of the distance to be closed between the current color and black, not a fixed amount to be added. The following shows a 30%(0.3) darkening.

black                color    white
  |--------------------|-------|
                ^
          color after darken(color, 0.3)

black       color             white
  |----------|-----------------|
         ^
    color after darken(color, 0.3)

black color                     white
  |---|-------------------------|
    ^
color after darken(color, 0.3)

See also lighten/2 for the opposite effect.

The function calls update(color, :lightness, fun). If you want more control you can call that function directly.

darkmode(color)

@spec darkmode(color :: color()) :: color()

Adjusts a color to work better against a dark background.

Highly saturated colors tend to not look as good against a dark background.

This function will reduce the saturation of a color.

It will adjust relative to a color's saturation, so less saturated colors will be adjusted less than more saturated colors.

distance(a, b, opts \\ [])

@spec distance(
  color :: color(),
  color2 :: color(),
  opts :: [fast: boolean(), norm: boolean()]
) ::
  float_0_to_1()

Calculates the distance between two colors.

The default algorithm will convert to CIELAB colorspace and calculate the distance there.

This is generally accepted to be one of the most accurate ways to calculate color distance, as the LAB colorspace was designed to uniformally distribute human perception of color.

You can also pass in a fast: true option which will instead use a faster approximation in RGB space called "redmean". It'll typically return the same results as the LAB algorithm as long as the colors are similar. i.e. which of 2 reds is closest to another red. But it tends to be less accurate the more different the colors are.

The fast distance algorithm is about 33x times faster than the LAB one(~6mil/s IPS vs ~180k/s on an M1 MBP)

The results for both algorithms by default are normalized to return a float between 0 and 1. 0 being identical colors, and 1.0 for the most different colors.

You can pass in the option norm: false if you want the raw results, but those numbers are pretty arbitrary as far as I know.

fast_distance(a, b)

@spec fast_distance(color :: color(), color2 :: color()) :: float_0_to_1()

Caclulate distance using the fast algorithm.

This is an alias of distance/3 with the options [fast: true]

fast_similarity(a, b)

@spec fast_similarity(color(), color()) :: float_0_to_1()

Convenience function for 1 - fast_distance(color, color2)

flatten_alpha(color, bg \\ nil)

@spec flatten_alpha(color :: color(), background :: color() | nil) :: color()

Flattens any alpha value to 1.0 against the passed in color or white.

Colors that contain an alpha value have some transparency to them, so their actual color would depend on what is behind them. This function converts a color with transparency, to a solid color based on the background color passed on, or the default of white

You can also set the background color in your config with :colorex, :background_color

Examples:

  iex> Colorex.flatten_alpha(Colorex.parse!("#3355FF7F"), Colorex.parse!("#FF5533"))
  Colorex.parse!("#995599")
  iex> Colorex.flatten_alpha(Colorex.parse!("#3355FF7F"), Colorex.parse!("#000000"))
  Colorex.parse!("#192A7F")
  iex> Colorex.flatten_alpha(Colorex.parse!("#3355FF7F"))
  Colorex.parse!("#99AAFF")
  iex> Application.put_env(:colorex, :background_color, "#00FF00")
  iex> Colorex.flatten_alpha(Colorex.parse!("#3355FF7F"))
  Colorex.parse!("#19AA7F")

format(colorex, format)

@spec format(
  colorex :: Colorex.Color.t(),
  format :: :hex | :hex24 | :hex32 | :hsl | :hsla | :rgb | :rgba
) :: Colorex.Color.t()

Update color format of a Colorex.Color struct

options: [:hex, :hex24, :hex32, :hsl, :hsla, :rgb, :rgba]

hsla is an alias of hsl, and rgba of rgb. Both will automatically include the alpha component, if neccessary.

Examples:

  iex> c = Colorex.parse!("#3355FF")
  iex> c = Colorex.format(c, :hsl)
  Colorex.parse!("hsl(230 100% 60%)")
  iex> to_string(c)
  "hsl(230 100% 60%)"

get(color, key)

@spec get(color :: color(), key :: color_key()) :: number()

Get any value from any colorspace

This function will fetch the requested key, automatically converting colorspace if neccessary.

Examples:

  iex> Colorex.parse!("#114466") |> Colorex.rgb() |> Colorex.get(:saturation)
  0.714285714285

grayscale(color)

@spec grayscale(color :: color()) :: color()

Converts a color to grayscale.

grayscale?(color, threshold \\ 0)

@spec grayscale?(color :: color(), threshold :: integer_0_to_255()) :: boolean()

Returns true if a color is grayscale

If a color has equal Red, Green, and Blue values this will return true, else false.

Because we may perceive colors that aren't exactly grayscale as grayscale, an optional second argument takes a threshold.

This is a number 0 to 255 that determines how much absolute difference between R, G, B should be considered grayscale.

Default threshold is 0, so R, G, and B must be exactly equal. A threshold of 3 would consider R:120, G:117, B:123 to be grayscale, because they're all within 3 points of the average.

hsl(color)

@spec hsl(color()) :: Colorex.HSL.t()

Returns a Colorex.HSL struct which represents a color in the HSL colorspace

lab(color)

@spec lab(color()) :: Colorex.LAB.t()

Returns a Colorex.LAB struct which represents a color in the LAB colorspace

lighten(color, amount)

@spec lighten(color :: color(), amount :: number()) :: color()

Makes a color lighter.

Takes a color and a number between 0 and 1, and returns a color with the lightness increased by that amount.

The given amount is a percent of the distance to be closed between the current color and white, not a fixed amount to be added. The following shows a 30%(0.3) lightening

black   color                 white
  |-------|--------------------|
                ^
      color after lighten(color, 0.3)

black             color       white
  |-----------------|----------|
                       ^
              color after lighten(color, 0.3)

black                   color  white
  |------------------------|---|
                            ^
                    color after lighten(color, 0.3)

See also darken/2 for the opposite effect.

The function calls update(color, :lightness, fun). If you want more control you can call that function directly.

mix(color1, color2, weight \\ 0.5)

@spec mix(color(), color(), number()) :: color()

Mixes two colors together.

Specifically, takes the average of each of the RGB components, optionally weighted by the given percentage. The opacity of the colors is also considered when weighting the components.

The weight specifies the amount of the first color that should be included in the returned color. The default, 0.5, means that half the first color and half the second color should be used. 25% means that a quarter of the first color and three quarters of the second color should be used.

Examples:

  iex> mix(parse!("#00f"), parse!("#f00"))
  "#800080"
  iex> mix(parse!("#00f"), parse!("#f00"), 0.25)
  "#BF0040"
  iex> mix(rgb(255, 0, 0, 0.5), parse!("#00f"))
  "rgba(64, 0, 191, 0.75)"

most_similar(color, colors, opts \\ [fast: true])

@spec most_similar(color(), [color()], [{:fast, boolean()}]) :: color()

Returns the color in colors that is most similar to color

Options:

fast: Whether to use the faster or more accurate algorithm to compute similarity. Since the faster algorithms weakness is in more different colors, it should be fine for calculating most similar. Default true

parse(string)

@spec parse(String.t()) :: {:ok, Colorex.Color.t()} | {:error, atom()}

Parses a string into a Colorex struct

Returns {:ok, color} on successful parse, or {:error, reason} otherwise

parse!(string)

@spec parse!(String.t()) :: Colorex.Color.t()

Parses a string into a Colorex struct

Similar to parse/1 but throws on invalid input.

put(color, key, value)

@spec put(color :: color(), key :: color_key(), value :: number()) :: color()

Set any value in any colorspace

Allows you to set the value of the given key. Colorspace will be automatically converted if needed

Examples:

  iex> Colorex.parse!("#3355DD") |> Colorex.hsl() |> Colorex.put(:red, 55) |> Colorex.put(:saturation, 0.75)
  %Colorex.HSL{
    hue: 229.1566265060241,
    saturation: 0.75,
    lightness: 0.5411764705882354,
    alpha: 1.0
  }

If you want to set a value based on the current value, see update/3

rgb(color)

@spec rgb(color()) :: Colorex.RGB.t()

Returns a Colorex.RGB struct which represents a color in the RGB colorspace

rgba_tuple(color)

@spec rgba_tuple(color :: color()) ::
  {integer_0_to_255(), integer_0_to_255(), integer_0_to_255(), float_0_to_1()}

Returns a 4-element tuple {red, green, blue, alpha}.

red, green, and blue will be integers from 0 to 255 inclusive, alpha will be a float from 0.0 to 1.0 inclusive

shade_number(color)

@spec shade_number(color :: color()) :: pos_integer()

Returns an integer 0-255 that represents how light/bright a color is.

Black returns 0. White returns 255.

This is different than just getting the HSL lightness value, as this will incorporate saturation too.

Here is an example of some colors where sorting by shade_number/1 and hsl/1 lightness give the most different results. Shade number is on the top. HSL lightness on the bottom. Both sorted darkest to lightest.

<img src="images/shade_number.png" style="width: 100%;" />

similarity(a, b, opts \\ [])

@spec similarity(color :: color(), color2 :: color(), opts :: [{:fast, boolean()}]) ::
  float_0_to_1()

This is a convenience function for 1 - distance/3.

See that functions docs for a more in depth explanation

Results with a larger score from distance/3 are more different, and results with a larger score from this function similarity/3 are more similar.

spectral_mix(color, color2, weight \\ 0.5)

@spec spectral_mix(color1 :: color(), color2 :: color(), weight :: float()) :: color()

Color mixing using Kubelka-Munk theory / pigment mixing.

This can often more closely resemble real-world color mixing(e.g. paint mixing).

For example, in school you learn mixing blue and yellow gives you green. If you mix them in traditional RGB color mixing, you'll get gray, instead of green.

mix/3 will mix #0000FF and #FFFF00 as #808080

spectral_mix/3 will mix #0000FF and #FFFF00 as #388F54

This function uses the algorithm from the Spectral.js Github library rather than the more popular Mixbox library, as Mixbox is not FOSS and while its source code is open, it's licensing(CC BY-NC) is too restrictive for me to include it in this library.

In some quick tests I did, though they often both yield similar results, the Mixbox library does better overall, so if a CC BY-NC license works for your needs and you like this type of color mixing effect, you might want to checkout Mixbox(though no Elixir library is available to my knowledge).

Spectral.js - Lic: MIT

Mixbox - Lic: CC BY-NC

Here is a comparison of mix/3(on the left) and spectral_mix/3(on the right).

spectral mixing comparison 1
spectral mixing comparison 2
spectral mixing comparison 3

text_color(color, opts \\ [fast: true])

@spec text_color(
  color :: color(),
  opts :: [black: term(), white: term(), fast: boolean()]
) ::
  color() | term()

Attempts to find the best color to use for displaying text on top of the given color.

It will return a Colorex.Color.t() struct representing either black or white.

If you want a different value returned, for example "text-gray-100" and "text-gray-900", you can pass them in as options [black: "text-gray-900", white: "text-gray-100"]

## Example:

  iex> Colorex.text_color(Colorex.black())
  Colorex.parse!("#FFFFFF")
  iex> Colorex.text_color(Colorex.white(), white: "text-gray-100", black: "text-gray-900")
  "text-gray-900"

to_colorspace(color, colorspace)

@spec to_colorspace(color :: color(), :rgb | :hsl | :cmyk | :lab | :xyz) :: color()

Converts any color/colorspace to the colorspace of the given atom.

This function is basically the same as calling rgb/1, hsl/1, xyz/1, lab/1, or cmyk/1 except you can convert by using an atom rather than calling the specific function.

trunc_alpha(color)

Sets the alpha channel to 100% without accounting for background color

Examples:

  iex> Colorex.trunc_alpha(Colorex.parse!("#3355FF7F"))
  Colorex.parse!("#3355FF")

If you want the color to be resolved taking into account a background color, use flatten_alpha/1/flatten_alpha/2

update(color, key, fun)

@spec update(color :: color(), key :: color_key(), fun :: (number() -> number())) ::
  color()
@spec update(
  color :: color(),
  key :: color_key(),
  fun :: (number(), {number(), number()} -> number())
) :: color()

Update any attribute in any colorspace

Examples:

  iex> Colorex.parse!("#3355DD") |> Colorex.update(:lightness, & &1 * 1.3) |> Colorex.update(:blue, & &1 - 40) |> Colorex.update(:hue, & &1 + 15) |> Colorex.update(:saturation, & &1 * 1.1)
  Colorex.parse!("#7579C5")

There's also a 2-arity version which will receive {min, max} as the second argument.

So the following would bring the current color halfway between it's current red value, and the max red value.

  iex> Colorex.parse!("#3355DD") |> Colorex.update(:red, fn val, {_, max} -> val + ((max - val) / 2) end)

All values, except hue are clamped to their {min, max}.

  iex> Colorex.update(rgb_color, :red, fn _ -> 500 end) |> Map.get(:red)
  255
  iex> Colorex.update(hsl_color, :hue, fn _ -> 500 end) |> Map.get(:hue)
  140 # Hue is a 0-360 degree measurement, so 500 - 360 == 140

xyz(color)

@spec xyz(color()) :: Colorex.XYZ.t()

Returns a Colorex.XYZ struct which represents a color in the XYZ colorspace