How do I map out-of-gamut colors to a valid range in Ruby?
Wide-gamut color spaces like OKLCH can describe colors that lie outside what sRGB monitors can display. This is not a problem in itself — in fact, it is one of the advantages of working in a wider space during computation. The challenge arises when you need to render that color on screen or emit it as CSS: you need to map it back to a displayable range. Abachrome provides gamut objects for sRGB, Display P3, and Rec.2020 that do exactly this.
Checking Gamut Membership
require 'abachrome'
# A vivid OKLCH color that exceeds sRGB chroma limits
vivid = Abachrome.from_oklch(0.7, 0.35, 145)
gamut = Abachrome::Gamut::SRGB.new
puts gamut.in_gamut?(vivid.coordinates) # => false
Checking membership before mapping is useful when you want to treat in-gamut colors differently, or when you need to signal to the user that their chosen color cannot be reproduced faithfully on standard displays.
Mapping to sRGB
vivid = Abachrome.from_oklch(0.7, 0.35, 145)
gamut = Abachrome::Gamut::SRGB.new
mapped = gamut.map(vivid.coordinates)
puts Abachrome::Outputs::CSS.format(Abachrome.from_srgb(*mapped))
Targeting Other Gamuts
p3_gamut = Abachrome::Gamut::P3.new
rec2020_gamut = Abachrome::Gamut::Rec2020.new
# Map to Display P3 (wider than sRGB but narrower than Rec.2020)
p3_safe = p3_gamut.map(vivid.coordinates)
You might wonder which gamut to target. For most web work, sRGB remains the baseline because it is what the vast majority of users see. Display P3 is increasingly relevant for modern Apple devices and high-end monitors, and some browsers support the color(display-p3 ...) CSS syntax. Rec.2020 is generally a concern only in video production contexts.
Practical Use: Processing User Input
When your application accepts arbitrary CSS color input, gamut-map before rendering to avoid invalid CSS values:
def safe_css(input)
color = Abachrome.parse(input).to_srgb
gamut = Abachrome::Gamut::SRGB.new
coords = gamut.map(color.coordinates)
Abachrome::Outputs::CSS.format(Abachrome.from_srgb(*coords))
end
puts safe_css('oklch(0.8 0.4 140)') # vivid green → clamped hex
This pattern is particularly useful in design tools, color pickers, or any interface where users can enter colors in modern wide-gamut notations.
Notes
- Gamut mapping reduces chroma (colorfulness) rather than clipping individual channels, preserving the color’s hue and lightness as much as possible. The result tends to look better than a naive channel clamp.
- If you only need to clamp and don’t care about perceptual accuracy, simple
coordinates.map { |v| v.clamp(0, 1) }on sRGB coordinates is sufficient.