Fork me on GitHub

Project Notes

#345 Color tools for Ruby

A quick look at the color gem: a Ruby library used to represent colors in a range of color models and offer color space manipulation support to applications that require it.

Notes

Capabilities:

  • supported color models: RGB, CIELAB, CMYK, HSL, Grayscale, XYZ, YIQ
  • conversion between color models
  • optional named RGB colors for HTML, SVG, and X11 applications
  • CIELAB perceptual color distance metric calculation:
    • CIELAB ΔE* 2000
    • CIELAB ΔE* 1994
  • CIELAB to Color::XYZ based on a reference white
    • Color provides D65 and D50 reference white values in Color::XYZ

The Color library performs purely mathematical manipulation of the colors based on color theory without reference to device color profiles such as sRGB or Adobe RGB (opRGB)

RGB

The RGB color model is an additive color model where the primary colors (red, green, and blue) of light are added to produce millions of colors. RGB rendering is device-dependent and without color management, the same “red” color will render differently.

This class does not implement color management and is not RGB color space aware; that is, unless otherwise noted, it does not assume that the RGB represented is sRGB or Adobe RGB (opRGB).

Construction examples:

Color::RGB.new(0.2, 1.0, 0.4)
Color::RGB.new(r: 0.2, g: 1.0, b: 0.4)
Color::RGB.from_html('#33ff66')
Color::RGB.from_values(51, 255, 102)

Special features:

  • Color::RGB::x named colors
  • delta_e2000(other)
  • delta_e94(other)
  • lighten_by(percent)
  • darken_by(percent)
  • mix_with(mask, opacity)
  • adjust_brightness(percent)
  • adjust_saturation(percent)
  • adjust_hue(percent)
  • closest_match
  • max_rgb_as_grayscale

CMYK

The CMYK color model is a subtractive color model based on additive percentages of colored inks: cyan, magenta, yellow, and key (most often black). CMYK [30% 0% 80% 30%] would be mixed from 30% cyan, 0% magenta, 80% yellow, and 30% black.

Construction examples:

Color::CMYK.new(0.3, 0.0, 0.8, 0.3)
Color::CMYK.new(c: 0.3, m: 0.0, y: 0.8, k: 0.3)
Color::CMYK.from_values(30, 0, 80, 30)

CIELAB

A Color object for the CIELAB color space (also known as Lab). Color is expressed in a three-dimensional, device-independent “standard observer” model, often in relation to a “reference white” color, usually Color::XYZ::D65 (most purposes) or Color::XYZ::D50 (printing).

  • L* is the perceptual lightness, bounded to values between 0 (black) and 100 (white).
  • a* is the range of green (negative) / red (positive)
  • b* is the range of blue (negative) / yellow (positive).

The a* and b* ranges are technically unbounded but Color clamps them to the values -128..127.

Construction examples:

Color::CIELAB.new(10, 35, -35)
Color::CIELAB.new(l: 10, a: 35, b: -35)
Color::CIELAB.from_values(10, 35, -35)

Special features:

  • delta_e2000
  • delta_e94

HSL

The HSL color model is a cylindrical-coordinate representation of the sRGB color model, standing for hue (measured in degrees on the cylinder), saturation (measured in percentage), and lightness (measured in percentage).

Construction examples:

Color::HSL.new(0.25, 0.3, 0.5)
Color::HSL.from_values(90, 30, 50)

Grayscale

Grayscale is a color object representing shades of gray as a ratio of black to white, where 0% (0.0) gray is black and 100% (1.0) gray is white.

Construction examples:

Color::Grayscale.new(0.5)
Color::Grayscale.from_values(50)

Special features:

  • lighten_by(percent)
  • darken_by(percent)

XYZ

A Color object for the CIE 1931 XYZ color space derived from the original CIE RGB color space as linear transformation functions x̅(λ), y̅(λ), and z̅(λ) that describe the device-independent CIE standard observer. It underpins most other CIE color systems (such as CIELAB), but is directly used mostly for color instrument readings and color space transformations particularly in color profiles. The XYZ color space ranges describe the mixture of wavelengths of light required to stimulate cone cells in the human eye, as well as the luminance (brightness) required. The Y component describes the luminance while the X and Z components describe two axes of chromaticity. Definitionally, the minimum value for any XYZ color component is 0. As XYZ describes imaginary colors, the color gamut is usually expressed in relation to a reference white of an illuminant (frequently often D65 or D50) and expressed as the xyY color space, computed as:

x = X / (X + Y + Z)
y = Y / (X + Y + Z)
Y = Y

The range of Y values is conventionally clamped to 0..100, whereas the X and Z values must be no lower than 0 and on the same scale.

Construction examples:

Color::XYZ.from_values(95.047, 100.00, 108.883)
Color::XYZ.from_values(x: 95.047, y: 100.00, z: 108.883)
Color::XYZ.new(0.95047, 1.0, 1.08883)

YIQ

A Color object representing YIQ (NTSC) color encoding, where Y is the luma (brightness) value, and I (orange-blue) and Q (purple-green) are chrominance. All values are clamped between 0 and 1 inclusive.

YIQ is only partially implemented: other Color objects can only be converted to YIQ, but it has few conversion functions for converting from YIQ.

Construction examples:

Color::YIQ.new(0.3, 0.2, 0.1)
Color::YIQ.from_values(30, 20, 10)

Example

See examples.rb for a quick play with the library:

$ ./examples.rb

# RGB examples
Color::RGB.from_html('#0000FF')                   : #<data Color::RGB r=0.0, g=0.0, b=1.0, names=nil>
Color::RGB.from_values(51, 255, 102)              : #<data Color::RGB r=0.2, g=1.0, b=0.4, names=nil>
Color::RGB.from_values(51, 255, 102).hex          : 33ff66
Color::RGB.new(r: 1.0, g: 0.2, b: 0.45)           : #<data Color::RGB r=1.0, g=0.2, b=0.45, names=nil>
Color::RGB::Blue                                  : #<data Color::RGB r=0.0, g=0.0, b=1.0, names=["blue"]>
Color::RGB.new(1.0, 0.2, 0.45)                    : #<data Color::RGB r=1.0, g=0.2, b=0.45, names=nil>
Color::RGB.new(1.0, 0.2, 0.45).inspect            : RGB [#ff3373]
Color::RGB.new(1.0, 0.2, 0.45).to_h               : {:r=>1.0, :g=>0.2, :b=>0.45, :names=>nil}
Color::RGB.new(1.0, 0.2, 0.45).to_a               : [255.0, 51.0, 114.75]
Color::RGB.new(1.0, 0.2, 0.45).r                  : 1.0
Color::RGB.new(1.0, 0.2, 0.45).red                : 255.0
Color::RGB.new(1.0, 0.2, 0.45).g                  : 0.2
Color::RGB.new(1.0, 0.2, 0.45).green              : 51.0
Color::RGB.new(1.0, 0.2, 0.45).b                  : 0.45
Color::RGB.new(1.0, 0.2, 0.45).blue               : 114.75
Color::RGB.new(1.0, 0.2, 0.45).hex                : ff3373
Color::RGB.new(1.0, 0.2, 0.45).html               : #ff3373
Color::RGB.new(1.0, 0.2, 0.45).css                : rgb(100.00% 20.00% 45.00%)
Color::RGB.new(1.0, 0.2, 0.45).to_cmyk            : #<data Color::CMYK c=0.0, m=0.8, y=0.55, k=0.0>
Color::RGB.new(1.0, 0.2, 0.45).to_grayscale       : #<data Color::Grayscale g=0.6>
Color::RGB.new(1.0, 0.2, 0.45).to_hsl             : #<data Color::HSL h=0.9479166666666666, s=1.0, l=0.6>
Color::RGB.new(1.0, 0.2, 0.45).to_lab             : #<data Color::CIELAB l=56.94503080811005, a=76.74737744142817, b=14.904898010190104>
Color::RGB.new(1.0, 0.2, 0.45).to_xyz             : #<data Color::XYZ x=0.45508461892804625, y=0.24866414488614244, z=0.1854443054868735>
Color::RGB.new(1.0, 0.2, 0.45).to_yiq             : #<data Color::YIQ y=0.4677, i=0.39654999999999996, q=0.24734999999999996>
Color::RGB.new(1.0, 0.2, 0.45).to_internal        : [1.0, 0.2, 0.45]
Color::RGB.new(1.0, 0.2, 0.45).darken_by(10)      : #<data Color::RGB r=0.1, g=0.020000000000000004, b=0.045000000000000005, names=nil>
Color::RGB.new(1.0, 0.2, 0.45).lighten_by(10)     : #<data Color::RGB r=1.0, g=0.92, b=0.9450000000000001, names=nil>
Color::RGB.new(1.0, 0.2, 0.45).adjust_brightness(10) : #<data Color::RGB r=1.0, g=0.32000000000000006, b=0.5325000000000002, names=nil>
Color::RGB.new(1.0, 0.2, 0.45).adjust_saturation(10) : #<data Color::RGB r=1.0, g=0.19999999999999996, b=0.4500000000000002, names=nil>
Color::RGB.new(1.0, 0.2, 0.45).adjust_hue(10)     : #<data Color::RGB r=1.0, g=0.19999999999999996, b=0.19999999999999943, names=nil>

# HSL examples
Color::HSL.from_values(90, 30, 50)                : #<data Color::HSL h=0.25, s=0.3, l=0.5>
Color::HSL.new(h: 0.25, s: 0.6, l: 0.8)           : #<data Color::HSL h=0.25, s=0.6, l=0.8>
Color::HSL.new(0.25, 0.6, 0.8)                    : #<data Color::HSL h=0.25, s=0.6, l=0.8>
Color::HSL.new(0.25, 0.6, 0.8).inspect            : HSL [90.00deg 60.00% 80.00%]
Color::HSL.new(0.25, 0.6, 0.8).to_h               : {:h=>0.25, :s=>0.6, :l=>0.8}
Color::HSL.new(0.25, 0.6, 0.8).to_a               : [90.0, 60.0, 80.0]
Color::HSL.new(0.25, 0.6, 0.8).h                  : 0.25
Color::HSL.new(0.25, 0.6, 0.8).hue                : 90.0
Color::HSL.new(0.25, 0.6, 0.8).s                  : 0.6
Color::HSL.new(0.25, 0.6, 0.8).saturation         : 60.0
Color::HSL.new(0.25, 0.6, 0.8).l                  : 0.8
Color::HSL.new(0.25, 0.6, 0.8).luminosity         : 80.0
Color::HSL.new(0.25, 0.6, 0.8).brightness         : 0.8
Color::HSL.new(0.25, 0.6, 0.8).to_cmyk            : #<data Color::CMYK c=0.18854079999999995, m=0.06854080000000005, y=0.30854079999999984, k=0.011459200000000017>
Color::HSL.new(0.25, 0.6, 0.8).to_grayscale       : #<data Color::Grayscale g=0.8>
Color::HSL.new(0.25, 0.6, 0.8).to_lab             : #<data Color::CIELAB l=89.41981423782663, a=-20.580380391075238, b=26.48851021744585>
Color::HSL.new(0.25, 0.6, 0.8).to_rgb             : #<data Color::RGB r=0.8, g=0.9199999999999999, b=0.6800000000000002, names=nil>
Color::HSL.new(0.25, 0.6, 0.8).to_xyz             : #<data Color::XYZ x=0.6207617569276594, y=0.7505727138541277, z=0.5094735161723772>
Color::HSL.new(0.25, 0.6, 0.8).to_yiq             : #<data Color::YIQ y=0.85676, i=0.005519999999999942, q=0.0>
Color::HSL.new(0.25, 0.6, 0.8).to_internal        : [0.25, 0.6, 0.8]

# CIELAB examples
Color::CIELAB.from_values(10, 35, -35)            : #<data Color::CIELAB l=10, a=35, b=-35>
Color::CIELAB.new(l: 10, a: 35, b: -35)           : #<data Color::CIELAB l=10, a=35, b=-35>
Color::CIELAB.new(10, 35, -35)                    : #<data Color::CIELAB l=10, a=35, b=-35>
Color::CIELAB.new(10, 35, -35).inspect            : CIELAB [10.0000 35.0000 -35.0000]
Color::CIELAB.new(10, 35, -35).to_h               : {:l=>10, :a=>35, :b=>-35}
Color::CIELAB.new(10, 35, -35).to_a               : [10, 35, -35]
Color::CIELAB.new(10, 35, -35).l                  : 10
Color::CIELAB.new(10, 35, -35).a                  : 35
Color::CIELAB.new(10, 35, -35).b                  : -35
Color::CIELAB.new(10, 35, -35).css                : lab(10.00% 35.00 -35.00)
Color::CIELAB.new(10, 35, -35).to_cmyk            : #<data Color::CMYK c=0.18447093262606185, m=0.3548205599469607, y=0.06411107916518433, k=0.6379344598202296>
Color::CIELAB.new(10, 35, -35).to_grayscale       : #<data Color::Grayscale g=0.15259972062369795>
Color::CIELAB.new(10, 35, -35).to_hsl             : #<data Color::HSL h=0.7643298198501555, s=0.9525229783960389, l=0.15259972062369795>
Color::CIELAB.new(10, 35, -35).to_rgb             : #<data Color::RGB r=0.17759460755370857, g=0.007244980232809748, b=0.29795446101458617, names=nil>
Color::CIELAB.new(10, 35, -35).to_xyz             : #<data Color::XYZ x=0.024186512981709044, y=0.01126019927016278, z=0.06923404515364971>
Color::CIELAB.new(10, 35, -35).to_internal        : [10, 35, -35]
Color::CIELAB.new(10, 35, -35).delta_e2000(Color::RGB.from_html('#0000FF')) : 24.118528112469036
Color::CIELAB.new(10, 35, -35).delta_e94(Color::RGB.from_html('#0000FF')) : 35.07142143308636

# CMYK examples
Color::CMYK.from_values(30, 0, 80, 30)            : #<data Color::CMYK c=0.3, m=0.0, y=0.8, k=0.3>
Color::CMYK.new(c: 0.3, m: 0, y: 0.8, k: 0.3)     : #<data Color::CMYK c=0.3, m=0.0, y=0.8, k=0.3>
Color::CMYK.new(0.3, 0, 0.8, 0.3)                 : #<data Color::CMYK c=0.3, m=0.0, y=0.8, k=0.3>
Color::CMYK.new(0.3, 0, 0.8, 0.3).inspect         : CMYK [30.00% 0.00% 80.00% 30.00%]
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_h            : {:c=>0.3, :m=>0.0, :y=>0.8, :k=>0.3}
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_a            : [30.0, 0.0, 80.0, 30.0]
Color::CMYK.new(0.3, 0, 0.8, 0.3).c               : 0.3
Color::CMYK.new(0.3, 0, 0.8, 0.3).cyan            : 30.0
Color::CMYK.new(0.3, 0, 0.8, 0.3).m               : 0.0
Color::CMYK.new(0.3, 0, 0.8, 0.3).magenta         : 0.0
Color::CMYK.new(0.3, 0, 0.8, 0.3).y               : 0.8
Color::CMYK.new(0.3, 0, 0.8, 0.3).yellow          : 80.0
Color::CMYK.new(0.3, 0, 0.8, 0.3).k               : 0.3
Color::CMYK.new(0.3, 0, 0.8, 0.3).key             : 30.0
Color::CMYK.new(0.3, 0, 0.8, 0.3).b               : 0.3
Color::CMYK.new(0.3, 0, 0.8, 0.3).black           : 30.0
Color::CMYK.new(0.3, 0, 0.8, 0.3).css             : device-cmyk(30.00% 0 80.00% 30.00%, rgb(49.00% 70.00% 14.00%))
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_grayscale    : #<data Color::Grayscale g=0.5191>
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_hsl          : #<data Color::HSL h=0.22916666666666666, s=0.6666666666666664, l=0.42000000000000004>
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_lab          : #<data Color::CIELAB l=66.91625392756802, a=-37.95899860362956, b=61.38322781498079>
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_rgb          : #<data Color::RGB r=0.49, g=0.7, b=0.14000000000000012, names=nil>
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_xyz          : #<data Color::XYZ x=0.24784064044701537, y=0.36521201007079285, z=0.07388312039323361>
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_yiq          : #<data Color::YIQ y=0.5733699999999999, i=0.05459999999999992, q=0.0>
Color::CMYK.new(0.3, 0, 0.8, 0.3).to_internal     : [0.3, 0.0, 0.8, 0.3]

# Grayscale examples
Color::Grayscale.from_values(50)                  : #<data Color::Grayscale g=0.5>
Color::Grayscale.new(g: 0.5)                      : #<data Color::Grayscale g=0.5>
Color::Grayscale.new(0.5)                         : #<data Color::Grayscale g=0.5>
Color::Grayscale.new(0.5).inspect                 : Grayscale [50.00%]
Color::Grayscale.new(0.5).to_h                    : {:g=>0.5}
Color::Grayscale.new(0.5).to_a                    : [50.0]
Color::Grayscale.new(0.5).g                       : 0.5
Color::Grayscale.new(0.5).gray                    : 50.0
Color::Grayscale.new(0.5).brightness              : 0.5
Color::Grayscale.new(0.5).css                     : rgb(50.00% 50.00% 50.00%)
Color::Grayscale.new(0.5).to_cmyk                 : #<data Color::CMYK c=0.0, m=0.0, y=0.0, k=0.5>
Color::Grayscale.new(0.5).to_hsl                  : #<data Color::HSL h=0.0, s=0.0, l=0.5>
Color::Grayscale.new(0.5).to_lab                  : #<data Color::CIELAB l=53.38896705407974, a=0.004180380146878715, b=-0.00085701884331435>
Color::Grayscale.new(0.5).to_rgb                  : #<data Color::RGB r=0.5, g=0.5, b=0.5, names=nil>
Color::Grayscale.new(0.5).to_yiq                  : #<data Color::YIQ y=0.5, i=0.0, q=0.0>
Color::Grayscale.new(0.5).to_internal             : [0.5]
Color::Grayscale.new(0.5).lighten_by(20)          : #<data Color::Grayscale g=0.6>
Color::Grayscale.new(0.5).darken_by(20)           : #<data Color::Grayscale g=0.4>

# XYZ examples
Color::XYZ.from_values(95.047, 100.00, 108.883)   : #<data Color::XYZ x=0.9504699999999999, y=1.0, z=1.08883>
Color::XYZ.new(x: 0.95047, y: 1.0, z: 1.08883)    : #<data Color::XYZ x=0.95047, y=1.0, z=1.08883>
Color::XYZ.new(0.95047, 1.0, 1.08883)             : #<data Color::XYZ x=0.95047, y=1.0, z=1.08883>
Color::XYZ.new(0.95047, 1.0, 1.08883).inspect     : XYZ [0.95047 1.0 1.08883]
Color::XYZ.new(0.95047, 1.0, 1.08883).to_h        : {:x=>0.95047, :y=>1.0, :z=>1.08883}
Color::XYZ.new(0.95047, 1.0, 1.08883).to_a        : [95.047, 100.0, 108.883]
Color::XYZ.new(0.95047, 1.0, 1.08883).x           : 0.95047
Color::XYZ.new(0.95047, 1.0, 1.08883).y           : 1.0
Color::XYZ.new(0.95047, 1.0, 1.08883).z           : 1.08883
Color::XYZ.new(0.95047, 1.0, 1.08883).to_cmyk     : #<data Color::CMYK c=0.0, m=0.0, y=0.0, k=0.0>
Color::XYZ.new(0.95047, 1.0, 1.08883).to_grayscale: #<data Color::Grayscale g=1.0>
Color::XYZ.new(0.95047, 1.0, 1.08883).to_hsl      : #<data Color::HSL h=0.0, s=0.0, l=1.0>
Color::XYZ.new(0.95047, 1.0, 1.08883).to_lab      : #<data Color::CIELAB l=100.0, a=0.007005156822281755, b=-0.0014393755445318845>
Color::XYZ.new(0.95047, 1.0, 1.08883).to_rgb      : #<data Color::RGB r=1.0, g=1.0, b=1.0, names=nil>
Color::XYZ.new(0.95047, 1.0, 1.08883).to_yiq      : #<data Color::YIQ y=1.0, i=0.0, q=0.0>
Color::XYZ.new(0.95047, 1.0, 1.08883).to_internal : [0.95047, 1.0, 1.08883]

# YIQ examples
Color::YIQ.from_values(30, 20, 10)                : #<data Color::YIQ y=0.3, i=0.2, q=0.1>
Color::YIQ.new(y: 0.3, i: 0.2, q: 0.1)            : #<data Color::YIQ y=0.3, i=0.2, q=0.1>
Color::YIQ.new(0.3, 0.2, 0.1)                     : #<data Color::YIQ y=0.3, i=0.2, q=0.1>
Color::YIQ.new(0.3, 0.2, 0.1).inspect             : YIQ [30.00% 20.00% 10.00%]
Color::YIQ.new(0.3, 0.2, 0.1).to_h                : {:y=>0.3, :i=>0.2, :q=>0.1}
Color::YIQ.new(0.3, 0.2, 0.1).to_a                : [0.3, 0.2, 0.1]
Color::YIQ.new(0.3, 0.2, 0.1).y                   : 0.3
Color::YIQ.new(0.3, 0.2, 0.1).i                   : 0.2
Color::YIQ.new(0.3, 0.2, 0.1).q                   : 0.1
Color::YIQ.new(0.3, 0.2, 0.1).to_grayscale        : #<data Color::Grayscale g=0.3>
Color::YIQ.new(0.3, 0.2, 0.1).to_yiq              : #<data Color::YIQ y=0.3, i=0.2, q=0.1>
Color::YIQ.new(0.3, 0.2, 0.1).to_internal         : [0.3, 0.2, 0.1]

Credits and References

About LCK#345 ruby

This page is a web-friendly rendering of my project notes shared in the LittleCodingKata GitHub repository.

Project Source on GitHub Return to the LittleCodingKata Catalog
About LittleCodingKata

LittleCodingKata is my collection of programming exercises, research and code toys broadly spanning things that relate to programming and software development (languages, frameworks and tools).

These range from the trivial to the complex and serious. Many are inspired by existing work and I'll note credits and references where applicable. The focus is quite scattered, as I variously work on things new and important in the moment, or go back to revisit things from the past.

This is primarily a personal collection for my own edification and learning, but anyone who stumbles by is welcome to borrow, steal or reference the work here. And if you spot errors or issues I'd really appreciate some feedback - create an issue, send me an email or even send a pull-request.

Follow the Blog follow projects and notes as they are published in your favourite feed reader