Macaw.Layers.Color (macaw_layers v0.3.0)

Copy Markdown View Source

Colors (spec/04 §1): parsing, construction in RGB, HSL, Lab, HCL, OKLab and OKLCH, conversion, manipulation and interpolation.

color = Macaw.Layers.Color.parse("steelblue")
color |> Macaw.Layers.Color.darker() |> Macaw.Layers.Color.to_hex()
#=> "#044279"

A literal color is the term %Macaw.Layers.Color{}, every field a float in its range (literal?/1), whatever constructor built it. CSS text is an encoding of one, read and written at the edges (spec/04 §1.6): parse!/1 and ~RGB"#3b6fa8" in, to_css/1 out, and Macaw.Layers.Color.Form for JSON.

parse/1 reads untrusted text and answers nil for anything it does not read; it never raises. Lab is CIE L*a*b* against D50 with d3-color's constants, so lab/4 of to_lab/1 is the color again; OKLab is Ottosson's, and a color built from OKLab or OKLCH is brought into sRGB by CSS Color 4's gamut mapping (Macaw.Layers.Color.Ok). Macaw.Layers.Color.Vision simulates the dichromacies.

Ported from visualize's Visualize.Color and Visualize.Color.OK.

Summary

Types

t()

A literal color (spec/04 §1.1): every field a float, r, g and b in 0..255 and opacity in 0..1, as every constructor builds it (literal?/1).

A color as a design names it (spec/04 §1.3): a name of the theme's color table, a series or a ramp position, a literal color (t/0), or a transform of colors. A string is never a color.

Functions

The color c at opacity a in 0..1, whatever its opacity was (spec/04 §1.3).

The colors mixed in OKLab, each weighted by its intensity/2 (1 without one), as CSS's color-mix mixes them: weights summing below 1 leave the remainder transparent, and above 1 are scaled to sum to 1 (spec/04 §1.3).

Returns a brighter copy of the color.

The color name of the theme's color table (spec/04 §1.3, §2.2). A name is a string, as every name of a theme is (§2.1): one given as an atom is read as its string, so the value is the one a theme holds and its JSON form reads back.

The readable ink over whatever the text is drawn on (spec/04 §3.2): a mark's inline label inks against its element's fill, any other text against the theme's background.

The readable ink over c: the theme's text or background, else black or white, whichever contrasts more (spec/04 §1.4, §3.2).

Returns the contrast ratio between two colors (1-21)

The color c moved toward black by t in 0..1, in OKLCH lightness (spec/04 §1.3).

Returns a darker copy of the color.

ΔEOK, the distance between two colors in OKLab, their opacities aside.

Returns a desaturated copy (in HSL space)

Whether the color is 8-bit exact: whole channels, and an opacity that is a whole number of 255ths, so a hex literal writes it with nothing lost.

Returns a grayscale version of the color

Creates a color from HCL values (h: 0-360, c: 0-~230, l: 0-100)

Creates a color from HSL values (h: 0-360, s: 0-100, l: 0-100)

How much of the color c is present, t in 0..1; the rest is transparent. On its own it is c at t of its alpha; in blend/1 it is the color's weight (spec/04 §1.3).

Interpolates between two colors in HCL space (perceptually uniform)

Interpolates between two colors in HSL space

Interpolates between two colors in Lab space

Interpolates between two colors in RGB space

Inverts the color

Creates a color from Lab values (l: 0-100, a: -128 to 127, b: -128 to 127)

The color c moved toward white by t in 0..1, in OKLCH lightness (spec/04 §1.3).

Whether a term is a literal color (spec/04 §1.1): a %Macaw.Layers.Color{} of the canonical shape every constructor builds, each field a float in its range. A struct written by hand with integer fields, or out of range, is not one.

Returns the relative luminance (0-1)

Mixes two colors equally (in RGB space)

Whether a term is a name a theme may give a color, a ramp, a style or itself (spec/04 §1.1, §2.1, §2.2, §6): lower-case letters, digits and _, starting with a letter, as an atom or a string. It is the grammar every name meets on its way into a stylesheet (--mw-<name>, .mw-theme-<name>), so no name can break out of one.

Gets a named color's hex value

Returns all named color names

A color from OKLab, brought into sRGB by CSS Color 4's gamut mapping (spec/04 §1.2). Channels are left unrounded.

A color from OKLCH (h in degrees), gamut-mapped as oklab/4 is.

Parses a color string into a Color struct.

The color text spells, as parse/1 reads it (spec/04 §1.6): the input edge for text a program holds. Text parse/1 does not read raises ArgumentError naming it.

The color at t in 0..1 along the theme's ramp name (spec/04 §2.3), the name a string as color/1's is.

A color value resolved against a theme in mode (spec/04 §1.4, §3.2): a name, a series or a ramp position exactly as Macaw.Layers.Theme.resolve/3 resolves the slot, a literal as itself, and a transform as the literal it computes to, quantized to 8 bits. :literal answers a literal color, :css CSS text (to_css/1 for a literal).

Creates a color from RGB values (0-255)

Rotates the hue by the specified degrees

On a %Macaw.Layers.Color{}, a saturated copy: HSL saturation multiplied by amount (default 1.2).

The step n of c's scale, n in 0..1000 as designers number them (spec/04 §1.3): 500 is c, 0 near white and 1000 near black, on one OKLCH curve that keeps the hue and eases the chroma toward the ends. A step the theme pins of a name is the pinned color.

A literal color written in the source, ~RGB"#3b6fa8" (spec/04 §1.6): the text is read by parse/1 when the code compiles and the color is compiled in, so text it does not read is a CompileError there, never a fault at run time. Import it with import Macaw.Layers.Color, only: [sigil_RGB: 2].

The color as CSS writes it (spec/04 §1.6), the output edge: #rrggbb, or #rrggbbaa when translucent, where the color is 8-bit exact (exact?/1), else rgb(r g b / a) with each number in its shortest exact form. parse/1 reads either back to the color.

Converts to HCL map %{h: h, c: c, l: l, opacity: opacity}

Converts to hex string

Converts to HSL map %{h: h, s: s, l: l, opacity: a}

Converts to Lab map %{l: l, a: a, b: b, opacity: opacity}

The color in OKLab (spec/04 §1.2): %{l, a, b, opacity}, l in 0..1.

The color in OKLCH (spec/04 §1.2): %{l, c, h, opacity}, h in degrees [0, 360).

Converts to RGB tuple {r, g, b}

Converts to RGBA tuple {r, g, b, a}

Converts to CSS string

Whether a term is a color value (spec/04 §1.3), every operand of a transform included. A literal is the canonical %Macaw.Layers.Color{} (literal?/1); a string is never a color.

Returns a copy with the specified opacity

Types

t()

@type t() :: %Macaw.Layers.Color{b: float(), g: float(), opacity: float(), r: float()}

A literal color (spec/04 §1.1): every field a float, r, g and b in 0..255 and opacity in 0..1, as every constructor builds it (literal?/1).

value()

@type value() ::
  t()
  | {:color, atom() | String.t()}
  | {:series, pos_integer()}
  | {:ramp, atom(), number()}
  | {:intensity | :alpha | :lighten | :darken | :saturate | :shade, value(),
     number()}
  | {:blend, [value(), ...]}
  | {:contrast, value() | :under}

A color as a design names it (spec/04 §1.3): a name of the theme's color table, a series or a ramp position, a literal color (t/0), or a transform of colors. A string is never a color.

Functions

alpha(c, a)

@spec alpha(value(), number()) :: value()

The color c at opacity a in 0..1, whatever its opacity was (spec/04 §1.3).

iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.alpha(0.5)
{:alpha, {:color, "red"}, 0.5}

blend(colors)

@spec blend([value(), ...]) :: value()

The colors mixed in OKLab, each weighted by its intensity/2 (1 without one), as CSS's color-mix mixes them: weights summing below 1 leave the remainder transparent, and above 1 are scaled to sum to 1 (spec/04 §1.3).

iex> import Macaw.Layers.Color, only: [color: 1, intensity: 2, blend: 1, alpha: 2]
iex> [color(:blue) |> intensity(0.65), color(:red) |> intensity(0.3)] |> blend() |> alpha(0.65)
{:alpha, {:blend, [{:intensity, {:color, "blue"}, 0.65}, {:intensity, {:color, "red"}, 0.3}]}, 0.65}

brighter(color, k \\ 1)

@spec brighter(t(), number()) :: t()

Returns a brighter copy of the color.

k=1 is one "unit" brighter (default). k=2 is two units, etc.

color(name)

@spec color(atom() | String.t()) :: value()

The color name of the theme's color table (spec/04 §1.3, §2.2). A name is a string, as every name of a theme is (§2.1): one given as an atom is read as its string, so the value is the one a theme holds and its JSON form reads back.

iex> Macaw.Layers.Color.color(:red)
{:color, "red"}

contrast()

@spec contrast() :: value()

The readable ink over whatever the text is drawn on (spec/04 §3.2): a mark's inline label inks against its element's fill, any other text against the theme's background.

iex> Macaw.Layers.Color.contrast()
{:contrast, :under}

contrast(c)

@spec contrast(value()) :: value()

The readable ink over c: the theme's text or background, else black or white, whichever contrasts more (spec/04 §1.4, §3.2).

iex> Macaw.Layers.Color.color(:bad) |> Macaw.Layers.Color.contrast()
{:contrast, {:color, "bad"}}

contrast(c1, c2)

@spec contrast(t(), t()) :: float()

Returns the contrast ratio between two colors (1-21)

darken(c, t)

@spec darken(value(), number()) :: value()

The color c moved toward black by t in 0..1, in OKLCH lightness (spec/04 §1.3).

iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.darken(0.3)
{:darken, {:color, "red"}, 0.3}

darker(color, k \\ 1)

@spec darker(t(), number()) :: t()

Returns a darker copy of the color.

k=1 is one "unit" darker (default). k=2 is two units, etc.

delta_eok(c1, c2)

@spec delta_eok(t(), t()) :: float()

ΔEOK, the distance between two colors in OKLab, their opacities aside.

iex> Macaw.Layers.Color.delta_eok(Macaw.Layers.Color.parse("#fff"), Macaw.Layers.Color.parse("#000"))
...> |> Float.round(6)
1.0

desaturate(color, amount \\ 0.8)

@spec desaturate(t(), number()) :: t()

Returns a desaturated copy (in HSL space)

exact?(color)

@spec exact?(t()) :: boolean()

Whether the color is 8-bit exact: whole channels, and an opacity that is a whole number of 255ths, so a hex literal writes it with nothing lost.

iex> Macaw.Layers.Color.exact?(Macaw.Layers.Color.parse("#3b6fa880"))
true
iex> Macaw.Layers.Color.exact?(Macaw.Layers.Color.rgb(59.5, 111, 168))
false

grayscale(color)

@spec grayscale(t()) :: t()

Returns a grayscale version of the color

hcl(h, c, l, opacity \\ 1.0)

@spec hcl(number(), number(), number(), number()) :: t()

Creates a color from HCL values (h: 0-360, c: 0-~230, l: 0-100)

hsl(h, s, l, opacity \\ 1.0)

@spec hsl(number(), number(), number(), number()) :: t()

Creates a color from HSL values (h: 0-360, s: 0-100, l: 0-100)

intensity(c, t)

@spec intensity(value(), number()) :: value()

How much of the color c is present, t in 0..1; the rest is transparent. On its own it is c at t of its alpha; in blend/1 it is the color's weight (spec/04 §1.3).

iex> Macaw.Layers.Color.color(:blue) |> Macaw.Layers.Color.intensity(0.65)
{:intensity, {:color, "blue"}, 0.65}

interpolate_hcl(c1, c2, t)

@spec interpolate_hcl(t(), t(), number()) :: t()

Interpolates between two colors in HCL space (perceptually uniform)

interpolate_hsl(c1, c2, t)

@spec interpolate_hsl(t(), t(), number()) :: t()

Interpolates between two colors in HSL space

interpolate_lab(c1, c2, t)

@spec interpolate_lab(t(), t(), number()) :: t()

Interpolates between two colors in Lab space

interpolate_rgb(c1, c2, t)

@spec interpolate_rgb(t(), t(), number()) :: t()

Interpolates between two colors in RGB space

invert(color)

@spec invert(t()) :: t()

Inverts the color

lab(l, a, b, opacity \\ 1.0)

@spec lab(number(), number(), number(), number()) :: t()

Creates a color from Lab values (l: 0-100, a: -128 to 127, b: -128 to 127)

lighten(c, t)

@spec lighten(value(), number()) :: value()

The color c moved toward white by t in 0..1, in OKLCH lightness (spec/04 §1.3).

iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.lighten(0.3)
{:lighten, {:color, "red"}, 0.3}

literal?(color)

@spec literal?(term()) :: boolean()

Whether a term is a literal color (spec/04 §1.1): a %Macaw.Layers.Color{} of the canonical shape every constructor builds, each field a float in its range. A struct written by hand with integer fields, or out of range, is not one.

iex> Macaw.Layers.Color.literal?(Macaw.Layers.Color.rgb(59, 111, 168))
true
iex> Macaw.Layers.Color.literal?(%Macaw.Layers.Color{r: 59, g: 111, b: 168, opacity: 1})
false

luminance(color)

@spec luminance(t()) :: float()

Returns the relative luminance (0-1)

mix(c1, c2)

@spec mix(t(), t()) :: t()

Mixes two colors equally (in RGB space)

name?(name)

@spec name?(term()) :: boolean()

Whether a term is a name a theme may give a color, a ramp, a style or itself (spec/04 §1.1, §2.1, §2.2, §6): lower-case letters, digits and _, starting with a letter, as an atom or a string. It is the grammar every name meets on its way into a stylesheet (--mw-<name>, .mw-theme-<name>), so no name can break out of one.

iex> Enum.map([:info, "series_3", "Info", "3d", "", nil], &Macaw.Layers.Color.name?/1)
[true, true, false, false, false, false]

named(name)

@spec named(String.t()) :: String.t() | nil

Gets a named color's hex value

named_colors()

@spec named_colors() :: [String.t()]

Returns all named color names

oklab(l, a, b, opacity \\ 1.0)

@spec oklab(number(), number(), number(), number()) :: t()

A color from OKLab, brought into sRGB by CSS Color 4's gamut mapping (spec/04 §1.2). Channels are left unrounded.

oklch(l, c, h, opacity \\ 1.0)

@spec oklch(number(), number(), number(), number()) :: t()

A color from OKLCH (h in degrees), gamut-mapped as oklab/4 is.

parse(string)

@spec parse(term()) :: t() | nil

Parses a color string into a Color struct.

Supports:

  • Hex: "#rgb", "#rrggbb", "#rgba", "#rrggbbaa"
  • RGB: "rgb(r, g, b)", "rgba(r, g, b, a)"
  • HSL: "hsl(h, s%, l%)", "hsla(h, s%, l%, a)"
  • Named colors: "steelblue", "red", etc.

parse!(text)

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

The color text spells, as parse/1 reads it (spec/04 §1.6): the input edge for text a program holds. Text parse/1 does not read raises ArgumentError naming it.

iex> Macaw.Layers.Color.parse!("#3b6fa8")
%Macaw.Layers.Color{r: 59.0, g: 111.0, b: 168.0, opacity: 1.0}

ramp(name, t)

@spec ramp(atom() | String.t(), number()) :: value()

The color at t in 0..1 along the theme's ramp name (spec/04 §2.3), the name a string as color/1's is.

iex> Macaw.Layers.Color.ramp(:thermal, 0.7)
{:ramp, "thermal", 0.7}

resolve(value, theme, mode)

@spec resolve(value() | atom(), Macaw.Layers.Theme.t(), Macaw.Layers.Theme.mode()) ::
  t() | String.t()

A color value resolved against a theme in mode (spec/04 §1.4, §3.2): a name, a series or a ramp position exactly as Macaw.Layers.Theme.resolve/3 resolves the slot, a literal as itself, and a transform as the literal it computes to, quantized to 8 bits. :literal answers a literal color, :css CSS text (to_css/1 for a literal).

iex> theme = Macaw.Layers.Theme.default()
iex> Macaw.Layers.Color.resolve(Macaw.Layers.Color.color(:axis), theme, :css)
"var(--mw-axis, #666666)"
iex> red = Macaw.Layers.Color.parse!("#ff0000")
iex> Macaw.Layers.Color.resolve({:intensity, red, 0.5}, theme, :literal)
%Macaw.Layers.Color{r: 255.0, g: 0.0, b: 0.0, opacity: 128 / 255}
iex> Macaw.Layers.Color.resolve({:intensity, red, 0.5}, theme, :literal) |> Macaw.Layers.Color.to_css()
"#ff000080"

rgb(r, g, b, opacity \\ 1.0)

@spec rgb(number(), number(), number(), number()) :: t()

Creates a color from RGB values (0-255)

rotate(color, degrees)

@spec rotate(t(), number()) :: t()

Rotates the hue by the specified degrees

saturate(color, amount \\ 1.2)

@spec saturate(t(), number()) :: t()
@spec saturate(value(), number()) :: value()

On a %Macaw.Layers.Color{}, a saturated copy: HSL saturation multiplied by amount (default 1.2).

On any other color value (spec/04 §1.3), the transform {:saturate, c, t}: chroma moved toward full in OKLCH by t in 0..1, resolved against the theme. A literal keeps the first meaning, so the transform of a literal is written as its tuple, {:saturate, literal, t}.

iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.saturate(0.2)
{:saturate, {:color, "red"}, 0.2}

shade(c, n)

@spec shade(value(), number()) :: value()

The step n of c's scale, n in 0..1000 as designers number them (spec/04 §1.3): 500 is c, 0 near white and 1000 near black, on one OKLCH curve that keeps the hue and eases the chroma toward the ends. A step the theme pins of a name is the pinned color.

iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.shade(100)
{:shade, {:color, "red"}, 100}

sigil_RGB(arg1, modifiers)

(macro)

A literal color written in the source, ~RGB"#3b6fa8" (spec/04 §1.6): the text is read by parse/1 when the code compiles and the color is compiled in, so text it does not read is a CompileError there, never a fault at run time. Import it with import Macaw.Layers.Color, only: [sigil_RGB: 2].

iex> import Macaw.Layers.Color, only: [sigil_RGB: 2]
iex> ~RGB"steelblue"
%Macaw.Layers.Color{r: 70.0, g: 130.0, b: 180.0, opacity: 1.0}

to_css(color)

@spec to_css(t()) :: String.t()

The color as CSS writes it (spec/04 §1.6), the output edge: #rrggbb, or #rrggbbaa when translucent, where the color is 8-bit exact (exact?/1), else rgb(r g b / a) with each number in its shortest exact form. parse/1 reads either back to the color.

iex> Macaw.Layers.Color.to_css(Macaw.Layers.Color.rgb(59, 111, 168))
"#3b6fa8"
iex> Macaw.Layers.Color.to_css(Macaw.Layers.Color.rgb(59.5, 111, 168, 0.5))
"rgb(59.5 111.0 168.0 / 0.5)"

to_hcl(color)

@spec to_hcl(t()) :: %{h: float(), c: float(), l: float(), opacity: float()}

Converts to HCL map %{h: h, c: c, l: l, opacity: opacity}

to_hex(color)

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

Converts to hex string

to_hsl(color)

@spec to_hsl(t()) :: %{h: float(), s: float(), l: float(), opacity: float()}

Converts to HSL map %{h: h, s: s, l: l, opacity: a}

to_lab(color)

@spec to_lab(t()) :: %{l: float(), a: float(), b: float(), opacity: float()}

Converts to Lab map %{l: l, a: a, b: b, opacity: opacity}

to_oklab(color)

@spec to_oklab(t()) :: %{l: float(), a: float(), b: float(), opacity: number()}

The color in OKLab (spec/04 §1.2): %{l, a, b, opacity}, l in 0..1.

iex> %{l: l} = Macaw.Layers.Color.to_oklab(Macaw.Layers.Color.parse("#ffffff"))
iex> Float.round(l, 4)
1.0

to_oklch(color)

@spec to_oklch(t()) :: %{l: float(), c: float(), h: float(), opacity: number()}

The color in OKLCH (spec/04 §1.2): %{l, c, h, opacity}, h in degrees [0, 360).

to_rgb(color)

@spec to_rgb(t()) :: {integer(), integer(), integer()}

Converts to RGB tuple {r, g, b}

to_rgba(color)

@spec to_rgba(t()) :: {integer(), integer(), integer(), float()}

Converts to RGBA tuple {r, g, b, a}

to_string(color)

@spec to_string(t()) :: String.t()

Converts to CSS string

value?(term)

@spec value?(term()) :: boolean()

Whether a term is a color value (spec/04 §1.3), every operand of a transform included. A literal is the canonical %Macaw.Layers.Color{} (literal?/1); a string is never a color.

iex> Macaw.Layers.Color.value?({:alpha, {:color, :red}, 0.5})
true
iex> Macaw.Layers.Color.value?({:alpha, Macaw.Layers.Color.parse!("#3b6fa8"), 0.5})
true
iex> Enum.map([:red, "#3b6fa8", {:alpha, "#3b6fa8", 0.5}], &Macaw.Layers.Color.value?/1)
[false, false, false]

with_opacity(color, opacity)

@spec with_opacity(t(), number()) :: t()

Returns a copy with the specified opacity