color_contrast_calc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +9 -0
- data/.yardopts +4 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.ja.md +296 -0
- data/README.md +295 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/color_contrast_calc.gemspec +30 -0
- data/examples/color_instance.rb +12 -0
- data/examples/color_lists.rb +26 -0
- data/examples/grayscale.rb +10 -0
- data/examples/sort_colors.rb +30 -0
- data/examples/yellow_black_contrast.rb +12 -0
- data/examples/yellow_orange_contrast.rb +32 -0
- data/exe/color_contrast_calc +4 -0
- data/lib/color_contrast_calc.rb +108 -0
- data/lib/color_contrast_calc/checker.rb +126 -0
- data/lib/color_contrast_calc/color.rb +409 -0
- data/lib/color_contrast_calc/converter.rb +191 -0
- data/lib/color_contrast_calc/data/color_keywords.json +149 -0
- data/lib/color_contrast_calc/shim.rb +32 -0
- data/lib/color_contrast_calc/sorter.rb +227 -0
- data/lib/color_contrast_calc/threshold_finder.rb +276 -0
- data/lib/color_contrast_calc/utils.rb +240 -0
- data/lib/color_contrast_calc/version.rb +5 -0
- metadata +146 -0
@@ -0,0 +1,276 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'color_contrast_calc/color'
|
4
|
+
|
5
|
+
module ColorContrastCalc
|
6
|
+
##
|
7
|
+
# Collection of modules that implement the main logic of
|
8
|
+
# instance methods of Color, +Color#find_*_threshold()+.
|
9
|
+
|
10
|
+
module ThresholdFinder
|
11
|
+
# @private
|
12
|
+
|
13
|
+
module Criteria
|
14
|
+
class SearchDirection
|
15
|
+
attr_reader :level, :target_ratio
|
16
|
+
|
17
|
+
def initialize(level)
|
18
|
+
@level = level
|
19
|
+
@target_ratio = Checker.level_to_ratio(level)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ToDarkerSide < SearchDirection
|
24
|
+
# @private
|
25
|
+
|
26
|
+
def round(r)
|
27
|
+
(r * 10).floor / 10.0
|
28
|
+
end
|
29
|
+
|
30
|
+
# @private
|
31
|
+
|
32
|
+
def increment_condition(contrast_ratio)
|
33
|
+
contrast_ratio > @target_ratio
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class ToBrighterSide < SearchDirection
|
38
|
+
# @private
|
39
|
+
|
40
|
+
def round(r)
|
41
|
+
(r * 10).ceil / 10.0
|
42
|
+
end
|
43
|
+
|
44
|
+
# @private
|
45
|
+
|
46
|
+
def increment_condition(contrast_ratio)
|
47
|
+
@target_ratio > contrast_ratio
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @private
|
53
|
+
|
54
|
+
def self.threshold_criteria(level, fixed_color, other_color)
|
55
|
+
if should_scan_darker_side?(fixed_color, other_color)
|
56
|
+
return Criteria::ToDarkerSide.new(level)
|
57
|
+
end
|
58
|
+
|
59
|
+
Criteria::ToBrighterSide.new(level)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @private
|
63
|
+
|
64
|
+
def self.should_scan_darker_side?(fixed_color, other_color)
|
65
|
+
fixed_color.higher_luminance_than?(other_color) ||
|
66
|
+
fixed_color.same_luminance_as?(other_color) && fixed_color.light_color?
|
67
|
+
end
|
68
|
+
|
69
|
+
# @private
|
70
|
+
|
71
|
+
def self.binary_search_width(init_width, min)
|
72
|
+
i = 1
|
73
|
+
init_width = init_width.to_f
|
74
|
+
d = init_width / 2**i
|
75
|
+
|
76
|
+
while d > min
|
77
|
+
yield d
|
78
|
+
i += 1
|
79
|
+
d = init_width / 2**i
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Module that implements the main logic of the instance method
|
85
|
+
# +Color#find_brightness_threshold()+.
|
86
|
+
|
87
|
+
module Brightness
|
88
|
+
##
|
89
|
+
# Try to find a color who has a satisfying contrast ratio.
|
90
|
+
#
|
91
|
+
# The color returned by this method will be created by changing the
|
92
|
+
# brightness of +other_color+. Even when a color that satisfies the
|
93
|
+
# specified level is not found, the method returns a new color anyway.
|
94
|
+
# @param fixed_color [Color] The color which remains unchanged
|
95
|
+
# @param other_color [Color] Color before the adjustment of brightness
|
96
|
+
# @param level [String] "A", "AA" or "AAA"
|
97
|
+
# @return [Color] New color whose brightness is adjusted from that of
|
98
|
+
# +other_color+
|
99
|
+
|
100
|
+
def self.find(fixed_color, other_color, level = Checker::Level::AA)
|
101
|
+
criteria = ThresholdFinder.threshold_criteria(level,
|
102
|
+
fixed_color, other_color)
|
103
|
+
w = calc_upper_ratio_limit(other_color) / 2.0
|
104
|
+
|
105
|
+
upper_color = upper_limit_color(fixed_color, other_color, w * 2, level)
|
106
|
+
return upper_color if upper_color
|
107
|
+
|
108
|
+
r, sufficient_r = calc_brightness_ratio(fixed_color.relative_luminance,
|
109
|
+
other_color.rgb, criteria, w)
|
110
|
+
|
111
|
+
generate_satisfying_color(fixed_color, other_color, criteria,
|
112
|
+
r, sufficient_r)
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.upper_limit_color(fixed_color, other_color, max_ratio, level)
|
116
|
+
limit_color = other_color.new_brightness_color(max_ratio)
|
117
|
+
|
118
|
+
if exceed_upper_limit?(fixed_color, other_color, limit_color, level)
|
119
|
+
limit_color
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private_class_method :upper_limit_color
|
124
|
+
|
125
|
+
def self.exceed_upper_limit?(fixed_color, other_color, limit_color, level)
|
126
|
+
other_color.higher_luminance_than?(fixed_color) &&
|
127
|
+
!limit_color.sufficient_contrast?(fixed_color, level)
|
128
|
+
end
|
129
|
+
|
130
|
+
private_class_method :exceed_upper_limit?
|
131
|
+
|
132
|
+
def self.calc_brightness_ratio(fixed_luminance, other_rgb, criteria, w)
|
133
|
+
target_ratio = criteria.target_ratio
|
134
|
+
r = w
|
135
|
+
sufficient_r = nil
|
136
|
+
|
137
|
+
ThresholdFinder.binary_search_width(w, 0.01) do |d|
|
138
|
+
contrast_ratio = calc_contrast_ratio(fixed_luminance, other_rgb, r)
|
139
|
+
|
140
|
+
sufficient_r = r if contrast_ratio >= target_ratio
|
141
|
+
break if contrast_ratio == target_ratio
|
142
|
+
|
143
|
+
r += criteria.increment_condition(contrast_ratio) ? d : -d
|
144
|
+
end
|
145
|
+
|
146
|
+
[r, sufficient_r]
|
147
|
+
end
|
148
|
+
|
149
|
+
private_class_method :calc_brightness_ratio
|
150
|
+
|
151
|
+
def self.generate_satisfying_color(fixed_color, other_color, criteria,
|
152
|
+
r, sufficient_r)
|
153
|
+
level = criteria.level
|
154
|
+
nearest = other_color.new_brightness_color(criteria.round(r))
|
155
|
+
|
156
|
+
if sufficient_r && !nearest.sufficient_contrast?(fixed_color, level)
|
157
|
+
return other_color.new_brightness_color(criteria.round(sufficient_r))
|
158
|
+
end
|
159
|
+
|
160
|
+
nearest
|
161
|
+
end
|
162
|
+
|
163
|
+
private_class_method :generate_satisfying_color
|
164
|
+
|
165
|
+
def self.calc_contrast_ratio(fixed_luminance, other_rgb, r)
|
166
|
+
new_rgb = Converter::Brightness.calc_rgb(other_rgb, r)
|
167
|
+
new_luminance = Checker.relative_luminance(new_rgb)
|
168
|
+
Checker.luminance_to_contrast_ratio(fixed_luminance, new_luminance)
|
169
|
+
end
|
170
|
+
|
171
|
+
private_class_method :calc_contrast_ratio
|
172
|
+
|
173
|
+
# @private
|
174
|
+
|
175
|
+
def self.calc_upper_ratio_limit(color)
|
176
|
+
return 100 if color.same_color?(Color::BLACK)
|
177
|
+
darkest = color.rgb.reject(&:zero?).min
|
178
|
+
((255.0 / darkest) * 100).ceil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Module that implements the main logic of the instance method
|
184
|
+
# +Color#find_lightness_threshold()+.
|
185
|
+
|
186
|
+
module Lightness
|
187
|
+
##
|
188
|
+
# Try to find a color who has a satisfying contrast ratio.
|
189
|
+
#
|
190
|
+
# The color returned by this method will be created by changing the
|
191
|
+
# lightness of +other_color+. Even when a color that satisfies the
|
192
|
+
# specified level is not found, the method returns a new color anyway.
|
193
|
+
# @param fixed_color [Color] The color which remains unchanged
|
194
|
+
# @param other_color [Color] Color before the adjustment of lightness
|
195
|
+
# @param level [String] "A", "AA" or "AAA"
|
196
|
+
# @return [Color] New color whose lightness is adjusted from that of
|
197
|
+
# +other_color+
|
198
|
+
|
199
|
+
def self.find(fixed_color, other_color, level = Checker::Level::AA)
|
200
|
+
criteria = ThresholdFinder.threshold_criteria(level,
|
201
|
+
fixed_color, other_color)
|
202
|
+
init_l = other_color.hsl[2]
|
203
|
+
max, min = determine_minmax(fixed_color, other_color, init_l)
|
204
|
+
|
205
|
+
boundary_color = lightness_boundary_color(fixed_color, max, min, level)
|
206
|
+
return boundary_color if boundary_color
|
207
|
+
|
208
|
+
l, sufficient_l = calc_lightness_ratio(fixed_color, other_color.hsl,
|
209
|
+
criteria, max, min)
|
210
|
+
|
211
|
+
generate_satisfying_color(fixed_color, other_color.hsl, criteria,
|
212
|
+
l, sufficient_l)
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.determine_minmax(fixed_color, other_color, init_l)
|
216
|
+
scan_darker_side = ThresholdFinder.should_scan_darker_side?(fixed_color,
|
217
|
+
other_color)
|
218
|
+
scan_darker_side ? [init_l, 0] : [100, init_l] # [max, min]
|
219
|
+
end
|
220
|
+
|
221
|
+
private_class_method :determine_minmax
|
222
|
+
|
223
|
+
def self.lightness_boundary_color(color, max, min, level)
|
224
|
+
if min.zero? && !color.sufficient_contrast?(Color::BLACK, level)
|
225
|
+
return Color::BLACK
|
226
|
+
end
|
227
|
+
|
228
|
+
if max == 100 && !color.sufficient_contrast?(Color::WHITE, level)
|
229
|
+
return Color::WHITE
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private_class_method :lightness_boundary_color
|
234
|
+
|
235
|
+
def self.calc_lightness_ratio(fixed_color, other_hsl, criteria, max, min)
|
236
|
+
h, s, = other_hsl
|
237
|
+
l = (max + min) / 2.0
|
238
|
+
sufficient_l = nil
|
239
|
+
|
240
|
+
ThresholdFinder.binary_search_width(max - min, 0.01) do |d|
|
241
|
+
contrast_ratio = calc_contrast_ratio(fixed_color, [h, s, l])
|
242
|
+
|
243
|
+
sufficient_l = l if contrast_ratio >= criteria.target_ratio
|
244
|
+
break if contrast_ratio == criteria.target_ratio
|
245
|
+
|
246
|
+
l += criteria.increment_condition(contrast_ratio) ? d : -d
|
247
|
+
end
|
248
|
+
|
249
|
+
[l, sufficient_l]
|
250
|
+
end
|
251
|
+
|
252
|
+
private_class_method :calc_lightness_ratio
|
253
|
+
|
254
|
+
def self.calc_contrast_ratio(fixed_color, hsl)
|
255
|
+
fixed_color.contrast_ratio_against(Utils.hsl_to_rgb(hsl))
|
256
|
+
end
|
257
|
+
|
258
|
+
private_class_method :calc_contrast_ratio
|
259
|
+
|
260
|
+
def self.generate_satisfying_color(fixed_color, other_hsl, criteria,
|
261
|
+
l, sufficient_l)
|
262
|
+
h, s, = other_hsl
|
263
|
+
level = criteria.level
|
264
|
+
nearest = Color.new_from_hsl([h, s, l])
|
265
|
+
|
266
|
+
if sufficient_l && !nearest.sufficient_contrast?(fixed_color, level)
|
267
|
+
return Color.new_from_hsl([h, s, sufficient_l])
|
268
|
+
end
|
269
|
+
|
270
|
+
nearest
|
271
|
+
end
|
272
|
+
|
273
|
+
private_class_method :generate_satisfying_color
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'color_contrast_calc/shim'
|
4
|
+
|
5
|
+
module ColorContrastCalc
|
6
|
+
##
|
7
|
+
# Utility functions that provide basic operations on colors.
|
8
|
+
#
|
9
|
+
# This module provides basic operations on colors given as RGB values
|
10
|
+
# (including their hex code presentations) or HSL values.
|
11
|
+
|
12
|
+
module Utils
|
13
|
+
using Shim unless //.respond_to? :match?
|
14
|
+
|
15
|
+
HSL_UPPER_LIMIT = [360, 100, 100].freeze
|
16
|
+
|
17
|
+
private_constant :HSL_UPPER_LIMIT
|
18
|
+
|
19
|
+
HEX_RE = /\A#?[0-9a-f]{3}([0-9a-f]{3})?\z/i
|
20
|
+
|
21
|
+
private_constant :HEX_RE
|
22
|
+
|
23
|
+
##
|
24
|
+
# Convert a hex color code string to a RGB value.
|
25
|
+
#
|
26
|
+
# @param hex_code [String] Hex color code such as "#ffff00"
|
27
|
+
# @return [Array<Integer>] RGB value represented as an array of integers
|
28
|
+
|
29
|
+
def self.hex_to_rgb(hex_code)
|
30
|
+
hex_part = hex_code.start_with?('#') ? hex_code[1..-1] : hex_code
|
31
|
+
|
32
|
+
case hex_part.length
|
33
|
+
when 3
|
34
|
+
hex_part.chars.map {|c| c.hex * 17 }
|
35
|
+
when 6
|
36
|
+
[0, 2, 4].map {|i| hex_part[i, 2].hex }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Normalize a hex color code to a 6 digits, lowercased one.
|
42
|
+
#
|
43
|
+
# @param code [String] Hex color code such as "#ffff00", "#ff0" or "FFFF00"
|
44
|
+
# @param prefix [true, false] If set to False, "#" at the head of result is
|
45
|
+
# removed
|
46
|
+
# @return [String] 6-digit hexadecimal string in lowercase, with/without
|
47
|
+
# leading "#" depending on the value of +prefix+
|
48
|
+
|
49
|
+
def self.normalize_hex(code, prefix = true)
|
50
|
+
if code.length < 6
|
51
|
+
hex_part = code.start_with?('#') ? code[1..-1] : code
|
52
|
+
code = hex_part.chars.map {|c| c * 2 }.join
|
53
|
+
end
|
54
|
+
|
55
|
+
lowered = code.downcase
|
56
|
+
return lowered if prefix == lowered.start_with?('#')
|
57
|
+
prefix ? "##{lowered}" : lowered[1..-1]
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Convert a RGB value to a hex color code.
|
62
|
+
#
|
63
|
+
# @param rgb [Array<Integer>] RGB value represented as an array of integers
|
64
|
+
# @return [String] Hex color code such as "#ffff00"
|
65
|
+
|
66
|
+
def self.rgb_to_hex(rgb)
|
67
|
+
format('#%02x%02x%02x', *rgb)
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Convert HSL value to RGB value.
|
72
|
+
#
|
73
|
+
# @param hsl [Array<Float>] HSL value represented as an array of numbers
|
74
|
+
# @return [Array<Integer>] RGB value represented as an array of integers
|
75
|
+
|
76
|
+
def self.hsl_to_rgb(hsl)
|
77
|
+
# https://www.w3.org/TR/css3-color/#hsl-color
|
78
|
+
h = hsl[0] / 360.0
|
79
|
+
s = hsl[1] / 100.0
|
80
|
+
l = hsl[2] / 100.0
|
81
|
+
m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s
|
82
|
+
m1 = l * 2 - m2
|
83
|
+
[h + 1 / 3.0, h, h - 1 / 3.0].map do |adjusted_h|
|
84
|
+
(hue_to_rgb(m1, m2, adjusted_h) * 255).round
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @private
|
89
|
+
|
90
|
+
def self.hue_to_rgb(m1, m2, h)
|
91
|
+
h += 1 if h < 0
|
92
|
+
h -= 1 if h > 1
|
93
|
+
return m1 + (m2 - m1) * h * 6 if h * 6 < 1
|
94
|
+
return m2 if h * 2 < 1
|
95
|
+
return m1 + (m2 - m1) * (2 / 3.0 - h) * 6 if h * 3 < 2
|
96
|
+
m1
|
97
|
+
end
|
98
|
+
|
99
|
+
private_class_method :hue_to_rgb
|
100
|
+
|
101
|
+
##
|
102
|
+
# Convert HSL value to hex color code.
|
103
|
+
#
|
104
|
+
# @param hsl [Array<Float>] HSL value represented as an array of numbers
|
105
|
+
# @return [String] Hex color code such as "#ffff00"
|
106
|
+
|
107
|
+
def self.hsl_to_hex(hsl)
|
108
|
+
rgb_to_hex(hsl_to_rgb(hsl))
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Convert RGB value to HSL value.
|
113
|
+
#
|
114
|
+
# @param rgb [Array<Integer>] RGB value represented as an array of integers
|
115
|
+
# @return [Array<Float>] HSL value represented as an array of numbers
|
116
|
+
|
117
|
+
def self.rgb_to_hsl(rgb)
|
118
|
+
[rgb_to_hue(rgb), rgb_to_saturation(rgb), rgb_to_lightness(rgb)]
|
119
|
+
end
|
120
|
+
|
121
|
+
# @private
|
122
|
+
|
123
|
+
def self.rgb_to_lightness(rgb)
|
124
|
+
(rgb.max + rgb.min) * 100 / 510.0
|
125
|
+
end
|
126
|
+
|
127
|
+
private_class_method :rgb_to_lightness
|
128
|
+
|
129
|
+
# @private
|
130
|
+
|
131
|
+
def self.rgb_to_saturation(rgb)
|
132
|
+
l = rgb_to_lightness(rgb)
|
133
|
+
minmax_with_diff(rgb) do |min, max, d|
|
134
|
+
(l <= 50 ? d / (max + min) : d / (510 - max - min)) * 100
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private_class_method :rgb_to_saturation
|
139
|
+
|
140
|
+
# @private
|
141
|
+
|
142
|
+
def self.rgb_to_hue(rgb)
|
143
|
+
# References:
|
144
|
+
# Agoston, Max K. (2005).
|
145
|
+
# "Computer Graphics and Geometric Modeling: Implementation and Algorithms".
|
146
|
+
# London: Springer
|
147
|
+
#
|
148
|
+
# https://accessibility.kde.org/hsl-adjusted.php#hue
|
149
|
+
|
150
|
+
minmax_with_diff(rgb) do |_, _, d|
|
151
|
+
mi = rgb.each_with_index.max_by {|c| c[0] }[1] # max value index
|
152
|
+
h = mi * 120 + (rgb[(mi + 1) % 3] - rgb[(mi + 2) % 3]) * 60 / d
|
153
|
+
|
154
|
+
h < 0 ? h + 360 : h
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
private_class_method :rgb_to_hue
|
159
|
+
|
160
|
+
# @private
|
161
|
+
|
162
|
+
def self.minmax_with_diff(rgb)
|
163
|
+
min = rgb.min
|
164
|
+
max = rgb.max
|
165
|
+
return 0 if min == max
|
166
|
+
yield min, max, (max - min).to_f
|
167
|
+
end
|
168
|
+
|
169
|
+
private_class_method :minmax_with_diff
|
170
|
+
|
171
|
+
##
|
172
|
+
# Convert hex color code to HSL value.
|
173
|
+
#
|
174
|
+
# @param hex_code [String] Hex color code such as "#ffff00"
|
175
|
+
# @return [Array<Float>] HSL value represented as an array of numbers
|
176
|
+
|
177
|
+
def self.hex_to_hsl(hex_code)
|
178
|
+
rgb_to_hsl(hex_to_rgb(hex_code))
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Check if a given array is a valid representation of RGB color.
|
183
|
+
#
|
184
|
+
# @param rgb [Array<Integer>] RGB value represented as an array of integers
|
185
|
+
# @return [true, false] true if a valid RGB value is passed
|
186
|
+
|
187
|
+
def self.valid_rgb?(rgb)
|
188
|
+
rgb.length == 3 &&
|
189
|
+
rgb.all? {|c| c.is_a?(Integer) && c >= 0 && c <= 255 }
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Check if a given array is a valid representation of HSL color.
|
194
|
+
#
|
195
|
+
# @param hsl [Array<Float>] HSL value represented as an array of numbers
|
196
|
+
# @return [true, false] true if a valid HSL value is passed
|
197
|
+
|
198
|
+
def self.valid_hsl?(hsl)
|
199
|
+
return false unless hsl.length == 3
|
200
|
+
hsl.each_with_index do |c, i|
|
201
|
+
return false if !c.is_a?(Numeric) || c < 0 || c > HSL_UPPER_LIMIT[i]
|
202
|
+
end
|
203
|
+
true
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Check if a given string is a valid representation of RGB color.
|
208
|
+
#
|
209
|
+
# @param hex_code [String] RGB value in hex color code such as "#ffff00"
|
210
|
+
# @return [true, false] true if a vaild hex color code is passed
|
211
|
+
|
212
|
+
def self.valid_hex?(hex_code)
|
213
|
+
HEX_RE.match?(hex_code)
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Check if given two hex color codes represent a same color.
|
218
|
+
#
|
219
|
+
# @param hex1 [String] RGB value in hex color code such as "#ffff00",
|
220
|
+
# "#ffff00", "#FFFF00" or "#ff0"
|
221
|
+
# @param hex2 [String] RGB value in hex color code such as "#ffff00",
|
222
|
+
# "#ffff00", "#FFFF00" or "#ff0"
|
223
|
+
# @return [true, false] true if given two colors are same
|
224
|
+
|
225
|
+
def self.same_hex_color?(hex1, hex2)
|
226
|
+
normalize_hex(hex1) == normalize_hex(hex2)
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Check if a given string is consists of uppercase letters.
|
231
|
+
#
|
232
|
+
# @param str [String] string to be checked
|
233
|
+
# @return [true, false] true if letters in the passed string are all
|
234
|
+
# in uppercase.
|
235
|
+
|
236
|
+
def self.uppercase?(str)
|
237
|
+
!/[[:lower:]]/.match?(str)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|