This implements the gamut mapping algorithm according to CSS Level 4 specification.
from coloraide.gamut import Fit
from coloraide.util import NaN
class OklchChroma(Fit):
"""Lch chroma gamut mapping class."""
NAME = "oklch-chroma"
EPSILON = 0.0001
LIMIT = 0.02
DE = "ok"
SPACE = "oklch"
SPACE_COORDINATE = "{}.chroma".format(SPACE)
MIN_LIGHTNESS = 0
MAX_LIGHTNESS = 1
@classmethod
def fit(cls, color, **kwargs):
"""
Gamut mapping via Oklch chroma.
"""
space = color.space()
mapcolor = color.convert(cls.SPACE)
lightness = mapcolor.lightness
# Return white or black if lightness is out of range
if lightness >= cls.MAX_LIGHTNESS or lightness <= cls.MIN_LIGHTNESS:
mapcolor.chroma = 0
mapcolor.hue = NaN
return color.update(mapcolor).clip(in_place=True).coords()
# Set initial chroma boundaries
low = 0.0
high = mapcolor.chroma
# Adjust chroma (using binary search).
# This helps preserve the other attributes of the color.
# Compress chroma until we are are right on the edge of being in gamut.
while True:
if mapcolor.in_gamut(space, tolerance=0):
low = mapcolor.chroma
else:
color.update(mapcolor).clip(in_place=True)
if mapcolor.delta_e(color, method=cls.DE) < cls.LIMIT:
break
high = mapcolor.chroma
mapcolor.chroma = (high + low) * 0.5
# Update and clip off noise
return color.update(mapcolor).clip(in_place=True).coords()
Color.register(OklchChroma, overwrite=True)
Color('orange').interpolate(['green', 'blue'], space="oklab")
This implements the same algorithm just without the ∆Eok check and let's the low and high chroma converge.
from coloraide.gamut import Fit
from coloraide.util import NaN
class OklchChroma(Fit):
"""Lch chroma gamut mapping class."""
NAME = "oklch-chroma"
EPSILON = 0.0001
LIMIT = 0.02
DE = "ok"
SPACE = "oklch"
SPACE_COORDINATE = "{}.chroma".format(SPACE)
MIN_LIGHTNESS = 0
MAX_LIGHTNESS = 1
@classmethod
def fit(cls, color, **kwargs):
"""
Gamut mapping via Oklch chroma.
"""
space = color.space()
mapcolor = color.convert(cls.SPACE)
lightness = mapcolor.lightness
# Return white or black if lightness is out of range
if lightness >= cls.MAX_LIGHTNESS or lightness <= cls.MIN_LIGHTNESS:
mapcolor.chroma = 0
mapcolor.hue = NaN
return color.update(mapcolor).clip(in_place=True).coords()
# Set initial chroma boundaries
low = 0.0
high = mapcolor.chroma
# Adjust chroma (using binary search).
# This helps preserve the other attributes of the color.
# Compress chroma until we are are right on the edge of being in gamut.
while (high - low) > cls.EPSILON:
if mapcolor.in_gamut(space, tolerance=0):
low = mapcolor.chroma
else:
high = mapcolor.chroma
mapcolor.chroma = (high + low) * 0.5
# Update and clip off noise
return color.update(mapcolor).clip(in_place=True).coords()
Color.register(OklchChroma, overwrite=True)
Color('orange').interpolate(['green', 'blue'], space="oklab")