abachrome

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. Abachrome provides gamut objects for sRGB, Display P3, and Rec.2020 that clip or map those coordinates to the nearest in-gamut value.

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

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)

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

Notes

  • Gamut mapping reduces chroma (colorfulness) rather than clipping individual channels, preserving the color’s hue and lightness as much as possible.
  • 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.