Skip to content

Instantly share code, notes, and snippets.

@facelessuser
Last active February 16, 2022 18:11
Show Gist options
  • Save facelessuser/c8d5d4acc87dd7317868e0f8496c74e2 to your computer and use it in GitHub Desktop.
Save facelessuser/c8d5d4acc87dd7317868e0f8496c74e2 to your computer and use it in GitHub Desktop.
Gamut Comparison

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")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment