color_contrast_calc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|