WXY is a colour space I put together to enable working with colour in terms of wavelengths and nanometres instead of hues and degrees.

The WXY colour space is composed of:

- Dominant wavelength
`W`

- Excitation purity
`X`

- Relative luminance
`Y`

While the concepts themselves are not new *(see Helmholtz coordinates)*,
I’m not aware of them being used in this kind of colour space format.

WXY is fully implemented in Unicolour, allowing it to be converted to and from any other colour space, interpolated, and compared.

For example, it is trivial to take a WXY colour and convert it to RGB, the modern Oklab & Oklch now available in CSS, Google’s HCT colour system, or more obscure and specialised colour spaces like TSL and XYB.

```
var yellow = new Unicolour(ColourSpace.Wxy, 570, 0.75, 0.9);
Console.WriteLine(yellow.Rgb); // 0.97 0.99 0.19
Console.WriteLine(yellow.Oklab); // 0.96 -0.07 +0.19
```

WXY is based on the CIE xyY colour space but, instead of “colourfulness” being defined by the xy-chromaticity coordinates, it is defined relative to the spectral locus in terms of wavelength and purity.

The spectral locus is the horseshoe-shaped curve on the xy-chromaticity diagram. It is created by plotting the xy-chromaticity of wavelengths across the visible spectrum, and represents pure monochromatic light.

Wavelength and purity are calculated by finding where the line from the white point to a chosen chromaticity intersects the spectral locus.

This example shows a sample chromaticity that
intersects the spectral locus at 530 nm (dominant wavelength `W`

)
and is 50% along the line from the white point (excitation purity `X`

).

Chromaticities in the purple region do not lie between the white point and the spectral locus, and instead intersect the “line of purples”. Colours along this boundary are non-spectral, meaning they cannot be generated by monochromatic light. In this case the opposite intersect, known as the complementary wavelength, is used, but with a negative value.

This example shows a sample chromaticity inside the purple region that
intersects the spectral locus at 530 nm (complementary wavelength) so becomes -530 nm (dominant wavelength `W`

)
and is 75% along the line from the white point (excitation purity `X`

).

Monitors can only display a subset of visible colours. The gamut of colours available is determined by the RGB model, such as sRGB, Display P3, Rec. 2020, etc.

This example demonstrates how sRGB green (0, 1, 0), assuming standard illuminant D65 (2° observer), converts to a wavelength of 549.1 nm and a purity of 73.4%.

The full WXY value is (549.1, 0.7344, 0.7152).

```
var green = new Unicolour(ColourSpace.Rgb, 0, 1, 0);
Console.WriteLine(green.Wxy); // 549.1nm 73.4% 0.7152
```

The steps to convert between xyY and WXY are deceptively straightforward.

Forward transform from xyY to WXY:

- Draw a line through
`(x, y)`

and the white point - Find where the line intersects the boundary
- If
`(x, y)`

is between the white point and the spectral locus,`W`

= spectral locus intersect - If
`(x, y)`

is between the white point and the line of purples,`W`

= -(spectral locus intersect) `X`

= white point to`(x, y)`

distance / white point to boundary distance`Y`

=`Y`

from xyY

Reverse transform to xyY from WXY:

- Find the pure chromaticity of
`W`

- Draw a line through
`W`

and the white point `(x, y)`

= the chromaticity that lies`X`

% along the line starting from the white point`Y`

=`Y`

from WXY

The difficulties here are:

- the boundary doesn’t have a mathematical definition and is most easily expressed as a polygon
- the number of vertices of the polygon determines the accuracy of the calculations
- each vertex requires computing XYZ tristimulus values from a spectral power distribution
- computing XYZ from spectral data involves using colour matching functions (CMFs) of specific observers
- chromaticities converge at each end of the spectral locus, making wavelengths beyond a certain range impossible to distinguish
- many edge cases, such as imaginary colours and white points (colours outwith the boundary)

Unicolour constructs the spectral locus as 1 nm segments since that is the granularity of the CIE colour matching functions datasets, ranging from 360 nm to 700 nm because beyond that roundtrip conversions became unreliable.

Dominant wavelength can be considered cyclical like hue - starting at violet, following the spectral locus will lead to red, and following the non-spectral line of purples will lead back to violet.

However, the wavelength range of the line of purples is determined by the white point, so how to interpolate between wavelengths is open to interpretation. Using the standard illuminant D65 (2° observer) white point, the full range of wavelengths is [360 nm, 700 nm] ∪ [-493.3 nm, -566.4 nm].

I’ve implemented this in Unicolour in a way that I feel is natural and intuitive, where the spectral locus is mapped to [0°, 180°] and the line of purples is mapped to [180°, 360°]. Degrees can then be interpolated in the same way as hue.

The main downside is that half of the interpolation space is reserved for purples, but WXY is not concerned with hue linearity or perceptual uniformity, only the ability to make working with wavelengths more accessible.

With a notion of interpolation in place, WXY can easily be used in a colour picker as seen in this online demo.

Note in the image above that some parts of the sliders show a pattern instead of solid colours. This is my way of visualising that those colours cannot be displayed with sRGB because they are out of gamut.

WXY provides an instinctive method of mapping an out-of-gamut colour to an in-gamut colour: simply reduce the purity until the colour is in gamut. The result will be a colour of the original wavelength and luminance, with the maximum displayable purity.

Purity-reduction gamut mapping is not yet implemented in Unicolour, but can be achieved with only a few lines of code.

```
const double decrement = 0.05; // reduce for greater accuracy
var (w, x, y) = (530, 0.5, 0.7);
var colour = new Unicolour(ColourSpace.Wxy, w, x, y);
while (!colour.IsInDisplayGamut)
{
x -= decrement;
colour = new Unicolour(ColourSpace.Wxy, w, x, y);
}
Console.WriteLine(colour.Wxy); // 530.0nm 30.0% 0.7000
Console.WriteLine(colour.Hex); // #0FF993
```

Here is a quick comparison of how different gamut mapping techniques handle full-purity wavelength at 0.5 luminance. On the left is basic RGB clipping, in the middle the Oklch-based mapping from the CSS specification, and on the right the purity-reduction method described above.

Wacton.Unicolour is licensed under the MIT License, copyright © 2022-2024 William Acton.

Also available in American 🇺🇸.