# `Color`

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:

* `Color.XYZ` — CIE 1931 tristimulus.

* `Color.XyY` — CIE xyY (chromaticity + luminance).

* `Color.Lab` — CIE 1976 `L*a*b*`.

* `Color.LCHab` — cylindrical `L*a*b*`.

* `Color.Luv` — CIE 1976 `L*u*v*`.

* `Color.LCHuv` — cylindrical `L*u*v*`.

* `Color.Oklab` — Oklab perceptual color space (Björn Ottosson, 2020),
  defined against D65.

* `Color.Oklch` — cylindrical Oklab.

* `Color.HSLuv` / `Color.HPLuv` — perceptually-uniform HSL variants
  built on CIELUV (Boronine 2015).

* `Color.JzAzBz` — HDR/wide-gamut perceptual space (Safdar et al. 2017).

* `Color.ICtCp` — Rec. 2100 HDR (PQ or HLG).

* `Color.IPT` — Ebner & Fairchild 1998, ancestor of Oklab.

* `Color.CAM16UCS` — CAM16 Color Appearance Model uniform space.

* `Color.CMYK` — simple subtractive (no ICC).

* `Color.YCbCr` — digital video with BT.601/709/2020 variants.

* `Color.SRGB` — companded sRGB working space (D65).

* `Color.AdobeRGB` — companded Adobe RGB (1998) working space (D65).

* `Color.RGB` — linear RGB in any named working space.

* `Color.HSL`, `Color.HSV` — non-linear reparameterisations of sRGB.

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 atom | Module | Notes |
|---|---|---|
| `:srgb` | `Color.SRGB` | default |
| `:adobe_rgb` / `:adobe` | `Color.AdobeRGB` | |
| `:cmyk` | `Color.CMYK` | 4 or 5 channels |
| `:hsl` | `Color.HSL` | hue in `[0, 1]` |
| `:hsv` | `Color.HSV` | hue in `[0, 1]` |
| `:hsluv` | `Color.HSLuv` | hue in degrees, s/l in `[0, 100]` |
| `:hpluv` | `Color.HPLuv` | hue in degrees, s/l in `[0, 100]` |
| `:lab` | `Color.Lab` | D65 |
| `:lch` / `:lchab` | `Color.LCHab` | D65 |
| `:luv` | `Color.Luv` | D65 |
| `:lchuv` | `Color.LCHuv` | D65 |
| `:oklab` | `Color.Oklab` | D65 |
| `:oklch` | `Color.Oklch` | D65 |
| `:xyz` | `Color.XYZ` | D65 / 2° |
| `:xyy` / `:xyY` | `Color.XyY` | D65 / 2° |
| `:jzazbz` | `Color.JzAzBz` | |
| `:ictcp` | `Color.ICtCp` | defaults to `transfer: :pq` |
| `:ipt` | `Color.IPT` | |
| `:ycbcr` | `Color.YCbCr` | defaults to `variant: :bt709` |
| `:cam16` / `:cam16_ucs` | `Color.CAM16UCS` | default 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:

| Category | Spaces | Integer form? | Range check |
|---|---|---|---|
| **Strict display** | `:srgb`, `:adobe_rgb`, `:cmyk` | Yes — `0..255`, scaled to `[0.0, 1.0]` | Strict — each channel must be in its exact range |
| **Strict unit cylindrical** | `:hsl`, `:hsv` | No | Strict — all channels in `[0.0, 1.0]`, hue wraps |
| **Strict deg/percent cylindrical** | `:hsluv`, `:hpluv` | No | Strict — `h` wraps, `s` and `l` must be in `[0, 100]` |
| **Permissive 3-channel** | `:lab`, `:luv`, `:oklab`, `:jzazbz`, `:ipt`, `:xyz`, `:xyy`, `:ictcp`, `:ycbcr`, `:cam16_ucs` | No | None — accepts wide-gamut / HDR values; rejects `NaN` and `±∞` |
| **Permissive cylindrical** | `:lch`, `:lchuv`, `:oklch` | No | None — 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.

# `input`

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

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

```elixir
@type t() ::
  Color.SRGB.t()
  | Color.AdobeRGB.t()
  | Color.RGB.t()
  | Color.Lab.t()
  | Color.LCHab.t()
  | Color.Luv.t()
  | Color.LCHuv.t()
  | Color.Oklab.t()
  | Color.Oklch.t()
  | Color.HSL.t()
  | Color.HSV.t()
  | Color.HSLuv.t()
  | Color.HPLuv.t()
  | Color.CMYK.t()
  | Color.YCbCr.t()
  | Color.JzAzBz.t()
  | Color.ICtCp.t()
  | Color.IPT.t()
  | Color.CAM16UCS.t()
  | Color.XYZ.t()
  | Color.XyY.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`

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

# `color?`

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

```elixir
@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_colorimetric` — **no** 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.)

* `:bpc` — `true` 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`

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

# `convert`

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

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

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

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

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

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

```elixir
@spec premultiply(Color.SRGB.t() | Color.AdobeRGB.t() | Color.RGB.t()) ::
  Color.SRGB.t() | Color.AdobeRGB.t() | Color.RGB.t()
```

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`

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

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

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

```elixir
@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.SRGB` → `rgb(r g b / a)`

* `Color.HSL` → `hsl(h s% l% / a)`

* `Color.Lab` → `lab(L% a b / a)`

* `Color.LCHab` → `lch(L% C h / a)`

* `Color.Oklab` → `oklab(L% a b / a)`

* `Color.Oklch` → `oklch(L% C h / a)`

* `Color.XYZ` → `color(xyz-d65 X Y Z / a)` (or `xyz-d50` for a
  D50-tagged struct)

* `Color.AdobeRGB` → `color(a98-rgb r g b / a)`

* `Color.RGB` → `color(<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`

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

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

```elixir
@spec unpremultiply(Color.SRGB.t() | Color.AdobeRGB.t() | Color.RGB.t()) ::
  Color.SRGB.t() | Color.AdobeRGB.t() | Color.RGB.t()
```

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`

```elixir
@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` / `:none` → `0.0` (fully transparent).

* `:opaque` → `1.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)

---

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