# `Color.Gamut.Diagram`

Geometric primitives for drawing **chromaticity diagrams** —
the "horseshoe" plots used to visualise gamuts side-by-side.

This module is pure data. It does no rendering; it returns
lists of points and maps of triangles that any renderer (SVG,
PNG, a 3D engine) can consume. The companion renderer that
ships with the library is the Gamut tab of
`Color.Palette.Visualizer`, which emits inline SVG.

## Projections

Two chromaticity projections are supported:

* `:xy` — CIE 1931 `(x, y)`. The historical default, the plot
  everyone recognises. Badly perceptually skewed — the green
  region dominates visually, the blue corner is crushed.

* `:uv` — CIE 1976 `(u', v')`. Modern default. Same underlying
  chromaticity data, a more perceptually uniform projection:
  `u' = 4x / (−2x + 12y + 3)`, `v' = 9y / (−2x + 12y + 3)`.

## Primitives

* `spectral_locus/2` — the horseshoe itself, as a list of
  points tracing monochromatic light from 380 nm (violet) to
  700 nm (red).

* `triangle/2` — one working space's primaries and white
  point, as a four-entry map.

* `planckian_locus/2` — the curve of blackbody chromaticities
  from 1000 K to 25000 K, with CCT annotations.

* `chromaticity/2` — projects any `Color.input()` to a single
  point in the chosen plane.

* `xy_to_uv/1` / `uv_to_xy/1` — the projection conversions,
  exposed in case callers have raw chromaticities already.

# `point`

```elixir
@type point() :: xy_point() | uv_point()
```

# `projection`

```elixir
@type projection() :: :xy | :uv
```

# `spectral_point`

```elixir
@type spectral_point() ::
  %{wavelength: number(), x: float(), y: float()}
  | %{wavelength: number(), u: float(), v: float()}
```

# `triangle`

```elixir
@type triangle() :: %{red: point(), green: point(), blue: point(), white: point()}
```

# `uv_point`

```elixir
@type uv_point() :: %{u: float(), v: float()}
```

# `xy_point`

```elixir
@type xy_point() :: %{x: float(), y: float()}
```

# `chromaticity`

```elixir
@spec chromaticity(Color.input(), projection()) ::
  {:ok, point()} | {:error, Exception.t()}
```

Returns the chromaticity of any colour input in the requested
projection.

### Arguments

* `color` is anything accepted by `Color.new/1`.

* `projection` is `:xy` (default) or `:uv`.

### Returns

* `{:ok, %{x, y}}` or `{:ok, %{u, v}}` on success.

* `{:error, exception}` if the colour can't be parsed or
  converted.

### Examples

    iex> {:ok, point} = Color.Gamut.Diagram.chromaticity("#ff0000")
    iex> {Float.round(point.x, 3), Float.round(point.y, 3)}
    {0.64, 0.33}

    iex> {:ok, point} = Color.Gamut.Diagram.chromaticity("white")
    iex> {Float.round(point.x, 4), Float.round(point.y, 4)}
    {0.3127, 0.329}

# `planckian_locus`

```elixir
@spec planckian_locus(Range.t(), projection()) :: [map()]
```

Returns points along the **Planckian (blackbody) locus** from
`min` to `max` Kelvin at the given step.

### Arguments

* `range` is a `Range.t()` of Kelvin values, e.g.
  `1000..20000//500`.

* `projection` is `:xy` (default) or `:uv`.

### Returns

* A list of maps with `:kelvin` plus the projection's
  coordinate keys.

### Examples

    iex> points = Color.Gamut.Diagram.planckian_locus(2000..10000//1000)
    iex> length(points)
    9
    iex> d65ish = Enum.find(points, &(&1.kelvin == 6000))
    iex> Float.round(d65ish.x, 3)
    0.322

# `spectral_locus`

```elixir
@spec spectral_locus(
  projection(),
  keyword()
) :: [spectral_point()]
```

Returns the visible-spectrum locus — the outer curve of the
chromaticity diagram — as a list of points.

The list traces monochromatic light from the shortest to the
longest wavelength in the CIE 1931 2° observer's CMF table.
To close the diagram visually, callers typically add a line
segment from the last point back to the first (the "line of
purples") when rendering.

### Arguments

* `projection` is `:xy` (default) or `:uv`.

### Options

* `:observer` is `2` (default) or `10` — CIE 1931 2° or CIE
  1964 10° standard observer.

* `:step` is the wavelength step in nm to subsample the CMF
  table by. Default `5` nm (every point). Use a larger value
  (e.g. `10`) for faster rendering at small sizes.

### Returns

* A list of maps with `:wavelength` plus the projection's
  coordinate keys (`:x, :y` or `:u, :v`).

### Examples

    iex> points = Color.Gamut.Diagram.spectral_locus(:xy)
    iex> first = hd(points)
    iex> first.wavelength
    380.0
    iex> Float.round(first.x, 4)
    0.1741

    iex> points = Color.Gamut.Diagram.spectral_locus(:uv)
    iex> point = Enum.find(points, &(&1.wavelength == 520.0))
    iex> Float.round(point.u, 4)
    0.0231

# `triangle`

```elixir
@spec triangle(atom(), projection()) :: triangle()
```

Returns the primaries and white point of a named RGB working
space as chromaticities in the requested projection.

### Arguments

* `working_space` is a working-space atom (for example
  `:SRGB`, `:P3_D65`, `:AdobeRGB` / `:Adobe`, `:Rec2020`,
  `:ProPhoto`).

* `projection` is `:xy` (default) or `:uv`.

### Returns

* A map with `:red`, `:green`, `:blue`, `:white` keys, each a
  `%{x, y}` or `%{u, v}` point.

### Examples

    iex> t = Color.Gamut.Diagram.triangle(:SRGB)
    iex> {Float.round(t.red.x, 2), Float.round(t.red.y, 2)}
    {0.64, 0.33}

    iex> t = Color.Gamut.Diagram.triangle(:P3_D65)
    iex> {Float.round(t.green.x, 3), Float.round(t.green.y, 3)}
    {0.265, 0.69}

# `uv_to_xy`

```elixir
@spec uv_to_xy({number(), number()}) :: {float(), float()}
```

Converts a CIE 1976 `(u', v')` chromaticity back to CIE 1931
`(x, y)`.

### Arguments

* `{u, v}` is a u'v' chromaticity tuple.

### Returns

* `{x, y}` — the xy coordinates.

### Examples

    iex> {x, y} = Color.Gamut.Diagram.uv_to_xy({0.1978, 0.4683})
    iex> {Float.round(x, 3), Float.round(y, 3)}
    {0.313, 0.329}

# `xy_to_uv`

```elixir
@spec xy_to_uv({number(), number()}) :: {float(), float()}
```

Converts a CIE 1931 `(x, y)` chromaticity to CIE 1976
`(u', v')`.

### Arguments

* `{x, y}` is a chromaticity tuple.

### Returns

* `{u, v}` — the u'v' coordinates.

### Examples

    iex> {u, v} = Color.Gamut.Diagram.xy_to_uv({0.3127, 0.3290})
    iex> {Float.round(u, 4), Float.round(v, 4)}
    {0.1978, 0.4683}

---

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