class Color::RGB
An RGB colour object.
Constants
- PDF_FORMAT_STR
The format of a DeviceRGB colour for PDF. In color-tools 2.0 this will be removed from this package and added back as a modification by the PDF::Writer package.
Public Class Methods
Creates an RGB colour object from the standard range 0..255.
Color::RGB.new(32, 64, 128) Color::RGB.new(0x20, 0x40, 0x80)
# File lib/color/rgb.rb, line 19 def initialize(r = 0, g = 0, b = 0, radix = 255.0, &block) # :yields self: @r, @g, @b = [ r, g, b ].map { |v| Color.normalize(v / radix) } block.call(self) if block end
Public Instance Methods
Adds another colour to the current colour. The other colour will be converted to RGB before addition. This conversion depends upon a to_rgb method on the other colour.
The addition is done using the RGB Accessor methods to ensure a valid colour in the result.
# File lib/color/rgb.rb, line 527 def +(other) self.class.from_fraction(r + other.r, g + other.g, b + other.b) end
Subtracts another colour to the current colour. The other colour will be converted to RGB before subtraction. This conversion depends upon a to_rgb method on the other colour.
The subtraction is done using the RGB Accessor methods to ensure a valid colour in the result.
# File lib/color/rgb.rb, line 537 def -(other) self + (-other) end
Numerically negate the color. This results in a color that is only usable for subtraction.
# File lib/color/rgb.rb, line 558 def -@ rgb = self.dup rgb.instance_variable_set(:@r, -rgb.r) rgb.instance_variable_set(:@g, -rgb.g) rgb.instance_variable_set(:@b, -rgb.b) rgb end
Returns a new colour with the brightness adjusted by the specified percentage. Negative percentages will darken the colour; positive percentages will brighten the colour.
Color::RGB::DarkBlue.adjust_brightness(10) Color::RGB::DarkBlue.adjust_brightness(-10)
# File lib/color/rgb.rb, line 296 def adjust_brightness(percent) percent = normalize_percent(percent) hsl = to_hsl hsl.l *= percent hsl.to_rgb end
Returns a new colour with the hue adjusted by the specified percentage. Negative percentages will reduce the hue; positive percentages will increase the hue.
Color::RGB::DarkBlue.adjust_hue(10) Color::RGB::DarkBlue.adjust_hue(-10)
# File lib/color/rgb.rb, line 322 def adjust_hue(percent) percent = normalize_percent(percent) hsl = to_hsl hsl.h *= percent hsl.to_rgb end
Returns a new colour with the saturation adjusted by the specified percentage. Negative percentages will reduce the saturation; positive percentages will increase the saturation.
Color::RGB::DarkBlue.adjust_saturation(10) Color::RGB::DarkBlue.adjust_saturation(-10)
# File lib/color/rgb.rb, line 309 def adjust_saturation(percent) percent = normalize_percent(percent) hsl = to_hsl hsl.s *= percent hsl.to_rgb end
Returns the blue component of the colour as a fraction in the range 0.0 .. 1.0.
# File lib/color/rgb.rb, line 504 def b @b end
Sets the blue component of the colour as a fraction in the range 0.0 .. 1.0.
# File lib/color/rgb.rb, line 517 def b=(bb) @b = Color.normalize(bb) end
Returns the blue component of the colour in the normal 0 .. 255 range.
# File lib/color/rgb.rb, line 495 def blue @b * 255.0 end
Sets the blue component of the colour in the normal 0 .. 255 range.
# File lib/color/rgb.rb, line 508 def blue=(bb) @b = Color.normalize(bb / 255.0) end
Returns the blue component of the colour as a percentage.
# File lib/color/rgb.rb, line 499 def blue_p @b * 100.0 end
Sets the blue component of the colour as a percentage.
# File lib/color/rgb.rb, line 512 def blue_p=(bb) @b = Color.normalize(bb / 100.0) end
Returns the brightness value for a colour, a number between 0..1. Based on the Y value of YIQ encoding, representing luminosity, or perceived brightness.
This may be modified in a future version of color-tools to use the luminosity value of HSL.
# File lib/color/rgb.rb, line 281 def brightness to_yiq.y end
Return a colour as identified by the colour name, or by hex.
# File lib/color/rgb.rb, line 643 def by_css(name_or_hex, &block) by_name(name_or_hex) { by_hex(name_or_hex, &block) } end
Find or create a colour by an HTML hex code. This differs from the from_html method in that if the colour code matches a named colour, the existing colour will be returned.
Color::RGB.by_hex('ff0000').name # => 'red' Color::RGB.by_hex('ff0001').name # => nil
If a block is provided, the value that is returned by the block will be returned instead of the exception caused by an error in providing a correct hex format.
# File lib/color/rgb.rb, line 627 def by_hex(hex, &block) __by_hex.fetch(html_hexify(hex)) { from_html(hex) } rescue if block block.call else raise end end
Return a colour as identified by the colour name.
# File lib/color/rgb.rb, line 638 def by_name(name, &block) __by_name.fetch(name.to_s.downcase, &block) end
Calculates and returns the closest match to this colour from a list of
provided colours. Returns nil
if color_list
is
empty or if there is no colour within the threshold_distance
.
threshold_distance
is used to determine the minimum colour
distance permitted. Uses the CIE Delta E 1994 algorithm (CIE94) to find
near matches based on perceived visual colour. The default value (1000.0)
is an arbitrarily large number. The values :jnd
and
:just_noticeable
may be passed as the
threshold_distance
to use the value 2.3
.
# File lib/color/rgb.rb, line 342 def closest_match(color_list, threshold_distance = 1000.0) color_list = [ color_list ].flatten(1) return nil if color_list.empty? threshold_distance = case threshold_distance when :jnd, :just_noticeable 2.3 else threshold_distance.to_f end lab = to_lab closest_distance = threshold_distance best_match = nil color_list.each do |c| distance = delta_e94(lab, c.to_lab) if (distance < closest_distance) closest_distance = distance best_match = c end end best_match end
Coerces the other Color object into RGB.
# File lib/color/rgb.rb, line 11 def coerce(other) other.to_rgb end
Outputs how much contrast this color has with another RGB color. Computes the same regardless of which one is considered foreground. If the other color does not have a to_rgb method, this will throw an exception. Anything over about 0.22 should have a high likelihood of being legible, but the larger the difference, the more contrast. Otherwise, to be safe go with something > 0.3
# File lib/color/rgb/contrast.rb, line 9 def contrast(other) other = coerce(other) # The following numbers have been set with some care. ((diff_brightness(other) * 0.65) + (diff_hue(other) * 0.20) + (diff_luminosity(other) * 0.15)) end
Present the colour as an HSL HTML/CSS colour string (e.g., “hsl(180, 25%, 35%)”). Note that this will perform a to_hsl operation using the default conversion formula.
# File lib/color/rgb.rb, line 72 def css_hsl to_hsl.css_hsl end
Present the colour as an HSLA (with alpha) HTML/CSS colour string (e.g., “hsla(180, 25%, 35%, 1)”). Note that this will perform a to_hsl operation using the default conversion formula.
# File lib/color/rgb.rb, line 79 def css_hsla to_hsl.css_hsla end
Present the colour as an RGBA (with alpha) HTML/CSS colour string (e.g., “rgb(0%, 50%, 100%, 1)”). Note that this will perform a to_rgb operation using the default conversion formula.
# File lib/color/rgb.rb, line 65 def css_rgba "rgba(%3.2f%%, %3.2f%%, %3.2f%%, %3.2f)" % [ red_p, green_p, blue_p, 1 ] end
Mix the RGB hue with Black so that the RGB hue is the specified percentage of the resulting colour. Strictly speaking, this isn't a #darken_by operation.
# File lib/color/rgb.rb, line 258 def darken_by(percent) mix_with(Black, percent) end
The Delta E (CIE94) algorithm en.wikipedia.org/wiki/Color_difference#CIE94
There is a newer version, CIEDE2000, that uses slightly more complicated math, but addresses “the perceptual uniformity issue” left lingering by the CIE94 algorithm. color_1 and color_2 are both L*a*b* hashes, rendered by to_lab.
Since our source is treated as sRGB, we use the “graphic arts” presets for k_L, k_1, and k_2
The calculations go through LCH(ab). (?)
See also www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
NOTE: This should be moved to Color::Lab.
# File lib/color/rgb.rb, line 382 def delta_e94(color_1, color_2, weighting_type = :graphic_arts) case weighting_type when :graphic_arts k_1 = 0.045 k_2 = 0.015 k_L = 1 when :textiles k_1 = 0.048 k_2 = 0.014 k_L = 2 else raise ArgumentError, "Unsupported weighting type #{weighting_type}." end # delta_E = Math.sqrt( # ((delta_L / (k_L * s_L)) ** 2) + # ((delta_C / (k_C * s_C)) ** 2) + # ((delta_H / (k_H * s_H)) ** 2) # ) # # Under some circumstances in real computers, delta_H could be an # imaginary number (it's a square root value), so we're going to treat # this as: # # delta_E = Math.sqrt( # ((delta_L / (k_L * s_L)) ** 2) + # ((delta_C / (k_C * s_C)) ** 2) + # (delta_H2 / ((k_H * s_H) ** 2))) # ) # # And not perform the square root when calculating delta_H2. k_C = k_H = 1 l_1, a_1, b_1 = color_1.values_at(:L, :a, :b) l_2, a_2, b_2 = color_2.values_at(:L, :a, :b) delta_a = a_1 - a_2 delta_b = b_1 - b_2 c_1 = Math.sqrt((a_1 ** 2) + (b_1 ** 2)) c_2 = Math.sqrt((a_2 ** 2) + (b_2 ** 2)) delta_L = color_1[:L] - color_2[:L] delta_C = c_1 - c_2 delta_H2 = (delta_a ** 2) + (delta_b ** 2) - (delta_C ** 2) s_L = 1 s_C = 1 + k_1 * c_1 s_H = 1 + k_2 * c_1 composite_L = (delta_L / (k_L * s_L)) ** 2 composite_C = (delta_C / (k_C * s_C)) ** 2 composite_H = delta_H2 / ((k_H * s_H) ** 2) Math.sqrt(composite_L + composite_C + composite_H) end
Extract named or hex colours from the provided text.
# File lib/color/rgb.rb, line 648 def extract_colors(text, mode = :both) text = text.downcase regex = case mode when :name Regexp.union(__by_name.keys) when :hex Regexp.union(__by_hex.keys) when :both Regexp.union(__by_hex.keys + __by_name.keys) end text.scan(regex).map { |match| case mode when :name by_name(match) when :hex by_hex(match) when :both by_css(match) end } end
Creates an RGB colour object from fractional values 0..1.
Color::RGB.from_fraction(.3, .2, .1)
# File lib/color/rgb.rb, line 587 def from_fraction(r = 0.0, g = 0.0, b = 0.0, &block) new(r, g, b, 1.0, &block) end
Creates an RGB colour object from a grayscale fractional value 0..1.
# File lib/color/rgb.rb, line 592 def from_grayscale_fraction(l = 0.0, &block) new(l, l, l, 1.0, &block) end
Creates an RGB colour object from an HTML colour
descriptor (e.g., "fed"
or
"#cabbed;"
.
Color::RGB.from_html("fed") Color::RGB.from_html("#fed") Color::RGB.from_html("#cabbed") Color::RGB.from_html("cabbed")
# File lib/color/rgb.rb, line 604 def from_html(html_colour, &block) # When we can move to 1.9+ only, this will be \h h = html_colour.scan(/[0-9a-f]/i) case h.size when 3 new(*h.map { |v| (v * 2).to_i(16) }, &block) when 6 new(*h.each_slice(2).map { |v| v.join.to_i(16) }, &block) else raise ArgumentError, "Not a supported HTML colour type." end end
Creates an RGB colour object from percentages 0..100.
Color::RGB.from_percentage(10, 20, 30)
# File lib/color/rgb.rb, line 580 def from_percentage(r = 0, g = 0, b = 0, &block) new(r, g, b, 100.0, &block) end
Returns the green component of the colour as a fraction in the range 0.0 .. 1.0.
# File lib/color/rgb.rb, line 477 def g @g end
Sets the green component of the colour as a fraction in the range 0.0 .. 1.0.
# File lib/color/rgb.rb, line 490 def g=(gg) @g = Color.normalize(gg) end
Returns the green component of the colour in the normal 0 .. 255 range.
# File lib/color/rgb.rb, line 468 def green @g * 255.0 end
Sets the green component of the colour in the normal 0 .. 255 range.
# File lib/color/rgb.rb, line 481 def green=(gg) @g = Color.normalize(gg / 255.0) end
Returns the green component of the colour as a percentage.
# File lib/color/rgb.rb, line 472 def green_p @g * 100.0 end
Sets the green component of the colour as a percentage.
# File lib/color/rgb.rb, line 485 def green_p=(gg) @g = Color.normalize(gg / 100.0) end
Present the colour as an RGB hex triplet.
# File lib/color/rgb.rb, line 37 def hex r = (@r * 255).round r = 255 if r > 255 g = (@g * 255).round g = 255 if g > 255 b = (@b * 255).round b = 255 if b > 255 "%02x%02x%02x" % [ r, g, b ] end
Present the colour as an HTML/CSS colour string.
# File lib/color/rgb.rb, line 51 def html "##{hex}" end
# File lib/color/rgb.rb, line 548 def inspect "RGB [#{html}]" end
Mix the RGB hue with White so that the RGB hue is the specified percentage of the resulting colour. Strictly speaking, this isn't a #darken_by operation.
# File lib/color/rgb.rb, line 251 def lighten_by(percent) mix_with(White, percent) end
Retrieve the maxmum RGB value from the current colour as a GrayScale colour
# File lib/color/rgb.rb, line 543 def max_rgb_as_grayscale Color::GrayScale.from_fraction([@r, @g, @b].max) end
Mix the mask colour (which must be an RGB object) with the current colour at the stated opacity percentage (0..100).
# File lib/color/rgb.rb, line 264 def mix_with(mask, opacity) opacity /= 100.0 rgb = self.dup rgb.r = (@r * opacity) + (mask.r * (1 - opacity)) rgb.g = (@g * opacity) + (mask.g * (1 - opacity)) rgb.b = (@b * opacity) + (mask.b * (1 - opacity)) rgb end
Present the colour as a DeviceRGB fill colour string for PDF. This will be removed from the default package in color-tools 2.0.
# File lib/color/rgb.rb, line 26 def pdf_fill PDF_FORMAT_STR % [ @r, @g, @b, "rg" ] end
Present the colour as a DeviceRGB stroke colour string for PDF. This will be removed from the default package in color-tools 2.0.
# File lib/color/rgb.rb, line 32 def pdf_stroke PDF_FORMAT_STR % [ @r, @g, @b, "RG" ] end
Returns the red component of the colour as a fraction in the range 0.0 .. 1.0.
# File lib/color/rgb.rb, line 450 def r @r end
Sets the red component of the colour as a fraction in the range 0.0 .. 1.0.
# File lib/color/rgb.rb, line 463 def r=(rr) @r = Color.normalize(rr) end
Returns the red component of the colour in the normal 0 .. 255 range.
# File lib/color/rgb.rb, line 441 def red @r * 255.0 end
Sets the red component of the colour in the normal 0 .. 255 range.
# File lib/color/rgb.rb, line 454 def red=(rr) @r = Color.normalize(rr / 255.0) end
Returns the red component of the colour as a percentage.
# File lib/color/rgb.rb, line 445 def red_p @r * 100.0 end
Sets the red component of the colour as a percentage.
# File lib/color/rgb.rb, line 458 def red_p=(rr) @r = Color.normalize(rr / 100.0) end
# File lib/color/rgb.rb, line 552 def to_a [ r, g, b ] end
Converts the RGB colour to CMYK. Most colour experts strongly suggest that this is not a good idea (some even suggesting that it's a very bad idea). CMYK represents additive percentages of inks on white paper, whereas RGB represents mixed colour intensities on a black screen.
However, the colour conversion can be done. The basic method is multi-step:
-
Convert the R, G, and B components to C, M, and Y components.
c = 1.0 - r m = 1.0 - g y = 1.0 - b
-
Compute the minimum amount of black (K) required to smooth the colour in inks.
k = min(c, m, y)
-
Perform undercolour removal on the C, M, and Y components of the colours because less of each colour is needed for each bit of black. Also, regenerate the black (K) based on the undercolour removal so that the colour is more accurately represented in ink.
c = min(1.0, max(0.0, c - UCR(k))) m = min(1.0, max(0.0, m - UCR(k))) y = min(1.0, max(0.0, y - UCR(k))) k = min(1.0, max(0.0, BG(k)))
The undercolour removal function and the black generation functions return a value based on the brightness of the RGB colour.
# File lib/color/rgb.rb, line 109 def to_cmyk c = 1.0 - @r.to_f m = 1.0 - @g.to_f y = 1.0 - @b.to_f k = [c, m, y].min k = k - (k * brightness) c = [1.0, [0.0, c - k].max].min m = [1.0, [0.0, m - k].max].min y = [1.0, [0.0, y - k].max].min k = [1.0, [0.0, k].max].min Color::CMYK.from_fraction(c, m, y, k) end
Convert to grayscale.
# File lib/color/rgb.rb, line 285 def to_grayscale Color::GrayScale.from_fraction(to_hsl.l) end
Returns the HSL colour encoding of the RGB value. The conversions here are based on forumlas from www.easyrgb.com/math.php and elsewhere.
# File lib/color/rgb.rb, line 140 def to_hsl min = [ @r, @g, @b ].min max = [ @r, @g, @b ].max delta = (max - min).to_f lum = (max + min) / 2.0 if Color.near_zero?(delta) # close to 0.0, so it's a grey hue = 0 sat = 0 else if Color.near_zero_or_less?(lum - 0.5) sat = delta / (max + min).to_f else sat = delta / (2 - max - min).to_f end # This is based on the conversion algorithm from # http://en.wikipedia.org/wiki/HSV_color_space#Conversion_from_RGB_to_HSL_or_HSV # Contributed by Adam Johnson sixth = 1 / 6.0 if @r == max # Color.near_zero_or_less?(@r - max) hue = (sixth * ((@g - @b) / delta)) hue += 1.0 if @g < @b elsif @g == max # Color.near_zero_or_less(@g - max) hue = (sixth * ((@b - @r) / delta)) + (1.0 / 3.0) elsif @b == max # Color.near_zero_or_less?(@b - max) hue = (sixth * ((@r - @g) / delta)) + (2.0 / 3.0) end hue += 1 if hue < 0 hue -= 1 if hue > 1 end Color::HSL.from_fraction(hue, sat, lum) end
Returns the L*a*b* colour encoding of the value via the XYZ colour encoding. Based on the XYZ to Lab formula presented by Bruce Lindbloom.
Currently only the sRGB colour space is supported and defaults to using a D65 reference white.
# File lib/color/rgb.rb, line 212 def to_lab(color_space = :sRGB, reference_white = [ 95.047, 100.00, 108.883 ]) xyz = to_xyz # Calculate the ratio of the XYZ values to the reference white. # http://www.brucelindbloom.com/index.html?Equations.html xr = xyz[:x] / reference_white[0] yr = xyz[:y] / reference_white[1] zr = xyz[:z] / reference_white[2] # NOTE: This should be using Rational instead of floating point values, # otherwise there will be discontinuities. # http://www.brucelindbloom.com/LContinuity.html epsilon = (216 / 24389.0) kappa = (24389 / 27.0) # And now transform # http://en.wikipedia.org/wiki/Lab_color_space#Forward_transformation # There is a brief explanation there as far as the nature of the calculations, # as well as a much nicer looking modeling of the algebra. fx, fy, fz = [ xr, yr, zr ].map { |t| if (t > (epsilon)) t ** (1.0 / 3) else # t <= epsilon ((kappa * t) + 16) / 116.0 # The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 - # 16 = 0, which is the correct value for L* with black. # ((1.0/3)*((29.0/6)**2) * t) + (4.0/29) end } { :L => ((116 * fy) - 16), :a => (500 * (fx - fy)), :b => (200 * (fy - fz)) } end
# File lib/color/rgb.rb, line 125 def to_rgb(ignored = nil) self end
Returns the XYZ colour encoding of the value. Based on the RGB to XYZ formula presented by Bruce Lindbloom.
Currently only the sRGB colour space is supported.
# File lib/color/rgb.rb, line 181 def to_xyz(color_space = :sRGB) unless color_space.to_s.downcase == 'srgb' raise ArgumentError, "Unsupported colour space #{color_space}." end # Inverse sRGB companding. Linearizes RGB channels with respect to # energy. r, g, b = [ @r, @g, @b ].map { |v| if (v > 0.04045) (((v + 0.055) / 1.055) ** 2.4) * 100 else (v / 12.92) * 100 end } # Convert using the RGB/XYZ matrix at: # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices { :x => (r * 0.4124564 + g * 0.3575761 + b * 0.1804375), :y => (r * 0.2126729 + g * 0.7151522 + b * 0.0721750), :z => (r * 0.0193339 + g * 0.1191920 + b * 0.9503041) } end
Returns the YIQ (NTSC) colour encoding of the RGB value.
# File lib/color/rgb.rb, line 130 def to_yiq y = (@r * 0.299) + (@g * 0.587) + (@b * 0.114) i = (@r * 0.596) + (@g * -0.275) + (@b * -0.321) q = (@r * 0.212) + (@g * -0.523) + (@b * 0.311) Color::YIQ.from_fraction(y, i, q) end
Private Instance Methods
# File lib/color/rgb.rb, line 687 def __by_hex @__by_hex ||= {} end
# File lib/color/rgb.rb, line 691 def __by_name @__by_name ||= {} end
# File lib/color/rgb.rb, line 674 def __named_color(mod, rgb, *names) if names.any? { |n| mod.const_defined? n } raise ArgumentError, "#{names.join(', ')} already defined in #{mod}" end names.each { |n| mod.const_set(n, rgb) } rgb.names = names rgb.names.each { |n| __by_name[n] = rgb } __by_hex[rgb.hex] = rgb rgb.freeze end
Provides the brightness difference.
# File lib/color/rgb/contrast.rb, line 34 def diff_brightness(other) other = other.to_rgb br1 = (299 * other.r + 587 * other.g + 114 * other.b) br2 = (299 * self.r + 587 * self.g + 114 * self.b) (br1 - br2).abs / 1000.0; end
Provides the euclidean distance between the two color values
# File lib/color/rgb/contrast.rb, line 42 def diff_euclidean(other) other = other.to_rgb ((((other.r - self.r) ** 2) + ((other.g - self.g) ** 2) + ((other.b - self.b) ** 2)) ** 0.5) / 1.7320508075688772 end
Difference in the two colors' hue
# File lib/color/rgb/contrast.rb, line 50 def diff_hue(other) other = other.to_rgb ((self.r - other.r).abs + (self.g - other.g).abs + (self.b - other.b).abs) / 3 end
Provides the luminosity difference between two rbg vals
# File lib/color/rgb/contrast.rb, line 20 def diff_luminosity(other) other = coerce(other) l1 = (0.2126 * (other.r) ** 2.2) + (0.7152 * (other.b) ** 2.2) + (0.0722 * (other.g) ** 2.2); l2 = (0.2126 * (self.r) ** 2.2) + (0.7152 * (self.b) ** 2.2) + (0.0722 * (self.g) ** 2.2); ((([l1, l2].max) + 0.05) / ( ([l1, l2].min) + 0.05 ) - 1) / 20.0 end
# File lib/color/rgb.rb, line 695 def html_hexify(hex) # When we can move to 1.9+ only, this will be \h h = hex.to_s.downcase.scan(/[0-9a-f]/) case h.size when 3 h.map { |v| (v * 2) }.join when 6 h.join else raise ArgumentError, "Not a supported HTML colour type." end end
# File lib/color/rgb/metallic.rb, line 9 def metallic(rgb, *names) __named_color(Metallic, new(*rgb), *names) end
# File lib/color/rgb/colors.rb, line 4 def named(rgb, *names) __named_color(self, new(*rgb), *names) end
# File lib/color/rgb.rb, line 567 def normalize_percent(percent) percent /= 100.0 percent += 1.0 percent = [ percent, 2.0 ].min percent = [ 0.0, percent ].max percent end