color 1.7.1 → 2.0.0.pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +5 -13
  2. data/CHANGELOG.md +298 -0
  3. data/CODE_OF_CONDUCT.md +128 -0
  4. data/CONTRIBUTING.md +70 -0
  5. data/CONTRIBUTORS.md +10 -0
  6. data/LICENCE.md +27 -0
  7. data/Manifest.txt +11 -21
  8. data/README.md +54 -0
  9. data/Rakefile +74 -53
  10. data/SECURITY.md +34 -0
  11. data/lib/color/cielab.rb +348 -0
  12. data/lib/color/cmyk.rb +279 -213
  13. data/lib/color/grayscale.rb +128 -160
  14. data/lib/color/hsl.rb +205 -173
  15. data/lib/color/rgb/colors.rb +177 -163
  16. data/lib/color/rgb.rb +534 -537
  17. data/lib/color/version.rb +5 -0
  18. data/lib/color/xyz.rb +214 -0
  19. data/lib/color/yiq.rb +91 -46
  20. data/lib/color.rb +208 -141
  21. data/test/fixtures/cielab.json +444 -0
  22. data/test/minitest_helper.rb +20 -4
  23. data/test/test_cmyk.rb +49 -71
  24. data/test/test_color.rb +58 -106
  25. data/test/test_grayscale.rb +35 -56
  26. data/test/test_hsl.rb +72 -76
  27. data/test/test_rgb.rb +195 -267
  28. data/test/test_yiq.rb +12 -30
  29. metadata +165 -150
  30. checksums.yaml.gz.sig +0 -0
  31. data/.autotest +0 -5
  32. data/.gemtest +0 -0
  33. data/.hoerc +0 -2
  34. data/.minitest.rb +0 -2
  35. data/.travis.yml +0 -35
  36. data/Contributing.rdoc +0 -60
  37. data/Gemfile +0 -9
  38. data/History.rdoc +0 -172
  39. data/Licence.rdoc +0 -27
  40. data/README.rdoc +0 -50
  41. data/lib/color/css.rb +0 -7
  42. data/lib/color/palette/adobecolor.rb +0 -260
  43. data/lib/color/palette/gimp.rb +0 -104
  44. data/lib/color/palette/monocontrast.rb +0 -164
  45. data/lib/color/palette.rb +0 -4
  46. data/lib/color/rgb/contrast.rb +0 -57
  47. data/lib/color/rgb/metallic.rb +0 -28
  48. data/test/test_adobecolor.rb +0 -405
  49. data/test/test_css.rb +0 -19
  50. data/test/test_gimp.rb +0 -87
  51. data/test/test_monocontrast.rb +0 -130
  52. data.tar.gz.sig +0 -0
  53. metadata.gz.sig +0 -0
data/lib/color/rgb.rb CHANGED
@@ -1,660 +1,652 @@
1
- # An RGB colour object.
1
+ # frozen_string_literal: true
2
+
3
+ # The \RGB color model is an additive color model where the primary colors (red, green,
4
+ # and blue) of light are added to produce millions of colors. \RGB rendering is
5
+ # device-dependent and without color management, the same "red" color will render
6
+ # differently.
7
+ #
8
+ # This class does not implement color management and is not \RGB colorspace aware; that is,
9
+ # unless otherwise noted, it does not assume that the \RGB represented is sRGB or Adobe
10
+ # \RGB (opRGB).
11
+ #
12
+ # \RGB colors are immutable Data class instances. Array deconstruction is `[red, green,
13
+ # blue]` and hash deconstruction is `{r:, red:, g:, green:, b:, blue}`. See #r, #red, #g,
14
+ # #green, #b, #blue.
2
15
  class Color::RGB
3
16
  include Color
4
17
 
5
- # The format of a DeviceRGB colour for PDF. In color-tools 2.0 this will
6
- # be removed from this package and added back as a modification by the
7
- # PDF::Writer package.
8
- PDF_FORMAT_STR = "%.3f %.3f %.3f %s"
18
+ ##
19
+ # :attr_reader: r
20
+ # Returns the red component of the color in the range 0.0..1.0.
9
21
 
10
- # Coerces the other Color object into RGB.
11
- def coerce(other)
12
- other.to_rgb
13
- end
22
+ ##
23
+ # :attr_reader: red
24
+ # Returns the red component of the color in the normal 0..255 range.
14
25
 
15
- # Creates an RGB colour object from the standard range 0..255.
26
+ ##
27
+ # :attr_reader: red_p
28
+ # Returns the red component of the color as a percentage (0.0 .. 100.0).
16
29
  #
17
- # Color::RGB.new(32, 64, 128)
18
- # Color::RGB.new(0x20, 0x40, 0x80)
19
- def initialize(r = 0, g = 0, b = 0, radix = 255.0, &block) # :yields self:
20
- @r, @g, @b = [ r, g, b ].map { |v| Color.normalize(v / radix) }
21
- block.call(self) if block
22
- end
30
+ ##
31
+ # :attr_reader: g
32
+ # Returns the green component of the color in the range 0.0..1.0.
23
33
 
24
- # Present the colour as a DeviceRGB fill colour string for PDF. This will
25
- # be removed from the default package in color-tools 2.0.
26
- def pdf_fill
27
- PDF_FORMAT_STR % [ @r, @g, @b, "rg" ]
28
- end
34
+ ##
35
+ # :attr_reader: green
36
+ # Returns the green component of the color in the normal 0 .. 255 range.
29
37
 
30
- # Present the colour as a DeviceRGB stroke colour string for PDF. This
31
- # will be removed from the default package in color-tools 2.0.
32
- def pdf_stroke
33
- PDF_FORMAT_STR % [ @r, @g, @b, "RG" ]
34
- end
38
+ ##
39
+ # :attr_reader: green_p
40
+ # Returns the green component of the color as a percentage (0.0 .. 100.0).
35
41
 
36
- # Present the colour as an RGB hex triplet.
37
- def hex
38
- r = (@r * 255).round
39
- r = 255 if r > 255
42
+ ##
43
+ # :attr_reader: b
44
+ # Returns the blue component of the color in the range 0.0..1.0.
40
45
 
41
- g = (@g * 255).round
42
- g = 255 if g > 255
46
+ ##
47
+ # :attr_reader: blue
48
+ # Returns the blue component of the color in the normal 0 .. 255 range.
43
49
 
44
- b = (@b * 255).round
45
- b = 255 if b > 255
50
+ ##
51
+ # :attr_reader: blue_p
52
+ # Returns the blue component of the color as a percentage (0.0 .. 100.0).
46
53
 
47
- "%02x%02x%02x" % [ r, g, b ]
48
- end
49
-
50
- # Present the colour as an HTML/CSS colour string.
51
- def html
52
- "##{hex}"
54
+ ##
55
+ # Creates a \RGB color object from fractional values (0.0 .. 1.0).
56
+ #
57
+ # ```ruby
58
+ # Color::RGB.from_fraction(0.3, 0.2, 0.1) # => RGB [#4d331a]
59
+ # Color::RGB.new(0.3, 0.2, 0.1) # => RGB [#4d331a]
60
+ # Color::RGB[r: 0.3, g: 0.2, b: 0.1] # => RGB [#4d331a]
61
+ # ```
62
+ def initialize(r:, g:, b:, names: nil)
63
+ super(r: normalize(r), g: normalize(g), b: normalize(b), names: names)
53
64
  end
54
65
 
55
- # Present the colour as an RGB HTML/CSS colour string (e.g., "rgb(0%, 50%,
56
- # 100%)"). Note that this will perform a #to_rgb operation using the
57
- # default conversion formula.
58
- def css_rgb
59
- "rgb(%3.2f%%, %3.2f%%, %3.2f%%)" % [ red_p, green_p, blue_p ]
60
- end
66
+ ##
67
+ # :attr_reader: name
68
+ # The primary name for this \RGB color.
61
69
 
62
- # Present the colour as an RGBA (with alpha) HTML/CSS colour string (e.g.,
63
- # "rgb(0%, 50%, 100%, 1)"). Note that this will perform a #to_rgb
64
- # operation using the default conversion formula.
65
- def css_rgba
66
- "rgba(%3.2f%%, %3.2f%%, %3.2f%%, %3.2f)" % [ red_p, green_p, blue_p, 1 ]
67
- end
70
+ ##
71
+ # :attr_reader: names
72
+ # The names for this \RGB color.
68
73
 
69
- # Present the colour as an HSL HTML/CSS colour string (e.g., "hsl(180,
70
- # 25%, 35%)"). Note that this will perform a #to_hsl operation using the
71
- # default conversion formula.
72
- def css_hsl
73
- to_hsl.css_hsl
74
- end
74
+ ##
75
+ def name = names&.first # :nodoc:
75
76
 
76
- # Present the colour as an HSLA (with alpha) HTML/CSS colour string (e.g.,
77
- # "hsla(180, 25%, 35%, 1)"). Note that this will perform a #to_hsl
78
- # operation using the default conversion formula.
79
- def css_hsla
80
- to_hsl.css_hsla
81
- end
77
+ ##
78
+ # Coerces the other Color object into \RGB.
79
+ def coerce(other) = other.to_rgb
82
80
 
83
- # Converts the RGB colour to CMYK. Most colour experts strongly suggest
84
- # that this is not a good idea (some even suggesting that it's a very bad
85
- # idea). CMYK represents additive percentages of inks on white paper,
86
- # whereas RGB represents mixed colour intensities on a black screen.
81
+ ##
82
+ # Converts the \RGB color to Color::CMYK.
87
83
  #
88
- # However, the colour conversion can be done. The basic method is
89
- # multi-step:
84
+ # Most color experts strongly suggest that this is not a good idea (some suggesting that
85
+ # it's a very bad idea). CMYK represents additive percentages of inks on white paper,
86
+ # whereas \RGB represents mixed color intensities on an unlit (black) screen.
90
87
  #
91
88
  # 1. Convert the R, G, and B components to C, M, and Y components.
92
- # c = 1.0 - r
93
- # m = 1.0 - g
94
- # y = 1.0 - b
95
- # 2. Compute the minimum amount of black (K) required to smooth the colour
96
- # in inks.
97
- # k = min(c, m, y)
98
- # 3. Perform undercolour removal on the C, M, and Y components of the
99
- # colours because less of each colour is needed for each bit of black.
100
- # Also, regenerate the black (K) based on the undercolour removal so
101
- # that the colour is more accurately represented in ink.
102
- # c = min(1.0, max(0.0, c - UCR(k)))
103
- # m = min(1.0, max(0.0, m - UCR(k)))
104
- # y = min(1.0, max(0.0, y - UCR(k)))
105
- # k = min(1.0, max(0.0, BG(k)))
106
- #
107
- # The undercolour removal function and the black generation functions
108
- # return a value based on the brightness of the RGB colour.
109
- def to_cmyk
110
- c = 1.0 - @r.to_f
111
- m = 1.0 - @g.to_f
112
- y = 1.0 - @b.to_f
89
+ #
90
+ # c = 1.0 - r
91
+ # m = 1.0 - g
92
+ # y = 1.0 - b
93
+ #
94
+ # 2. Compute the minimum amount of black (K) required to smooth the color in inks.
95
+ #
96
+ # k = min(c, m, y)
97
+ #
98
+ # 3. Perform undercolor removal on the C, M, and Y components of the colors because less
99
+ # of each color is needed for each bit of black. Also, regenerate the black (K) based
100
+ # on the undercolor removal so that the color is more accurately represented in ink.
101
+ #
102
+ # c = min(1.0, max(0.0, c - UCR(k)))
103
+ # m = min(1.0, max(0.0, m - UCR(k)))
104
+ # y = min(1.0, max(0.0, y - UCR(k)))
105
+ # k = min(1.0, max(0.0, BG(k)))
106
+ #
107
+ # The undercolor removal function and the black generation functions return a value
108
+ # based on the brightness of the \RGB color.
109
+ def to_cmyk(...)
110
+ c = 1.0 - r.to_f
111
+ m = 1.0 - g.to_f
112
+ y = 1.0 - b.to_f
113
113
 
114
114
  k = [c, m, y].min
115
- k = k - (k * brightness)
115
+ k -= (k * brightness)
116
116
 
117
- c = [1.0, [0.0, c - k].max].min
118
- m = [1.0, [0.0, m - k].max].min
119
- y = [1.0, [0.0, y - k].max].min
120
- k = [1.0, [0.0, k].max].min
117
+ c = normalize(c - k)
118
+ m = normalize(m - k)
119
+ y = normalize(y - k)
120
+ k = normalize(k)
121
121
 
122
122
  Color::CMYK.from_fraction(c, m, y, k)
123
123
  end
124
124
 
125
- def to_rgb(ignored = nil)
126
- self
127
- end
125
+ ##
126
+ def to_rgb(...) = self
127
+
128
+ ##
129
+ # Convert \RGB to Color::Grayscale via Color::HSL (for the luminance value).
130
+ def to_grayscale(...) = Color::Grayscale.from_fraction(to_hsl.l)
128
131
 
129
- # Returns the YIQ (NTSC) colour encoding of the RGB value.
130
- def to_yiq
131
- y = (@r * 0.299) + (@g * 0.587) + (@b * 0.114)
132
- i = (@r * 0.596) + (@g * -0.275) + (@b * -0.321)
133
- q = (@r * 0.212) + (@g * -0.523) + (@b * 0.311)
132
+ ##
133
+ # Converts \RGB to Color::YIQ.
134
+ def to_yiq(...)
135
+ y = (r * 0.299) + (g * 0.587) + (b * 0.114)
136
+ i = (r * 0.596) + (g * -0.275) + (b * -0.321)
137
+ q = (r * 0.212) + (g * -0.523) + (b * 0.311)
134
138
  Color::YIQ.from_fraction(y, i, q)
135
139
  end
136
140
 
137
- # Returns the HSL colour encoding of the RGB value. The conversions here
138
- # are based on forumlas from http://www.easyrgb.com/math.php and
141
+ ##
142
+ # Converts \RGB to Color::HSL.
143
+ #
144
+ # The conversion here is based on formulas from http://www.easyrgb.com/math.php and
139
145
  # elsewhere.
140
- def to_hsl
141
- min = [ @r, @g, @b ].min
142
- max = [ @r, @g, @b ].max
146
+ def to_hsl(...)
147
+ min, max = [r, g, b].minmax
143
148
  delta = (max - min).to_f
144
149
 
145
- lum = (max + min) / 2.0
150
+ l = (max + min) / 2.0
146
151
 
147
- if Color.near_zero?(delta) # close to 0.0, so it's a grey
148
- hue = 0
149
- sat = 0
152
+ if near_zero?(delta) # close to 0.0, so it's a gray
153
+ h = 0
154
+ s = 0
150
155
  else
151
- if Color.near_zero_or_less?(lum - 0.5)
152
- sat = delta / (max + min).to_f
156
+ s = if near_zero_or_less?(l - 0.5)
157
+ delta / (max + min).to_f
153
158
  else
154
- sat = delta / (2 - max - min).to_f
159
+ delta / (2 - max - min).to_f
155
160
  end
156
161
 
157
162
  # This is based on the conversion algorithm from
158
163
  # http://en.wikipedia.org/wiki/HSV_color_space#Conversion_from_RGB_to_HSL_or_HSV
159
164
  # Contributed by Adam Johnson
160
165
  sixth = 1 / 6.0
161
- if @r == max # Color.near_zero_or_less?(@r - max)
162
- hue = (sixth * ((@g - @b) / delta))
163
- hue += 1.0 if @g < @b
164
- elsif @g == max # Color.near_zero_or_less(@g - max)
165
- hue = (sixth * ((@b - @r) / delta)) + (1.0 / 3.0)
166
- elsif @b == max # Color.near_zero_or_less?(@b - max)
167
- hue = (sixth * ((@r - @g) / delta)) + (2.0 / 3.0)
166
+ if r == max # near_zero_or_less?(r - max)
167
+ h = (sixth * ((g - b) / delta))
168
+ h += 1.0 if g < b
169
+ elsif g == max # near_zero_or_less(g - max)
170
+ h = (sixth * ((b - r) / delta)) + (1.0 / 3.0)
171
+ elsif b == max # near_zero_or_less?(b - max)
172
+ h = (sixth * ((r - g) / delta)) + (2.0 / 3.0)
168
173
  end
169
174
 
170
- hue += 1 if hue < 0
171
- hue -= 1 if hue > 1
175
+ h += 1 if h < 0
176
+ h -= 1 if h > 1
172
177
  end
173
- Color::HSL.from_fraction(hue, sat, lum)
178
+
179
+ Color::HSL.from_fraction(h, s, l)
174
180
  end
175
181
 
176
- # Returns the XYZ colour encoding of the value. Based on the
177
- # {RGB to XYZ}[http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html]
178
- # formula presented by Bruce Lindbloom.
182
+ ##
183
+ # Converts \RGB to Color::XYZ using the D65 reference white. This is based on conversion
184
+ # formulas presented by Bruce Lindbloom, in particular [RGB to XYZ][rgbxyz].
185
+ #
186
+ # [rgbxyz]: http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
187
+ #
188
+ # The conversion is performed assuming the \RGB value is in the sRGB color space. No
189
+ # other \RGB color spaces are currently supported.
179
190
  #
180
- # Currently only the sRGB colour space is supported.
181
- def to_xyz(color_space = :sRGB)
182
- unless color_space.to_s.downcase == 'srgb'
183
- raise ArgumentError, "Unsupported colour space #{color_space}."
191
+ # :call-seq:
192
+ # to_xyz(color_space: :srgb)
193
+ def to_xyz(*args, **kwargs)
194
+ color_space = kwargs[:color_space] || args.first || :sRGB
195
+
196
+ case color_space.to_s.downcase
197
+ when "srgb"
198
+ # Inverse sRGB companding. Linearizes RGB channels with respect to energy.
199
+ rr, gg, bb = [r, g, b].map {
200
+ if _1 > 0.04045
201
+ (((_1 + 0.055) / 1.055)**2.4)
202
+ else
203
+ (_1 / 12.92)
204
+ end * 100.0
205
+ }
206
+
207
+ # Convert using the RGB/XYZ matrix at:
208
+ # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices
209
+ Color::XYZ.from_values(
210
+ rr * 0.4124564 + gg * 0.3575761 + bb * 0.1804375,
211
+ rr * 0.2126729 + gg * 0.7151522 + bb * 0.0721750,
212
+ rr * 0.0193339 + gg * 0.1191920 + bb * 0.9503041
213
+ )
214
+ else
215
+ raise ArgumentError, "Unsupported color space #{color_space}."
184
216
  end
217
+ end
185
218
 
186
- # Inverse sRGB companding. Linearizes RGB channels with respect to
187
- # energy.
188
- r, g, b = [ @r, @g, @b ].map { |v|
189
- if (v > 0.04045)
190
- (((v + 0.055) / 1.055) ** 2.4) * 100
191
- else
192
- (v / 12.92) * 100
193
- end
194
- }
219
+ ##
220
+ # Converts \RGB to Color::CIELAB via Color::XYZ.
221
+ #
222
+ # Based on the [XYZ to CIELAB][xyztolab] formula presented by Bruce Lindbloom.
223
+ #
224
+ # [xyztolab]: http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
225
+ #
226
+ # The conversion is performed assuming the \RGB value is in the sRGB color space. No
227
+ # other \RGB color spaces are currently supported. By default, uses the D65 reference
228
+ # white for the conversion.
229
+ #
230
+ # :call-seq:
231
+ # to_lab(color_space: :sRGB, white: Color::XYZ::D65)
232
+ def to_lab(...) = to_xyz(...).to_lab(...)
195
233
 
196
- # Convert using the RGB/XYZ matrix at:
197
- # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices
198
- {
199
- :x => (r * 0.4124564 + g * 0.3575761 + b * 0.1804375),
200
- :y => (r * 0.2126729 + g * 0.7151522 + b * 0.0721750),
201
- :z => (r * 0.0193339 + g * 0.1191920 + b * 0.9503041)
202
- }
234
+ ##
235
+ # Present the color as an HTML/CSS \RGB hex triplet (+ccddee+).
236
+ def hex
237
+ "%02x%02x%02x" % [red, green, blue].map(&:round)
203
238
  end
204
239
 
205
- # Returns the L*a*b* colour encoding of the value via the XYZ colour
206
- # encoding. Based on the
207
- # {XYZ to Lab}[http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html]
208
- # formula presented by Bruce Lindbloom.
209
- #
210
- # Currently only the sRGB colour space is supported and defaults to using
211
- # a D65 reference white.
212
- def to_lab(color_space = :sRGB, reference_white = [ 95.047, 100.00, 108.883 ])
213
- xyz = to_xyz
214
-
215
- # Calculate the ratio of the XYZ values to the reference white.
216
- # http://www.brucelindbloom.com/index.html?Equations.html
217
- xr = xyz[:x] / reference_white[0]
218
- yr = xyz[:y] / reference_white[1]
219
- zr = xyz[:z] / reference_white[2]
220
-
221
- # NOTE: This should be using Rational instead of floating point values,
222
- # otherwise there will be discontinuities.
223
- # http://www.brucelindbloom.com/LContinuity.html
224
- epsilon = (216 / 24389.0)
225
- kappa = (24389 / 27.0)
226
-
227
- # And now transform
228
- # http://en.wikipedia.org/wiki/Lab_color_space#Forward_transformation
229
- # There is a brief explanation there as far as the nature of the calculations,
230
- # as well as a much nicer looking modeling of the algebra.
231
- fx, fy, fz = [ xr, yr, zr ].map { |t|
232
- if (t > (epsilon))
233
- t ** (1.0 / 3)
234
- else # t <= epsilon
235
- ((kappa * t) + 16) / 116.0
236
- # The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 -
237
- # 16 = 0, which is the correct value for L* with black.
238
- # ((1.0/3)*((29.0/6)**2) * t) + (4.0/29)
239
- end
240
- }
241
- {
242
- :L => ((116 * fy) - 16),
243
- :a => (500 * (fx - fy)),
244
- :b => (200 * (fy - fz))
245
- }
240
+ ##
241
+ # Present the color as an HTML/CSS color string (+#ccddee+).
242
+ def html
243
+ "##{hex}"
246
244
  end
247
245
 
248
- # Mix the RGB hue with White so that the RGB hue is the specified
249
- # percentage of the resulting colour. Strictly speaking, this isn't a
250
- # darken_by operation.
251
- def lighten_by(percent)
252
- mix_with(White, percent)
253
- end
246
+ ##
247
+ # Present the color as an CSS `rgb` function with optional `alpha`.
248
+ #
249
+ # ```ruby
250
+ # rgb = Color::RGB.from_percentage(0, 50, 100)
251
+ # rgb.css # => rgb(0 50.00% 100.00%)
252
+ # rgb.css(alpha: 0.5) # => rgb(0 50.00% 100.00% / 0.50)
253
+ # ```
254
+ def css(alpha: nil)
255
+ params = [css_value(red_p, :percent), css_value(green_p, :percent), css_value(blue_p, :percent)].join(" ")
256
+ params = "#{params} / #{css_value(alpha)}" if alpha
254
257
 
255
- # Mix the RGB hue with Black so that the RGB hue is the specified
256
- # percentage of the resulting colour. Strictly speaking, this isn't a
257
- # darken_by operation.
258
- def darken_by(percent)
259
- mix_with(Black, percent)
258
+ "rgb(#{params})"
260
259
  end
261
260
 
262
- # Mix the mask colour (which must be an RGB object) with the current
263
- # colour at the stated opacity percentage (0..100).
264
- def mix_with(mask, opacity)
265
- opacity /= 100.0
266
- rgb = self.dup
261
+ ##
262
+ # Computes the ΔE* 2000 difference via Color::CIELAB. See Color::CIELAB#delta_e2000.
263
+ def delta_e2000(other) = to_lab.delta_e2000(coerce(other).to_lab)
264
+
265
+ ##
266
+ # Mix the \RGB hue with White so that the \RGB hue is the specified percentage of the
267
+ # resulting color.
268
+ #
269
+ # Strictly speaking, this isn't a `lighten_by` operation, but it mostly works.
270
+ def lighten_by(percent) = mix_with(Color::RGB::White, percent)
267
271
 
268
- rgb.r = (@r * opacity) + (mask.r * (1 - opacity))
269
- rgb.g = (@g * opacity) + (mask.g * (1 - opacity))
270
- rgb.b = (@b * opacity) + (mask.b * (1 - opacity))
272
+ ##
273
+ # Mix the \RGB hue with Black so that the \RGB hue is the specified percentage of the
274
+ # resulting color.
275
+ #
276
+ # Strictly speaking, this isn't a `darken_by` operation, but it mostly works.
277
+ def darken_by(percent) = mix_with(Color::RGB::Black, percent)
278
+
279
+ ##
280
+ # Mix the mask color with the current color at the stated opacity percentage (0..100).
281
+ def mix_with(mask, opacity)
282
+ opacity = normalize(opacity / 100.0)
283
+ mask = coerce(mask)
271
284
 
272
- rgb
285
+ with(
286
+ r: (r * opacity) + (mask.r * (1 - opacity)),
287
+ g: (g * opacity) + (mask.g * (1 - opacity)),
288
+ b: (b * opacity) + (mask.b * (1 - opacity))
289
+ )
273
290
  end
274
291
 
275
- # Returns the brightness value for a colour, a number between 0..1. Based
276
- # on the Y value of YIQ encoding, representing luminosity, or perceived
277
- # brightness.
292
+ ##
293
+ # Returns the brightness value for a color, a number between 0..1.
278
294
  #
279
- # This may be modified in a future version of color-tools to use the
280
- # luminosity value of HSL.
281
- def brightness
282
- to_yiq.y
283
- end
284
- # Convert to grayscale.
285
- def to_grayscale
286
- Color::GrayScale.from_fraction(to_hsl.l)
287
- end
288
- alias to_greyscale to_grayscale
295
+ # Based on the Y value of Color::YIQ encoding, representing luminosity, or perceived
296
+ # brightness.
297
+ def brightness = to_yiq.y
289
298
 
290
- # Returns a new colour with the brightness adjusted by the specified
291
- # percentage. Negative percentages will darken the colour; positive
292
- # percentages will brighten the colour.
299
+ ##
300
+ # Returns a new \RGB color with the brightness adjusted by the specified percentage via
301
+ # Color::HSL. Negative percentages will darken the color; positive percentages will
302
+ # brighten the color.
293
303
  #
294
- # Color::RGB::DarkBlue.adjust_brightness(10)
295
- # Color::RGB::DarkBlue.adjust_brightness(-10)
304
+ # ```ruby
305
+ # dark_blue = Color::RGB::DarkBlue # => RGB [#00008b]
306
+ # dark_blue.adjust_brightness(10) # => RGB [#000099]
307
+ # dark_blue.adjust_brightness(-10) # => RGB [#00007d]
308
+ # ```
296
309
  def adjust_brightness(percent)
297
- percent = normalize_percent(percent)
298
- hsl = to_hsl
299
- hsl.l *= percent
300
- hsl.to_rgb
310
+ hsl = to_hsl
311
+ hsl.with(l: hsl.l * percent_adjustment(percent)).to_rgb
301
312
  end
302
313
 
303
- # Returns a new colour with the saturation adjusted by the specified
304
- # percentage. Negative percentages will reduce the saturation; positive
305
- # percentages will increase the saturation.
314
+ ##
315
+ # Returns a new \RGB color with the saturation adjusted by the specified percentage via
316
+ # Color::HSL. Negative percentages will reduce the saturation; positive percentages will
317
+ # increase the saturation.
306
318
  #
307
- # Color::RGB::DarkBlue.adjust_saturation(10)
308
- # Color::RGB::DarkBlue.adjust_saturation(-10)
319
+ # ```ruby
320
+ # dark_blue = Color::RGB::DarkBlue # => RGB [#00008b]
321
+ # dark_blue.adjust_saturation(10) # => RGB [#00008b]
322
+ # dark_blue.adjust_saturation(-10) # => RGB [#070784]
323
+ # ```
309
324
  def adjust_saturation(percent)
310
- percent = normalize_percent(percent)
311
- hsl = to_hsl
312
- hsl.s *= percent
313
- hsl.to_rgb
325
+ hsl = to_hsl
326
+ hsl.with(s: hsl.s * percent_adjustment(percent)).to_rgb
314
327
  end
315
328
 
316
- # Returns a new colour with the hue adjusted by the specified percentage.
317
- # Negative percentages will reduce the hue; positive percentages will
329
+ ##
330
+ # Returns a new \RGB color with the hue adjusted by the specified percentage via
331
+ # Color::HSL. Negative percentages will reduce the hue; positive percentages will
318
332
  # increase the hue.
319
333
  #
320
- # Color::RGB::DarkBlue.adjust_hue(10)
321
- # Color::RGB::DarkBlue.adjust_hue(-10)
334
+ # ```ruby
335
+ # dark_blue = Color::RGB::DarkBlue # => RGB [#00008b]
336
+ # dark_blue.adjust_hue(10) # => RGB [#38008b]
337
+ # dark_blue.adjust_hue(-10) # => RGB [#00388b]
338
+ # ```
322
339
  def adjust_hue(percent)
323
- percent = normalize_percent(percent)
324
- hsl = to_hsl
325
- hsl.h *= percent
326
- hsl.to_rgb
340
+ hsl = to_hsl
341
+ hsl.with(h: hsl.h * percent_adjustment(percent)).to_rgb
327
342
  end
328
343
 
329
- # TODO: Identify the base colour profile used for L*a*b* and XYZ
330
- # conversions.
331
-
332
- # Calculates and returns the closest match to this colour from a list of
333
- # provided colours. Returns +nil+ if +color_list+ is empty or if there is
334
- # no colour within the +threshold_distance+.
335
- #
336
- # +threshold_distance+ is used to determine the minimum colour distance
337
- # permitted. Uses the CIE Delta E 1994 algorithm (CIE94) to find near
338
- # matches based on perceived visual colour. The default value (1000.0) is
339
- # an arbitrarily large number. The values <tt>:jnd</tt> and
340
- # <tt>:just_noticeable</tt> may be passed as the +threshold_distance+ to
341
- # use the value <tt>2.3</tt>.
342
- def closest_match(color_list, threshold_distance = 1000.0)
343
- color_list = [ color_list ].flatten(1)
344
+ ##
345
+ # Determines the closest match to this color from a list of provided colors or `nil` if
346
+ # `color_list` is empty or no color is found within the `threshold_distance`.
347
+ #
348
+ # The default search uses the CIE ΔE* 1994 algorithm (CIE94) to find near matches based
349
+ # on the perceived visual differences between the colors. The default value for
350
+ # `algorithm` is `:delta_e94`.
351
+ #
352
+ # `threshold_distance` is used to determine the minimum color distance permitted. Uses
353
+ # the CIE ΔE* 1994 algorithm (CIE94) to find near matches based on perceived visual
354
+ # color. The default value (1000.0) is an arbitrarily large number. The values `:jnd`
355
+ # and `:just_noticeable` may be passed as the `threshold_distance` to use the value
356
+ # `2.3`.
357
+ #
358
+ # All ΔE* formulae were designed to use 1.0 as a "just noticeable difference" (JND),
359
+ # but CIE ΔE*ab 1976 defined JND as 2.3.
360
+ #
361
+ # :call-seq:
362
+ # closest_match(color_list, algorithm: :delta_e94, threshold_distance: 1000.0)
363
+ def closest_match(color_list, *args, **kwargs)
364
+ color_list = [color_list].flatten(1)
344
365
  return nil if color_list.empty?
345
366
 
346
- threshold_distance = case threshold_distance
347
- when :jnd, :just_noticeable
348
- 2.3
349
- else
350
- threshold_distance.to_f
351
- end
352
- lab = to_lab
367
+ algorithm = kwargs[:algorithm] || args.first || :delta_e94
368
+ threshold_distance = kwargs[:threshold_distance] || args[1] || 1000.0
369
+
370
+ threshold_distance =
371
+ case threshold_distance
372
+ when :jnd, :just_noticeable
373
+ 2.3
374
+ else
375
+ threshold_distance.to_f
376
+ end
377
+
353
378
  closest_distance = threshold_distance
354
379
  best_match = nil
355
380
 
356
381
  color_list.each do |c|
357
- distance = delta_e94(lab, c.to_lab)
358
- if (distance < closest_distance)
382
+ distance = contrast(c, algorithm)
383
+ if distance < closest_distance
359
384
  closest_distance = distance
360
385
  best_match = c
361
386
  end
362
387
  end
388
+
363
389
  best_match
364
390
  end
365
391
 
366
- # The Delta E (CIE94) algorithm
367
- # http://en.wikipedia.org/wiki/Color_difference#CIE94
392
+ ##
393
+ # The Delta E (CIE94) algorithm http://en.wikipedia.org/wiki/Color_difference#CIE94
368
394
  #
369
- # There is a newer version, CIEDE2000, that uses slightly more complicated
370
- # math, but addresses "the perceptual uniformity issue" left lingering by
371
- # the CIE94 algorithm. color_1 and color_2 are both L*a*b* hashes,
372
- # rendered by #to_lab.
395
+ # There is a newer version, CIEDE2000, that uses slightly more complicated math, but
396
+ # addresses "the perceptual uniformity issue" left lingering by the CIE94 algorithm.
373
397
  #
374
- # Since our source is treated as sRGB, we use the "graphic arts" presets
375
- # for k_L, k_1, and k_2
398
+ # Since our source is treated as sRGB, we use the "graphic arts" presets for k_L, k_1,
399
+ # and k_2
376
400
  #
377
401
  # The calculations go through LCH(ab). (?)
378
402
  #
379
403
  # See also http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
380
- #
381
- # NOTE: This should be moved to Color::Lab.
382
- def delta_e94(color_1, color_2, weighting_type = :graphic_arts)
383
- case weighting_type
384
- when :graphic_arts
385
- k_1 = 0.045
386
- k_2 = 0.015
387
- k_L = 1
388
- when :textiles
389
- k_1 = 0.048
390
- k_2 = 0.014
391
- k_L = 2
392
- else
393
- raise ArgumentError, "Unsupported weighting type #{weighting_type}."
394
- end
404
+ def delta_e94(...) = to_lab.delta_e94(...)
395
405
 
396
- # delta_E = Math.sqrt(
397
- # ((delta_L / (k_L * s_L)) ** 2) +
398
- # ((delta_C / (k_C * s_C)) ** 2) +
399
- # ((delta_H / (k_H * s_H)) ** 2)
400
- # )
401
- #
402
- # Under some circumstances in real computers, delta_H could be an
403
- # imaginary number (it's a square root value), so we're going to treat
404
- # this as:
405
- #
406
- # delta_E = Math.sqrt(
407
- # ((delta_L / (k_L * s_L)) ** 2) +
408
- # ((delta_C / (k_C * s_C)) ** 2) +
409
- # (delta_H2 / ((k_H * s_H) ** 2)))
410
- # )
411
- #
412
- # And not perform the square root when calculating delta_H2.
413
-
414
- k_C = k_H = 1
415
-
416
- l_1, a_1, b_1 = color_1.values_at(:L, :a, :b)
417
- l_2, a_2, b_2 = color_2.values_at(:L, :a, :b)
418
-
419
- delta_a = a_1 - a_2
420
- delta_b = b_1 - b_2
421
-
422
- c_1 = Math.sqrt((a_1 ** 2) + (b_1 ** 2))
423
- c_2 = Math.sqrt((a_2 ** 2) + (b_2 ** 2))
424
-
425
- delta_L = color_1[:L] - color_2[:L]
426
- delta_C = c_1 - c_2
427
-
428
- delta_H2 = (delta_a ** 2) + (delta_b ** 2) - (delta_C ** 2)
429
-
430
- s_L = 1
431
- s_C = 1 + k_1 * c_1
432
- s_H = 1 + k_2 * c_1
433
-
434
- composite_L = (delta_L / (k_L * s_L)) ** 2
435
- composite_C = (delta_C / (k_C * s_C)) ** 2
436
- composite_H = delta_H2 / ((k_H * s_H) ** 2)
437
- Math.sqrt(composite_L + composite_C + composite_H)
438
- end
406
+ ##
407
+ def red = normalize(r * 255.0, 0.0..255.0) # :nodoc:
439
408
 
440
- # Returns the red component of the colour in the normal 0 .. 255 range.
441
- def red
442
- @r * 255.0
443
- end
444
- # Returns the red component of the colour as a percentage.
445
- def red_p
446
- @r * 100.0
447
- end
448
- # Returns the red component of the colour as a fraction in the range 0.0
449
- # .. 1.0.
450
- def r
451
- @r
452
- end
453
- # Sets the red component of the colour in the normal 0 .. 255 range.
454
- def red=(rr)
455
- @r = Color.normalize(rr / 255.0)
456
- end
457
- # Sets the red component of the colour as a percentage.
458
- def red_p=(rr)
459
- @r = Color.normalize(rr / 100.0)
460
- end
461
- # Sets the red component of the colour as a fraction in the range 0.0 ..
462
- # 1.0.
463
- def r=(rr)
464
- @r = Color.normalize(rr)
465
- end
409
+ ##
410
+ def red_p = normalize(r * 100.0, 0.0..100.0) # :nodoc:
466
411
 
467
- # Returns the green component of the colour in the normal 0 .. 255 range.
468
- def green
469
- @g * 255.0
470
- end
471
- # Returns the green component of the colour as a percentage.
472
- def green_p
473
- @g * 100.0
474
- end
475
- # Returns the green component of the colour as a fraction in the range 0.0
476
- # .. 1.0.
477
- def g
478
- @g
479
- end
480
- # Sets the green component of the colour in the normal 0 .. 255 range.
481
- def green=(gg)
482
- @g = Color.normalize(gg / 255.0)
483
- end
484
- # Sets the green component of the colour as a percentage.
485
- def green_p=(gg)
486
- @g = Color.normalize(gg / 100.0)
487
- end
488
- # Sets the green component of the colour as a fraction in the range 0.0 ..
489
- # 1.0.
490
- def g=(gg)
491
- @g = Color.normalize(gg)
492
- end
412
+ ##
413
+ def green = normalize(g * 255.0, 0.0..255.0) # :nodoc:
493
414
 
494
- # Returns the blue component of the colour in the normal 0 .. 255 range.
495
- def blue
496
- @b * 255.0
497
- end
498
- # Returns the blue component of the colour as a percentage.
499
- def blue_p
500
- @b * 100.0
501
- end
502
- # Returns the blue component of the colour as a fraction in the range 0.0
503
- # .. 1.0.
504
- def b
505
- @b
506
- end
507
- # Sets the blue component of the colour in the normal 0 .. 255 range.
508
- def blue=(bb)
509
- @b = Color.normalize(bb / 255.0)
510
- end
511
- # Sets the blue component of the colour as a percentage.
512
- def blue_p=(bb)
513
- @b = Color.normalize(bb / 100.0)
514
- end
515
- # Sets the blue component of the colour as a fraction in the range 0.0 ..
516
- # 1.0.
517
- def b=(bb)
518
- @b = Color.normalize(bb)
519
- end
415
+ ##
416
+ def green_p = normalize(g * 100.0, 0.0..100.0) # :nodoc:
520
417
 
521
- # Adds another colour to the current colour. The other colour will be
522
- # converted to RGB before addition. This conversion depends upon a #to_rgb
523
- # method on the other colour.
524
- #
525
- # The addition is done using the RGB Accessor methods to ensure a valid
526
- # colour in the result.
527
- def +(other)
528
- self.class.from_fraction(r + other.r, g + other.g, b + other.b)
418
+ ##
419
+ def blue = normalize(b * 255.0, 0.0..255.0) # :nodoc:
420
+
421
+ ##
422
+ def blue_p = normalize(b * 100.0, 0.0..100.0) # :nodoc:
423
+
424
+ ##
425
+ # Return a Grayscale color object created from the largest of the `r`, `g`, and `b`
426
+ # values.
427
+ def max_rgb_as_grayscale = Color::Grayscale.from_fraction([r, g, b].max)
428
+
429
+ ##
430
+ def inspect = "RGB [#{html}]" # :nodoc:
431
+
432
+ ##
433
+ def pretty_print(q) # :nodoc:
434
+ q.text "RGB"
435
+ q.breakable
436
+ q.group 2, "[", "]" do
437
+ q.text html
438
+ end
529
439
  end
530
440
 
531
- # Subtracts another colour to the current colour. The other colour will be
532
- # converted to RGB before subtraction. This conversion depends upon a
533
- # #to_rgb method on the other colour.
441
+ ##
442
+ def to_a = [red, green, blue] # :nodoc:
443
+
444
+ ##
445
+ alias_method :deconstruct, :to_a # :nodoc:
446
+
447
+ ##
448
+ def deconstruct_keys(_keys) = {r:, g:, b:, red:, green:, blue:} # :nodoc:
449
+
450
+ ##
451
+ def to_internal = [r, g, b] # :nodoc:
452
+
453
+ ##
454
+ # Outputs how much contrast this color has with another RGB color.
455
+ #
456
+ # The `delta_e94` algorithm uses ΔE*94 for contrast calculations and the `delta_e2000`
457
+ # algorithm uses ΔE*2000.
534
458
  #
535
- # The subtraction is done using the RGB Accessor methods to ensure a valid
536
- # colour in the result.
537
- def -(other)
538
- self + (-other)
459
+ # The `naive` algorithm treats the foreground and background colors as the same.
460
+ # Any result over about 0.22 should have a high likelihood of being legible, but the
461
+ # larger the difference, the more contrast. Otherwise, to be safe go with something
462
+ # > 0.3.
463
+ #
464
+ # :call-seq:
465
+ # contrast(other, algorithm: :naive)
466
+ # contrast(other, algorithm: :delta_e94)
467
+ # contrast(other, algorithm: :delta_e2000)
468
+ def contrast(other, *args, **kwargs)
469
+ other = coerce(other)
470
+
471
+ algorithm = kwargs[:algorithm] || args.first || :naive
472
+
473
+ case algorithm
474
+ when :delta_e94
475
+ delta_e94(other)
476
+ when :delta_e2000
477
+ delta_e2000(other)
478
+ when :naive
479
+ # The following numbers have been set with some care.
480
+ ((diff_brightness(other) * 0.65) +
481
+ (diff_hue(other) * 0.20) +
482
+ (diff_luminosity(other) * 0.15))
483
+ else
484
+ raise ARgumentError, "Unknown algorithm #{algorithm.inspect}"
485
+ end
539
486
  end
540
487
 
541
- # Retrieve the maxmum RGB value from the current colour as a GrayScale
542
- # colour
543
- def max_rgb_as_grayscale
544
- Color::GrayScale.from_fraction([@r, @g, @b].max)
488
+ private
489
+
490
+ ##
491
+ def percent_adjustment(percent) # :nodoc:
492
+ percent /= 100.0
493
+ percent += 1.0
494
+ percent = [percent, 2.0].min
495
+ [0.0, percent].max
545
496
  end
546
- alias max_rgb_as_greyscale max_rgb_as_grayscale
547
497
 
548
- def inspect
549
- "RGB [#{html}]"
498
+ ##
499
+ # Provides the luminosity difference between two rbg vals
500
+ def diff_luminosity(other) # :nodoc:
501
+ l1 = (0.2126 * other.r**2.2) +
502
+ (0.7152 * other.b**2.2) +
503
+ (0.0722 * other.g**2.2)
504
+
505
+ l2 = (0.2126 * r**2.2) +
506
+ (0.7152 * b**2.2) +
507
+ (0.0722 * g**2.2)
508
+
509
+ (([l1, l2].max + 0.05) / ([l1, l2].min + 0.05) - 1) / 20.0
550
510
  end
551
511
 
552
- def to_a
553
- [ r, g, b ]
512
+ ##
513
+ # Provides the brightness difference.
514
+ def diff_brightness(other) # :nodoc:
515
+ br1 = (299 * other.r + 587 * other.g + 114 * other.b)
516
+ br2 = (299 * r + 587 * g + 114 * b)
517
+ (br1 - br2).abs / 1000.0
554
518
  end
555
519
 
556
- # Numerically negate the color. This results in a color that is only
557
- # usable for subtraction.
558
- def -@
559
- rgb = self.dup
560
- rgb.instance_variable_set(:@r, -rgb.r)
561
- rgb.instance_variable_set(:@g, -rgb.g)
562
- rgb.instance_variable_set(:@b, -rgb.b)
563
- rgb
520
+ ##
521
+ # Provides the euclidean distance between the two color values
522
+ def diff_euclidean(other)
523
+ ((((other.r - r)**2) +
524
+ ((other.g - g)**2) +
525
+ ((other.b - b)**2))**0.5) / 1.7320508075688772
564
526
  end
565
527
 
566
- private
567
- def normalize_percent(percent)
568
- percent /= 100.0
569
- percent += 1.0
570
- percent = [ percent, 2.0 ].min
571
- percent = [ 0.0, percent ].max
572
- percent
528
+ ##
529
+ # Difference in the two colors' hue
530
+ def diff_hue(other) # :nodoc:
531
+ ((r - other.r).abs +
532
+ (g - other.g).abs +
533
+ (b - other.b).abs) / 3
573
534
  end
574
535
  end
575
536
 
576
537
  class << Color::RGB
577
- # Creates an RGB colour object from percentages 0..100.
538
+ ##
539
+ # Creates a RGB color object from percentage values (0.0 .. 100.0).
578
540
  #
579
- # Color::RGB.from_percentage(10, 20, 30)
580
- def from_percentage(r = 0, g = 0, b = 0, &block)
581
- new(r, g, b, 100.0, &block)
541
+ # ```ruby
542
+ # Color::RGB.from_percentage(10, 20, 30)
543
+ # ```
544
+ def from_percentage(*args, **kwargs)
545
+ r, g, b, names =
546
+ case [args, kwargs]
547
+ in [[r, g, b], {}]
548
+ [r, g, b, nil]
549
+ in [[_, _, _, _], {}]
550
+ args
551
+ in [[], {r:, g:, b:}]
552
+ [r, g, b, nil]
553
+ in [[], {r:, g:, b:, names:}]
554
+ [r, g, b, names]
555
+ else
556
+ new(*args, **kwargs)
557
+ end
558
+
559
+ new(r: r / 100.0, g: g / 100.0, b: b / 100.0, names: names)
582
560
  end
583
561
 
584
- # Creates an RGB colour object from fractional values 0..1.
562
+ # Creates a RGB color object from the standard three byte range (0 .. 255).
585
563
  #
586
- # Color::RGB.from_fraction(.3, .2, .1)
587
- def from_fraction(r = 0.0, g = 0.0, b = 0.0, &block)
588
- new(r, g, b, 1.0, &block)
589
- end
564
+ # ```ruby
565
+ # Color::RGB.from_values(32, 64, 128)
566
+ # Color::RGB.from_values(0x20, 0x40, 0x80)
567
+ # ```
568
+ def from_values(*args, **kwargs)
569
+ r, g, b, names =
570
+ case [args, kwargs]
571
+ in [[r, g, b], {}]
572
+ [r, g, b, nil]
573
+ in [[_, _, _, _], {}]
574
+ args
575
+ in [[], {r:, g:, b:}]
576
+ [r, g, b, nil]
577
+ in [[], {r:, g:, b:, names:}]
578
+ [r, g, b, names]
579
+ else
580
+ new(*args, **kwargs)
581
+ end
590
582
 
591
- # Creates an RGB colour object from a grayscale fractional value 0..1.
592
- def from_grayscale_fraction(l = 0.0, &block)
593
- new(l, l, l, 1.0, &block)
583
+ new(r: r / 255.0, g: g / 255.0, b: b / 255.0, names: names)
594
584
  end
595
- alias_method :from_greyscale_fraction, :from_grayscale_fraction
596
-
597
- # Creates an RGB colour object from an HTML colour descriptor (e.g.,
598
- # <tt>"fed"</tt> or <tt>"#cabbed;"</tt>.
599
- #
600
- # Color::RGB.from_html("fed")
601
- # Color::RGB.from_html("#fed")
602
- # Color::RGB.from_html("#cabbed")
603
- # Color::RGB.from_html("cabbed")
604
- def from_html(html_colour, &block)
605
- # When we can move to 1.9+ only, this will be \h
606
- h = html_colour.scan(/[0-9a-f]/i)
607
- case h.size
585
+
586
+ ##
587
+ alias_method :from_fraction, :new
588
+
589
+ ##
590
+ alias_method :from_internal, :new # :nodoc:
591
+
592
+ ##
593
+ # Creates a RGB color object from an HTML color descriptor (e.g., `"fed"` or
594
+ # `"#cabbed;"`.
595
+ #
596
+ # ```ruby
597
+ # Color::RGB.from_html("fed")
598
+ # Color::RGB.from_html("#fed")
599
+ # Color::RGB.from_html("#cabbed")
600
+ # Color::RGB.from_html("cabbed")
601
+ # ```
602
+ def from_html(html_color)
603
+ h = html_color.scan(/\h/i)
604
+ r, g, b = case h.size
608
605
  when 3
609
- new(*h.map { |v| (v * 2).to_i(16) }, &block)
606
+ h.map { |v| (v * 2).to_i(16) }
610
607
  when 6
611
- new(*h.each_slice(2).map { |v| v.join.to_i(16) }, &block)
608
+ h.each_slice(2).map { |v| v.join.to_i(16) }
612
609
  else
613
- raise ArgumentError, "Not a supported HTML colour type."
610
+ raise ArgumentError, "Not a supported HTML color type."
614
611
  end
615
- end
616
612
 
617
- # Find or create a colour by an HTML hex code. This differs from the
618
- # #from_html method in that if the colour code matches a named colour,
619
- # the existing colour will be returned.
620
- #
621
- # Color::RGB.by_hex('ff0000').name # => 'red'
622
- # Color::RGB.by_hex('ff0001').name # => nil
623
- #
624
- # If a block is provided, the value that is returned by the block will
625
- # be returned instead of the exception caused by an error in providing a
626
- # correct hex format.
627
- def by_hex(hex, &block)
628
- __by_hex.fetch(html_hexify(hex)) { from_html(hex) }
629
- rescue
630
- if block
631
- block.call
632
- else
633
- raise
634
- end
613
+ from_values(r, g, b)
635
614
  end
636
615
 
637
- # Return a colour as identified by the colour name.
638
- def by_name(name, &block)
639
- __by_name.fetch(name.to_s.downcase, &block)
640
- end
616
+ ##
617
+ # Find or create a color by an HTML hex code. This differs from the #from_html method
618
+ # in that if the color code matches a named color, the existing color will be
619
+ # returned.
620
+ #
621
+ # ```ruby
622
+ # Color::RGB.by_hex('ff0000').name # => 'red'
623
+ # Color::RGB.by_hex('ff0001').name # => nil
624
+ # ```
625
+ #
626
+ # An exception will be raised if the value provided is not found or cannot be
627
+ # interpreted as a valid hex colour.
628
+ def by_hex(hex) = __by_hex.fetch(html_hexify(hex)) { from_html(hex) }
641
629
 
642
- # Return a colour as identified by the colour name, or by hex.
643
- def by_css(name_or_hex, &block)
644
- by_name(name_or_hex) { by_hex(name_or_hex, &block) }
645
- end
630
+ ##
631
+ # Return a color as identified by the color name.
632
+ def by_name(name, &block) = __by_name.fetch(name.to_s.downcase, &block)
633
+
634
+ ##
635
+ # Return a color as identified by the color name, or by hex.
636
+ def by_css(name_or_hex, &block) = by_name(name_or_hex) { by_hex(name_or_hex, &block) }
646
637
 
647
- # Extract named or hex colours from the provided text.
638
+ ##
639
+ # Extract named or hex colors from the provided text.
648
640
  def extract_colors(text, mode = :both)
649
- text = text.downcase
641
+ text = text.downcase
650
642
  regex = case mode
651
- when :name
652
- Regexp.union(__by_name.keys)
653
- when :hex
654
- Regexp.union(__by_hex.keys)
655
- when :both
656
- Regexp.union(__by_hex.keys + __by_name.keys)
657
- end
643
+ when :name
644
+ Regexp.union(__by_name.keys)
645
+ when :hex
646
+ Regexp.union(__by_hex.keys)
647
+ when :both
648
+ Regexp.union(__by_hex.keys + __by_name.keys)
649
+ end
658
650
 
659
651
  text.scan(regex).map { |match|
660
652
  case mode
@@ -667,43 +659,48 @@ class << Color::RGB
667
659
  end
668
660
  }
669
661
  end
670
- end
671
662
 
672
- class << Color::RGB
673
663
  private
674
- def __named_color(mod, rgb, *names)
675
- if names.any? { |n| mod.const_defined? n }
676
- raise ArgumentError, "#{names.join(', ')} already defined in #{mod}"
664
+
665
+ ##
666
+ def __create_named_color(mod, rgb, *names) # :nodoc:
667
+ used = names - mod.constants.map(&:to_sym)
668
+
669
+ if used.length < names.length
670
+ raise ArgumentError, "#{names.join(", ")} already defined in #{mod}"
677
671
  end
678
672
 
679
- names.each { |n| mod.const_set(n, rgb) }
673
+ rgb = rgb.with(names: Array(names).flatten.compact.map { _1.to_s.downcase }.sort.uniq)
674
+
675
+ names.each { mod.const_set(_1, rgb) }
680
676
 
681
- rgb.names = names
682
- rgb.names.each { |n| __by_name[n] = rgb }
677
+ rgb.names.each { __by_name[_1] = __by_name[_1.to_s] = rgb }
678
+ lower = rgb.name.downcase
679
+
680
+ __by_name[lower] = __by_name[lower.to_s] = rgb
683
681
  __by_hex[rgb.hex] = rgb
684
- rgb.freeze
685
682
  end
686
683
 
687
- def __by_hex
684
+ ##
685
+ def __by_hex # :nodoc:
688
686
  @__by_hex ||= {}
689
687
  end
690
688
 
691
- def __by_name
689
+ ##
690
+ def __by_name # :nodoc:
692
691
  @__by_name ||= {}
693
692
  end
694
693
 
695
- def html_hexify(hex)
696
- # When we can move to 1.9+ only, this will be \h
697
- h = hex.to_s.downcase.scan(/[0-9a-f]/)
694
+ ##
695
+ def html_hexify(hex) # :nodoc:
696
+ h = hex.to_s.downcase.scan(/\h/)
698
697
  case h.size
699
698
  when 3
700
699
  h.map { |v| (v * 2) }.join
701
700
  when 6
702
701
  h.join
703
702
  else
704
- raise ArgumentError, "Not a supported HTML colour type."
703
+ raise ArgumentError, "Not a supported HTML color type."
705
704
  end
706
705
  end
707
706
  end
708
-
709
- require 'color/rgb/colors'