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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +301 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +70 -0
- data/CONTRIBUTORS.md +10 -0
- data/LICENCE.md +27 -0
- data/Manifest.txt +11 -23
- data/README.md +54 -0
- data/Rakefile +74 -61
- data/SECURITY.md +34 -0
- data/lib/color/cielab.rb +348 -0
- data/lib/color/cmyk.rb +279 -213
- data/lib/color/grayscale.rb +128 -160
- data/lib/color/hsl.rb +205 -173
- data/lib/color/rgb/colors.rb +177 -163
- data/lib/color/rgb.rb +536 -541
- data/lib/color/version.rb +5 -0
- data/lib/color/xyz.rb +214 -0
- data/lib/color/yiq.rb +91 -46
- data/lib/color.rb +208 -142
- data/test/fixtures/cielab.json +444 -0
- data/test/minitest_helper.rb +20 -4
- data/test/test_cmyk.rb +49 -72
- data/test/test_color.rb +58 -112
- data/test/test_grayscale.rb +35 -57
- data/test/test_hsl.rb +71 -77
- data/test/test_rgb.rb +195 -267
- data/test/test_yiq.rb +12 -30
- metadata +90 -107
- data/.autotest +0 -5
- data/.coveralls.yml +0 -2
- data/.gemtest +0 -0
- data/.hoerc +0 -2
- data/.minitest.rb +0 -2
- data/.travis.yml +0 -41
- data/Code-of-Conduct.rdoc +0 -41
- data/Contributing.rdoc +0 -62
- data/Gemfile +0 -9
- data/History.rdoc +0 -194
- data/Licence.rdoc +0 -27
- data/README.rdoc +0 -52
- data/lib/color/css.rb +0 -7
- data/lib/color/palette/adobecolor.rb +0 -260
- data/lib/color/palette/gimp.rb +0 -104
- data/lib/color/palette/monocontrast.rb +0 -164
- data/lib/color/palette.rb +0 -4
- data/lib/color/rgb/contrast.rb +0 -57
- data/lib/color/rgb/metallic.rb +0 -28
- data/test/test_adobecolor.rb +0 -405
- data/test/test_css.rb +0 -19
- data/test/test_gimp.rb +0 -87
- data/test/test_monocontrast.rb +0 -130
data/lib/color/rgb.rb
CHANGED
@@ -1,665 +1,655 @@
|
|
1
|
-
#
|
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
|
-
|
6
|
-
#
|
7
|
-
#
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
22
|
+
##
|
23
|
+
# :attr_reader: red
|
24
|
+
# Returns the red component of the color in the normal 0..255 range.
|
14
25
|
|
15
|
-
|
26
|
+
##
|
27
|
+
# :attr_reader: red_p
|
28
|
+
# Returns the red component of the color as a percentage (0.0 .. 100.0).
|
16
29
|
#
|
17
|
-
|
18
|
-
#
|
19
|
-
|
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
|
-
|
25
|
-
#
|
26
|
-
|
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
|
-
|
31
|
-
#
|
32
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
42
|
-
|
46
|
+
##
|
47
|
+
# :attr_reader: blue
|
48
|
+
# Returns the blue component of the color in the normal 0 .. 255 range.
|
43
49
|
|
44
|
-
|
45
|
-
|
50
|
+
##
|
51
|
+
# :attr_reader: blue_p
|
52
|
+
# Returns the blue component of the color as a percentage (0.0 .. 100.0).
|
46
53
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
#
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
#
|
57
|
-
#
|
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
|
-
|
63
|
-
#
|
64
|
-
#
|
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
|
-
|
75
|
-
|
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
|
-
|
82
|
-
#
|
83
|
-
|
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
|
-
|
89
|
-
#
|
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
|
-
#
|
94
|
-
#
|
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
|
-
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
115
|
+
k -= (k * brightness)
|
121
116
|
|
122
|
-
c =
|
123
|
-
m =
|
124
|
-
y =
|
125
|
-
k =
|
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
|
-
|
131
|
-
|
132
|
-
end
|
125
|
+
##
|
126
|
+
def to_rgb(...) = self
|
133
127
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
143
|
-
#
|
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
|
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
|
-
|
150
|
+
l = (max + min) / 2.0
|
151
151
|
|
152
|
-
if
|
153
|
-
|
154
|
-
|
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
|
157
|
-
|
156
|
+
s = if near_zero_or_less?(l - 0.5)
|
157
|
+
delta / (max + min).to_f
|
158
158
|
else
|
159
|
-
|
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
|
167
|
-
|
168
|
-
|
169
|
-
elsif
|
170
|
-
|
171
|
-
elsif
|
172
|
-
|
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
|
-
|
176
|
-
|
175
|
+
h += 1 if h < 0
|
176
|
+
h -= 1 if h > 1
|
177
177
|
end
|
178
|
-
|
178
|
+
|
179
|
+
Color::HSL.from_fraction(h, s, l)
|
179
180
|
end
|
180
181
|
|
181
|
-
|
182
|
-
#
|
183
|
-
#
|
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
|
-
#
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
211
|
-
#
|
212
|
-
|
213
|
-
|
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
|
-
|
254
|
-
#
|
255
|
-
#
|
256
|
-
|
257
|
-
|
258
|
-
|
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
|
-
|
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
|
-
|
268
|
-
#
|
269
|
-
def
|
270
|
-
|
271
|
-
|
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
|
-
|
274
|
-
|
275
|
-
|
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
|
-
|
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
|
-
|
281
|
-
#
|
282
|
-
# brightness.
|
292
|
+
##
|
293
|
+
# Returns the brightness value for a color, a number between 0..1.
|
283
294
|
#
|
284
|
-
#
|
285
|
-
#
|
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
|
-
|
296
|
-
#
|
297
|
-
# percentages will
|
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
|
-
#
|
300
|
-
#
|
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
|
-
|
303
|
-
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
|
-
|
309
|
-
#
|
310
|
-
# percentages will
|
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
|
-
#
|
313
|
-
#
|
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
|
-
|
316
|
-
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
|
-
|
322
|
-
#
|
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
|
-
#
|
326
|
-
#
|
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
|
-
|
329
|
-
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
|
-
|
335
|
-
#
|
336
|
-
|
337
|
-
#
|
338
|
-
#
|
339
|
-
#
|
340
|
-
#
|
341
|
-
#
|
342
|
-
#
|
343
|
-
#
|
344
|
-
# an arbitrarily large number. The values
|
345
|
-
#
|
346
|
-
#
|
347
|
-
|
348
|
-
|
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
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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 =
|
363
|
-
if
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
402
|
-
|
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
|
-
|
446
|
-
def
|
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
|
-
|
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
|
-
|
500
|
-
def
|
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
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
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
|
-
|
537
|
-
|
538
|
-
|
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
|
-
#
|
541
|
-
#
|
542
|
-
|
543
|
-
|
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
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
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
|
-
|
554
|
-
|
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
|
-
|
558
|
-
|
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
|
-
|
562
|
-
#
|
563
|
-
def
|
564
|
-
|
565
|
-
|
566
|
-
|
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
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
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
|
-
|
538
|
+
##
|
539
|
+
# Creates a RGB color object from percentage values (0.0 .. 100.0).
|
583
540
|
#
|
584
|
-
#
|
585
|
-
|
586
|
-
|
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
|
562
|
+
# Creates a RGB color object from the standard three byte range (0 .. 255).
|
590
563
|
#
|
591
|
-
#
|
592
|
-
|
593
|
-
|
594
|
-
|
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
|
-
|
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
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
#
|
606
|
-
|
607
|
-
|
608
|
-
#
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
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
|
-
|
606
|
+
h.map { |v| (v * 2).to_i(16) }
|
615
607
|
when 6
|
616
|
-
|
608
|
+
h.each_slice(2).map { |v| v.join.to_i(16) }
|
617
609
|
else
|
618
|
-
raise ArgumentError, "Not a supported HTML
|
610
|
+
raise ArgumentError, "Not a supported HTML color type."
|
619
611
|
end
|
620
|
-
end
|
621
612
|
|
622
|
-
|
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
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
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
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
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
|
-
|
638
|
+
##
|
639
|
+
# Extract named or hex colors from the provided text.
|
653
640
|
def extract_colors(text, mode = :both)
|
654
|
-
|
641
|
+
require "color/rgb/colors"
|
642
|
+
text = text.downcase
|
655
643
|
regex = case mode
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
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
|
-
|
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(
|
673
|
+
raise ArgumentError, "#{names.join(", ")} already defined in #{mod}"
|
684
674
|
end
|
685
675
|
|
686
|
-
names.
|
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 =
|
689
|
-
|
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
|
-
|
687
|
+
##
|
688
|
+
def __by_hex # :nodoc:
|
689
|
+
require "color/rgb/colors"
|
695
690
|
@__by_hex ||= {}
|
696
691
|
end
|
697
692
|
|
698
|
-
|
693
|
+
##
|
694
|
+
def __by_name # :nodoc:
|
695
|
+
require "color/rgb/colors"
|
699
696
|
@__by_name ||= {}
|
700
697
|
end
|
701
698
|
|
702
|
-
|
703
|
-
|
704
|
-
h = hex.to_s.downcase.scan(/
|
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
|
708
|
+
raise ArgumentError, "Not a supported HTML color type."
|
712
709
|
end
|
713
710
|
end
|
714
711
|
end
|
715
|
-
|
716
|
-
require 'color/rgb/colors'
|