Skip to content

Gamut Mapping

Many color spaces are designed to represent a specific range of colors. This is often done to target specific display types or mediums. The monitor this is being displayed on can likely display millions of colors, but there are still colors it is not capable of displaying. So color spaces are often designed to represent such mediums to make it easy for authors and artists to know exactly where those color boundaries are. This range of colors that a color space is limited to and designed for is called a color gamut.

There are some color spaces that are theoretically unbounded, and even some color spaces that are bounded but can actually still give meaningful data if extended, but often, when it comes time to display a color, paint a product, or print a book, the actual colors are limited to what that device or process can handle.

The sRGB and Display P3 color spaces are both RGB color spaces, but they actually can represent a different amount of colors. Display P3 has a wider gamut and allows for greener greens and redder reds, etc. In the image below, we show four different RGB color spaces, each with varying different gamut sizes. Display P3 contains all the colors in sRGB and extends it even further. Rec. 2020, another RGB color space, is even wider. ProPhoto is so wide that it contains colors that the human eye can't even see.

Gamut Comparison

In order to visually represent a color from a wider gamut color space, such as Display P3, in a more narrow color space, such as sRGB, a suitable color within the more narrow color space must must be selected and be shown in its place. This selecting of a suitable replacement is called gamut mapping.

ColorAide defines a couple methods to help identify when a color is outside the gamut bounds of a color space and to help find a suitable, alternative color that is within the gamut.

Checking Gamut

When dealing with colors, it can be important to know whether a color is within its own gamut. Let's say we are working with colors in Display P3, but we want to output to an sRGB display. Let's say the color of interest is color(display-p3 1 0 0). If we plot the color as shown below, we can see that it is in Display P3, the faint transparent shell, but it is outside the sRGB gamut, the color solid in the middle. We'd like to detect these cases and make an adjustment to ensure we don't get unexpected behavior.

Out of Gamut

The in_gamut function allows for comparing the current color's specified values against the target color space's gamut.

Let's assume we have a color rgb(30% 105% 0%). The color is out of gamut due to the green channel exceeding the channel's limit of 100%. When we execute in_gamut, we can see that the color is not in its own gamut.

>>> Color("rgb(30% 105% 0%)").in_gamut()
False

On the other hand, some color spaces do not have a limit. CIELab and Oklab are such color spaces and can be represented in any gamut that'd you'd like.

Out of Gamut

Sometimes limits will be placed on the color space's channels (as done above) for practicality, but theoretically, there are no exact bounds.

When we check a CIELab color, we will find that it is always considered in gamut as it has no gamut itself.

>>> Color("lab(200% -20 40 / 1)").in_gamut()
True

While checking CIELab's own gamut isn't very useful, we can test it against a different color space's gamut. By simply passing in the name of a different color space, the current color will be converted to the provided space and then will run in_gamut on the new color. You could do this manually, but using in_gamut in this manner can be very convenient. In the example below, we can see that the CIELab color of lab(200% -20 40 / 1) is outside the narrow gamut of sRGB.

>>> Color("lab(200% -20 40 / 1)").in_gamut('srgb')
False

Tolerance

Generally, ColorAide does not round off values in order to guarantee the best possible values for round tripping, but due to limitations of floating-point arithmetic and precision of conversion algorithms, there can be edge cases where colors don't round trip perfectly. By default, in_gamut allows for a tolerance of 0.000075 to account for such cases where a color is "close enough". If desired, this "tolerance" can be adjusted.

Let's consider the oRGB color model. When converting from sRGB to oRGB, both of which share the same gamut, we can see that the conversion back is very, very close to being correct, but still technically out of gamut with one channel value falling below zero, but only slightly. This is due to the perils of floating point arithmetic.

>>> Color('red').convert('orgb')[:]
[0.299, 1.905283832848159e-05, 0.9999779995764854, 1.0]
>>> Color('red').convert('orgb').convert('srgb')[:]
[1.0, 1.1102230246251565e-16, -1.3877787807814457e-16, 1.0]

When testing with a tolerance, the color is considered in gamut, but when testing with no tolerance (a tolerance of zero), the color is considered out of gamut. Depending on what you are doing, this may not be an issue up until you are ready to finalize the color as very close to in gamut is usually good enough, so sometimes it may be desirable to have some tolerance, and other times not.

>>> Color('red').convert('orgb').convert('srgb')[:]
[1.0, 1.1102230246251565e-16, -1.3877787807814457e-16, 1.0]
>>> Color('red').convert('orgb').convert('srgb').in_gamut()
True
>>> Color('red').convert('orgb').convert('srgb').in_gamut(tolerance=0)
False

Let's consider some color models that handle out of gamut colors in a less subtle way. HSL, HSV, and HWB are color models designed to represent an RGB color space in a cylindrical format, traditionally sRGB. Each of these spaces isolate different attributes of a color: saturation, whiteness, lightness, etc. Because these models are just representing the color space in a different way, they share the same gamut as the reference RGB color space. So it stands to reason that simply using the sRGB gamut check for them should be sufficient, and if we are using strict tolerance, this would be true.

>>> Color('rgb(255 255 255)').in_gamut('srgb', tolerance=0)
True
>>> Color('hsl(0 0% 100%)').in_gamut('srgb', tolerance=0)
True
>>> Color('color(--hsv 0 0% 100%)').in_gamut('srgb', tolerance=0)
True
>>> Color('rgb(255.05 255 255)').in_gamut('srgb', tolerance=0)
False
>>> Color('hsl(0 0% 100.05%)').in_gamut('srgb', tolerance=0)
False
>>> Color('color(--hsv 0 0% 100.05%)').in_gamut('srgb', tolerance=0)
False

But when we are using a tolerance, and we check one of these models only using the sRGB gamut, there are some cases where these cylindrical colors can exhibit coordinates wildly outside of the model's range but still very close to the sRGB gamut. This isn't an error or a bug, but simply how the color model behaves with out of gamut colors. These values can still convert right back to the original color, but this might not always be the case with all color models.

In this example, we have an sRGB color that is extremely close to being in gamut, but when we convert it to HSL, we can see wildly large saturation. But since it round trips back to sRGB just fine, it can exhibit extreme saturation, but can still be considered in the sRGB gamut.

>>> hsl = Color('color(srgb 0.999999 1.000002 0.999999)').convert('hsl')
>>> hsl
color(--hsl 300 3 1 / 1)
>>> hsl.in_gamut('srgb')
True

This happens because these cylindrical color models do not represent out of gamut colors in a very sane way. When lightness exceeds the SDR range of 0 - 1 (or 0 - 100% as people generally associate HSL), they can return extremely high saturation. So even a slightly out of gamut sRGB color could translate to a value way outside the cylindrical color model's boundaries.

For this reason, gamut checks in the HSL, HSV, and HWB models apply tolerance checks on the color's coordinates in the sRGB color space and the respective cylindrical model ensuring we have coordinates that are close to the color's actual gamut and reasonably close to the cylindrical model's constraints as well. But if we specifically request srgb, we will see that only srgb is referenced.

>>> hsl = Color('color(srgb 0.999999 1.000002 0.999999)').convert('hsl')
>>> hsl
color(--hsl 300 3 1 / 1)
>>> hsl.in_gamut()
False
>>> hsl.in_gamut('hsl')
False
>>> hsl.in_gamut('srgb')
True

In short, ColorAide will figure out what best to test unless you explicitly tell it to use something else. If the Cartesian check is the only desired check, and the strange cylindrical values that are returned are not a problem, srgb can always be specified. tolerance=0 can also be used to constrain the check to values exactly in the gamut.

HSL has a very tight conversion to and from sRGB, so when an sRGB color is precisely in gamut, it will remain in gamut throughout the conversion to and from HSL, both forwards and backwards. On the other hand, there may be color models that have a looser conversion algorithm. There may even be cases where it may be beneficial to increase the threshold.

Gamut Mapping Colors

Gamut mapping is the process of taking a color that is out of gamut and adjusting it in such a way that it fits within the gamut. Essentially, gamut mapping takes a color that is out of gamut and maps it to some place on the target gamut that makes sense.

Gamut Mapping

There are various ways to map or compress values of an out of bound color to an in bound color, each with their own pros and cons. ColorAide offers a couple of methods related to gamut mapping: clip() and fit(). clip() is a dedicated function that performs the speedy, yet naive, approach of simply truncating a color channel's value to fit within the specified gamut, and fit() is a method that allows using more advanced gamut mapping approaches that, while often not as performant and simple as naive clipping, generally yield much better results in certain contexts.

While clipping won't always yield the most perceptually correct results, it can be a useful way get a color back into gamut. There may be better approaches, but clipping is still very important and can be used to trim channel noise after certain mathematical operations or even used in other gamut mapping algorithms if used carefully. For this reason, clip has its own dedicated method for quick access: clip().

>>> Color('rgb(270 30 120)').clip()
color(srgb 1 0.11765 0.47059 / 1)

The fit() method, is the generic gamut mapping method that exposes access to all the different gamut mapping methods available. By default, fit() uses a more advanced method of gamut mapping that tries to preserve hue and lightness, hue being the attribute the human eye is most sensitive to. If desired, a user can also specify any currently registered gamut mapping algorithm via the method parameter.

>>> Color('rgb(270 30 120)').fit()
color(srgb 1 0.18296 0.47421 / 1)
>>> Color('rgb(270 30 120)').fit(method='clip')
color(srgb 1 0.11765 0.47059 / 1)

Gamut mapping can also be used to indirectly fit colors in another gamut. For instance, fitting a Display P3 color into an sRGB gamut.

>>> c1 = Color('color(display-p3 1 1 0)')
>>> c1.in_gamut('srgb')
False
>>> c1.fit('srgb')
color(display-p3 0.9986 0.99232 0.32855 / 1)
>>> c1.in_gamut()
True

This can also be done with clip().

>>> Color('color(display-p3 1 1 0)').clip('srgb')
color(display-p3 1 1 0.3309 / 1)

Indirectly Gamut Mapping a Color Space

When indirectly gamut mapping in another color space, results may vary depending on what color space you are in and what color space you are using to fit the color. The operation may not get the color precisely in gamut. This is because we must convert the color to the gamut mapping space, apply the gamut mapping, and then convert it back to the original color. The process will be subject to any errors that occur in the round trip to and from the targeted space. This is mainly mentioned as fitting in one color space and round tripping back may not give exact results and, in some cases, exceed "in gamut" thresholds.

There are actually many different ways to gamut map a color. Some are computationally expensive, some are quite simple, and many do really good in some cases and not so well in others. There is probably no perfect gamut mapping method, but some are better than others.

Clip

The clip gamut mapping is registered in Color by default and cannot be unregistered

Clipping is a simple and naive approach to gamut mapping. If the color space is bounded by a gamut, clip will compare each channel's value against the bounds for that channel and set the value to the limit it exceeds.

Clip can be performed via fit by using the method name clip or by using the clip() method.

>>> c = Color('srgb', [2, 1, 1.5])
>>> c.fit(method='clip')
color(srgb 1 1 1 / 1)
>>> c = Color('srgb', [2, 1, 1.5])
>>> c.clip()
color(srgb 1 1 1 / 1)

Clipping is unique to all other clipping methods in that it has its own dedicated method clip() method and that its method name clip is reserved. While not always the best approach for all gamut mapping needs in general, clip is very important and its speed and simplicity are of great value.

MINDE Chroma Reduction

Chroma reduction is an approach that reduces the chroma in a polar color space until the color is within the gamut of a targeted color space. Pure chroma reduction has the advantage of preserving as much lightness and hue as possible, and when performed within a perceptual space, it can preserve perceptual hue and lightness, but it comes at the cost of colorfulness/chroma. Pure chroma reduction may not always be desirable when gamut mapping images as some colorfulness will be lost.

MINDE is an approach that tries to find the in gamut color with the shortest distance to out of gamut color. This can be better at finding an in gamut color with closer colorfulness but often comes at the cost of larger hue shifts and variances in lightness.

Combining both chroma reduction and something similar to MINDE can allow you to reduce the chroma of a color, but along the way, if there is a color near the chroma reduction path below the "just noticeable difference" (JND) in color distance, it can be returned early.

The way this works is as follows. A given out of gamut color will have its chroma reduced via bisection to bring the color into gamut. In addition, local clipping will be applied at each step and the distance between that clipped color and the chroma reduced color will be compared. If the distance between the clipped and chroma reduced color is close enough to the JND, the clipped color will be returned.

Visually, MINDE chroma reduction will allow a color that has decent constant lightness and will allow some hue shift as long as it is under what is noticeable by the eye. This doesn't mean no hue shift, and in certain regions, such as very dark colors or very light colors, hue shift can be greater because it is more difficult to notice in such lightness ranges. Every perceptual space is different and some do better than others and may allow for a larger or smaller JND limits, so the JND is relative to the color space and the ∆E color distancing algorithm used.

Preserving lightness in this way is useful when creating tones or mixing and interpolating colors. A constant lightness can also be useful when trying to control contrast.

Computationally, chroma reduction is slower to compute than clipping. Chroma reduction by bisecting can have varying performance as it is unknown how many iterations will be required to reduce the color into the gamut. Additionally, by combining the reduction with MINDE, the algorithm takes additional performance hits has it must now perform costly color distancing checks. Using a perceptual space with more uniform color distancing can reducing the complexity required to determine the color distance and, in turn, can speed up the process.

Lastly, all provided MINDE chroma reduction methods allow the controlling of the JND. This is useful if you want to adjust how close to the gamut boundary you approach before clipping. A larger JND may provide even more colorful colors while a lower JND will provide more accurate colors (relative to the perceptual space). If desired, setting the JND to 0 can improve performance by bypassing the MINDE logic altogether, but keep in mind that some perceptual spaces can have a geometry that can cause overly under-saturated colors just due to how the colors are distributed in the color space.

Consider the color color(display-p3 1 1 0). If we were to gamut map it in CIELCh with a very low JND, we can see that the odd shape of CIELCh in the yellow region can cause us to get a very desaturated color. By using the default JND of 2 for CIELCh, the MINDE logic will catch a more saturated yellow before it reduces chroma all the way to the gamut surface. Such geometric quirks aren't present in all color spaces, so in a space like OkLCh, it will have a less noticeable difference.

JND 0

JND 2

As a final note, it should be noted that most color spaces that have a defined gamut are tied to specific RGB gamuts. And when they are gamut mapped, they are done so in those RGB spaces. For instance, HSL which represents the sRGB gamut in a cylindrical form will be gamut mapped in sRGB (though simple clipping may be done directly in HSL).

There are a few color spaces/models that do not have a clearly defined gamuts. One such case is HPLuv, which is only defined as a cylindrical color space that represent only a subset of the sRGB color space. Additionally Okhsl and Okhsv are two cylindrical color spaces based on the perceptual Oklab color space that are meant to target the sRGB gamut, but are only a loose approximation which can actually slightly clip the sRGB gamut while simultaneously containing a few colors that exceed the sRGB gamut. ColorAide will not automatically associate these color spaces with an RGB gamut. In the case of HPLuv, there is no specifically defined RGB gamut, and in the case of Okhsl and Okhsv, sRGB is the closest, but does not precisely represent the colors in Okhsl and Okhsv.

Gamut mapping in HPLuv usually provides fine results, but you may find that gamut mapping Okhsl may not provide the intended results. When gamut mapping such spaces, you may want to use the closest RGB gamut.

>>> Steps([c.fit('okhsl', method='oklch-chroma') for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.9 0.03085 0 / 1), color(--oklch 0.9 0.03372 3.6364 / 1), color(--oklch 0.9 0.03428 7.2727 / 1), color(--oklch 0.9 0.03333 10.909 / 1), color(--oklch 0.9 0.03093 14.545 / 1), color(--oklch 0.9 0.02652 18.182 / 1), color(--oklch 0.9 0.01818 21.818 / 1), color(--oklch 0.9 0.00125 205.45 / 1), color(--oklch 0.9 0.08837 209.09 / 1), color(--oklch 0.9 0.00249 32.727 / 1), color(--oklch 0.9 0.01967 36.364 / 1), color(--oklch 0.9 0.02693 40 / 1), color(--oklch 0.9 0.03129 43.636 / 1), color(--oklch 0.9 0.03452 47.273 / 1), color(--oklch 0.9 0.03733 50.909 / 1), color(--oklch 0.9 0.04004 54.545 / 1), color(--oklch 0.9 0.04282 58.182 / 1), color(--oklch 0.9 0.04576 61.818 / 1), color(--oklch 0.9 0.04888 65.455 / 1), color(--oklch 0.9 0.05211 69.091 / 1), color(--oklch 0.9 0.05531 72.727 / 1), color(--oklch 0.9 0.0581 76.364 / 1), color(--oklch 0.9 0.05966 80 / 1), color(--oklch 0.9 0.05812 83.636 / 1), color(--oklch 0.9 0.04843 87.273 / 1), color(--oklch 0.9 0.01209 90.909 / 1), color(--oklch 0.9 0.15515 94.545 / 1), color(--oklch 0.9 0.1861 98.182 / 1), color(--oklch 0.9 0.18832 101.82 / 1), color(--oklch 0.9 0.19138 105.45 / 1), color(--oklch 0.9 0.01909 289.09 / 1), color(--oklch 0.9 0.03162 292.73 / 1), color(--oklch 0.9 0.02113 296.36 / 1), color(--oklch 0.9 0.03591 300 / 1), color(--oklch 0.9 0.22247 123.64 / 1), color(--oklch 0.9 0.23299 127.27 / 1), color(--oklch 0.9 0.2456 130.91 / 1), color(--oklch 0.9 0.24384 134.55 / 1), color(--oklch 0.9 0.22238 138.18 / 1), color(--oklch 0.9 0.20619 141.82 / 1), color(--oklch 0.9 0.19362 145.45 / 1), color(--oklch 0.9 0.03067 329.09 / 1), color(--oklch 0.9 0.00536 152.73 / 1), color(--oklch 0.9 0.0202 156.36 / 1), color(--oklch 0.9 0.02441 160 / 1), color(--oklch 0.9 0.02118 163.64 / 1), color(--oklch 0.9 0.01041 167.27 / 1), color(--oklch 0.9 0.01147 350.91 / 1), color(--oklch 0.9 0.05689 354.55 / 1), color(--oklch 0.9 0.15497 178.18 / 1), color(--oklch 0.9 0.1556 181.82 / 1), color(--oklch 0.9 0.15708 185.45 / 1), color(--oklch 0.9 0.15589 189.09 / 1), color(--oklch 0.9 0.15425 192.73 / 1), color(--oklch 0.9 0.15008 196.36 / 1), color(--oklch 0.9 0.12658 200 / 1), color(--oklch 0.9 0.10983 203.64 / 1), color(--oklch 0.9 0.01686 27.273 / 1), color(--oklch 0.9 0.0287 210.91 / 1), color(--oklch 0.9 0.04084 214.55 / 1), color(--oklch 0.9 0.04427 218.18 / 1), color(--oklch 0.9 0.04441 221.82 / 1), color(--oklch 0.9 0.0431 225.45 / 1), color(--oklch 0.9 0.04112 229.09 / 1), color(--oklch 0.9 0.03883 232.73 / 1), color(--oklch 0.9 0.03641 236.36 / 1), color(--oklch 0.9 0.03393 240 / 1), color(--oklch 0.9 0.03138 243.64 / 1), color(--oklch 0.9 0.02866 247.27 / 1), color(--oklch 0.9 0.02552 250.91 / 1), color(--oklch 0.9 0.0212 254.55 / 1), color(--oklch 0.9 0.01317 258.18 / 1), color(--oklch 0.9 0.01887 81.818 / 1), color(--oklch 0.9 0.04813 265.45 / 1), color(--oklch 0.9 0.04811 269.09 / 1), color(--oklch 0.9 0.0831 92.727 / 1), color(--oklch 0.9 0.00036 276.36 / 1), color(--oklch 0.9 0.01827 280 / 1), color(--oklch 0.9 0.02615 283.64 / 1), color(--oklch 0.9 0.03053 287.27 / 1), color(--oklch 0.9 0.0331 290.91 / 1), color(--oklch 0.9 0.03429 294.55 / 1), color(--oklch 0.9 0.03388 298.18 / 1), color(--oklch 0.9 0.03076 301.82 / 1), color(--oklch 0.9 0.02106 305.45 / 1), color(--oklch 0.9 0.01717 129.09 / 1), color(--oklch 0.9 0.06811 312.73 / 1), color(--oklch 0.9 0.07289 316.36 / 1), color(--oklch 0.9 0.07874 320 / 1), color(--oklch 0.9 0.08599 323.64 / 1), color(--oklch 0.9 0.09033 327.27 / 1), color(--oklch 0.9 0.08316 330.91 / 1), color(--oklch 0.9 0.07729 334.55 / 1), color(--oklch 0.9 0.07245 338.18 / 1), color(--oklch 0.9 0.06842 341.82 / 1), color(--oklch 0.9 0.06506 345.45 / 1), color(--oklch 0.9 0.0235 169.09 / 1), color(--oklch 0.9 0.00322 172.73 / 1), color(--oklch 0.9 0.02296 356.36 / 1), color(--oklch 0.9 0.03085 0 / 1)]
>>> Steps([c.fit('srgb', method='oklch-chroma') for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.88717 0.06673 355.33 / 1), color(--oklch 0.88718 0.06441 359.67 / 1), color(--oklch 0.88719 0.06255 4.0928 / 1), color(--oklch 0.88718 0.06113 8.5912 / 1), color(--oklch 0.88719 0.06011 13.152 / 1), color(--oklch 0.88715 0.05951 17.75 / 1), color(--oklch 0.88717 0.05925 22.369 / 1), color(--oklch 0.88713 0.05941 26.987 / 1), color(--oklch 0.88715 0.05991 31.574 / 1), color(--oklch 0.88713 0.06083 36.121 / 1), color(--oklch 0.88716 0.0621 40.588 / 1), color(--oklch 0.88717 0.06379 44.978 / 1), color(--oklch 0.88716 0.06593 49.278 / 1), color(--oklch 0.88715 0.06854 53.473 / 1), color(--oklch 0.88712 0.07167 57.565 / 1), color(--oklch 0.88715 0.07531 61.519 / 1), color(--oklch 0.88713 0.07964 65.378 / 1), color(--oklch 0.88712 0.08471 69.122 / 1), color(--oklch 0.88712 0.09063 72.748 / 1), color(--oklch 0.88713 0.09758 76.267 / 1), color(--oklch 0.88713 0.10584 79.694 / 1), color(--oklch 0.88717 0.11564 83.019 / 1), color(--oklch 0.88715 0.12763 86.283 / 1), color(--oklch 0.88719 0.14226 89.462 / 1), color(--oklch 0.88719 0.16071 92.59 / 1), color(--oklch 0.88726 0.18232 95.438 / 1), color(--oklch 0.89218 0.18372 96.494 / 1), color(--oklch 0.8987 0.18567 97.858 / 1), color(--oklch 0.90192 0.18793 100.67 / 1), color(--oklch 0.90192 0.19107 104.69 / 1), color(--oklch 0.90193 0.19529 108.7 / 1), color(--oklch 0.90192 0.20068 112.7 / 1), color(--oklch 0.90192 0.20737 116.68 / 1), color(--oklch 0.90193 0.21554 120.62 / 1), color(--oklch 0.90194 0.22539 124.54 / 1), color(--oklch 0.90194 0.23722 128.42 / 1), color(--oklch 0.90194 0.25142 132.26 / 1), color(--oklch 0.89219 0.26266 135.44 / 1), color(--oklch 0.88263 0.27371 138.18 / 1), color(--oklch 0.88175 0.25494 141.76 / 1), color(--oklch 0.88189 0.23689 145.66 / 1), color(--oklch 0.88209 0.22286 149.58 / 1), color(--oklch 0.88221 0.21214 153.5 / 1), color(--oklch 0.88229 0.20391 157.42 / 1), color(--oklch 0.88313 0.19644 161.07 / 1), color(--oklch 0.88558 0.18764 164.42 / 1), color(--oklch 0.88805 0.17994 167.95 / 1), color(--oklch 0.8905 0.17342 171.57 / 1), color(--oklch 0.89291 0.168 175.25 / 1), color(--oklch 0.8953 0.1636 178.96 / 1), color(--oklch 0.89767 0.16015 182.69 / 1), color(--oklch 0.90003 0.15756 186.42 / 1), color(--oklch 0.90238 0.15577 190.11 / 1), color(--oklch 0.90473 0.15472 193.75 / 1), color(--oklch 0.9054 0.15455 194.77 / 1), color(--oklch 0.90357 0.15414 195.17 / 1), color(--oklch 0.89711 0.15276 196.59 / 1), color(--oklch 0.89505 0.13708 199.31 / 1), color(--oklch 0.89504 0.12221 202.11 / 1), color(--oklch 0.89502 0.11028 204.96 / 1), color(--oklch 0.89502 0.10034 207.93 / 1), color(--oklch 0.895 0.09216 210.95 / 1), color(--oklch 0.89499 0.08517 214.1 / 1), color(--oklch 0.89499 0.07918 217.37 / 1), color(--oklch 0.89499 0.07401 220.78 / 1), color(--oklch 0.89499 0.06955 224.33 / 1), color(--oklch 0.89499 0.06569 228.03 / 1), color(--oklch 0.89498 0.06237 231.88 / 1), color(--oklch 0.89499 0.05949 235.92 / 1), color(--oklch 0.89497 0.05709 240.08 / 1), color(--oklch 0.89498 0.05504 244.48 / 1), color(--oklch 0.89498 0.05341 249.01 / 1), color(--oklch 0.89497 0.05215 253.7 / 1), color(--oklch 0.89497 0.05126 258.51 / 1), color(--oklch 0.89496 0.05075 263.44 / 1), color(--oklch 0.89495 0.05063 268.45 / 1), color(--oklch 0.89496 0.05088 273.51 / 1), color(--oklch 0.89495 0.05156 278.57 / 1), color(--oklch 0.89494 0.05266 283.61 / 1), color(--oklch 0.89494 0.0542 288.57 / 1), color(--oklch 0.89494 0.05623 293.44 / 1), color(--oklch 0.89493 0.0588 298.19 / 1), color(--oklch 0.89493 0.06192 302.78 / 1), color(--oklch 0.89492 0.0657 307.21 / 1), color(--oklch 0.89491 0.07022 311.49 / 1), color(--oklch 0.89492 0.07558 315.56 / 1), color(--oklch 0.8949 0.08201 319.49 / 1), color(--oklch 0.8949 0.0897 323.25 / 1), color(--oklch 0.89413 0.09833 326.36 / 1), color(--oklch 0.88972 0.10274 326.4 / 1), color(--oklch 0.88709 0.10539 326.42 / 1), color(--oklch 0.8866 0.10588 326.42 / 1), color(--oklch 0.8873 0.10102 328.09 / 1), color(--oklch 0.88727 0.09341 331.65 / 1), color(--oklch 0.88723 0.087 335.3 / 1), color(--oklch 0.88726 0.08147 339.1 / 1), color(--oklch 0.88723 0.07682 342.98 / 1), color(--oklch 0.88724 0.07284 347 / 1), color(--oklch 0.88718 0.06954 351.1 / 1), color(--oklch 0.88717 0.06673 355.33 / 1)]

LCh Chroma

The lch-chroma gamut mapping is registered in Color by default

LCh Chroma applies MINDE Chroma Reduction within the CIELCh color space and is currently the default approach in ColorAide.

Note

As most colors in ColorAide use a D65 white point by default, LCh D65 is used as the gamut mapping color space.

CIELCh, is not necessarily the best perceptual color space available, but it is a generally well understood color space that has been available a long time. It does suffer from a purple shift when dealing with blue colors, but can generally handle colors in very wide gamuts reasonably due to its fairly consistent shape well past the spectral locus.

CSS originally proposed MINDE Chroma Reduction with CIELCh, but has later changed to OkLCh. It is possible that the entire choice in algorithms could change as well in the future. We do offer an OkLCh variant, but we currently still use CIELCh due to its consistency even with colors far outside the gamut. If you are working within reasonable gamuts, OkLCh may be a better choice.

LCh Chroma is the default gamut mapping algorithm unless otherwise changed, and can be performed by simply calling fit() or by calling fit(method='lch-chroma').

>>> c = Color('srgb', [2, -1, 0])
>>> c.fit(method='lch-chroma')
color(srgb 1 0.39658 0.38511 / 1)

Additionally, the JND target can be controlled for tighter or looser gamut mapping via the jnd option. The default is 2.

>>> c = Color('srgb', [2, -1, 0])
>>> c.fit(method='lch-chroma', jnd=0.2)
color(srgb 1 0.4342 0.41183 / 1)

OkLCh Chroma

The lch-chroma gamut mapping is registered in Color by default

The CSS Color Level 4 specification currently recommends using the perceptually uniform OkLCh color space with the MINDE Chroma Reduction approach.

OkLCh does a much better job holding hues constant. When combined with gamut mapping, it generally does a better job than CIELCh, but it does have limitations. When colors get near the edge of the visible spectrum, the shape of the color space distorts, and gamut mapping will not be as good. But if you are working within reasonable gamuts, it may be an excellent option.

>>> c = Color('srgb', [2, -1, 0])
>>> c.fit(method='oklch-chroma')
color(srgb 1 0.60354 0.66617 / 1)

Additionally, the JND target can be controlled for tighter or looser gamut mapping via the jnd option. The default is 0.02.

>>> c = Color('srgb', [2, -1, 0])
>>> c.fit(method='oklch-chroma', jnd=0.002)
color(srgb 1 0.63219 0.68048 / 1)

HCT Chroma

The hct-chroma gamut mapping is not registered in Color by default

Warning

This approach was specifically added to help produce tonal palettes, but with the recent addition of the ray trace approach to chroma reduction in any perceptual space, it is recommended that users apply that approach as it performs a tight chroma reduction much quicker, and it doesn't require a special ∆E method.

On occasions, MINDE approach can be slightly more accurate very close to white due to the way ray trace handles HCT's atypical achromatic response, but differences should be imperceptible to the eye at such lightness levels making the the improved performance of the ray trace approach much more desirable.

>>> c = Color('hct', [325, 24, 50])
>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit={'method': 'raytrace', 'pspace': 'hct'}) for tone in tones])
['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']

If more accuracy in HCT's atypical achromatic region is desired, the MINDE approach is available.

Much like the other LCh chroma reduction algorithms, HCT Chroma performs gamut mapping exactly like LCh Chroma with the exception that it uses the HCT color space as the working LCh color space.

Google's Material Design uses a new color space called HCT. It uses the hue and chroma from CAM16 space and the tone/lightness from the CIELab space. HCT takes advantage of the good hue preservation of CAM16 and has the better lightness predictability of CIELab. Using these characteristics, the color space is adept at generating tonal palettes with predictable lightness. This makes it easier to construct UIs with decent contrast. But to do this well, you must work in HCT and gamut map in HCT. For this reason, the HCT Chroma gamut mapping method was added.

HCT Chroma is computationally the most expensive gamut mapping method that is offered. Since the color space used is based on the already computationally expensive CAM16 color space, and is made more expensive by blending that color space with CIELab, it is not the most performant approach, but when used in conjunction with the HCT color space, it can allow creating good tonal palettes:

>>> c = Color('hct', [325, 24, 50])
>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit={'method': 'hct-chroma', 'jnd': 0.0}) for tone in tones])
['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']

As shown above, the JND target can be controlled for tighter or looser gamut mapping via the jnd option. The default is 2, but to get tonal palette results comparable to Google Material, we are using 0.0.

To HCT Chroma plugin is not registered by default, but can be added by subclassing Color. You must register the ∆Ehct distancing algorithm and the HCT color space as well.

from coloraide import Color as Base
from coloraide.gamut.fit_hct_chroma import HCTChroma
from coloraide.distance.delta_e_hct import DEHCT
from coloraide.spaces.hct import HCT

class Color(Base): ...

Color.register([HCT(), DEHCT(), HCTChroma()])

Ray Tracing Chroma Reduction

Experimental Gamut Mapping

New in 4.0

The default perceptual space is now OkLCh.

Please see Ray Tracing Chroma Reduction in Any Perceptual Space to learn how to use different perceptual spaces and how to set your own default.

ColorAide has developed a chroma reduction technique that employs ray tracing. Its aim is to provide faster chrome reduction for gamut mapping using constant lightness. This approach specifically targets RGB gamuts, or spaces that can be represented with RGB gamuts. Additionally, if ColorAide can detect a linear version of the targeted RGB gamut, that version will be used automatically for best results. Currently ColorAide can gamut map all officially supported color spaces as they either have an RGB gamut or can be coerced into one.

The ray trace approach works by taking a given color and converting it to a perceptual Lab-ish or LCh-ish color space (the default being OkLCh) and then calculates the achromatic version of the color (chroma set to zero) which will be our anchor point. Assuming our anchor point is within bounds, a ray is cast from the inside of the cube, from the anchor point to the current color. The intersection along this path with the RGB gamut surface is then found. If the achromatic color exceeds the maximum or minimum lightness of the gamut, the respective maximum or minimum achromatic color is returned.

Ray Trace Algorithm

The ray trace algorithm is based on the slab method. The intersection that is selected is the first one encountered when following the ray from the origin point in the direction of the specified end point.

The intersection of the line and the gamut surface represents an approximation of the the most saturated color for that lightness and hue, but because the RGB space is not perceptual, the initial approximation is likely to be off because decreasing chroma and holding lightness and hue constant in a perceptual space will create a curved path through the RGB space. In order to converge on a point as close as possible to the actual most saturated color with the given hue and lightness, we must refine our result with a few additional iterations.

In order to converge on the actual chroma reduced color we seek, we can take the first intersection we find and correct the color in the perceptual color space by projecting the point back onto the chroma reduction path, correcting the color's hue and lightness. The corrected color becomes our new current color and should be a much closer color on the reduced chroma line. We can repeat this process a few more times, each time finding a better, closer color on the path. After about three additional iterations (a combined total of four for the entire process), we will be close enough where we can stop. Finally, we can then clip off floating point math errors. With this, we will now have a more accurate approximation of the color we seek.

Ray Trace Gamut Mapping Example

One final improvement is that during the correction step, where we adjust surface point back onto the chroma reduction path, if we find a point below the gamut surface, we can adjust our anchor to be this new point, closer to the gamut surface, which in some spaces will help to converge closer to our ideal color than they would without the adjustment.

Ray Trace Gamut Mapping Example

The results are comparable to MINDE using a low JND, but resolves much faster and within more predictable, consistent time.

>>> Color('oklch(90% 0.8 270)').fit('srgb', method='raytrace', pspace='lch-d65')
color(--oklch 0.76773 0.15855 309.37 / 1)
>>> Color('oklch(90% 0.8 270)').fit('srgb', method='lch-chroma', jnd=0)
color(--oklch 0.76773 0.15855 309.37 / 1)

As noted earlier, this method specifically targets RGB gamuts. This is because the ray tracing is performed on a simple RGB cube which is easy to calculate. ColorAide maps almost all colors to an RGB gamut, if they have one. And those gamuts are often associated with a linear RGB counterpart which is preferred when gamut mapping, but there are a few color spaces/models that do not map to an obvious RGB gamut.

HPLuv, which is only defined as a cylindrical color space that represent only a subset of the sRGB color space, has no defined RGB gamut on which to operate on. Additionally Okhsl and Okhsv are two cylindrical color spaces, based on the perceptual Oklab color space, that are meant to target the sRGB gamut, but are only a loose approximation which actually can slightly clip the sRGB gamut while simultaneously containing a few colors that exceed the sRGB gamut. ColorAide will not automatically associate these color spaces with an RGB gamut as their is not one that precisely represent the colors in Okhsl and Okhsv.

With that said, ColorAide will translate these spaces into a cube shape to apply gamut mapping on them if they are specifically used. In the case of HPLuv, results are usually fine, but you may find that gamut mapping Okhsl may not provide the intended results. It should be noted that the currently suggested CSS gamut mapping algorithm (oklch-chroma) does not do much better, so, for Okhsl and Okhsv, it is better to use the closest RGB gamut.

>>> Steps([c.fit('okhsl', method='raytrace') for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.9 0.03085 0 / 1), color(--oklch 0.9 0.03372 3.6364 / 1), color(--oklch 0.9 0.03428 7.2727 / 1), color(--oklch 0.9 0.03333 10.909 / 1), color(--oklch 0.9 0.03093 14.545 / 1), color(--oklch 0.9 0.02652 18.182 / 1), color(--oklch 0.9 0.01818 21.818 / 1), color(--oklch 0.9 0.00125 205.45 / 1), color(--oklch 0.9 0.08837 209.09 / 1), color(--oklch 0.9 0.00249 32.727 / 1), color(--oklch 0.9 0.01967 36.364 / 1), color(--oklch 0.9 0.02693 40 / 1), color(--oklch 0.9 0.03129 43.636 / 1), color(--oklch 0.9 0.03452 47.273 / 1), color(--oklch 0.9 0.03733 50.909 / 1), color(--oklch 0.9 0.04004 54.545 / 1), color(--oklch 0.9 0.04282 58.182 / 1), color(--oklch 0.9 0.04576 61.818 / 1), color(--oklch 0.9 0.04888 65.455 / 1), color(--oklch 0.9 0.05211 69.091 / 1), color(--oklch 0.9 0.05531 72.727 / 1), color(--oklch 0.9 0.0581 76.364 / 1), color(--oklch 0.9 0.05966 80 / 1), color(--oklch 0.9 0.05812 83.636 / 1), color(--oklch 0.9 0.04843 87.273 / 1), color(--oklch 0.9 0.01209 90.909 / 1), color(--oklch 0.9 0.15515 94.545 / 1), color(--oklch 0.9 0.1861 98.182 / 1), color(--oklch 0.9 0.18832 101.82 / 1), color(--oklch 0.9 0.19138 105.45 / 1), color(--oklch 0.9 0.01909 289.09 / 1), color(--oklch 0.9 0.03162 292.73 / 1), color(--oklch 0.9 0.02113 296.36 / 1), color(--oklch 0.9 0.03591 300 / 1), color(--oklch 0.9 0.22247 123.64 / 1), color(--oklch 0.9 0.23299 127.27 / 1), color(--oklch 0.9 0.2456 130.91 / 1), color(--oklch 0.9 0.24384 134.55 / 1), color(--oklch 0.9 0.22238 138.18 / 1), color(--oklch 0.9 0.20619 141.82 / 1), color(--oklch 0.9 0.19362 145.45 / 1), color(--oklch 0.9 0.03067 329.09 / 1), color(--oklch 0.9 0.00536 152.73 / 1), color(--oklch 0.9 0.0202 156.36 / 1), color(--oklch 0.9 0.02441 160 / 1), color(--oklch 0.9 0.02118 163.64 / 1), color(--oklch 0.9 0.01041 167.27 / 1), color(--oklch 0.9 0.01147 350.91 / 1), color(--oklch 0.9 0.05689 354.55 / 1), color(--oklch 0.9 0.15497 178.18 / 1), color(--oklch 0.9 0.1556 181.82 / 1), color(--oklch 0.9 0.15708 185.45 / 1), color(--oklch 0.9 0.15589 189.09 / 1), color(--oklch 0.9 0.15425 192.73 / 1), color(--oklch 0.9 0.15008 196.36 / 1), color(--oklch 0.9 0.12658 200 / 1), color(--oklch 0.9 0.10983 203.64 / 1), color(--oklch 0.9 0.01686 27.273 / 1), color(--oklch 0.9 0.0287 210.91 / 1), color(--oklch 0.9 0.04084 214.55 / 1), color(--oklch 0.9 0.04427 218.18 / 1), color(--oklch 0.9 0.04441 221.82 / 1), color(--oklch 0.9 0.0431 225.45 / 1), color(--oklch 0.9 0.04112 229.09 / 1), color(--oklch 0.9 0.03883 232.73 / 1), color(--oklch 0.9 0.03641 236.36 / 1), color(--oklch 0.9 0.03393 240 / 1), color(--oklch 0.9 0.03138 243.64 / 1), color(--oklch 0.9 0.02866 247.27 / 1), color(--oklch 0.9 0.02552 250.91 / 1), color(--oklch 0.9 0.0212 254.55 / 1), color(--oklch 0.9 0.01317 258.18 / 1), color(--oklch 0.9 0.01887 81.818 / 1), color(--oklch 0.9 0.04813 265.45 / 1), color(--oklch 0.9 0.04811 269.09 / 1), color(--oklch 0.9 0.0831 92.727 / 1), color(--oklch 0.9 0.00036 276.36 / 1), color(--oklch 0.9 0.01827 280 / 1), color(--oklch 0.9 0.02615 283.64 / 1), color(--oklch 0.9 0.03053 287.27 / 1), color(--oklch 0.9 0.0331 290.91 / 1), color(--oklch 0.9 0.03429 294.55 / 1), color(--oklch 0.9 0.03388 298.18 / 1), color(--oklch 0.9 0.03076 301.82 / 1), color(--oklch 0.9 0.02106 305.45 / 1), color(--oklch 0.9 0.01717 129.09 / 1), color(--oklch 0.9 0.06811 312.73 / 1), color(--oklch 0.9 0.07289 316.36 / 1), color(--oklch 0.9 0.07874 320 / 1), color(--oklch 0.9 0.08599 323.64 / 1), color(--oklch 0.9 0.09033 327.27 / 1), color(--oklch 0.9 0.08316 330.91 / 1), color(--oklch 0.9 0.07729 334.55 / 1), color(--oklch 0.9 0.07245 338.18 / 1), color(--oklch 0.9 0.06842 341.82 / 1), color(--oklch 0.9 0.06506 345.45 / 1), color(--oklch 0.9 0.0235 169.09 / 1), color(--oklch 0.9 0.00322 172.73 / 1), color(--oklch 0.9 0.02296 356.36 / 1), color(--oklch 0.9 0.03085 0 / 1)]
>>> Steps([c.fit('okhsl', method='oklch-chroma') for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.9 0.03085 0 / 1), color(--oklch 0.9 0.03372 3.6364 / 1), color(--oklch 0.9 0.03428 7.2727 / 1), color(--oklch 0.9 0.03333 10.909 / 1), color(--oklch 0.9 0.03093 14.545 / 1), color(--oklch 0.9 0.02652 18.182 / 1), color(--oklch 0.9 0.01818 21.818 / 1), color(--oklch 0.9 0.00125 205.45 / 1), color(--oklch 0.9 0.08837 209.09 / 1), color(--oklch 0.9 0.00249 32.727 / 1), color(--oklch 0.9 0.01967 36.364 / 1), color(--oklch 0.9 0.02693 40 / 1), color(--oklch 0.9 0.03129 43.636 / 1), color(--oklch 0.9 0.03452 47.273 / 1), color(--oklch 0.9 0.03733 50.909 / 1), color(--oklch 0.9 0.04004 54.545 / 1), color(--oklch 0.9 0.04282 58.182 / 1), color(--oklch 0.9 0.04576 61.818 / 1), color(--oklch 0.9 0.04888 65.455 / 1), color(--oklch 0.9 0.05211 69.091 / 1), color(--oklch 0.9 0.05531 72.727 / 1), color(--oklch 0.9 0.0581 76.364 / 1), color(--oklch 0.9 0.05966 80 / 1), color(--oklch 0.9 0.05812 83.636 / 1), color(--oklch 0.9 0.04843 87.273 / 1), color(--oklch 0.9 0.01209 90.909 / 1), color(--oklch 0.9 0.15515 94.545 / 1), color(--oklch 0.9 0.1861 98.182 / 1), color(--oklch 0.9 0.18832 101.82 / 1), color(--oklch 0.9 0.19138 105.45 / 1), color(--oklch 0.9 0.01909 289.09 / 1), color(--oklch 0.9 0.03162 292.73 / 1), color(--oklch 0.9 0.02113 296.36 / 1), color(--oklch 0.9 0.03591 300 / 1), color(--oklch 0.9 0.22247 123.64 / 1), color(--oklch 0.9 0.23299 127.27 / 1), color(--oklch 0.9 0.2456 130.91 / 1), color(--oklch 0.9 0.24384 134.55 / 1), color(--oklch 0.9 0.22238 138.18 / 1), color(--oklch 0.9 0.20619 141.82 / 1), color(--oklch 0.9 0.19362 145.45 / 1), color(--oklch 0.9 0.03067 329.09 / 1), color(--oklch 0.9 0.00536 152.73 / 1), color(--oklch 0.9 0.0202 156.36 / 1), color(--oklch 0.9 0.02441 160 / 1), color(--oklch 0.9 0.02118 163.64 / 1), color(--oklch 0.9 0.01041 167.27 / 1), color(--oklch 0.9 0.01147 350.91 / 1), color(--oklch 0.9 0.05689 354.55 / 1), color(--oklch 0.9 0.15497 178.18 / 1), color(--oklch 0.9 0.1556 181.82 / 1), color(--oklch 0.9 0.15708 185.45 / 1), color(--oklch 0.9 0.15589 189.09 / 1), color(--oklch 0.9 0.15425 192.73 / 1), color(--oklch 0.9 0.15008 196.36 / 1), color(--oklch 0.9 0.12658 200 / 1), color(--oklch 0.9 0.10983 203.64 / 1), color(--oklch 0.9 0.01686 27.273 / 1), color(--oklch 0.9 0.0287 210.91 / 1), color(--oklch 0.9 0.04084 214.55 / 1), color(--oklch 0.9 0.04427 218.18 / 1), color(--oklch 0.9 0.04441 221.82 / 1), color(--oklch 0.9 0.0431 225.45 / 1), color(--oklch 0.9 0.04112 229.09 / 1), color(--oklch 0.9 0.03883 232.73 / 1), color(--oklch 0.9 0.03641 236.36 / 1), color(--oklch 0.9 0.03393 240 / 1), color(--oklch 0.9 0.03138 243.64 / 1), color(--oklch 0.9 0.02866 247.27 / 1), color(--oklch 0.9 0.02552 250.91 / 1), color(--oklch 0.9 0.0212 254.55 / 1), color(--oklch 0.9 0.01317 258.18 / 1), color(--oklch 0.9 0.01887 81.818 / 1), color(--oklch 0.9 0.04813 265.45 / 1), color(--oklch 0.9 0.04811 269.09 / 1), color(--oklch 0.9 0.0831 92.727 / 1), color(--oklch 0.9 0.00036 276.36 / 1), color(--oklch 0.9 0.01827 280 / 1), color(--oklch 0.9 0.02615 283.64 / 1), color(--oklch 0.9 0.03053 287.27 / 1), color(--oklch 0.9 0.0331 290.91 / 1), color(--oklch 0.9 0.03429 294.55 / 1), color(--oklch 0.9 0.03388 298.18 / 1), color(--oklch 0.9 0.03076 301.82 / 1), color(--oklch 0.9 0.02106 305.45 / 1), color(--oklch 0.9 0.01717 129.09 / 1), color(--oklch 0.9 0.06811 312.73 / 1), color(--oklch 0.9 0.07289 316.36 / 1), color(--oklch 0.9 0.07874 320 / 1), color(--oklch 0.9 0.08599 323.64 / 1), color(--oklch 0.9 0.09033 327.27 / 1), color(--oklch 0.9 0.08316 330.91 / 1), color(--oklch 0.9 0.07729 334.55 / 1), color(--oklch 0.9 0.07245 338.18 / 1), color(--oklch 0.9 0.06842 341.82 / 1), color(--oklch 0.9 0.06506 345.45 / 1), color(--oklch 0.9 0.0235 169.09 / 1), color(--oklch 0.9 0.00322 172.73 / 1), color(--oklch 0.9 0.02296 356.36 / 1), color(--oklch 0.9 0.03085 0 / 1)]
>>> Steps([c.fit('srgb', method='raytrace') for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.9 0.05626 0 / 1), color(--oklch 0.9 0.05492 3.6364 / 1), color(--oklch 0.9 0.05385 7.2727 / 1), color(--oklch 0.9 0.05303 10.909 / 1), color(--oklch 0.9 0.05244 14.545 / 1), color(--oklch 0.9 0.05207 18.182 / 1), color(--oklch 0.9 0.0519 21.818 / 1), color(--oklch 0.9 0.05195 25.455 / 1), color(--oklch 0.9 0.0522 29.091 / 1), color(--oklch 0.9 0.05266 32.727 / 1), color(--oklch 0.9 0.05334 36.364 / 1), color(--oklch 0.9 0.05426 40 / 1), color(--oklch 0.9 0.05543 43.636 / 1), color(--oklch 0.9 0.05688 47.273 / 1), color(--oklch 0.9 0.05865 50.909 / 1), color(--oklch 0.9 0.06078 54.546 / 1), color(--oklch 0.9 0.06333 58.182 / 1), color(--oklch 0.9 0.06636 61.819 / 1), color(--oklch 0.9 0.07 65.456 / 1), color(--oklch 0.9 0.07435 69.093 / 1), color(--oklch 0.9 0.0796 72.733 / 1), color(--oklch 0.9 0.08601 76.374 / 1), color(--oklch 0.9 0.09391 80.021 / 1), color(--oklch 0.9 0.10384 83.675 / 1), color(--oklch 0.9 0.1166 87.343 / 1), color(--oklch 0.9 0.13348 91.035 / 1), color(--oklch 0.9 0.15639 94.717 / 1), color(--oklch 0.9 0.1861 98.182 / 1), color(--oklch 0.9 0.18832 101.82 / 1), color(--oklch 0.9 0.19138 105.45 / 1), color(--oklch 0.9 0.19534 109.09 / 1), color(--oklch 0.9 0.20029 112.73 / 1), color(--oklch 0.9 0.20635 116.36 / 1), color(--oklch 0.9 0.21367 120 / 1), color(--oklch 0.9 0.22247 123.64 / 1), color(--oklch 0.9 0.23299 127.27 / 1), color(--oklch 0.9 0.2456 130.91 / 1), color(--oklch 0.9 0.24384 134.55 / 1), color(--oklch 0.9 0.22238 138.18 / 1), color(--oklch 0.9 0.20619 141.82 / 1), color(--oklch 0.9 0.19362 145.45 / 1), color(--oklch 0.9 0.1837 149.09 / 1), color(--oklch 0.9 0.17581 152.73 / 1), color(--oklch 0.9 0.16954 156.36 / 1), color(--oklch 0.9 0.16461 160 / 1), color(--oklch 0.9 0.16083 163.64 / 1), color(--oklch 0.9 0.15804 167.27 / 1), color(--oklch 0.9 0.15617 170.91 / 1), color(--oklch 0.9 0.15516 174.55 / 1), color(--oklch 0.9 0.15497 178.18 / 1), color(--oklch 0.9 0.1556 181.82 / 1), color(--oklch 0.9 0.15708 185.45 / 1), color(--oklch 0.9 0.15589 189.09 / 1), color(--oklch 0.9 0.15425 192.73 / 1), color(--oklch 0.9 0.15008 196.36 / 1), color(--oklch 0.9 0.12658 200 / 1), color(--oklch 0.9 0.10983 203.64 / 1), color(--oklch 0.9 0.09734 207.27 / 1), color(--oklch 0.9 0.08773 210.91 / 1), color(--oklch 0.9 0.08013 214.54 / 1), color(--oklch 0.9 0.07402 218.18 / 1), color(--oklch 0.9 0.06903 221.82 / 1), color(--oklch 0.9 0.06492 225.45 / 1), color(--oklch 0.9 0.06151 229.09 / 1), color(--oklch 0.9 0.05866 232.73 / 1), color(--oklch 0.9 0.05628 236.36 / 1), color(--oklch 0.9 0.0543 240 / 1), color(--oklch 0.9 0.05265 243.64 / 1), color(--oklch 0.9 0.0513 247.27 / 1), color(--oklch 0.9 0.05022 250.91 / 1), color(--oklch 0.9 0.04938 254.55 / 1), color(--oklch 0.9 0.04876 258.18 / 1), color(--oklch 0.9 0.04834 261.82 / 1), color(--oklch 0.9 0.04813 265.45 / 1), color(--oklch 0.9 0.04811 269.09 / 1), color(--oklch 0.9 0.04828 272.73 / 1), color(--oklch 0.9 0.04865 276.36 / 1), color(--oklch 0.9 0.04923 280 / 1), color(--oklch 0.9 0.05003 283.64 / 1), color(--oklch 0.9 0.05106 287.27 / 1), color(--oklch 0.9 0.05235 290.91 / 1), color(--oklch 0.9 0.05392 294.55 / 1), color(--oklch 0.9 0.05583 298.18 / 1), color(--oklch 0.9 0.05813 301.82 / 1), color(--oklch 0.9 0.06087 305.45 / 1), color(--oklch 0.9 0.06416 309.09 / 1), color(--oklch 0.9 0.06811 312.73 / 1), color(--oklch 0.9 0.07289 316.36 / 1), color(--oklch 0.9 0.07874 320 / 1), color(--oklch 0.9 0.08599 323.64 / 1), color(--oklch 0.9 0.09033 327.27 / 1), color(--oklch 0.9 0.08316 330.91 / 1), color(--oklch 0.9 0.07729 334.55 / 1), color(--oklch 0.9 0.07245 338.18 / 1), color(--oklch 0.9 0.06842 341.82 / 1), color(--oklch 0.9 0.06506 345.45 / 1), color(--oklch 0.9 0.06223 349.09 / 1), color(--oklch 0.9 0.05987 352.73 / 1), color(--oklch 0.9 0.05789 356.36 / 1), color(--oklch 0.9 0.05626 0 / 1)]

Gamut Mapping in Any Perceptual Space

ColorAide provides a couple gamut mapping approaches and a few using different perceptual spaces to perform the operation, but th minde-chroma and the raytrace gamut mapping allow for using any perceptual space to perform the gamut mapping operation in. Any perceptual space in the LCh-ish or Lab-ish form can be specified via the pspace parameter.

>>> Color('oklch(50% 0.4 270)').fit('srgb', method='raytrace', pspace='cam16-jmh')
color(--oklch 0.48631 0.29467 277.07 / 1)
>>> Color('oklch(50% 0.4 270)').fit('srgb', method='raytrace', pspace='luv')
color(--oklch 0.47398 0.29406 276.64 / 1)

For the MINDE chroma approach, you would need to specify and configure an appropriate ∆E distancing approach and determine an appropriate JND limit for said distancing approach. Ideally, a ∆E algorithm specific to the perceptual space is preferred to help prevent costly conversions to other color spaces. Performance of algorithm in different perceptual spaces is dependent on the ∆E algorithm used, the JND selected, and how uniformly perceptual the color space is.

As an example, minde-chroma method uses OkLCh and uses the ∆Eok distancing algorithm with a JND of 0.02. The LCh variant uses CIELCh D65 and uses ∆E2000 with a JND of 2. We can manually perform both of these by setting the perceptual space with psapce, the ∆E configuration via de_options, and the JND with jnd.

>>> gma_lch = {'pspace': 'lch-d65', 'de_options': {'method': '2000'}, 'jnd': 2}
>>> gma_oklch = {'pspace': 'oklch', 'de_options': {'method': 'ok'}, 'jnd': 0.02}
>>> Steps([c.fit('srgb', **gma_lch) for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.84918 0.09121 357.15 / 1), color(--oklch 0.84901 0.0884 1.4712 / 1), color(--oklch 0.8489 0.08612 5.9235 / 1), color(--oklch 0.84886 0.08434 10.532 / 1), color(--oklch 0.84883 0.0831 15.333 / 1), color(--oklch 0.84898 0.08232 20.426 / 1), color(--oklch 0.84924 0.08213 25.959 / 1), color(--oklch 0.84978 0.08269 32.293 / 1), color(--oklch 0.85052 0.08518 41.028 / 1), color(--oklch 0.85133 0.0939 54.434 / 1), color(--oklch 0.85212 0.10289 62.615 / 1), color(--oklch 0.85303 0.1107 67.921 / 1), color(--oklch 0.85411 0.11725 71.661 / 1), color(--oklch 0.85537 0.12273 74.479 / 1), color(--oklch 0.85683 0.12732 76.714 / 1), color(--oklch 0.85848 0.13118 78.567 / 1), color(--oklch 0.8603 0.13454 80.176 / 1), color(--oklch 0.86231 0.13744 81.612 / 1), color(--oklch 0.86448 0.14009 82.942 / 1), color(--oklch 0.8668 0.1426 84.212 / 1), color(--oklch 0.86924 0.14512 85.452 / 1), color(--oklch 0.87184 0.14761 86.668 / 1), color(--oklch 0.87454 0.15033 87.899 / 1), color(--oklch 0.87736 0.15332 89.146 / 1), color(--oklch 0.88022 0.15699 90.455 / 1), color(--oklch 0.88312 0.16151 91.831 / 1), color(--oklch 0.88608 0.16715 93.284 / 1), color(--oklch 0.88896 0.17519 94.91 / 1), color(--oklch 0.89209 0.1837 96.476 / 1), color(--oklch 0.89662 0.18504 97.429 / 1), color(--oklch 0.90149 0.18653 98.428 / 1), color(--oklch 0.90666 0.18817 99.462 / 1), color(--oklch 0.91192 0.18997 100.6 / 1), color(--oklch 0.91436 0.19222 102.97 / 1), color(--oklch 0.91655 0.19511 105.67 / 1), color(--oklch 0.91839 0.19901 108.83 / 1), color(--oklch 0.91973 0.20459 112.66 / 1), color(--oklch 0.92028 0.21328 117.54 / 1), color(--oklch 0.91945 0.22858 124.11 / 1), color(--oklch 0.90154 0.25286 132.63 / 1), color(--oklch 0.89131 0.23503 140.47 / 1), color(--oklch 0.89656 0.19826 146.57 / 1), color(--oklch 0.90131 0.174 152.26 / 1), color(--oklch 0.90572 0.15674 157.52 / 1), color(--oklch 0.90948 0.14448 162.32 / 1), color(--oklch 0.9128 0.13538 166.67 / 1), color(--oklch 0.91541 0.12902 170.66 / 1), color(--oklch 0.9175 0.12451 174.36 / 1), color(--oklch 0.91897 0.12175 177.88 / 1), color(--oklch 0.91997 0.12035 181.28 / 1), color(--oklch 0.92027 0.12065 184.69 / 1), color(--oklch 0.91993 0.12264 188.17 / 1), color(--oklch 0.91905 0.12632 191.79 / 1), color(--oklch 0.90985 0.14607 194.89 / 1), color(--oklch 0.9054 0.15455 194.77 / 1), color(--oklch 0.92843 0.10387 196.86 / 1), color(--oklch 0.9285 0.08567 201.11 / 1), color(--oklch 0.9276 0.07299 205.88 / 1), color(--oklch 0.92601 0.06378 211.24 / 1), color(--oklch 0.92387 0.05713 217.12 / 1), color(--oklch 0.92131 0.05236 223.49 / 1), color(--oklch 0.91839 0.04909 230.23 / 1), color(--oklch 0.91519 0.04706 237.16 / 1), color(--oklch 0.91173 0.04615 243.97 / 1), color(--oklch 0.90805 0.04613 250.51 / 1), color(--oklch 0.9042 0.04686 256.64 / 1), color(--oklch 0.90027 0.04816 262.37 / 1), color(--oklch 0.89633 0.04993 267.75 / 1), color(--oklch 0.89248 0.05207 272.76 / 1), color(--oklch 0.88877 0.05452 277.36 / 1), color(--oklch 0.88519 0.05721 281.48 / 1), color(--oklch 0.88173 0.06008 285.14 / 1), color(--oklch 0.87843 0.06308 288.41 / 1), color(--oklch 0.8753 0.06617 291.35 / 1), color(--oklch 0.87237 0.0693 294.02 / 1), color(--oklch 0.86967 0.07247 296.49 / 1), color(--oklch 0.86721 0.07566 298.8 / 1), color(--oklch 0.86501 0.07887 300.99 / 1), color(--oklch 0.86308 0.08212 303.1 / 1), color(--oklch 0.86143 0.08543 305.17 / 1), color(--oklch 0.86006 0.08884 307.23 / 1), color(--oklch 0.85896 0.09241 309.3 / 1), color(--oklch 0.85815 0.09619 311.4 / 1), color(--oklch 0.85762 0.1003 313.57 / 1), color(--oklch 0.85736 0.10487 315.84 / 1), color(--oklch 0.85739 0.11002 318.19 / 1), color(--oklch 0.85769 0.11599 320.67 / 1), color(--oklch 0.8583 0.12303 323.27 / 1), color(--oklch 0.85923 0.13159 326.02 / 1), color(--oklch 0.85347 0.14015 326.72 / 1), color(--oklch 0.84964 0.14423 326.75 / 1), color(--oklch 0.85277 0.14008 326.96 / 1), color(--oklch 0.852 0.13069 330.27 / 1), color(--oklch 0.85131 0.12251 333.7 / 1), color(--oklch 0.85078 0.11526 337.28 / 1), color(--oklch 0.85031 0.10895 340.99 / 1), color(--oklch 0.84994 0.10344 344.85 / 1), color(--oklch 0.84962 0.0987 348.83 / 1), color(--oklch 0.84938 0.09464 352.93 / 1), color(--oklch 0.84918 0.09121 357.15 / 1)]
>>> Steps([c.fit('srgb', **gma_oklch) for c in Color.steps(['oklch(90% 0.4 0)', 'oklch(90% 0.4 360)'], steps=100, space='oklch', hue='longer')])
[color(--oklch 0.88717 0.06673 355.33 / 1), color(--oklch 0.88718 0.06441 359.67 / 1), color(--oklch 0.88719 0.06255 4.0928 / 1), color(--oklch 0.88718 0.06113 8.5912 / 1), color(--oklch 0.88719 0.06011 13.152 / 1), color(--oklch 0.88715 0.05951 17.75 / 1), color(--oklch 0.88717 0.05925 22.369 / 1), color(--oklch 0.88713 0.05941 26.987 / 1), color(--oklch 0.88715 0.05991 31.574 / 1), color(--oklch 0.88713 0.06083 36.121 / 1), color(--oklch 0.88716 0.0621 40.588 / 1), color(--oklch 0.88717 0.06379 44.978 / 1), color(--oklch 0.88716 0.06593 49.278 / 1), color(--oklch 0.88715 0.06854 53.473 / 1), color(--oklch 0.88712 0.07167 57.565 / 1), color(--oklch 0.88715 0.07531 61.519 / 1), color(--oklch 0.88713 0.07964 65.378 / 1), color(--oklch 0.88712 0.08471 69.122 / 1), color(--oklch 0.88712 0.09063 72.748 / 1), color(--oklch 0.88713 0.09758 76.267 / 1), color(--oklch 0.88713 0.10584 79.694 / 1), color(--oklch 0.88717 0.11564 83.019 / 1), color(--oklch 0.88715 0.12763 86.283 / 1), color(--oklch 0.88719 0.14226 89.462 / 1), color(--oklch 0.88719 0.16071 92.59 / 1), color(--oklch 0.88726 0.18232 95.438 / 1), color(--oklch 0.89218 0.18372 96.494 / 1), color(--oklch 0.8987 0.18567 97.858 / 1), color(--oklch 0.90192 0.18793 100.67 / 1), color(--oklch 0.90192 0.19107 104.69 / 1), color(--oklch 0.90193 0.19529 108.7 / 1), color(--oklch 0.90192 0.20068 112.7 / 1), color(--oklch 0.90192 0.20737 116.68 / 1), color(--oklch 0.90193 0.21554 120.62 / 1), color(--oklch 0.90194 0.22539 124.54 / 1), color(--oklch 0.90194 0.23722 128.42 / 1), color(--oklch 0.90194 0.25142 132.26 / 1), color(--oklch 0.89219 0.26266 135.44 / 1), color(--oklch 0.88263 0.27371 138.18 / 1), color(--oklch 0.88175 0.25494 141.76 / 1), color(--oklch 0.88189 0.23689 145.66 / 1), color(--oklch 0.88209 0.22286 149.58 / 1), color(--oklch 0.88221 0.21214 153.5 / 1), color(--oklch 0.88229 0.20391 157.42 / 1), color(--oklch 0.88313 0.19644 161.07 / 1), color(--oklch 0.88558 0.18764 164.42 / 1), color(--oklch 0.88805 0.17994 167.95 / 1), color(--oklch 0.8905 0.17342 171.57 / 1), color(--oklch 0.89291 0.168 175.25 / 1), color(--oklch 0.8953 0.1636 178.96 / 1), color(--oklch 0.89767 0.16015 182.69 / 1), color(--oklch 0.90003 0.15756 186.42 / 1), color(--oklch 0.90238 0.15577 190.11 / 1), color(--oklch 0.90473 0.15472 193.75 / 1), color(--oklch 0.9054 0.15455 194.77 / 1), color(--oklch 0.90357 0.15414 195.17 / 1), color(--oklch 0.89711 0.15276 196.59 / 1), color(--oklch 0.89505 0.13708 199.31 / 1), color(--oklch 0.89504 0.12221 202.11 / 1), color(--oklch 0.89502 0.11028 204.96 / 1), color(--oklch 0.89502 0.10034 207.93 / 1), color(--oklch 0.895 0.09216 210.95 / 1), color(--oklch 0.89499 0.08517 214.1 / 1), color(--oklch 0.89499 0.07918 217.37 / 1), color(--oklch 0.89499 0.07401 220.78 / 1), color(--oklch 0.89499 0.06955 224.33 / 1), color(--oklch 0.89499 0.06569 228.03 / 1), color(--oklch 0.89498 0.06237 231.88 / 1), color(--oklch 0.89499 0.05949 235.92 / 1), color(--oklch 0.89497 0.05709 240.08 / 1), color(--oklch 0.89498 0.05504 244.48 / 1), color(--oklch 0.89498 0.05341 249.01 / 1), color(--oklch 0.89497 0.05215 253.7 / 1), color(--oklch 0.89497 0.05126 258.51 / 1), color(--oklch 0.89496 0.05075 263.44 / 1), color(--oklch 0.89495 0.05063 268.45 / 1), color(--oklch 0.89496 0.05088 273.51 / 1), color(--oklch 0.89495 0.05156 278.57 / 1), color(--oklch 0.89494 0.05266 283.61 / 1), color(--oklch 0.89494 0.0542 288.57 / 1), color(--oklch 0.89494 0.05623 293.44 / 1), color(--oklch 0.89493 0.0588 298.19 / 1), color(--oklch 0.89493 0.06192 302.78 / 1), color(--oklch 0.89492 0.0657 307.21 / 1), color(--oklch 0.89491 0.07022 311.49 / 1), color(--oklch 0.89492 0.07558 315.56 / 1), color(--oklch 0.8949 0.08201 319.49 / 1), color(--oklch 0.8949 0.0897 323.25 / 1), color(--oklch 0.89413 0.09833 326.36 / 1), color(--oklch 0.88972 0.10274 326.4 / 1), color(--oklch 0.88709 0.10539 326.42 / 1), color(--oklch 0.8866 0.10588 326.42 / 1), color(--oklch 0.8873 0.10102 328.09 / 1), color(--oklch 0.88727 0.09341 331.65 / 1), color(--oklch 0.88723 0.087 335.3 / 1), color(--oklch 0.88726 0.08147 339.1 / 1), color(--oklch 0.88723 0.07682 342.98 / 1), color(--oklch 0.88724 0.07284 347 / 1), color(--oklch 0.88718 0.06954 351.1 / 1), color(--oklch 0.88717 0.06673 355.33 / 1)]

It should be noted that gamut mapping will be limited by the capabilities of the perceptual space being used. Some color spaces can swing to varying degrees outside the visible spectrum and some perceptual models can tolerate this more than others. While the characteristics of the color space can affect gamut mapping results, this does not mean the gamut mapping approach does not work, only that some color models may work best under more constraints.

Consider the example below. We take a very saturated yellow in Display P3 (color(display-p3 1 1 0)) and then we interpolate it's lightness between 0, masking off chroma so that we are only interpolating lightness. We do this interpolation in CIELCh, whose chroma can swing very far outside the visible spectrum when interpolating hues at more extreme lightness. Finally, we gamut map in various LCh models. What we can observe is some models will struggle to map some of these colors as the hue preservation can break down at extreme limits. In the cases below, this specifically happens due to negative XYZ values that are produced due to high chroma in lower lightness. Some models can tolerate this more than others.

>>> yellow = Color('color(display-p3 1 1 0)')
>>> lightness_mask = Color('lch(0% none none)')
>>> Steps([c.fit('srgb', method='raytrace', pspace='oklch') for c in Color.steps([yellow, lightness_mask], steps=20, space='lch')])
[color(--lch 97.252 94.474 99.968 / 1), color(--lch 92.116 90.244 100.33 / 1), color(--lch 86.981 86.019 100.72 / 1), color(--lch 81.848 81.8 101.15 / 1), color(--lch 76.716 77.587 101.63 / 1), color(--lch 71.583 73.375 102.14 / 1), color(--lch 66.446 69.159 102.67 / 1), color(--lch 61.303 64.934 103.2 / 1), color(--lch 56.155 60.694 103.7 / 1), color(--lch 50.999 56.432 104.17 / 1), color(--lch 45.834 52.143 104.56 / 1), color(--lch 40.663 47.836 104.91 / 1), color(--lch 35.631 43.818 106.43 / 1), color(--lch 30.421 39.374 106.27 / 1), color(--lch 25.124 34.439 105.77 / 1), color(--lch 19.731 28.372 105.2 / 1), color(--lch 14.159 21.103 104.22 / 1), color(--lch 8.1264 12.265 101.36 / 1), color(--lch 1.9671 2.9842 79.847 / 1), color(--lch 0 0 none / 1)]
>>> Steps([c.fit('srgb', method='raytrace', pspace='lch99o') for c in Color.steps([yellow, lightness_mask], steps=20, space='lch')])
[color(--lch 97.038 22.96 96.153 / 1), color(--lch 92.214 89.989 97.362 / 1), color(--lch 87.091 85.717 97.183 / 1), color(--lch 81.969 81.446 96.949 / 1), color(--lch 76.847 77.184 96.878 / 1), color(--lch 71.724 72.924 96.861 / 1), color(--lch 66.6 68.664 96.843 / 1), color(--lch 61.473 64.401 96.827 / 1), color(--lch 56.343 60.137 96.818 / 1), color(--lch 51.21 55.869 96.823 / 1), color(--lch 46.071 51.599 96.852 / 1), color(--lch 40.924 47.323 96.92 / 1), color(--lch 35.767 43.039 97.053 / 1), color(--lch 30.594 38.745 97.292 / 1), color(--lch 25.396 34.033 97.641 / 1), color(--lch 20.157 28.339 98.228 / 1), color(--lch 14.844 21.718 99.385 / 1), color(--lch 9.3945 14.051 98.579 / 1), color(--lch 4.0615 6.0794 98.13 / 1), color(--lch 0 0 none / 1)]
>>> Steps([c.fit('srgb', method='raytrace', pspace='hct') for c in Color.steps([yellow, lightness_mask], steps=20, space='lch')])
[color(--lch 97.316 94.474 99.598 / 1), color(--lch 92.189 90.239 99.889 / 1), color(--lch 87.062 86.008 100.21 / 1), color(--lch 81.937 81.782 100.56 / 1), color(--lch 76.813 77.56 100.95 / 1), color(--lch 71.688 73.334 101.34 / 1), color(--lch 66.563 69.099 101.69 / 1), color(--lch 61.437 64.832 101.85 / 1), color(--lch 56.31 60.536 101.82 / 1), color(--lch 51.178 56.255 101.94 / 1), color(--lch 46.042 51.962 102.01 / 1), color(--lch 40.898 47.651 101.99 / 1), color(--lch 35.745 43.318 101.81 / 1), color(--lch 30.577 38.956 101.39 / 1), color(--lch 25.384 34.191 100.69 / 1), color(--lch 20.152 28.407 99.637 / 1), color(--lch 14.849 21.647 97.546 / 1), color(--lch 9.4049 13.959 92.117 / 1), color(--lch 4.0759 6.2258 78.058 / 1), color(--lch 0 0 none / 1)]
>>> Steps([c.fit('srgb', method='raytrace', pspace='jzczhz') for c in Color.steps([yellow, lightness_mask], steps=20, space='lch')])
[color(--lch 97.139 94.455 100.44 / 1), color(--lch 91.987 90.221 100.84 / 1), color(--lch 86.837 85.991 101.27 / 1), color(--lch 81.689 81.766 101.73 / 1), color(--lch 76.544 77.547 102.23 / 1), color(--lch 71.395 73.327 102.76 / 1), color(--lch 66.237 69.101 103.33 / 1), color(--lch 61.067 64.865 103.92 / 1), color(--lch 55.883 60.613 104.53 / 1), color(--lch 50.68 56.34 105.17 / 1), color(--lch 45.456 52.052 105.87 / 1), color(--lch 40.235 47.847 107.09 / 1), color(--lch 25.052 77.611 313.27 / 1), color(--lch 20.684 69.351 313.27 / 1), color(--lch 16.234 60.938 313.27 / 1), color(--lch 11.635 52.242 313.27 / 1), color(--lch 6.7735 42.632 313.1 / 1), color(--lch 2.6403 23.628 305.32 / 1), color(--lch 0 0 none / 1), color(--lch 0 0 none / 1)]
>>> Steps([c.fit('srgb', method='raytrace', pspace='lchuv') for c in Color.steps([yellow, lightness_mask], steps=20, space='lch')])
[color(--lch 97.317 94.47 99.572 / 1), color(--lch 92.189 90.24 99.895 / 1), color(--lch 87.062 86.016 100.26 / 1), color(--lch 81.936 81.799 100.68 / 1), color(--lch 76.811 77.59 101.16 / 1), color(--lch 71.686 73.383 101.67 / 1), color(--lch 66.559 69.173 102.18 / 1), color(--lch 61.431 64.955 102.68 / 1), color(--lch 56.302 60.723 103.14 / 1), color(--lch 51.169 56.469 103.51 / 1), color(--lch 46.032 52.183 103.74 / 1), color(--lch 40.889 47.854 103.71 / 1), color(--lch 35.738 43.476 103.34 / 1), color(--lch 30.572 39.052 102.52 / 1), color(--lch 25.382 34.234 101.29 / 1), color(--lch 20.151 28.423 99.916 / 1), color(--lch 14.846 21.683 98.546 / 1), color(--lch 8.7143 33.663 285.14 / 1), color(--lch 3.7511 22.378 280.24 / 1), color(--lch 0 0 none / 1)]

Almost any perceptual model, if pushed far enough, can start to break down. Converting to and from these spaces before reducing chroma can introduce such disparities. Every color space has limitations, some spaces just have more agreeable ones.

If you are working within reasonable gamuts, most will work fine. And if you want to do something like above, holding chroma really high for all lightness values, you will often find that it works best when you do it directly in the color space that is doing the gamut mapping as you will have to "fit" the color before converting to another color space.

>>> yellow = Color('color(display-p3 1 1 0)')
>>> lightness_mask = Color('oklch(0% none none)')
>>> Steps([c.fit('srgb', method='raytrace', pspace='oklch') for c in Color.steps([yellow, lightness_mask], steps=20, space='oklch')])
[color(--oklch 0.96476 0.21094 110.23 / 1), color(--oklch 0.91399 0.19984 110.23 / 1), color(--oklch 0.86321 0.18874 110.23 / 1), color(--oklch 0.81243 0.17763 110.23 / 1), color(--oklch 0.76166 0.16653 110.23 / 1), color(--oklch 0.71088 0.15543 110.23 / 1), color(--oklch 0.6601 0.14433 110.23 / 1), color(--oklch 0.60932 0.13323 110.23 / 1), color(--oklch 0.55855 0.12212 110.23 / 1), color(--oklch 0.50777 0.11102 110.23 / 1), color(--oklch 0.45699 0.09992 110.23 / 1), color(--oklch 0.40622 0.08882 110.23 / 1), color(--oklch 0.35544 0.07772 110.23 / 1), color(--oklch 0.30466 0.06661 110.23 / 1), color(--oklch 0.25389 0.05551 110.23 / 1), color(--oklch 0.20311 0.04441 110.23 / 1), color(--oklch 0.15233 0.03331 110.23 / 1), color(--oklch 0.10155 0.0222 110.23 / 1), color(--oklch 0.05078 0.0111 110.23 / 1), color(--oklch 0 0 none / 1)]
>>> yellow = Color('color(display-p3 1 1 0)')
>>> lightness_mask = Color('color(--lch99o 0% none none)')
>>> Steps([c.fit('srgb', method='raytrace', pspace='lch99o') for c in Color.steps([yellow, lightness_mask], steps=20, space='lch99o')])
[color(--lch99o 97.304 20.716 98.051 / 1), color(--lch99o 92.182 43.933 97.877 / 1), color(--lch99o 87.061 42.844 97.877 / 1), color(--lch99o 81.94 41.72 97.877 / 1), color(--lch99o 76.819 40.558 97.877 / 1), color(--lch99o 71.697 39.356 97.877 / 1), color(--lch99o 66.576 38.109 97.877 / 1), color(--lch99o 61.455 36.813 97.877 / 1), color(--lch99o 56.334 35.464 97.877 / 1), color(--lch99o 51.212 34.055 97.877 / 1), color(--lch99o 46.091 32.58 97.877 / 1), color(--lch99o 40.97 31.031 97.877 / 1), color(--lch99o 35.849 29.399 97.877 / 1), color(--lch99o 30.727 27.581 97.877 / 1), color(--lch99o 25.606 25.272 97.877 / 1), color(--lch99o 20.485 22.368 97.877 / 1), color(--lch99o 15.364 18.721 97.877 / 1), color(--lch99o 10.242 14.071 97.877 / 1), color(--lch99o 5.1212 8.0424 97.877 / 1), color(--lch99o 0 0 none / 1)]
>>> yellow = Color('color(display-p3 1 1 0)')
>>> lightness_mask = Color('color(--hct none none 0%)')
>>> Steps([c.fit('srgb', method='raytrace', pspace='hct') for c in Color.steps([yellow, lightness_mask], steps=20, space='hct')])
[color(--hct 111.07 75.366 96.849 / 1), color(--hct 111.07 72.594 91.752 / 1), color(--hct 111.07 69.788 86.655 / 1), color(--hct 111.07 66.946 81.557 / 1), color(--hct 111.07 64.066 76.46 / 1), color(--hct 111.07 61.147 71.363 / 1), color(--hct 111.07 58.186 66.265 / 1), color(--hct 111.07 55.18 61.168 / 1), color(--hct 111.07 52.127 56.071 / 1), color(--hct 111.07 49.024 50.973 / 1), color(--hct 111.07 45.866 45.876 / 1), color(--hct 111.07 42.649 40.779 / 1), color(--hct 111.07 39.368 35.681 / 1), color(--hct 111.07 36.015 30.584 / 1), color(--hct 111.07 32.581 25.487 / 1), color(--hct 111.07 29.056 20.389 / 1), color(--hct 111.07 25.424 15.292 / 1), color(--hct 111.07 21.666 10.195 / 1), color(--hct 111.07 17.402 5.0973 / 1), color(--hct none 0 0 / 1)]
>>> yellow = Color('color(display-p3 1 1 0)')
>>> lightness_mask = Color('color(jzczhz 0% none none)')
>>> Steps([c.fit('srgb', method='raytrace', pspace='jzczhz') for c in Color.steps([yellow, lightness_mask], steps=20, space='jzczhz')])
[color(jzczhz 0.2083 0.13765 102.74 / 1), color(jzczhz 0.19734 0.13381 102.74 / 1), color(jzczhz 0.18637 0.12977 102.74 / 1), color(jzczhz 0.17541 0.12552 102.74 / 1), color(jzczhz 0.16445 0.12103 102.74 / 1), color(jzczhz 0.15348 0.1163 102.74 / 1), color(jzczhz 0.14252 0.1113 102.74 / 1), color(jzczhz 0.13156 0.10601 102.74 / 1), color(jzczhz 0.12059 0.10039 102.74 / 1), color(jzczhz 0.10963 0.09443 102.74 / 1), color(jzczhz 0.09867 0.08808 102.74 / 1), color(jzczhz 0.08771 0.0813 102.74 / 1), color(jzczhz 0.07674 0.07405 102.74 / 1), color(jzczhz 0.06578 0.06625 102.74 / 1), color(jzczhz 0.05482 0.05785 102.74 / 1), color(jzczhz 0.04385 0.04873 102.74 / 1), color(jzczhz 0.03289 0.03876 102.74 / 1), color(jzczhz 0.02193 0.02772 102.74 / 1), color(jzczhz 0.01096 0.01523 102.74 / 1), color(jzczhz 0 0 none / 1)]
>>> yellow = Color('color(display-p3 1 1 0)')
>>> lightness_mask = Color('color(--lchuv 0% none none)')
>>> Steps([c.fit('srgb', method='raytrace', pspace='lchuv') for c in Color.steps([yellow, lightness_mask], steps=20, space='lchuv')])
[color(--lchuv 96.849 106.77 85.874 / 1), color(--lchuv 91.752 101.15 85.874 / 1), color(--lchuv 86.655 95.528 85.874 / 1), color(--lchuv 81.557 89.909 85.874 / 1), color(--lchuv 76.46 84.29 85.874 / 1), color(--lchuv 71.363 78.67 85.874 / 1), color(--lchuv 66.265 73.051 85.874 / 1), color(--lchuv 61.168 67.432 85.874 / 1), color(--lchuv 56.071 61.812 85.874 / 1), color(--lchuv 50.973 56.193 85.874 / 1), color(--lchuv 45.876 50.574 85.874 / 1), color(--lchuv 40.779 44.954 85.874 / 1), color(--lchuv 35.681 39.335 85.874 / 1), color(--lchuv 30.584 33.716 85.874 / 1), color(--lchuv 25.487 28.097 85.874 / 1), color(--lchuv 20.389 22.477 85.874 / 1), color(--lchuv 15.292 16.858 85.874 / 1), color(--lchuv 10.195 11.239 85.874 / 1), color(--lchuv 5.0973 5.6193 85.874 / 1), color(--lchuv 0 0 none / 1)]

Lastly, if we have a particular color space that we'd like to have as the default, we can derive a variant for our particular color space using our preferred method of gamut mapping. Generally it is recommended to use the base plugins minde-chroma or raytrace as the base.

>>> from coloraide.gamut.fit_raytrace import RayTrace
>>> from coloraide.spaces.hct import HCT
>>> from coloraide import Color as Base
>>> class HCTRayTrace(RayTrace):
...     """Apply gamut mapping using ray tracing."""
... 
...     NAME = 'hct-raytrace'
...     PSPACE = "hct"
... 
>>> class Color(Base): ...
... 
>>> Color.register([HCT(), HCTRayTrace()])
>>> c = Color('hct', [325, 24, 50])
>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit='hct-raytrace') for tone in tones])
['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']

If we want to make ray tracing the default algorithm for all gamut mapping, we can simply set FIT to our method.

class Color(Base):
    FIT = 'hct-raytrace'

Color.register(HCTRayTrace(), overwrite=True)
>>> from coloraide import Color as Base
>>> from coloraide.spaces.hct import HCT
>>> from coloraide.distance.delta_e_hct import DEHCT
>>> from coloraide.gamut.fit_minde_chroma import MINDEChroma
>>> class HCTChroma(MINDEChroma):
...     """HCT chroma gamut mapping class."""
... 
...     NAME = "hct-chroma"
...     JND = 2.0
...     DE_OPTIONS = {"method": "hct"}
...     PSPACE = "hct"
... 
>>> class Color(Base): ...
... 
>>> Color.register([HCT(), DEHCT(), HCTChroma()])
>>> c = Color('hct', [325, 24, 50])
>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit='hct-chroma') for tone in tones])
['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffe8ff', '#ffffff']

If we want to make ray tracing the default algorithm for all gamut mapping, we can simply set FIT to our method.

class Color(Base):
    FIT = 'hct-chroma'

Color.register(HCTChroma(), overwrite=True)

Adaptive Lightness

Both MINDE chroma reduction and ray trace chroma reduction support adaptive lightness. This is an option to allow for a non-constant lightness chroma reduction. Adaptive lightness allows us to essentially choose a dynamic anchor point different than our constant lightness anchor. Choosing a different anchor point can allow us to preserve more chroma/colorfulness by sacrificing some lightness.

Essentially, a lightness focal point is selected by which the chroma reduction will be biased towards by some factor ɑ. This is done by taking this focal lightness and using it as a reference by which a new, non-constant lightness anchor point is calculated relative to this focal point. The anchor point is biased more or less towards this focal lightness depending on how large or small the ɑ value is respectively.

Below shows examples of chroma reduction with a focal lightness of 50% and an ɑ value of 0, 0.5, and 5 respectively.

Adaptive Lightness ɑ = 0

Adaptive Lightness ɑ = 0.5

Adaptive Lightness ɑ = 5

There are various ways in which adaptive lightness could be implemented, but we will talk about two basic approaches: hue independent and hue dependent.

A hue independent approach will select some lightness focal point. This focal point can be anywhere, but it will be the same regardless of the hue. This focal point will be used to calculate new anchor points relative to the focal point based on the given ɑ that will guide the chroma reduction.

A hue dependent approach will select a lightness focal point for each hue. In such approaches, the lightness selected is often a lightness value that corresponds with the maximum chroma for the current hue in the target gamut, this lightness and chroma point being called the cusp. By biasing chroma reduction towards the lightness of the cusp, it better accounts for the geometry of the perceptual color space and ensures that the process is providing colors with more, not less, chroma in all situations.

Hue Independent ɑ = 0

Hue Dependent ɑ = 0.5

Note

The current algorithms we use to determine anchor points relative to a lightness focal point is described in the following article by the author of Oklab:

These algorithms are not specific to Oklab and are general functions that can be applied to any similar color space.

Generally, the hue dependent approach is the more preferable approach, but it is more complex to calculate the cusp in every supported gamut for a every perceptual space in a performant, generic way. Conversely, the hue independent approach requires no specialized calculation as the lightness focal point is a single, arbitrary point with no relation to the perceptual hue or the gamut making it fast and easy to calculate the required anchor points, but its practical range will be more limited. If only a smaller factor is needed, the hue dependent approach could be a faster, and a more desirable approach.

Currently, ColorAide only implements a hue independent approach as a generalized approach for any perceptual space and any target gamut is difficult, that's not to say that a more specialized, limited hue dependent approach can't be offered in the future, but none are currently offered at this time.

To enable hue independent adaptive lightness, simply set the adaptive option in any MINDE chroma or ray trace gamut mapping method. A factor of 0 is equivalent to constant lightness and will disables adaptive lightness. Any other positive value will create a bias towards the focal point of 50% lightness. The biasing of the chroma reduction towards the focal point will increase as the provided factor increases. A practical range might be more in the ballpark of 0 - 0.5, maybe even lower, but higher values can be used if desired.

>>> Steps(
...     [
...         c.fit('srgb', method='raytrace', pspace='oklch', adaptive=0.0)
...         for c in Color.steps(['oklch(80% 0.4 0)', 'oklch(80% 0.4 360)'], space='oklch', steps=100, hue='longer')
...     ]
... )
[color(--oklch 0.8 0.12455 360 / 1), color(--oklch 0.8 0.12152 3.6364 / 1), color(--oklch 0.8 0.11908 7.2727 / 1), color(--oklch 0.8 0.11718 10.909 / 1), color(--oklch 0.8 0.11577 14.545 / 1), color(--oklch 0.8 0.11484 18.182 / 1), color(--oklch 0.8 0.11437 21.818 / 1), color(--oklch 0.8 0.11434 25.455 / 1), color(--oklch 0.8 0.11476 29.091 / 1), color(--oklch 0.8 0.11563 32.727 / 1), color(--oklch 0.8 0.11697 36.364 / 1), color(--oklch 0.8 0.11881 40 / 1), color(--oklch 0.8 0.12119 43.636 / 1), color(--oklch 0.8 0.12416 47.273 / 1), color(--oklch 0.8 0.12778 50.909 / 1), color(--oklch 0.8 0.13214 54.546 / 1), color(--oklch 0.8 0.13735 58.185 / 1), color(--oklch 0.8 0.14357 61.826 / 1), color(--oklch 0.8 0.15099 65.476 / 1), color(--oklch 0.8 0.15982 69.118 / 1), color(--oklch 0.8 0.17031 72.727 / 1), color(--oklch 0.8 0.16779 76.364 / 1), color(--oklch 0.8 0.1657 80 / 1), color(--oklch 0.8 0.1643 83.636 / 1), color(--oklch 0.8 0.16359 87.273 / 1), color(--oklch 0.8 0.16353 90.909 / 1), color(--oklch 0.8 0.16414 94.545 / 1), color(--oklch 0.8 0.16542 98.182 / 1), color(--oklch 0.8 0.1674 101.82 / 1), color(--oklch 0.8 0.17012 105.45 / 1), color(--oklch 0.8 0.17364 109.09 / 1), color(--oklch 0.8 0.17804 112.73 / 1), color(--oklch 0.8 0.18342 116.36 / 1), color(--oklch 0.8 0.18993 120 / 1), color(--oklch 0.8 0.19775 123.64 / 1), color(--oklch 0.8 0.2071 127.27 / 1), color(--oklch 0.8 0.21831 130.91 / 1), color(--oklch 0.8 0.23178 134.55 / 1), color(--oklch 0.8 0.24809 138.18 / 1), color(--oklch 0.8 0.26802 141.82 / 1), color(--oklch 0.8 0.24828 145.45 / 1), color(--oklch 0.8 0.22524 149.09 / 1), color(--oklch 0.8 0.20717 152.73 / 1), color(--oklch 0.8 0.19271 156.36 / 1), color(--oklch 0.8 0.18097 160 / 1), color(--oklch 0.8 0.17135 163.64 / 1), color(--oklch 0.8 0.16341 167.27 / 1), color(--oklch 0.8 0.15685 170.91 / 1), color(--oklch 0.8 0.15144 174.55 / 1), color(--oklch 0.8 0.14702 178.18 / 1), color(--oklch 0.8 0.14346 181.82 / 1), color(--oklch 0.8 0.14067 185.45 / 1), color(--oklch 0.8 0.13857 189.09 / 1), color(--oklch 0.8 0.13711 192.73 / 1), color(--oklch 0.8 0.13626 196.36 / 1), color(--oklch 0.8 0.136 200 / 1), color(--oklch 0.8 0.13634 203.64 / 1), color(--oklch 0.8 0.13726 207.27 / 1), color(--oklch 0.8 0.1388 210.91 / 1), color(--oklch 0.8 0.141 214.55 / 1), color(--oklch 0.8 0.14391 218.18 / 1), color(--oklch 0.8 0.14309 221.82 / 1), color(--oklch 0.8 0.13463 225.45 / 1), color(--oklch 0.8 0.1276 229.09 / 1), color(--oklch 0.8 0.12174 232.73 / 1), color(--oklch 0.8 0.11684 236.36 / 1), color(--oklch 0.8 0.11276 240 / 1), color(--oklch 0.8 0.10938 243.64 / 1), color(--oklch 0.8 0.10661 247.27 / 1), color(--oklch 0.8 0.10439 250.91 / 1), color(--oklch 0.8 0.10267 254.55 / 1), color(--oklch 0.8 0.1014 258.18 / 1), color(--oklch 0.8 0.10057 261.82 / 1), color(--oklch 0.8 0.10015 265.45 / 1), color(--oklch 0.8 0.10013 269.09 / 1), color(--oklch 0.8 0.10052 272.73 / 1), color(--oklch 0.8 0.10132 276.36 / 1), color(--oklch 0.8 0.10255 280 / 1), color(--oklch 0.8 0.10424 283.64 / 1), color(--oklch 0.8 0.10642 287.27 / 1), color(--oklch 0.8 0.10914 290.91 / 1), color(--oklch 0.8 0.11247 294.55 / 1), color(--oklch 0.8 0.11649 298.18 / 1), color(--oklch 0.8 0.12132 301.82 / 1), color(--oklch 0.8 0.1271 305.45 / 1), color(--oklch 0.8 0.13402 309.09 / 1), color(--oklch 0.8 0.14236 312.73 / 1), color(--oklch 0.8 0.15245 316.36 / 1), color(--oklch 0.8 0.1648 320 / 1), color(--oklch 0.8 0.18012 323.64 / 1), color(--oklch 0.8 0.19909 327.27 / 1), color(--oklch 0.8 0.18377 330.91 / 1), color(--oklch 0.8 0.1711 334.55 / 1), color(--oklch 0.8 0.16053 338.18 / 1), color(--oklch 0.8 0.15168 341.82 / 1), color(--oklch 0.8 0.14423 345.45 / 1), color(--oklch 0.8 0.13796 349.09 / 1), color(--oklch 0.8 0.13268 352.73 / 1), color(--oklch 0.8 0.12824 356.36 / 1), color(--oklch 0.8 0.12455 360 / 1)]
>>> Steps(
...     [
...         c.fit('srgb', method='raytrace', pspace='oklch', adaptive=0.5)
...         for c in Color.steps(['oklch(80% 0.4 0)', 'oklch(80% 0.4 360)'], space='oklch', steps=100, hue='longer')
...     ]
... )
[color(--oklch 0.73529 0.17729 0 / 1), color(--oklch 0.73426 0.17375 3.6364 / 1), color(--oklch 0.73342 0.17086 7.2727 / 1), color(--oklch 0.73275 0.16857 10.909 / 1), color(--oklch 0.73225 0.16684 14.545 / 1), color(--oklch 0.7319 0.16565 18.182 / 1), color(--oklch 0.73171 0.16498 21.818 / 1), color(--oklch 0.73167 0.16483 25.455 / 1), color(--oklch 0.73177 0.1652 29.091 / 1), color(--oklch 0.73203 0.16608 32.727 / 1), color(--oklch 0.73244 0.16751 36.364 / 1), color(--oklch 0.73302 0.16949 40 / 1), color(--oklch 0.73377 0.17207 43.636 / 1), color(--oklch 0.7347 0.17528 47.273 / 1), color(--oklch 0.73584 0.17918 50.91 / 1), color(--oklch 0.73709 0.18351 54.546 / 1), color(--oklch 0.73478 0.17554 58.182 / 1), color(--oklch 0.73286 0.16893 61.818 / 1), color(--oklch 0.73128 0.16349 65.455 / 1), color(--oklch 0.72998 0.15904 69.091 / 1), color(--oklch 0.72895 0.15549 72.726 / 1), color(--oklch 0.72815 0.15273 76.357 / 1), color(--oklch 0.72757 0.1507 79.992 / 1), color(--oklch 0.72717 0.14935 83.633 / 1), color(--oklch 0.72697 0.14865 87.271 / 1), color(--oklch 0.72695 0.1486 90.909 / 1), color(--oklch 0.72712 0.14919 94.545 / 1), color(--oklch 0.72748 0.15042 98.182 / 1), color(--oklch 0.72804 0.15234 101.82 / 1), color(--oklch 0.7288 0.15498 105.45 / 1), color(--oklch 0.7298 0.1584 109.09 / 1), color(--oklch 0.73105 0.16269 112.73 / 1), color(--oklch 0.73258 0.16796 116.36 / 1), color(--oklch 0.73444 0.17437 120 / 1), color(--oklch 0.73669 0.1821 123.64 / 1), color(--oklch 0.73939 0.19141 127.27 / 1), color(--oklch 0.74266 0.20266 130.91 / 1), color(--oklch 0.74663 0.21632 134.55 / 1), color(--oklch 0.75149 0.23305 138.18 / 1), color(--oklch 0.75752 0.25379 141.82 / 1), color(--oklch 0.75155 0.23325 145.45 / 1), color(--oklch 0.74469 0.20967 149.09 / 1), color(--oklch 0.73941 0.19148 152.73 / 1), color(--oklch 0.73524 0.17711 156.36 / 1), color(--oklch 0.73188 0.16556 160 / 1), color(--oklch 0.72915 0.15617 163.64 / 1), color(--oklch 0.72692 0.14848 167.27 / 1), color(--oklch 0.72508 0.14216 170.91 / 1), color(--oklch 0.72357 0.13698 174.55 / 1), color(--oklch 0.72235 0.13275 178.18 / 1), color(--oklch 0.72136 0.12936 181.82 / 1), color(--oklch 0.72059 0.12671 185.45 / 1), color(--oklch 0.72001 0.12471 189.09 / 1), color(--oklch 0.71961 0.12333 192.73 / 1), color(--oklch 0.71938 0.12253 196.36 / 1), color(--oklch 0.7193 0.12229 200 / 1), color(--oklch 0.7194 0.1226 203.64 / 1), color(--oklch 0.71965 0.12347 207.27 / 1), color(--oklch 0.72008 0.12494 210.91 / 1), color(--oklch 0.72068 0.12702 214.55 / 1), color(--oklch 0.72149 0.12979 218.18 / 1), color(--oklch 0.72251 0.13332 221.82 / 1), color(--oklch 0.72379 0.13771 225.46 / 1), color(--oklch 0.72535 0.14311 229.09 / 1), color(--oklch 0.72727 0.14971 232.73 / 1), color(--oklch 0.72963 0.15781 236.36 / 1), color(--oklch 0.72931 0.15671 240 / 1), color(--oklch 0.72816 0.15276 243.64 / 1), color(--oklch 0.72721 0.1495 247.27 / 1), color(--oklch 0.72645 0.14687 250.91 / 1), color(--oklch 0.72585 0.14482 254.55 / 1), color(--oklch 0.72542 0.14332 258.18 / 1), color(--oklch 0.72513 0.14233 261.82 / 1), color(--oklch 0.72499 0.14184 265.45 / 1), color(--oklch 0.72499 0.14185 269.09 / 1), color(--oklch 0.72513 0.14235 272.73 / 1), color(--oklch 0.72542 0.14335 276.36 / 1), color(--oklch 0.72586 0.14486 280 / 1), color(--oklch 0.72646 0.14692 283.64 / 1), color(--oklch 0.72723 0.14955 287.27 / 1), color(--oklch 0.72818 0.15282 290.91 / 1), color(--oklch 0.72933 0.15678 294.55 / 1), color(--oklch 0.7307 0.16151 298.18 / 1), color(--oklch 0.73233 0.16712 301.82 / 1), color(--oklch 0.73425 0.17373 305.45 / 1), color(--oklch 0.73652 0.18152 309.09 / 1), color(--oklch 0.73918 0.1907 312.73 / 1), color(--oklch 0.74233 0.20154 316.36 / 1), color(--oklch 0.74608 0.21443 320 / 1), color(--oklch 0.75056 0.22985 323.64 / 1), color(--oklch 0.75598 0.24851 327.27 / 1), color(--oklch 0.75313 0.2387 330.91 / 1), color(--oklch 0.7496 0.22655 334.55 / 1), color(--oklch 0.74655 0.21605 338.18 / 1), color(--oklch 0.74391 0.20697 341.82 / 1), color(--oklch 0.74163 0.19911 345.45 / 1), color(--oklch 0.73966 0.19233 349.09 / 1), color(--oklch 0.73796 0.1865 352.73 / 1), color(--oklch 0.73651 0.18151 356.36 / 1), color(--oklch 0.73529 0.17729 0 / 1)]
>>> Steps(
...     [
...         c.fit('srgb', method='minde-chroma', pspace='oklch', adaptive=0.0, jnd=0)
...         for c in Color.steps(['oklch(80% 0.4 0)', 'oklch(80% 0.4 360)'], space='oklch', steps=100, hue='longer')
...     ]
... )
[color(--oklch 0.79997 0.12458 359.99 / 1), color(--oklch 0.79997 0.12155 3.6308 / 1), color(--oklch 0.79997 0.11911 7.2679 / 1), color(--oklch 0.79999 0.11718 10.908 / 1), color(--oklch 0.79997 0.11579 14.543 / 1), color(--oklch 0.8 0.11484 18.182 / 1), color(--oklch 0.79995 0.1144 21.818 / 1), color(--oklch 0.79999 0.11435 25.455 / 1), color(--oklch 0.79995 0.11479 29.094 / 1), color(--oklch 0.79994 0.11567 32.733 / 1), color(--oklch 0.79999 0.11698 36.365 / 1), color(--oklch 0.79998 0.11883 40.003 / 1), color(--oklch 0.79995 0.12124 43.647 / 1), color(--oklch 0.79997 0.12419 47.28 / 1), color(--oklch 0.79997 0.12781 50.916 / 1), color(--oklch 0.79995 0.13219 54.558 / 1), color(--oklch 0.79997 0.13738 58.189 / 1), color(--oklch 0.8 0.14355 61.818 / 1), color(--oklch 0.79998 0.15097 65.46 / 1), color(--oklch 0.79999 0.15976 69.093 / 1), color(--oklch 0.8 0.17031 72.728 / 1), color(--oklch 0.80001 0.1678 76.347 / 1), color(--oklch 0.8 0.1657 79.995 / 1), color(--oklch 0.80001 0.16431 83.627 / 1), color(--oklch 0.80001 0.16359 87.26 / 1), color(--oklch 0.8 0.16353 90.904 / 1), color(--oklch 0.8 0.16414 94.543 / 1), color(--oklch 0.8 0.16542 98.181 / 1), color(--oklch 0.80001 0.1674 101.81 / 1), color(--oklch 0.80001 0.17012 105.45 / 1), color(--oklch 0.80001 0.17364 109.09 / 1), color(--oklch 0.80001 0.17804 112.73 / 1), color(--oklch 0.80001 0.18343 116.36 / 1), color(--oklch 0.8 0.18993 120 / 1), color(--oklch 0.8 0.19775 123.64 / 1), color(--oklch 0.8 0.20711 127.27 / 1), color(--oklch 0.8 0.21832 130.91 / 1), color(--oklch 0.8 0.2318 134.55 / 1), color(--oklch 0.8 0.24811 138.19 / 1), color(--oklch 0.8 0.26804 141.82 / 1), color(--oklch 0.80001 0.24832 145.45 / 1), color(--oklch 0.80002 0.22527 149.09 / 1), color(--oklch 0.80002 0.2072 152.72 / 1), color(--oklch 0.80002 0.19274 156.36 / 1), color(--oklch 0.80003 0.18101 159.99 / 1), color(--oklch 0.80001 0.17136 163.63 / 1), color(--oklch 0.80003 0.16343 167.27 / 1), color(--oklch 0.80004 0.15687 170.9 / 1), color(--oklch 0.80001 0.15145 174.54 / 1), color(--oklch 0.80002 0.14703 178.18 / 1), color(--oklch 0.80004 0.14348 181.81 / 1), color(--oklch 0.80003 0.14067 185.45 / 1), color(--oklch 0.8 0.13857 189.09 / 1), color(--oklch 0.8 0.13711 192.73 / 1), color(--oklch 0.80003 0.13627 196.37 / 1), color(--oklch 0.80001 0.13601 200 / 1), color(--oklch 0.80004 0.13634 203.64 / 1), color(--oklch 0.80002 0.13727 207.28 / 1), color(--oklch 0.80003 0.13881 210.92 / 1), color(--oklch 0.80001 0.141 214.55 / 1), color(--oklch 0.80002 0.14392 218.19 / 1), color(--oklch 0.79998 0.14314 221.8 / 1), color(--oklch 0.79999 0.13465 225.44 / 1), color(--oklch 0.79999 0.12762 229.08 / 1), color(--oklch 0.79999 0.12176 232.72 / 1), color(--oklch 0.79999 0.11687 236.35 / 1), color(--oklch 0.79999 0.11277 239.99 / 1), color(--oklch 0.79998 0.10941 243.61 / 1), color(--oklch 0.79999 0.10662 247.27 / 1), color(--oklch 0.8 0.10439 250.91 / 1), color(--oklch 0.79998 0.10268 254.53 / 1), color(--oklch 0.79998 0.10141 258.17 / 1), color(--oklch 0.79999 0.10057 261.81 / 1), color(--oklch 0.79999 0.10015 265.45 / 1), color(--oklch 0.79998 0.10014 269.08 / 1), color(--oklch 0.79998 0.10053 272.72 / 1), color(--oklch 0.79999 0.10133 276.36 / 1), color(--oklch 0.79998 0.10256 280 / 1), color(--oklch 0.79998 0.10425 283.64 / 1), color(--oklch 0.79999 0.10642 287.27 / 1), color(--oklch 0.79999 0.10915 290.91 / 1), color(--oklch 0.79999 0.11247 294.55 / 1), color(--oklch 0.8 0.11649 298.18 / 1), color(--oklch 0.79998 0.12134 301.83 / 1), color(--oklch 0.79999 0.12712 305.46 / 1), color(--oklch 0.79999 0.13405 309.1 / 1), color(--oklch 0.79999 0.14237 312.73 / 1), color(--oklch 0.79998 0.1525 316.38 / 1), color(--oklch 0.79999 0.16483 320.01 / 1), color(--oklch 0.79999 0.18016 323.64 / 1), color(--oklch 0.79999 0.19912 327.27 / 1), color(--oklch 0.79999 0.18379 330.91 / 1), color(--oklch 0.79996 0.17117 334.54 / 1), color(--oklch 0.79999 0.16054 338.18 / 1), color(--oklch 0.79997 0.15173 341.81 / 1), color(--oklch 0.8 0.14424 345.45 / 1), color(--oklch 0.79999 0.13798 349.09 / 1), color(--oklch 0.79998 0.1327 352.72 / 1), color(--oklch 0.79996 0.12828 356.35 / 1), color(--oklch 0.79997 0.12458 359.99 / 1)]
>>> Steps(
...     [
...         c.fit('srgb', method='minde-chroma', pspace='oklch', adaptive=0.5, jnd=0)
...         for c in Color.steps(['oklch(80% 0.4 0)', 'oklch(80% 0.4 360)'], space='oklch', steps=100, hue='longer')
...     ]
... )
[color(--oklch 0.73528 0.17729 360 / 1), color(--oklch 0.73426 0.17375 3.6363 / 1), color(--oklch 0.73341 0.17087 7.2719 / 1), color(--oklch 0.73275 0.16857 10.908 / 1), color(--oklch 0.73225 0.16684 14.545 / 1), color(--oklch 0.7319 0.16565 18.182 / 1), color(--oklch 0.73171 0.16499 21.818 / 1), color(--oklch 0.73166 0.16484 25.455 / 1), color(--oklch 0.73177 0.1652 29.091 / 1), color(--oklch 0.73203 0.16609 32.727 / 1), color(--oklch 0.73243 0.16752 36.365 / 1), color(--oklch 0.73301 0.1695 40.001 / 1), color(--oklch 0.73377 0.17207 43.637 / 1), color(--oklch 0.7347 0.17529 47.274 / 1), color(--oklch 0.73583 0.17919 50.911 / 1), color(--oklch 0.7371 0.18351 54.543 / 1), color(--oklch 0.73479 0.17555 58.177 / 1), color(--oklch 0.73286 0.16894 61.815 / 1), color(--oklch 0.73128 0.16349 65.451 / 1), color(--oklch 0.72999 0.15905 69.087 / 1), color(--oklch 0.72895 0.15549 72.725 / 1), color(--oklch 0.72815 0.15272 76.361 / 1), color(--oklch 0.72756 0.15069 79.997 / 1), color(--oklch 0.72718 0.14935 83.633 / 1), color(--oklch 0.72697 0.14865 87.272 / 1), color(--oklch 0.72695 0.1486 90.908 / 1), color(--oklch 0.72712 0.14919 94.545 / 1), color(--oklch 0.72749 0.15043 98.18 / 1), color(--oklch 0.72804 0.15234 101.82 / 1), color(--oklch 0.72881 0.15498 105.45 / 1), color(--oklch 0.72981 0.1584 109.09 / 1), color(--oklch 0.73105 0.16269 112.73 / 1), color(--oklch 0.73258 0.16796 116.36 / 1), color(--oklch 0.73445 0.17437 120 / 1), color(--oklch 0.73669 0.1821 123.64 / 1), color(--oklch 0.7394 0.19142 127.27 / 1), color(--oklch 0.74266 0.20266 130.91 / 1), color(--oklch 0.74663 0.21632 134.55 / 1), color(--oklch 0.75149 0.23305 138.18 / 1), color(--oklch 0.75752 0.2538 141.82 / 1), color(--oklch 0.75155 0.23325 145.45 / 1), color(--oklch 0.7447 0.20967 149.09 / 1), color(--oklch 0.73942 0.19149 152.72 / 1), color(--oklch 0.73524 0.17712 156.36 / 1), color(--oklch 0.73189 0.16557 160 / 1), color(--oklch 0.72915 0.15617 163.64 / 1), color(--oklch 0.72692 0.14848 167.27 / 1), color(--oklch 0.72508 0.14216 170.91 / 1), color(--oklch 0.72358 0.13698 174.54 / 1), color(--oklch 0.72235 0.13276 178.18 / 1), color(--oklch 0.72137 0.12936 181.82 / 1), color(--oklch 0.72059 0.12671 185.45 / 1), color(--oklch 0.72002 0.12471 189.09 / 1), color(--oklch 0.71962 0.12333 192.73 / 1), color(--oklch 0.71938 0.12253 196.36 / 1), color(--oklch 0.71931 0.12229 200 / 1), color(--oklch 0.7194 0.1226 203.64 / 1), color(--oklch 0.71966 0.12348 207.27 / 1), color(--oklch 0.72009 0.12494 210.91 / 1), color(--oklch 0.72068 0.12702 214.55 / 1), color(--oklch 0.7215 0.12979 218.19 / 1), color(--oklch 0.72252 0.13332 221.82 / 1), color(--oklch 0.7238 0.13771 225.46 / 1), color(--oklch 0.72536 0.14311 229.09 / 1), color(--oklch 0.72729 0.14972 232.73 / 1), color(--oklch 0.72964 0.15783 236.37 / 1), color(--oklch 0.72931 0.15671 240 / 1), color(--oklch 0.72816 0.15276 243.64 / 1), color(--oklch 0.72721 0.1495 247.27 / 1), color(--oklch 0.72645 0.14687 250.91 / 1), color(--oklch 0.72585 0.14482 254.55 / 1), color(--oklch 0.72541 0.14332 258.18 / 1), color(--oklch 0.72513 0.14233 261.82 / 1), color(--oklch 0.72499 0.14184 265.45 / 1), color(--oklch 0.72499 0.14185 269.09 / 1), color(--oklch 0.72513 0.14235 272.73 / 1), color(--oklch 0.72542 0.14335 276.36 / 1), color(--oklch 0.72586 0.14486 280 / 1), color(--oklch 0.72646 0.14692 283.64 / 1), color(--oklch 0.72723 0.14955 287.27 / 1), color(--oklch 0.72818 0.15282 290.91 / 1), color(--oklch 0.72933 0.15678 294.55 / 1), color(--oklch 0.7307 0.16151 298.18 / 1), color(--oklch 0.73233 0.16712 301.82 / 1), color(--oklch 0.73425 0.17374 305.46 / 1), color(--oklch 0.73652 0.18153 309.09 / 1), color(--oklch 0.73918 0.1907 312.73 / 1), color(--oklch 0.74233 0.20155 316.37 / 1), color(--oklch 0.74608 0.21443 320 / 1), color(--oklch 0.75056 0.22986 323.64 / 1), color(--oklch 0.75598 0.24851 327.27 / 1), color(--oklch 0.75313 0.2387 330.91 / 1), color(--oklch 0.7496 0.22656 334.54 / 1), color(--oklch 0.74654 0.21606 338.18 / 1), color(--oklch 0.74391 0.20698 341.82 / 1), color(--oklch 0.74162 0.19912 345.45 / 1), color(--oklch 0.73966 0.19233 349.09 / 1), color(--oklch 0.73796 0.1865 352.73 / 1), color(--oklch 0.73651 0.18151 356.36 / 1), color(--oklch 0.73528 0.17729 360 / 1)]

Note

Generally, adaptive lightness can be used within any perceptual space against any target gamut using the ray trace or MINDE chroma reduction approach. With that said, it should be noted that some spaces will not perform as well at high ɑ values due to their geometry regardless of whether the ray trace or MINDE chroma reduction approach is used.

The one exception to the above statement is the Luv/LCHuv color space which exhibited less accurate results in our testing when using the ray trace approach and high ɑ values that strayed too far from constant lightness. The dark blue region of Luv/LCHuv created chroma reduction curves that pushed the ray trace algorithm too hard with adaptive lightness.

While Luv/LCHuv is currently the only space provided by ColorAide that had trouble with high ɑ values when using the ray trace algorithm, there may be others that have yet to be implemented. In these rare cases, it may be better to use MINDE chroma reduction which, while slower, has a straightforward algorithm that is less likely to have issues. For all others, ray trace will give the best performance with comparable results.

>>> Steps(
...     [
...         c.fit('srgb', method='raytrace', pspace='lchuv', adaptive=0.0)
...         for c in Color.steps(['color(--lchuv 10% 100 270)', 'color(--lchuv 90% 100 270)'], space='luv', steps=100)
...     ]
... )
[color(--luv 10 0 -36.496 / 1), color(--luv 10.808 0 -39.445 / 1), color(--luv 11.616 0 -42.394 / 1), color(--luv 12.424 0 -45.343 / 1), color(--luv 13.232 0 -48.292 / 1), color(--luv 14.04 0 -51.241 / 1), color(--luv 14.848 0 -54.19 / 1), color(--luv 15.657 0 -57.14 / 1), color(--luv 16.465 0 -60.089 / 1), color(--luv 17.273 0 -63.038 / 1), color(--luv 18.081 0 -65.987 / 1), color(--luv 18.889 0 -68.936 / 1), color(--luv 19.697 0 -71.885 / 1), color(--luv 20.505 0 -74.834 / 1), color(--luv 21.313 0 -77.783 / 1), color(--luv 22.121 0 -80.733 / 1), color(--luv 22.929 0 -83.682 / 1), color(--luv 23.737 0 -86.631 / 1), color(--luv 24.545 0 -89.58 / 1), color(--luv 25.354 0 -92.529 / 1), color(--luv 26.162 0 -95.478 / 1), color(--luv 26.97 0 -98.427 / 1), color(--luv 27.778 0 -100 / 1), color(--luv 28.586 0 -100 / 1), color(--luv 29.394 0 -100 / 1), color(--luv 30.202 0 -100 / 1), color(--luv 31.01 0 -100 / 1), color(--luv 31.818 0 -100 / 1), color(--luv 32.626 0 -100 / 1), color(--luv 33.434 0 -100 / 1), color(--luv 34.242 0 -100 / 1), color(--luv 35.051 0 -100 / 1), color(--luv 35.859 0 -100 / 1), color(--luv 36.667 0 -100 / 1), color(--luv 37.475 0 -100 / 1), color(--luv 38.283 0 -100 / 1), color(--luv 39.091 0 -100 / 1), color(--luv 39.899 0 -100 / 1), color(--luv 40.707 0 -100 / 1), color(--luv 41.515 0 -100 / 1), color(--luv 42.323 0 -100 / 1), color(--luv 43.131 0 -100 / 1), color(--luv 43.939 0 -100 / 1), color(--luv 44.747 0 -100 / 1), color(--luv 45.556 0 -100 / 1), color(--luv 46.364 0 -100 / 1), color(--luv 47.172 0 -100 / 1), color(--luv 47.98 0 -100 / 1), color(--luv 48.788 0 -100 / 1), color(--luv 49.596 0 -100 / 1), color(--luv 50.404 0 -100 / 1), color(--luv 51.212 0 -100 / 1), color(--luv 52.02 0 -100 / 1), color(--luv 52.828 0 -100 / 1), color(--luv 53.636 0 -100 / 1), color(--luv 54.444 0 -100 / 1), color(--luv 55.253 0 -100 / 1), color(--luv 56.061 0 -100 / 1), color(--luv 56.869 0 -100 / 1), color(--luv 57.677 0 -100 / 1), color(--luv 58.485 0 -100 / 1), color(--luv 59.293 0 -100 / 1), color(--luv 60.101 0 -100 / 1), color(--luv 60.909 0 -100 / 1), color(--luv 61.717 0 -99.125 / 1), color(--luv 62.525 0 -97.155 / 1), color(--luv 63.333 0 -95.16 / 1), color(--luv 64.141 0 -93.143 / 1), color(--luv 64.949 0 -91.105 / 1), color(--luv 65.758 0 -89.049 / 1), color(--luv 66.566 0 -86.976 / 1), color(--luv 67.374 0 -84.888 / 1), color(--luv 68.182 0 -82.786 / 1), color(--luv 68.99 0 -80.672 / 1), color(--luv 69.798 0 -78.548 / 1), color(--luv 70.606 0 -76.414 / 1), color(--luv 71.414 0 -74.272 / 1), color(--luv 72.222 0 -72.124 / 1), color(--luv 73.03 0 -69.971 / 1), color(--luv 73.838 0 -67.813 / 1), color(--luv 74.646 0 -65.652 / 1), color(--luv 75.455 0 -63.488 / 1), color(--luv 76.263 0 -61.324 / 1), color(--luv 77.071 0 -59.159 / 1), color(--luv 77.879 0 -56.994 / 1), color(--luv 78.687 0 -54.831 / 1), color(--luv 79.495 0 -52.67 / 1), color(--luv 80.303 0 -50.511 / 1), color(--luv 81.111 0 -48.356 / 1), color(--luv 81.919 0 -46.205 / 1), color(--luv 82.727 0 -44.059 / 1), color(--luv 83.535 0 -41.918 / 1), color(--luv 84.343 0 -39.782 / 1), color(--luv 85.152 0 -37.652 / 1), color(--luv 85.96 0 -35.53 / 1), color(--luv 86.768 0 -33.414 / 1), color(--luv 87.576 0 -31.305 / 1), color(--luv 88.384 0 -29.204 / 1), color(--luv 89.192 0 -27.111 / 1), color(--luv 90 0 -25.027 / 1)]
>>> Steps(
...     [
...         c.fit('srgb', method='raytrace', pspace='lchuv', adaptive=0.5)
...         for c in Color.steps(['color(--lchuv 10% 100 270)', 'color(--lchuv 90% 100 270)'], space='luv', steps=100)
...     ]
... )
[color(--luv 17.611 0 -64.273 / 1), color(--luv 17.451 0 -63.689 / 1), color(--luv 17.753 0 -64.79 / 1), color(--luv 16.916 0 -61.737 / 1), color(--luv 18.393 0 -67.125 / 1), color(--luv 18.909 0 -69.008 / 1), color(--luv 20.223 0 -73.806 / 1), color(--luv 20.03 0 -73.099 / 1), color(--luv 35.778 0 -130.57 / 1), color(--luv 32.872 0 -119.97 / 1), color(--luv 30.938 0 -112.91 / 1), color(--luv 29.592 0 -108 / 1), color(--luv 28.638 0 -104.52 / 1), color(--luv 27.96 0 -102.04 / 1), color(--luv 26.531 0 -96.827 / 1), color(--luv 25.668 0 -93.677 / 1), color(--luv 25.499 0 -93.061 / 1), color(--luv 25.618 0 -93.493 / 1), color(--luv 25.886 0 -94.474 / 1), color(--luv 26.246 0 -95.788 / 1), color(--luv 26.669 0 -97.328 / 1), color(--luv 27.136 0 -99.035 / 1), color(--luv 27.778 0 -100 / 1), color(--luv 28.586 0 -100 / 1), color(--luv 29.394 0 -100 / 1), color(--luv 30.202 0 -100 / 1), color(--luv 31.01 0 -100 / 1), color(--luv 31.818 0 -100 / 1), color(--luv 32.626 0 -100 / 1), color(--luv 33.434 0 -100 / 1), color(--luv 34.242 0 -100 / 1), color(--luv 35.051 0 -100 / 1), color(--luv 35.859 0 -100 / 1), color(--luv 36.667 0 -100 / 1), color(--luv 37.475 0 -100 / 1), color(--luv 38.283 0 -100 / 1), color(--luv 39.091 0 -100 / 1), color(--luv 39.899 0 -100 / 1), color(--luv 40.707 0 -100 / 1), color(--luv 41.515 0 -100 / 1), color(--luv 42.323 0 -100 / 1), color(--luv 43.131 0 -100 / 1), color(--luv 43.939 0 -100 / 1), color(--luv 44.747 0 -100 / 1), color(--luv 45.556 0 -100 / 1), color(--luv 46.364 0 -100 / 1), color(--luv 47.172 0 -100 / 1), color(--luv 47.98 0 -100 / 1), color(--luv 48.788 0 -100 / 1), color(--luv 49.596 0 -100 / 1), color(--luv 50.404 0 -100 / 1), color(--luv 51.212 0 -100 / 1), color(--luv 52.02 0 -100 / 1), color(--luv 52.828 0 -100 / 1), color(--luv 53.636 0 -100 / 1), color(--luv 54.444 0 -100 / 1), color(--luv 55.253 0 -100 / 1), color(--luv 56.061 0 -100 / 1), color(--luv 56.869 0 -100 / 1), color(--luv 57.677 0 -100 / 1), color(--luv 58.485 0 -100 / 1), color(--luv 59.293 0 -100 / 1), color(--luv 60.101 0 -100 / 1), color(--luv 60.909 0 -100 / 1), color(--luv 61.67 0 -99.239 / 1), color(--luv 62.362 0 -97.554 / 1), color(--luv 63.041 0 -95.885 / 1), color(--luv 63.705 0 -94.235 / 1), color(--luv 64.356 0 -92.605 / 1), color(--luv 64.993 0 -90.996 / 1), color(--luv 65.616 0 -89.41 / 1), color(--luv 66.227 0 -87.847 / 1), color(--luv 66.825 0 -86.308 / 1), color(--luv 67.41 0 -84.794 / 1), color(--luv 67.983 0 -83.304 / 1), color(--luv 68.544 0 -81.84 / 1), color(--luv 69.093 0 -80.401 / 1), color(--luv 69.631 0 -78.987 / 1), color(--luv 70.157 0 -77.6 / 1), color(--luv 70.673 0 -76.237 / 1), color(--luv 71.178 0 -74.9 / 1), color(--luv 71.672 0 -73.588 / 1), color(--luv 72.156 0 -72.302 / 1), color(--luv 72.629 0 -71.04 / 1), color(--luv 73.093 0 -69.802 / 1), color(--luv 73.548 0 -68.589 / 1), color(--luv 73.993 0 -67.4 / 1), color(--luv 74.429 0 -66.235 / 1), color(--luv 74.855 0 -65.092 / 1), color(--luv 75.274 0 -63.973 / 1), color(--luv 75.683 0 -62.876 / 1), color(--luv 76.084 0 -61.801 / 1), color(--luv 76.478 0 -60.748 / 1), color(--luv 76.863 0 -59.716 / 1), color(--luv 77.24 0 -58.705 / 1), color(--luv 77.61 0 -57.715 / 1), color(--luv 77.972 0 -56.745 / 1), color(--luv 78.327 0 -55.794 / 1), color(--luv 78.675 0 -54.863 / 1), color(--luv 79.016 0 -53.951 / 1)]
>>> Steps(
...     [
...         c.fit('srgb', method='minde-chroma', pspace='lchuv', adaptive=0.0, jnd=0)
...         for c in Color.steps(['color(--lchuv 10% 100 270)', 'color(--lchuv 90% 100 270)'], space='luv', steps=100)
...     ]
... )
[color(--luv 10 -0.00001 -36.496 / 1), color(--luv 10.808 -0.00001 -39.445 / 1), color(--luv 11.616 -0.00001 -42.394 / 1), color(--luv 12.424 -0.00001 -45.343 / 1), color(--luv 13.232 -0.00001 -48.292 / 1), color(--luv 14.04 -0.00001 -51.241 / 1), color(--luv 14.849 -0.00001 -54.19 / 1), color(--luv 15.657 -0.00001 -57.14 / 1), color(--luv 16.465 -0.00001 -60.089 / 1), color(--luv 17.273 0 -63.038 / 1), color(--luv 18.081 0 -65.987 / 1), color(--luv 18.889 0 -68.936 / 1), color(--luv 19.697 0 -71.885 / 1), color(--luv 20.505 0 -74.834 / 1), color(--luv 21.313 -0.00001 -77.783 / 1), color(--luv 22.121 -0.00001 -80.733 / 1), color(--luv 22.929 -0.00001 -83.682 / 1), color(--luv 23.737 -0.00001 -86.631 / 1), color(--luv 24.545 -0.00001 -89.58 / 1), color(--luv 25.354 -0.00001 -92.529 / 1), color(--luv 26.162 -0.00001 -95.478 / 1), color(--luv 26.97 -0.00001 -98.427 / 1), color(--luv 27.778 0 -100 / 1), color(--luv 28.586 0 -100 / 1), color(--luv 29.394 0 -100 / 1), color(--luv 30.202 0 -100 / 1), color(--luv 31.01 0 -100 / 1), color(--luv 31.818 0 -100 / 1), color(--luv 32.626 0 -100 / 1), color(--luv 33.434 0 -100 / 1), color(--luv 34.242 0 -100 / 1), color(--luv 35.051 0 -100 / 1), color(--luv 35.859 0 -100 / 1), color(--luv 36.667 0 -100 / 1), color(--luv 37.475 0 -100 / 1), color(--luv 38.283 0 -100 / 1), color(--luv 39.091 0 -100 / 1), color(--luv 39.899 0 -100 / 1), color(--luv 40.707 0 -100 / 1), color(--luv 41.515 0 -100 / 1), color(--luv 42.323 0 -100 / 1), color(--luv 43.131 0 -100 / 1), color(--luv 43.939 0 -100 / 1), color(--luv 44.747 0 -100 / 1), color(--luv 45.556 0 -100 / 1), color(--luv 46.364 0 -100 / 1), color(--luv 47.172 0 -100 / 1), color(--luv 47.98 0 -100 / 1), color(--luv 48.788 0 -100 / 1), color(--luv 49.596 0 -100 / 1), color(--luv 50.404 0 -100 / 1), color(--luv 51.212 0 -100 / 1), color(--luv 52.02 0 -100 / 1), color(--luv 52.828 0 -100 / 1), color(--luv 53.636 0 -100 / 1), color(--luv 54.444 0 -100 / 1), color(--luv 55.253 0 -100 / 1), color(--luv 56.061 0 -100 / 1), color(--luv 56.869 0 -100 / 1), color(--luv 57.677 0 -100 / 1), color(--luv 58.485 0 -100 / 1), color(--luv 59.293 0 -100 / 1), color(--luv 60.101 0 -100 / 1), color(--luv 60.909 0 -100 / 1), color(--luv 61.717 0 -99.125 / 1), color(--luv 62.525 0 -97.155 / 1), color(--luv 63.333 0.00001 -95.16 / 1), color(--luv 64.141 0 -93.143 / 1), color(--luv 64.949 0.00001 -91.105 / 1), color(--luv 65.758 0 -89.049 / 1), color(--luv 66.566 0.00001 -86.976 / 1), color(--luv 67.374 0.00001 -84.888 / 1), color(--luv 68.182 0 -82.786 / 1), color(--luv 68.99 0.00001 -80.672 / 1), color(--luv 69.798 0.00001 -78.548 / 1), color(--luv 70.606 0.00001 -76.414 / 1), color(--luv 71.414 0.00001 -74.272 / 1), color(--luv 72.222 0 -72.124 / 1), color(--luv 73.03 0 -69.971 / 1), color(--luv 73.838 0.00001 -67.813 / 1), color(--luv 74.646 0 -65.652 / 1), color(--luv 75.455 0 -63.488 / 1), color(--luv 76.263 0 -61.324 / 1), color(--luv 77.071 0 -59.159 / 1), color(--luv 77.879 0 -56.994 / 1), color(--luv 78.687 0.00001 -54.831 / 1), color(--luv 79.495 0 -52.67 / 1), color(--luv 80.303 0 -50.511 / 1), color(--luv 81.111 0 -48.356 / 1), color(--luv 81.919 0 -46.205 / 1), color(--luv 82.727 0 -44.059 / 1), color(--luv 83.535 0.00001 -41.918 / 1), color(--luv 84.343 0 -39.782 / 1), color(--luv 85.152 0 -37.652 / 1), color(--luv 85.96 0.00001 -35.53 / 1), color(--luv 86.768 0 -33.414 / 1), color(--luv 87.576 0 -31.305 / 1), color(--luv 88.384 0 -29.204 / 1), color(--luv 89.192 0 -27.111 / 1), color(--luv 90 0 -25.027 / 1)]
>>> Steps(
...     [
...         c.fit('srgb', method='minde-chroma', pspace='lchuv', adaptive=0.5, jnd=0)
...         for c in Color.steps(['color(--lchuv 10% 100 270)', 'color(--lchuv 90% 100 270)'], space='luv', steps=100)
...     ]
... )
[color(--luv 18.099 -0.00091 -66.054 / 1), color(--luv 18.433 -0.00004 -67.273 / 1), color(--luv 18.778 -0.00146 -68.533 / 1), color(--luv 19.129 -0.00145 -69.815 / 1), color(--luv 19.489 -0.00153 -71.127 / 1), color(--luv 19.857 -0.00147 -72.47 / 1), color(--luv 20.233 -0.00106 -73.842 / 1), color(--luv 20.617 -0.00008 -75.244 / 1), color(--luv 21.014 -0.00154 -76.693 / 1), color(--luv 21.417 -0.00033 -78.162 / 1), color(--luv 21.832 -0.00103 -79.677 / 1), color(--luv 22.255 -0.00021 -81.22 / 1), color(--luv 22.689 -0.00076 -82.807 / 1), color(--luv 23.134 -0.00084 -84.431 / 1), color(--luv 23.589 -0.00022 -86.09 / 1), color(--luv 24.056 -0.00014 -87.793 / 1), color(--luv 24.534 -0.00031 -89.539 / 1), color(--luv 25.024 -0.00045 -91.327 / 1), color(--luv 25.526 -0.00027 -93.158 / 1), color(--luv 26.041 -0.00093 -95.039 / 1), color(--luv 26.568 -0.00071 -96.961 / 1), color(--luv 27.108 -0.00071 -98.933 / 1), color(--luv 27.778 0 -100 / 1), color(--luv 28.586 0 -100 / 1), color(--luv 29.394 0 -100 / 1), color(--luv 30.202 0 -100 / 1), color(--luv 31.01 0 -100 / 1), color(--luv 31.818 0 -100 / 1), color(--luv 32.626 0 -100 / 1), color(--luv 33.434 0 -100 / 1), color(--luv 34.242 0 -100 / 1), color(--luv 35.051 0 -100 / 1), color(--luv 35.859 0 -100 / 1), color(--luv 36.667 0 -100 / 1), color(--luv 37.475 0 -100 / 1), color(--luv 38.283 0 -100 / 1), color(--luv 39.091 0 -100 / 1), color(--luv 39.899 0 -100 / 1), color(--luv 40.707 0 -100 / 1), color(--luv 41.515 0 -100 / 1), color(--luv 42.323 0 -100 / 1), color(--luv 43.131 0 -100 / 1), color(--luv 43.939 0 -100 / 1), color(--luv 44.747 0 -100 / 1), color(--luv 45.556 0 -100 / 1), color(--luv 46.364 0 -100 / 1), color(--luv 47.172 0 -100 / 1), color(--luv 47.98 0 -100 / 1), color(--luv 48.788 0 -100 / 1), color(--luv 49.596 0 -100 / 1), color(--luv 50.404 0 -100 / 1), color(--luv 51.212 0 -100 / 1), color(--luv 52.02 0 -100 / 1), color(--luv 52.828 0 -100 / 1), color(--luv 53.636 0 -100 / 1), color(--luv 54.444 0 -100 / 1), color(--luv 55.253 0 -100 / 1), color(--luv 56.061 0 -100 / 1), color(--luv 56.869 0 -100 / 1), color(--luv 57.677 0 -100 / 1), color(--luv 58.485 0 -100 / 1), color(--luv 59.293 0 -100 / 1), color(--luv 60.101 0 -100 / 1), color(--luv 60.909 0 -100 / 1), color(--luv 61.67 0.00041 -99.239 / 1), color(--luv 62.362 0.00052 -97.554 / 1), color(--luv 63.041 0.00014 -95.885 / 1), color(--luv 63.705 0.00037 -94.235 / 1), color(--luv 64.356 0.00041 -92.605 / 1), color(--luv 64.993 0.00013 -90.996 / 1), color(--luv 65.616 0.00006 -89.41 / 1), color(--luv 66.227 0.0001 -87.847 / 1), color(--luv 66.825 0.0002 -86.308 / 1), color(--luv 67.41 0.00029 -84.793 / 1), color(--luv 67.983 0.00032 -83.304 / 1), color(--luv 68.544 0.00027 -81.839 / 1), color(--luv 69.093 0.00012 -80.4 / 1), color(--luv 69.631 0.0005 -78.986 / 1), color(--luv 70.158 0.0001 -77.599 / 1), color(--luv 70.673 0.00025 -76.236 / 1), color(--luv 71.178 0.00029 -74.899 / 1), color(--luv 71.672 0.00024 -73.587 / 1), color(--luv 72.156 0.00012 -72.301 / 1), color(--luv 72.63 0.00063 -71.038 / 1), color(--luv 73.094 0.00045 -69.801 / 1), color(--luv 73.548 0.00029 -68.588 / 1), color(--luv 73.993 0.00018 -67.399 / 1), color(--luv 74.429 0.00016 -66.234 / 1), color(--luv 74.856 0.00028 -65.091 / 1), color(--luv 75.274 0.00057 -63.971 / 1), color(--luv 75.684 0.00039 -62.874 / 1), color(--luv 76.085 0.00047 -61.799 / 1), color(--luv 76.478 0.00015 -60.747 / 1), color(--luv 76.863 0.00019 -59.715 / 1), color(--luv 77.241 0.00065 -58.702 / 1), color(--luv 77.61 0.00011 -57.714 / 1), color(--luv 77.972 0.00009 -56.743 / 1), color(--luv 78.328 0.00063 -55.791 / 1), color(--luv 78.676 0.00031 -54.861 / 1), color(--luv 79.017 0.00066 -53.947 / 1)]

Pointer's Gamut

New 2.4

The Pointer’s gamut is (an approximation of) the gamut of real surface colors as can be seen by the human eye, based on the research by Michael R. Pointer (1980). What this means is that every color that can be reflected by the surface of an object of any material should be is inside the Pointer’s gamut. This does not include, however, those that do not occur naturally, such as neon lights, etc.

Pointer's Gamut

While in the above image, it may appear that most of sRGB is in the gamut, it is important to note that the image is showing the maximum range of the gamut. The actual boundary will be different at different luminance levels.

Pointer's Gamut lightness Levels

The gamuts previously discussed are bound by a color space's limits, but the Pointer's gamut applies to colors more generally and was created from observed data via research. Because it doesn't quite fit with the color space gamut API, ColorAide exposes two special functions to test if a color is in the Pointer's gamut and to fit a color to the gamut.

To test if a color is within the gamut, simply call in_pointer_gamut():

>>> Color('red').in_pointer_gamut()
False
>>> Color('orange').in_pointer_gamut()
True

ColorAide also provides a way to fit a color to the Pointer's gamut. The original gamut's data is described in LCh using illuminant C. Using this color space, we can estimate the chroma limit for any color based on it's lightness and hue. We can then reduce the chroma, preserving the lightness and hue. The image below shows the out of Pointer's gamut color red (indicated by the x) which is clamped to the Pointer's gamut by reducing the chroma (indicated by the dot).

Pointer's Gamut Fitted

ColorAide provides the fit_pointer_gamut() method to perform this "fitting" of the color.

>>> color = Color('red')
>>> color
color(srgb 1 0 0 / 1)
>>> color.in_pointer_gamut()
False
>>> color.fit_pointer_gamut()
color(srgb 0.95687 0.18251 0.09074 / 1)
>>> color.in_pointer_gamut()
True

Tip

Much like in_gamut(), in_pointer_gamut() allows adjusting tolerance as well via the tolerance parameter.