# `Macaw.Layers.Color`
[🔗](https://github.com/dcoai/macaw_layers/blob/v0.3.0/lib/macaw/layers/color.ex#L1)

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

# `t`

```elixir
@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`

```elixir
@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:t/0`), or a transform of colors. A string is never
a color.

# `alpha`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `darken`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Returns a desaturated copy (in HSL space)

# `exact?`

```elixir
@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`

```elixir
@spec grayscale(t()) :: t()
```

Returns a grayscale version of the color

# `hcl`

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

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

# `hsl`

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

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

# `intensity`

```elixir
@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`

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

Interpolates between two colors in HCL space (perceptually uniform)

# `interpolate_hsl`

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

Interpolates between two colors in HSL space

# `interpolate_lab`

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

Interpolates between two colors in Lab space

# `interpolate_rgb`

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

Interpolates between two colors in RGB space

# `invert`

```elixir
@spec invert(t()) :: t()
```

Inverts the color

# `lab`

```elixir
@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`

```elixir
@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?`

```elixir
@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`

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

Returns the relative luminance (0-1)

# `mix`

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

Mixes two colors equally (in RGB space)

# `name?`

```elixir
@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`

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

Gets a named color's hex value

# `named_colors`

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

Returns all named color names

# `oklab`

```elixir
@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`

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

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

# `parse`

```elixir
@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!`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Creates a color from RGB values (0-255)

# `rotate`

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

Rotates the hue by the specified degrees

# `saturate`

```elixir
@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`

```elixir
@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`
*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`

```elixir
@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`

```elixir
@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`

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

Converts to hex string

# `to_hsl`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Converts to RGB tuple {r, g, b}

# `to_rgba`

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

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

# `to_string`

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

Converts to CSS string

# `value?`

```elixir
@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`

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

Returns a copy with the specified opacity

---

*Consult [api-reference.md](api-reference.md) for complete listing*
