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
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).
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].
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
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).
@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
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}
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}
Returns a brighter copy of the color.
k=1 is one "unit" brighter (default). k=2 is two units, etc.
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"}
@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}
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"}}
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).
iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.darken(0.3)
{:darken, {:color, "red"}, 0.3}
Returns a darker copy of the color.
k=1 is one "unit" darker (default). k=2 is two units, etc.
Δ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
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.
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
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).
iex> Macaw.Layers.Color.color(:blue) |> Macaw.Layers.Color.intensity(0.65)
{:intensity, {:color, "blue"}, 0.65}
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).
iex> Macaw.Layers.Color.color(:red) |> Macaw.Layers.Color.lighten(0.3)
{:lighten, {:color, "red"}, 0.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.
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
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.
iex> Enum.map([:info, "series_3", "Info", "3d", "", nil], &Macaw.Layers.Color.name?/1)
[true, true, false, false, false, false]
Gets a named color's hex value
@spec named_colors() :: [String.t()]
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.
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.
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}
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}
@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"
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).
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}
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}
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}
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)"
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.
iex> %{l: l} = Macaw.Layers.Color.to_oklab(Macaw.Layers.Color.parse("#ffffff"))
iex> Float.round(l, 4)
1.0
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.
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]
Returns a copy with the specified opacity