Saturon LogoSaturon

🚧 This documentation covers a pre-1.0 release. Expect breaking changes.

Guides & Tutorials

Gamut Mapping & Out-of-Gamut Handling

Learn to detect, constrain, and fit colors using inGamut(), within(), fit(), and output fit options to safely handle colors that exceed display gamuts.

Colors defined in wide-gamut spaces (Display P3, Rec.2020, etc.) may not be displayable on standard sRGB screens. Saturon gives you full control over:

  • Detection β€” inGamut()
  • Correction into a target gamut β€” within()
  • Pre-normalizing the color in its own colorspace β€” fit()
  • Output safety β€” fit option in to() / toString() / toArray() / toObject()

Why Gamut Mapping Matters

DeviceGamutExample
Standard monitorsRGBMost web content
High-end laptopDisplay P3Apple Retina, modern phones
TV / CinemaRec.2020HDR content

Example: color(display-p3 1 0 0) is pure red in P3, but out of range in sRGB β€” clipping or dulling occurs on standard screens.

Detect Out-of-Gamut Colors β€” Color.prototype.inGamut()

Check whether a color is safely representable in a target gamut.

Syntax

inGamut(gamut: ColorSpace | string, epsilon = 1e-5): boolean

Example

import { Color } from "saturon";

const p3Red = Color.from("color(display-p3 1 0 0)");
const sRGBRed = Color.from("rgb(255 0 0)");

console.log(p3Red.inGamut("srgb")); // false
console.log(sRGBRed.inGamut("srgb")); // true
console.log(p3Red.inGamut("display-p3")); // true

When to Use

  • Feature fallback colors or polyfills
  • Design system token validation
  • Unit tests preventing unsafe output

Fit a Color Inside Its Own Colorspace β€” Color.prototype.fit()

fit() adjusts the current color in its own colorspace, ensuring that its raw component values respect that colorspace's valid component ranges or gamut boundaries. This is useful when:

  • You want the canonical, gamut-respecting version of the color before converting it.
  • The original color was created with component values outside the valid range (e.g., manually constructed, interpolated, or procedurally generated).
  • You want to ensure a color is β€œreal” in its native model instead of letting the target space hide the out-of-gamut issue.

Syntax

fit(options?: { fit?: FitMethod }): Color

Behavior

Fits the color in its current colorspace, using the chosen method. Unlike within(), it does not convert spaces β€” it only normalizes the current one.

Example

const oklab = new Color("oklab", [0.5, -0.5, 0.2]);

// Not recommended: fits after converting to P3
const notRecommended = oklab.to("display-p3", { fit: "clip" });

// Recommended: fit Oklab first
const recommended = oklab.fit({ fit: "clip" }).to("display-p3");

console.log(notRecommended); // color(display-p3 0 0.60774 0)
console.log(recommended); // color(display-p3 0 0.57416 0)

Why? notRecommended converts Oklab β†’ P3, but the Oklab source values (e.g., a = -0.5 < min -0.4) were invalid. P3 conversion silently hides that. fit() ensures the source colorspace is physically valid before converting anywhere.

Note

Oklab is theoretically unbounded, but Saturon defines limits for clip. Methods like "css-gamut-map" or "chroma-reduction" ignore the limits when the colorspace has no target gamut.

Fit a Color Into a New Gamut β€” Color.prototype.within()

within() re-maps the color into a different colorspace's gamut, using a selected perceptual or non-perceptual fitting method.

Syntax

within(gamut: ColorSpace, method: FitMethod = config.defaults.fit): Color

Fit Methods

MethodBehavior
"clip"Clamp values to the nearest boundary (fast, may shift hue)
"chroma-reduction"Reduces chroma while keeping hue/lightness more stable
"css-gamut-map"Smooth, perceptual CSS Color 4 method

Example

const wide = Color.from("color(display-p3 1 0.8 0)");

console.log(wide.within("srgb").to("rgb")); // β†’ rgb(255 201 0)

console.log(wide.within("srgb", "css-gamut-map").to("rgb")); // β†’ rgb(255 205 46)

Safe Output with fit Option

The output-level fit option applies gamut mapping only to the target colorspace of the formatting operation.

Applies To

  • to(type, { fit })
  • toString({ fit })
  • toArray({ fit })
  • toObject({ fit })

Example

const p3Color = Color.from("color(display-p3 1 0 0)");

// Unsafe
console.log(p3Color.to("rgb", { fit: "none" })); // β†’ rgb(279 -58 -38)

// Safe
console.log(p3Color.to("rgb", { fit: "clip" })); // β†’ rgb(255 0 0)
console.log(p3Color.to("rgb", { fit: "css-gamut-map" })); // β†’ rgb(255 52 40)

Important

fit on output only affects the target gamut. It cannot correct the color's source space. For source-space correction, use color.fit() (preferred) or within() (when you know the target gamut).

Set Global Default

import { configure } from "saturon/utils";

configure({ defaults: { fit: "css-gamut-map" } });

console.log(p3Color.to("rgb")); // Auto-fitted

Practical Workflows

1. Design Token Export

function exportToken(color: Color, format = "hex-color") {
    color = color.fit(); // ensure source is valid
    if (!color.inGamut("srgb")) {
        color = color.within("srgb", "css-gamut-map");
    }
    return color.to(format);
}

2. Theme Fallback

const base = Color.from("color(display-p3 0.2 0.8 1)").fit();

const prefersP3 = window.matchMedia("(color-gamut: p3)").matches;
const display = prefersP3 ? base : base.within("srgb", "css-gamut-map");

document.body.style.color = display.to("rgb");

3. Unit Tests

test("brand colors must be sRGB-safe", () => {
    const colors = ["#ff5733", "#33b5e5", "color(display-p3 1 0 0)"].map(Color.from).map((c) => c.fit());

    colors.forEach((c) => {
        expect(c.within("srgb").inGamut("srgb")).toBe(true);
    });
});

Summary

GoalMethod
Validate source validitycolor.fit()
Check if target gamut accepts itcolor.inGamut("srgb")
Convert into a safe gamutcolor.within("srgb", "css-gamut-map")
Safe formattingcolor.to("rgb", { fit: "css-gamut-map" })