Color (Color v0.4.0)

Copy Markdown

Top-level color conversion dispatch.

Every color struct in this library implements to_xyz/1 and from_xyz/1. convert/2 and convert/3 use Color.XYZ as the hub: any source color is converted to XYZ, then converted to the requested target color.

The color spaces currently supported are:

All struct modules also expose their own to_xyz/1 and from_xyz/1 functions directly if you want to skip dispatch.

Building colors with new/1 and new/2

new/1 and new/2 accept any of:

  • A color struct (passed through unchanged, second argument ignored).

  • A hex string ("#ff0000", "#f80", "#ff000080", with or without the leading #) or a CSS named color ("rebeccapurple").

  • A bare list of 3, 4, or 5 numbers, interpreted as the color space given by the optional second argument (default :srgb).

convert/2 and convert/3 accept the same inputs, so you can write Color.convert([1.0, 0.0, 0.0], Color.Lab) without first wrapping the list in an SRGB struct.

Color space argument

The second argument to new/2 (and the space implicitly passed through convert/2,3) is either a short atom or the module for a supported color space. The recognised aliases are:

Short atomModuleNotes
:srgbColor.SRGBdefault
:adobe_rgb / :adobeColor.AdobeRGB
:cmykColor.CMYK4 or 5 channels
:hslColor.HSLhue in [0, 1]
:hsvColor.HSVhue in [0, 1]
:hsluvColor.HSLuvhue in degrees, s/l in [0, 100]
:hpluvColor.HPLuvhue in degrees, s/l in [0, 100]
:labColor.LabD65
:lch / :lchabColor.LCHabD65
:luvColor.LuvD65
:lchuvColor.LCHuvD65
:oklabColor.OklabD65
:oklchColor.OklchD65
:xyzColor.XYZD65 / 2°
:xyy / :xyYColor.XyYD65 / 2°
:jzazbzColor.JzAzBz
:ictcpColor.ICtCpdefaults to transfer: :pq
:iptColor.IPT
:ycbcrColor.YCbCrdefaults to variant: :bt709
:cam16 / :cam16_ucsColor.CAM16UCSdefault viewing conditions

Color.RGB (linear, any working space) is intentionally not list-constructible via new/2 — use Color.convert([1.0, 1.0, 1.0], Color.RGB, :Rec2020) instead.

Validation rules for list inputs

Validation depends on the space. The table below is the complete specification:

CategorySpacesInteger form?Range check
Strict display:srgb, :adobe_rgb, :cmykYes — 0..255, scaled to [0.0, 1.0]Strict — each channel must be in its exact range
Strict unit cylindrical:hsl, :hsvNoStrict — all channels in [0.0, 1.0], hue wraps
Strict deg/percent cylindrical:hsluv, :hpluvNoStrict — h wraps, s and l must be in [0, 100]
Permissive 3-channel:lab, :luv, :oklab, :jzazbz, :ipt, :xyz, :xyy, :ictcp, :ycbcr, :cam16_ucsNoNone — accepts wide-gamut / HDR values; rejects NaN and ±∞
Permissive cylindrical:lch, :lchuv, :oklchNoNone — hue wraps to [0, 360), other channels unrestricted

Additional rules that apply across the board:

  • The list is always either 3 or 4 numbers, with 4 meaning "plus alpha". :cmyk additionally accepts 5 numbers (c, m, y, k, alpha).

  • In the strict display category, the list must be uniform: either all integers (assumed 0..255) or all floats (assumed [0.0, 1.0]). Mixing integers and floats is an error. Integer alpha is also assumed to be 0..255.

  • Integer form is only accepted for the three strict display RGB spaces. Every other space rejects integer lists with a clear error pointing to the float form.

  • Permissive validation accepts out-of-nominal-range values because wide-gamut and HDR sources legitimately exceed the textbook ranges (e.g. wide-gamut Lab can exceed ±128, HDR Oklab can exceed ±0.4, HDR XYZ can exceed Y = 1.0). It still rejects NaN and infinity.

  • Cylindrical hues are normalised, not errored. oklch [0.7, 0.2, 390.0] becomes h = 30.0, and oklch [0.7, 0.2, -45.0] becomes h = 315.0. The same applies to HSL/HSV hue in [0, 1].

  • CIE-tagged spaces default to D65 / 2° observer. If you need a different illuminant, construct the struct directly or use Color.XYZ.adapt/3 after the fact.

Alpha

Every color struct has an :alpha field. Alpha is passed straight through every conversion — it is never touched by the color math itself. The library's API assumes straight (unassociated) alpha. If you are working with pre-multiplied pixel data, un-premultiply before converting (see the "Pre-multiplied alpha" section below and Color.unpremultiply/1 / Color.premultiply/1) and re-multiply afterwards.

Pre-multiplied alpha

Pre-multiplied alpha matters only when a color space's transfer function is non-linear with respect to the RGB channels. In a pre-multiplied pixel (R·a, G·a, B·a, a), the stored channels are no longer the pixel's true color — they are the color scaled by its own coverage. Applying a non-linear operation like sRGB companding to R·a does not give you sRGB(R)·a because (R·a)^γ ≠ R^γ · a^γ.

Since this library's forward pipeline is always source → linear RGB → XYZ → target, and several stages contain non-linear steps (sRGB companding, Adobe RGB gamma, the PQ / HLG transfer functions, Lab's f(x), Oklab's cube root, CAM16's post-adaptation compression, etc.), converting a pre-multiplied color directly is incorrect and will produce subtly wrong output.

The rule is:

  1. If your color is straight-alpha: just call convert/2. The alpha value is carried through untouched.

  2. If your color is pre-multiplied: call Color.unpremultiply/1, then convert/2, then Color.premultiply/1 on the result if you still need pre-multiplied output.

premultiply/1 and unpremultiply/1 are only defined for Color.SRGB, Color.AdobeRGB and Color.RGB (linear), since pre-multiplication is only meaningful in spaces with an RGB tuple.

Summary

Types

Anything Color.new/1,2 accepts: a colour struct, a list of 3, 4 or 5 numbers, a hex string, a CSS named-colour string, or an atom naming a CSS colour. See Color.new/2 for the full set of rules.

A {:ok, color} or {:error, exception_struct} result. The error side is always one of the structured Color.*Error exceptions in lib/color/exceptions/.

t()

Any colour struct supported by the library. Used as the parameter type of convert/2,3,4, to_xyz/1, premultiply/1, unpremultiply/1, luminance/1, and similar.

A target colour space module — anything that can appear as the second argument to convert/2,3,4.

Functions

Returns true when value can be built into a color by Color.new/1, and false otherwise.

Converts a color to a different color space.

Converts a color to Color.RGB (linear) in the given working space with rendering-intent options.

Converts a list (or stream) of colors to the same target space.

Converts a list of colors to a Color.RGB target in the named working space, with rendering-intent options.

Returns true when value is actually a known CSS named color. Accepts atoms or strings; ignores underscores, hyphens, whitespace and case, so :misty_rose, "Misty Rose" and "MistyRose" all resolve to the same entry.

Compile-time guard that returns true when value has a shape that might be a color. The guard does a cheap structural check

Compile-time guard that returns true when value looks like it could be a CSS named color — that is, an atom or a binary. The guard does not check that the name is actually in the lookup table; for that use Color.css_name?/1.

Returns the WCAG 2.x relative luminance of a color — the CIE Y component of its linear sRGB, on the [0, 1] scale.

Builds a color struct from a variety of inputs.

Pre-multiplies a color's channels by its alpha.

Alias for luminance/1. Returns the WCAG 2.x relative luminance of a color, the same value Color.Contrast.relative_luminance/1 computes. The longer name is the canonical one because the bare luminance is ambiguous between absolute photometric luminance, perceived lightness, and the WCAG definition.

Sorts a list of colors by a perceptual criterion.

Serialises a colour as an ANSI SGR escape sequence for terminal output.

Serialises a color to a CSS Color Module Level 4 string.

Serialises a color to a hex string, converting it to sRGB first if necessary.

Converts any supported color to Color.XYZ.

Inverts premultiply/1. A color with nil alpha is returned unchanged; a color with alpha = 0.0 is returned unchanged (the original channels are unrecoverable and the alpha is authoritative).

Validates a transparency value from the union of forms used by Image.Color and its callers, returning an alpha as a float in [0.0, 1.0].

Types

input()

@type input() :: t() | [number()] | String.t() | atom()

Anything Color.new/1,2 accepts: a colour struct, a list of 3, 4 or 5 numbers, a hex string, a CSS named-colour string, or an atom naming a CSS colour. See Color.new/2 for the full set of rules.

result()

@type result() :: {:ok, t()} | {:error, Exception.t()}

A {:ok, color} or {:error, exception_struct} result. The error side is always one of the structured Color.*Error exceptions in lib/color/exceptions/.

t()

Any colour struct supported by the library. Used as the parameter type of convert/2,3,4, to_xyz/1, premultiply/1, unpremultiply/1, luminance/1, and similar.

target()

@type target() ::
  Color.SRGB
  | Color.AdobeRGB
  | Color.RGB
  | Color.Lab
  | Color.LCHab
  | Color.Luv
  | Color.LCHuv
  | Color.Oklab
  | Color.Oklch
  | Color.HSL
  | Color.HSV
  | Color.HSLuv
  | Color.HPLuv
  | Color.CMYK
  | Color.YCbCr
  | Color.JzAzBz
  | Color.ICtCp
  | Color.IPT
  | Color.CAM16UCS
  | Color.XYZ
  | Color.XyY

A target colour space module — anything that can appear as the second argument to convert/2,3,4.

Functions

color?(value)

@spec color?(any()) :: boolean()

Returns true when value can be built into a color by Color.new/1, and false otherwise.

This is a stricter, runtime version of is_color/1. It fully parses hex strings and looks up CSS names, so an unknown name returns false.

Arguments

  • value is anything accepted by new/1.

Examples

iex> Color.color?("red")
true

iex> Color.color?("#ff0000")
true

iex> Color.color?([255, 128, 0])
true

iex> Color.color?([1.0, 0, 0])   # mixed integers and floats
false

iex> Color.color?("notacolor")
false

iex> Color.color?(42)
false

convert(color, target)

@spec convert(input(), target()) :: result()

Converts a color to a different color space.

color may be anything accepted by new/1, including a bare list [r, g, b] or [r, g, b, a] (interpreted as sRGB), a hex string, a CSS named color, or a color struct.

For Color.RGB (linear, any working space) use convert/3 and pass the working-space atom as the third argument.

Arguments

  • color is any input accepted by new/1.

  • target is the target module (for example Color.Lab, Color.SRGB).

  • options is a keyword list — see below.

Options

  • :intent — the ICC rendering intent. One of:

    • :relative_colorimetric (default) — chromatically adapt the source white to the target white using Bradford. Out-of-gamut colors are not altered; any clipping is left to the caller or deferred to a later step. This matches the current default behaviour.

    • :absolute_colorimetricno chromatic adaptation. The source XYZ is handed to the target's from_xyz/1 verbatim. Use this when you want to preserve the exact XYZ values regardless of reference-white mismatch.

    • :perceptual — chromatically adapt and gamut-map so the result is inside the target's gamut (when the target has a gamut, i.e. an RGB working space). The gamut-mapping algorithm is CSS Color 4's Oklch binary search (same as Color.Gamut.to_gamut/3 with method: :oklch).

    • :saturation — currently an alias for :perceptual. Treated as a gamut-compressing intent. (A true "saturation" intent that preserves chroma at the cost of hue shift is deferred to a future version.)

  • :bpctrue to apply black point compensation after chromatic adaptation, false (default) to skip it. See Color.XYZ.apply_bpc/3.

  • :adaptation — the chromatic adaptation method used by :relative_colorimetric and :perceptual. One of :bradford (default), :xyz_scaling, :von_kries, :sharp, :cmccat2000, :cat02.

Returns

  • {:ok, %target{}} on success.

  • {:error, reason} if the conversion can't be performed.

Examples

iex> {:ok, lab} = Color.convert(%Color.SRGB{r: 1.0, g: 0.0, b: 0.0}, Color.Lab)
iex> {Float.round(lab.l, 2), Float.round(lab.a, 2), Float.round(lab.b, 2)}
{53.24, 80.09, 67.2}

iex> {:ok, lab} = Color.convert([1.0, 0.0, 0.0], Color.Lab)
iex> Float.round(lab.l, 2)
53.24

iex> {:ok, srgb} = Color.convert("#ff0000", Color.SRGB)
iex> {Float.round(srgb.r, 6), Float.round(srgb.g, 6), Float.round(srgb.b, 6)}
{1.0, 0.0, 0.0}

iex> {:ok, srgb} = Color.convert(%Color.Lab{l: 53.2408, a: 80.0925, b: 67.2032}, Color.SRGB)
iex> {Float.round(srgb.r, 3), Float.round(srgb.g, 3), Float.round(srgb.b, 3)}
{1.0, 0.0, 0.0}

iex> {:ok, c} = Color.convert([1.0, 0.0, 0.0, 0.5], Color.Lab)
iex> c.alpha
0.5

Wide-gamut Display P3 red gamut-mapped into sRGB via the :perceptual intent:

iex> p3_red = %Color.RGB{r: 1.0, g: 0.0, b: 0.0, working_space: :P3_D65}
iex> {:ok, mapped} = Color.convert(p3_red, Color.SRGB, intent: :perceptual)
iex> Color.Gamut.in_gamut?(mapped, :SRGB)
true

convert(color, target, working_space)

@spec convert(input(), target(), Color.Types.working_space() | keyword()) :: result()

convert(color, arg, working_space, options)

@spec convert(input(), Color.RGB, Color.Types.working_space(), keyword()) :: result()

Converts a color to Color.RGB (linear) in the given working space with rendering-intent options.

Arguments

  • color is any supported color struct or input accepted by new/1.

  • target must be Color.RGB.

  • working_space is an atom naming an RGB working space (for example :SRGB, :Adobe, :ProPhoto).

  • options is the same keyword list as convert/3, supporting :intent, :bpc, and :adaptation.

Returns

  • {:ok, %Color.RGB{}} on success.

Examples

iex> {:ok, rgb} = Color.convert(%Color.SRGB{r: 1.0, g: 1.0, b: 1.0}, Color.RGB, :SRGB)
iex> {Float.round(rgb.r, 3), Float.round(rgb.g, 3), Float.round(rgb.b, 3)}
{1.0, 1.0, 1.0}

Equivalent option-list form, which composes more cleanly with the rendering-intent options:

iex> {:ok, rgb} = Color.convert(%Color.SRGB{r: 1.0, g: 1.0, b: 1.0},
...>                            Color.RGB, working_space: :SRGB)
iex> {Float.round(rgb.r, 3), Float.round(rgb.g, 3), Float.round(rgb.b, 3)}
{1.0, 1.0, 1.0}

convert_many(colors, target, options \\ [])

@spec convert_many(Enumerable.t(), target(), keyword()) ::
  {:ok, [t()]} | {:error, Exception.t()}

Converts a list (or stream) of colors to the same target space.

This is the batch equivalent of convert/2,3,4. It is useful when you have many colors heading to the same destination — for example every pixel in a row, or every entry in a palette — and you want to amortise the per-call setup over the whole list.

The implementation:

  • Looks up the target's working-space matrix and chromatic adaptation matrix once.

  • Iterates the input list with a single fold, calling the underlying space's from_xyz/1 for each element.

  • Halts at the first {:error, _} and returns it.

Arguments

  • colors is a list, stream, or any other enumerable of inputs accepted by new/1.

  • target is the target color space module (or Color.RGB).

  • options is the same keyword list as convert/3. For Color.RGB targets pass working_space: (or use the convert_many/4 form).

Returns

  • {:ok, [color, ...]} with one entry per input.

  • {:error, exception} on the first failure.

Examples

iex> {:ok, [a, b, c]} = Color.convert_many(["red", "green", "blue"], Color.Lab)
iex> {Float.round(a.l, 1), Float.round(b.l, 1), Float.round(c.l, 1)}
{53.2, 46.2, 32.3}

iex> {:ok, list} = Color.convert_many([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
...>                                  Color.Oklab)
iex> length(list)
3

iex> {:ok, []} = Color.convert_many([], Color.Lab)

convert_many(colors, arg, working_space, options)

@spec convert_many(Enumerable.t(), Color.RGB, Color.Types.working_space(), keyword()) ::
  {:ok, [Color.RGB.t()]} | {:error, Exception.t()}

Converts a list of colors to a Color.RGB target in the named working space, with rendering-intent options.

Arguments

  • colors is an enumerable of inputs accepted by new/1.

  • target must be Color.RGB.

  • working_space is the working-space atom.

  • options is the same keyword list as convert/4.

Returns

  • {:ok, [%Color.RGB{}, ...]} on success.

Examples

iex> {:ok, list} = Color.convert_many(["red", "green", "blue"], Color.RGB, :SRGB)
iex> Enum.all?(list, &match?(%Color.RGB{working_space: :SRGB}, &1))
true

css_name?(value)

@spec css_name?(any()) :: boolean()

Returns true when value is actually a known CSS named color. Accepts atoms or strings; ignores underscores, hyphens, whitespace and case, so :misty_rose, "Misty Rose" and "MistyRose" all resolve to the same entry.

Arguments

  • value is an atom or string.

Examples

iex> Color.css_name?("rebeccapurple")
true

iex> Color.css_name?(:misty_rose)
true

iex> Color.css_name?("notacolor")
false

iex> Color.css_name?(nil)
false

is_color(value)

(macro)

Compile-time guard that returns true when value has a shape that might be a color. The guard does a cheap structural check:

  • any struct (tight coupling to the @color_structs list is not possible in a defguard because is_struct/2 only accepts a literal module).

  • a list of 3, 4 or 5 numbers — matches bare sRGB / CMYK lists.

  • a binary (hex or CSS name).

  • a non-boolean atom (CSS name).

For a strict check that the value is actually recognised, call Color.color?/1.

Arguments

  • value is anything.

Examples

iex> Color.is_color([1.0, 0.5, 0.0])
true

iex> Color.is_color("#ff0000")
true

iex> Color.is_color(:red)
true

iex> Color.is_color(42)
false

is_css_name(value)

(macro)

Compile-time guard that returns true when value looks like it could be a CSS named color — that is, an atom or a binary. The guard does not check that the name is actually in the lookup table; for that use Color.css_name?/1.

Arguments

  • value is anything.

Examples

iex> Color.is_css_name("red")
true

iex> Color.is_css_name(:misty_rose)
true

iex> Color.is_css_name(123)
false

luminance(color)

@spec luminance(input()) :: float()

Returns the WCAG 2.x relative luminance of a color — the CIE Y component of its linear sRGB, on the [0, 1] scale.

This is a convenience delegate to Color.Contrast.relative_luminance/1, exposed at the top level for discoverability since it's used all over the place (perceptual sort, accessibility checks, threshold-based luminance picking, HDR tone mapping, etc.).

Arguments

  • color is anything accepted by new/1.

Returns

  • A float in [0, 1].

Examples

iex> Color.luminance("white")
1.0

iex> Color.luminance("black")
0.0

iex> Float.round(Color.luminance("red"), 4)
0.2126

new(input, space \\ :srgb)

@spec new(input(), Color.Types.space() | target()) :: result()
@spec new(input(), atom() | module()) :: result()

Builds a color struct from a variety of inputs.

Arguments

  • input is one of:

    • A color struct — returned unchanged inside {:ok, struct}.

    • A bare list of numbers, interpreted as the color space selected by space (default :srgb). See the "List inputs" section below for the detailed rules.

    • A hex string ("#ff0000", "#ff000080", "#f80", or the same without the leading #).

    • A CSS named color string or atom ("rebeccapurple", :misty_rose, "Red").

  • space is the color space the list is in. One of the short atoms (:srgb, :adobe_rgb, :cmyk, :hsl, :hsv, :hsluv, :hpluv, :lab, :lch / :lchab, :luv, :lchuv, :oklab, :oklch, :xyz, :xyy, :jzazbz, :ictcp, :ipt, :ycbcr, :cam16_ucs) or the equivalent module (Color.SRGB, Color.Lab, …). Defaults to :srgb. Ignored for non-list inputs.

List inputs

Lists are always either 3 or 4 numbers (4 is a sRGB alpha channel, or an HSL/Lab/… alpha channel). :cmyk additionally accepts 4 or 5 numbers (c, m, y, k, and optionally alpha).

Display spaces (:srgb, :adobe_rgb, :cmyk, :hsl, :hsv, :hsluv, :hpluv) are strict:

  • All elements must be the same numeric type — either all integers or all floats. Mixing is an error.

  • Integer form is only accepted for :srgb, :adobe_rgb and :cmyk, where each channel is assumed to be in 0..255 and is normalised to [0.0, 1.0] internally.

  • Each channel is range-checked and a value outside the expected range is an error.

CIE / perceptual / HDR spaces (:lab, :lch, :luv, :lchuv, :oklab, :oklch, :xyz, :xyy, :jzazbz, :ictcp, :ipt, :ycbcr, :cam16_ucs) are permissive:

  • Floats only — integers are rejected with a clear error.

  • NaN and infinity are rejected.

  • Values outside the nominal range are not rejected, because wide-gamut and HDR inputs legitimately exceed the "textbook" ranges for these spaces.

For any cylindrical space (:lch, :lchuv, :oklch, :hsluv, :hpluv, :hsl, :hsv), hue values are normalised into the conventional range (not errored) — 370° becomes 10°, -45° becomes 315°, and so on.

Returns

  • {:ok, struct}.

  • {:error, reason} if the input can't be interpreted.

Examples

iex> {:ok, c} = Color.new([1.0, 0.5, 0.0])
iex> {c.r, c.g, c.b}
{1.0, 0.5, 0.0}

iex> {:ok, c} = Color.new([255, 128, 0])
iex> {c.r, Float.round(c.g, 4), c.b}
{1.0, 0.502, 0.0}

iex> {:ok, c} = Color.new([53.24, 80.09, 67.2], :lab)
iex> c.illuminant
:D65

iex> {:ok, c} = Color.new([0.63, 0.22, 0.13], :oklab)
iex> c.__struct__
Color.Oklab

iex> {:ok, c} = Color.new([0.7, 0.2, 30.0], :oklch)
iex> c.h
30.0

iex> {:ok, c} = Color.new([0.7, 0.2, -45.0], :oklch)
iex> c.h
315.0

iex> {:ok, c} = Color.new([0.0, 0.5, 1.0, 0.0], :cmyk)
iex> c.c
0.0

iex> {:error, %Color.InvalidComponentError{reason: :mixed_types}} = Color.new([1.0, 0, 0])

iex> {:error, %Color.InvalidComponentError{reason: :out_of_range, range: {0, 255}}} =
...>   Color.new([300, 0, 0])

iex> {:error, %Color.InvalidComponentError{reason: :integers_not_allowed, space: "Lab"}} =
...>   Color.new([1, 2, 3], :lab)

premultiply(c)

Pre-multiplies a color's channels by its alpha.

Only supported for RGB-tuple color spaces (Color.SRGB, Color.AdobeRGB, Color.RGB) since pre-multiplication is not meaningful for opponent or cylindrical spaces. A color with nil alpha is treated as fully opaque (alpha = 1.0) and is returned unchanged.

Arguments

  • color is an RGB-tuple color struct.

Returns

  • A new color struct of the same type with pre-multiplied channels.

Examples

iex> Color.premultiply(%Color.SRGB{r: 1.0, g: 0.5, b: 0.25, alpha: 0.5})
%Color.SRGB{r: 0.5, g: 0.25, b: 0.125, alpha: 0.5}

iex> Color.premultiply(%Color.SRGB{r: 1.0, g: 0.5, b: 0.25})
%Color.SRGB{r: 1.0, g: 0.5, b: 0.25, alpha: nil}

relative_luminance(color)

@spec relative_luminance(input()) :: float()

Alias for luminance/1. Returns the WCAG 2.x relative luminance of a color, the same value Color.Contrast.relative_luminance/1 computes. The longer name is the canonical one because the bare luminance is ambiguous between absolute photometric luminance, perceived lightness, and the WCAG definition.

Examples

iex> Color.relative_luminance("white")
1.0

iex> Color.relative_luminance("black")
0.0

sort(colors, options \\ [])

@spec sort(
  [input()],
  keyword()
) :: {:ok, [Color.SRGB.t()]} | {:error, Exception.t()}

Sorts a list of colors by a perceptual criterion.

Arguments

  • colors is a list of anything accepted by new/1.

  • options is a keyword list.

Options

  • :by selects the sort key. One of:

    • :luminance — WCAG relative luminance (default, dark → light).

    • :lightness — CIE Lab L* (dark → light).

    • :oklab_l — Oklab L (dark → light).

    • :chroma — CIELCh C* (grey → saturated).

    • :oklch_c — Oklch C (grey → saturated).

    • :hue — CIELCh hue in degrees (red → red, around the wheel).

    • :oklch_h — Oklch hue in degrees.

    • :hlv — the HLV hue/luminance/value bucketing from https://www.alanzucconi.com/2015/09/30/colour-sorting/ (good for palette display).

    • A 1-arity function that maps a color struct to a comparable sort key.

  • :order:asc (default) or :desc.

Returns

  • {:ok, sorted_colors} with each element as a Color.SRGB struct.

  • {:error, reason} if any input can't be parsed.

Examples

iex> {:ok, sorted} = Color.sort(["white", "black", "#888"], by: :luminance)
iex> Enum.map(sorted, &Color.SRGB.to_hex/1)
["#000000", "#888888", "#ffffff"]

iex> {:ok, sorted} = Color.sort(["red", "green", "blue"], by: :hue)
iex> Enum.map(sorted, &Color.SRGB.to_hex/1) |> length()
3

to_ansi(color, options \\ [])

@spec to_ansi(
  input(),
  keyword()
) :: String.t()

Serialises a colour as an ANSI SGR escape sequence for terminal output.

Accepts any input Color.new/1 accepts and delegates to Color.ANSI.to_string/2.

Arguments

  • color is any input accepted by new/1.

  • options is a keyword list. See Color.ANSI.to_string/2 for the full set of options:

    • :mode:truecolor (default), :ansi256, or :ansi16.

    • :layer:foreground (default) or :background.

Returns

  • A binary string containing the escape sequence.

Examples

iex> Color.to_ansi("red") == "\e[38;2;255;0;0m"
true

iex> Color.to_ansi("red", mode: :ansi256) == "\e[38;5;196m"
true

iex> Color.to_ansi("red", mode: :ansi16) == "\e[91m"
true

iex> Color.to_ansi("red", layer: :background) == "\e[48;2;255;0;0m"
true

iex> Color.to_ansi(%Color.Lab{l: 53.2408, a: 80.0925, b: 67.2032}) == "\e[38;2;255;0;0m"
true

to_css(color, options \\ [])

@spec to_css(
  input(),
  keyword()
) :: String.t()

Serialises a color to a CSS Color Module Level 4 string.

Accepts any input that Color.new/1 accepts — a color struct, a bare list, a hex string, a CSS named color, or an atom. String and list inputs are normalised to the appropriate colour space first.

The default serialiser form follows the resulting struct type:

  • Color.SRGBrgb(r g b / a)

  • Color.HSLhsl(h s% l% / a)

  • Color.Lablab(L% a b / a)

  • Color.LCHablch(L% C h / a)

  • Color.Oklaboklab(L% a b / a)

  • Color.Oklchoklch(L% C h / a)

  • Color.XYZcolor(xyz-d65 X Y Z / a) (or xyz-d50 for a D50-tagged struct)

  • Color.AdobeRGBcolor(a98-rgb r g b / a)

  • Color.RGBcolor(<working-space> r g b / a) when the working space has a CSS Color 4 name, otherwise color(srgb-linear …)

  • Any other supported colour space is converted to Color.SRGB first and emitted as rgb(…).

Arguments

  • color is any input accepted by new/1.

  • options is a keyword list. :as overrides the default form for RGB-family colours (:rgb, :hex, :color).

Returns

  • A string.

Examples

iex> Color.to_css("#ff0000")
"rgb(255 0 0)"

iex> Color.to_css(%Color.SRGB{r: 1.0, g: 0.0, b: 0.0, alpha: 0.5})
"rgb(255 0 0 / 0.5)"

iex> Color.to_css("rebeccapurple")
"rgb(102 51 153)"

iex> Color.to_css(%Color.Lab{l: 50.0, a: 40.0, b: 30.0})
"lab(50% 40 30)"

iex> Color.to_css(%Color.Oklch{l: 0.7, c: 0.15, h: 180.0})
"oklch(70% 0.15 180)"

iex> Color.to_css(%Color.SRGB{r: 1.0, g: 0.0, b: 0.0}, as: :hex)
"#ff0000"

to_hex(color)

@spec to_hex(input()) :: String.t()

Serialises a color to a hex string, converting it to sRGB first if necessary.

Accepts any input that Color.new/1 accepts. The output form follows the sRGB alpha: opaque colours return #rrggbb, translucent colours return #rrggbbaa.

Arguments

  • color is any input accepted by new/1.

Returns

  • A string starting with #.

Examples

iex> Color.to_hex(%Color.SRGB{r: 1.0, g: 0.0, b: 0.0})
"#ff0000"

iex> Color.to_hex("rebeccapurple")
"#663399"

iex> Color.to_hex(%Color.Lab{l: 53.2408, a: 80.0925, b: 67.2032})
"#ff0000"

iex> Color.to_hex(%Color.Oklch{l: 0.7, c: 0.15, h: 30.0})
"#ed7664"

iex> Color.to_hex(%Color.SRGB{r: 1.0, g: 0.0, b: 0.0, alpha: 0.5})
"#ff000080"

to_xyz(xyz)

@spec to_xyz(t()) :: {:ok, Color.XYZ.t()} | {:error, Exception.t()}

Converts any supported color to Color.XYZ.

Arguments

  • color is any supported color struct.

Returns

  • {:ok, %Color.XYZ{}}.

unpremultiply(c)

Inverts premultiply/1. A color with nil alpha is returned unchanged; a color with alpha = 0.0 is returned unchanged (the original channels are unrecoverable and the alpha is authoritative).

Arguments

  • color is an RGB-tuple color struct.

Returns

  • A new color struct of the same type with un-pre-multiplied channels.

Examples

iex> Color.unpremultiply(%Color.SRGB{r: 0.5, g: 0.25, b: 0.125, alpha: 0.5})
%Color.SRGB{r: 1.0, g: 0.5, b: 0.25, alpha: 0.5}

iex> Color.unpremultiply(%Color.SRGB{r: 0.0, g: 0.0, b: 0.0, alpha: 0.0})
%Color.SRGB{r: 0.0, g: 0.0, b: 0.0, alpha: 0.0}

validate_transparency(int)

@spec validate_transparency(any()) :: {:ok, float()} | {:error, Exception.t()}

Validates a transparency value from the union of forms used by Image.Color and its callers, returning an alpha as a float in [0.0, 1.0].

Accepts:

  • :transparent / :none0.0 (fully transparent).

  • :opaque1.0 (fully opaque).

  • an integer in 0..255 — scaled by 1/255.

  • a float in [0.0, 1.0] — returned unchanged.

Arguments

  • value is any of the above.

Returns

  • {:ok, float} in [0.0, 1.0].

  • {:error, %Color.InvalidComponentError{}} otherwise.

Examples

iex> Color.validate_transparency(:transparent)
{:ok, 0.0}

iex> Color.validate_transparency(:opaque)
{:ok, 1.0}

iex> Color.validate_transparency(128)
{:ok, 0.5019607843137255}

iex> Color.validate_transparency(0.75)
{:ok, 0.75}

iex> {:error, %Color.InvalidComponentError{}} = Color.validate_transparency(:maybe)

iex> {:error, %Color.InvalidComponentError{}} = Color.validate_transparency(300)