color 1.8 → 2.0.0.pre.1

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