Macaw.Layers.Color (macaw_layers v0.2.2)

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"

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 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 string, or a transform of colors.

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): the library's names are atoms, and a color a theme declares may be named by its string.

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)

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).

Returns the relative luminance (0-1)

Mixes two colors equally (in RGB space)

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 at t in 0..1 along the theme's ramp name (spec/04 §2.3).

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 written, and a transform as the literal it computes to, #rrggbb or #rrggbbaa.

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.

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.

Returns a copy with the specified opacity

Types

t()

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

value()

@type value() ::
  String.t()
  | {:color, atom()}
  | {: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 string, or a transform of colors.

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): the library's names are atoms, and a color a theme declares may be named by its string.

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)

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}

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)

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.

ramp(name, t)

@spec ramp(atom(), number()) :: value()

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

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

resolve(value, theme, mode)

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 written, and a transform as the literal it computes to, #rrggbb or #rrggbbaa.

iex> theme = Macaw.Layers.Theme.default()
iex> Macaw.Layers.Color.resolve(Macaw.Layers.Color.color(:axis), theme, :css)
"var(--mw-axis, #666666)"
iex> Macaw.Layers.Color.resolve({:intensity, "#ff0000", 0.5}, theme, :literal)
"#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 a 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.

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}

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.

iex> Macaw.Layers.Color.value?({:alpha, {:color, :red}, 0.5})
true
iex> Macaw.Layers.Color.value?(:red)
false

with_opacity(color, opacity)

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

Returns a copy with the specified opacity