color_contrast_calc 0.5.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +9 -1
- data/.travis.yml +13 -5
- data/README.ja.md +80 -11
- data/README.md +83 -11
- data/color_contrast_calc.gemspec +6 -6
- data/examples/color_instance.rb +2 -1
- data/examples/sort_colors.rb +17 -9
- data/lib/color_contrast_calc.rb +81 -5
- data/lib/color_contrast_calc/color.rb +115 -43
- data/lib/color_contrast_calc/color_function_parser.rb +649 -0
- data/lib/color_contrast_calc/converter.rb +52 -0
- data/lib/color_contrast_calc/data/color_keywords.json +1 -0
- data/lib/color_contrast_calc/deprecated.rb +1 -1
- data/lib/color_contrast_calc/invalid_color_representation_error.rb +44 -0
- data/lib/color_contrast_calc/sorter.rb +151 -107
- data/lib/color_contrast_calc/transparency_calc.rb +55 -0
- data/lib/color_contrast_calc/utils.rb +89 -5
- data/lib/color_contrast_calc/version.rb +1 -1
- metadata +17 -15
@@ -29,6 +29,58 @@ module ColorContrastCalc
|
|
29
29
|
vals.map {|val| val.round.clamp(0, 255) }
|
30
30
|
end
|
31
31
|
|
32
|
+
module AlphaCompositing
|
33
|
+
module Rgba
|
34
|
+
BLACK = [0, 0, 0, 1.0].freeze
|
35
|
+
WHITE = [255, 255, 255, 1.0].freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Return a pair of RGBA colors created from two other RGBA
|
40
|
+
# colors placed on a base color.
|
41
|
+
#
|
42
|
+
# The calculation is based on the definition found at
|
43
|
+
# https://www.w3.org/TR/compositing-1/#simplealphacompositing
|
44
|
+
# @param foreground [Array<Integer, Float>] RGBA value
|
45
|
+
# @param background [Array<Integer, Float>] RGBA value
|
46
|
+
# @param base [Array<Integer, Float>] RGBA value
|
47
|
+
# @return [Hash] A pair of RGBA values with :foreground and
|
48
|
+
# :background as its keys
|
49
|
+
|
50
|
+
def self.compose(foreground, background, base = Rgba::WHITE)
|
51
|
+
back = calc(background, base)
|
52
|
+
fore = calc(foreground, back)
|
53
|
+
|
54
|
+
{
|
55
|
+
foreground: normalize(fore),
|
56
|
+
background: normalize(back)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.normalize(raw_rgba)
|
61
|
+
rgb = Converter.rgb_map(raw_rgba[0, 3], &:round)
|
62
|
+
rgb.push(raw_rgba.last)
|
63
|
+
end
|
64
|
+
|
65
|
+
private_class_method :normalize
|
66
|
+
|
67
|
+
def self.calc(source, backdrop)
|
68
|
+
return source if source.last == 1
|
69
|
+
|
70
|
+
fore = source.dup
|
71
|
+
af = fore.pop
|
72
|
+
back = backdrop.dup
|
73
|
+
ab = back.pop
|
74
|
+
|
75
|
+
composed = fore.zip(back).map {|f, b| f * af + b * ab * (1 - af) }
|
76
|
+
a_composed = af + ab * (1 - af)
|
77
|
+
|
78
|
+
composed.push a_composed
|
79
|
+
end
|
80
|
+
|
81
|
+
private_class_method :calc
|
82
|
+
end
|
83
|
+
|
32
84
|
module Contrast
|
33
85
|
##
|
34
86
|
# Return contrast adjusted RGB value of passed color.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorContrastCalc
|
4
|
+
##
|
5
|
+
# Error raised if creating a Color instance with invalid value.
|
6
|
+
|
7
|
+
class InvalidColorRepresentationError < StandardError
|
8
|
+
module Template
|
9
|
+
RGB = 'An RGB value should be in form of [r, g, b], but %s.'
|
10
|
+
RGBA = <<~RGBA_MESSAGE
|
11
|
+
An RGB value should be in form of [r, g, b, opacity]
|
12
|
+
(r, g, b should be in the range between 0 and 255), but %s.
|
13
|
+
RGBA_MESSAGE
|
14
|
+
COLOR_NAME = '%s seems to be an undefined color name.'
|
15
|
+
HEX = 'A hex code #xxxxxx where 0 <= x <= f is expected, but %s.'
|
16
|
+
UNEXPECTED = 'A color should be given as an array or string, but %s.'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.may_be_name?(value)
|
20
|
+
# all of the color keywords contain an alphabet between g-z.
|
21
|
+
/^#/ !~ value && /[g-z]/i =~ value
|
22
|
+
end
|
23
|
+
|
24
|
+
private_class_method :may_be_name?
|
25
|
+
|
26
|
+
def self.select_message_template(value)
|
27
|
+
case value
|
28
|
+
when Array
|
29
|
+
value.length == 3 ? Template::RGB : Template::RGBA
|
30
|
+
when String
|
31
|
+
may_be_name?(value) ? Template::COLOR_NAME : Template::HEX
|
32
|
+
else
|
33
|
+
Template::UNEXPECTED
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private_class_method :select_message_template
|
38
|
+
|
39
|
+
def self.from_value(value)
|
40
|
+
message = format(select_message_template(value), value)
|
41
|
+
new(message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -18,6 +18,7 @@ module ColorContrastCalc
|
|
18
18
|
module ColorComponent
|
19
19
|
RGB = 'rgb'.chars
|
20
20
|
HSL = 'hsl'.chars
|
21
|
+
HWB = 'hwb'.chars
|
21
22
|
end
|
22
23
|
|
23
24
|
module CompFunc
|
@@ -45,6 +46,9 @@ module ColorContrastCalc
|
|
45
46
|
# The function returned by Sorter.compile_compare_function() expects
|
46
47
|
# hex color codes as key values when this constants is specified.
|
47
48
|
HEX = :hex
|
49
|
+
# The function returned by Sorter.compile_compare_function() expects
|
50
|
+
# color functions as key values when this constants is specified.
|
51
|
+
FUNCTION = :function
|
48
52
|
# @private
|
49
53
|
CLASS_TO_TYPE = {
|
50
54
|
Color => COLOR,
|
@@ -62,14 +66,157 @@ module ColorContrastCalc
|
|
62
66
|
|
63
67
|
def self.guess(color, key_mapper = nil)
|
64
68
|
key = key_mapper ? key_mapper[color] : color
|
69
|
+
return FUNCTION if non_hex_code_string?(key)
|
65
70
|
CLASS_TO_TYPE[key.class]
|
66
71
|
end
|
72
|
+
|
73
|
+
def self.non_hex_code_string?(color)
|
74
|
+
color.is_a?(String) && !Utils.valid_hex?(color)
|
75
|
+
end
|
76
|
+
|
77
|
+
private_class_method :non_hex_code_string?
|
78
|
+
end
|
79
|
+
|
80
|
+
class CompareFunctionCompiler
|
81
|
+
def initialize(converters = nil)
|
82
|
+
@converters = converters
|
83
|
+
end
|
84
|
+
|
85
|
+
def compile(color_order)
|
86
|
+
order = parse_color_order(color_order)
|
87
|
+
create_proc(order, color_order)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @private
|
91
|
+
|
92
|
+
def parse_color_order(color_order)
|
93
|
+
ordered_components = select_ordered_components(color_order)
|
94
|
+
pos = color_component_pos(color_order, ordered_components)
|
95
|
+
funcs = []
|
96
|
+
pos.each_with_index do |ci, i|
|
97
|
+
c = color_order[i]
|
98
|
+
funcs[ci] = Utils.uppercase?(c) ? CompFunc::DESCEND : CompFunc::ASCEND
|
99
|
+
end
|
100
|
+
{ pos: pos, funcs: funcs }
|
101
|
+
end
|
102
|
+
|
103
|
+
def select_ordered_components(color_order)
|
104
|
+
case color_order
|
105
|
+
when /[hsl]{3}/i
|
106
|
+
ColorComponent::HSL
|
107
|
+
when /[hwb]{3}/i
|
108
|
+
ColorComponent::HWB
|
109
|
+
else
|
110
|
+
ColorComponent::RGB
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private :select_ordered_components
|
115
|
+
|
116
|
+
# @private
|
117
|
+
|
118
|
+
def color_component_pos(color_order, ordered_components)
|
119
|
+
color_order.downcase.chars.map do |component|
|
120
|
+
ordered_components.index(component)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_proc(order, color_order)
|
125
|
+
if @converters
|
126
|
+
conv = select_converter(color_order)
|
127
|
+
proc {|color1, color2| compare(conv[color1], conv[color2], order) }
|
128
|
+
else
|
129
|
+
proc {|color1, color2| compare(color1, color2, order) }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private :create_proc
|
134
|
+
|
135
|
+
# @private
|
136
|
+
|
137
|
+
def compare_components(color1, color2, order)
|
138
|
+
funcs = order[:funcs]
|
139
|
+
order[:pos].each do |i|
|
140
|
+
result = funcs[i][color1[i], color2[i]]
|
141
|
+
return result unless result.zero?
|
142
|
+
end
|
143
|
+
|
144
|
+
0
|
145
|
+
end
|
146
|
+
|
147
|
+
alias compare compare_components
|
148
|
+
|
149
|
+
def select_converter(color_order)
|
150
|
+
scheme = select_scheme(color_order)
|
151
|
+
@converters[scheme]
|
152
|
+
end
|
153
|
+
|
154
|
+
private :select_converter
|
155
|
+
|
156
|
+
def select_scheme(color_order)
|
157
|
+
case color_order
|
158
|
+
when /[hsl]{3}/i
|
159
|
+
:hsl
|
160
|
+
when /[hwb]{3}/i
|
161
|
+
:hwb
|
162
|
+
else
|
163
|
+
:rgb
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private :select_scheme
|
168
|
+
end
|
169
|
+
|
170
|
+
class CachingCompiler < CompareFunctionCompiler
|
171
|
+
def create_proc(order, color_order)
|
172
|
+
converter = select_converter(color_order)
|
173
|
+
cache = {}
|
174
|
+
|
175
|
+
proc do |color1, color2|
|
176
|
+
c1 = to_components(color1, converter, cache)
|
177
|
+
c2 = to_components(color2, converter, cache)
|
178
|
+
|
179
|
+
compare(c1, c2, order)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_components(color, converter, cache)
|
184
|
+
cached_components = cache[color]
|
185
|
+
return cached_components if cached_components
|
186
|
+
|
187
|
+
components = converter[color]
|
188
|
+
cache[color] = components
|
189
|
+
|
190
|
+
components
|
191
|
+
end
|
192
|
+
|
193
|
+
private :to_components
|
67
194
|
end
|
68
195
|
|
69
|
-
|
70
|
-
|
196
|
+
hex_to_components = {
|
197
|
+
# shorthands for Utils.hex_to_rgb() and .hex_to_hsl()
|
71
198
|
rgb: Utils.method(:hex_to_rgb),
|
72
|
-
hsl: Utils.method(:hex_to_hsl)
|
199
|
+
hsl: Utils.method(:hex_to_hsl),
|
200
|
+
hwb: Utils.method(:hex_to_hwb)
|
201
|
+
}
|
202
|
+
|
203
|
+
function_to_components = {
|
204
|
+
rgb: proc {|func| ColorContrastCalc.color_from(func).rgb },
|
205
|
+
hsl: proc {|func| ColorContrastCalc.color_from(func).hsl },
|
206
|
+
hwb: proc {|func| ColorContrastCalc.color_from(func).hwb }
|
207
|
+
}
|
208
|
+
|
209
|
+
color_to_components = {
|
210
|
+
rgb: proc {|color| color.rgb },
|
211
|
+
hsl: proc {|color| color.hsl },
|
212
|
+
hwb: proc {|color| color.hwb }
|
213
|
+
}
|
214
|
+
|
215
|
+
COMPARE_FUNCTION_COMPILERS = {
|
216
|
+
KeyTypes::COLOR => CompareFunctionCompiler.new(color_to_components),
|
217
|
+
KeyTypes::COMPONENTS => CompareFunctionCompiler.new,
|
218
|
+
KeyTypes::HEX => CachingCompiler.new(hex_to_components),
|
219
|
+
KeyTypes::FUNCTION => CachingCompiler.new(function_to_components)
|
73
220
|
}.freeze
|
74
221
|
|
75
222
|
##
|
@@ -114,14 +261,7 @@ module ColorContrastCalc
|
|
114
261
|
key_mapper = nil, &key_mapper_block)
|
115
262
|
key_mapper = key_mapper_block if !key_mapper && key_mapper_block
|
116
263
|
|
117
|
-
|
118
|
-
when KeyTypes::COLOR
|
119
|
-
compare = compile_color_compare_function(color_order)
|
120
|
-
when KeyTypes::COMPONENTS
|
121
|
-
compare = compile_components_compare_function(color_order)
|
122
|
-
when KeyTypes::HEX
|
123
|
-
compare = compile_hex_compare_function(color_order)
|
124
|
-
end
|
264
|
+
compare = COMPARE_FUNCTION_COMPILERS[key_type].compile(color_order)
|
125
265
|
|
126
266
|
compose_function(compare, key_mapper)
|
127
267
|
end
|
@@ -135,101 +275,5 @@ module ColorContrastCalc
|
|
135
275
|
compare_function[key_mapper[color1], key_mapper[color2]]
|
136
276
|
end
|
137
277
|
end
|
138
|
-
|
139
|
-
# @private
|
140
|
-
|
141
|
-
def self.color_component_pos(color_order, ordered_components)
|
142
|
-
color_order.downcase.chars.map do |component|
|
143
|
-
ordered_components.index(component)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
# @private
|
148
|
-
|
149
|
-
def self.parse_color_order(color_order)
|
150
|
-
ordered_components = ColorComponent::RGB
|
151
|
-
ordered_components = ColorComponent::HSL if hsl_order?(color_order)
|
152
|
-
pos = color_component_pos(color_order, ordered_components)
|
153
|
-
funcs = []
|
154
|
-
pos.each_with_index do |ci, i|
|
155
|
-
c = color_order[i]
|
156
|
-
funcs[ci] = Utils.uppercase?(c) ? CompFunc::DESCEND : CompFunc::ASCEND
|
157
|
-
end
|
158
|
-
{ pos: pos, funcs: funcs }
|
159
|
-
end
|
160
|
-
|
161
|
-
# @private
|
162
|
-
|
163
|
-
def self.hsl_order?(color_order)
|
164
|
-
/[hsl]{3}/i.match?(color_order)
|
165
|
-
end
|
166
|
-
|
167
|
-
# @private
|
168
|
-
|
169
|
-
def self.compare_color_components(color1, color2, order)
|
170
|
-
funcs = order[:funcs]
|
171
|
-
order[:pos].each do |i|
|
172
|
-
result = funcs[i][color1[i], color2[i]]
|
173
|
-
return result unless result.zero?
|
174
|
-
end
|
175
|
-
|
176
|
-
0
|
177
|
-
end
|
178
|
-
|
179
|
-
# @private
|
180
|
-
|
181
|
-
def self.compile_components_compare_function(color_order)
|
182
|
-
order = parse_color_order(color_order)
|
183
|
-
|
184
|
-
proc do |color1, color2|
|
185
|
-
compare_color_components(color1, color2, order)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
# @private
|
190
|
-
|
191
|
-
def self.compile_hex_compare_function(color_order)
|
192
|
-
order = parse_color_order(color_order)
|
193
|
-
converter = HEX_TO_COMPONENTS[:rgb]
|
194
|
-
converter = HEX_TO_COMPONENTS[:hsl] if hsl_order?(color_order)
|
195
|
-
cache = {}
|
196
|
-
|
197
|
-
proc do |hex1, hex2|
|
198
|
-
color1 = hex_to_components(hex1, converter, cache)
|
199
|
-
color2 = hex_to_components(hex2, converter, cache)
|
200
|
-
|
201
|
-
compare_color_components(color1, color2, order)
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
# @private
|
206
|
-
|
207
|
-
def self.hex_to_components(hex, converter, cache)
|
208
|
-
cached_components = cache[hex]
|
209
|
-
return cached_components if cached_components
|
210
|
-
|
211
|
-
components = converter[hex]
|
212
|
-
cache[hex] = components
|
213
|
-
|
214
|
-
components
|
215
|
-
end
|
216
|
-
|
217
|
-
private_class_method :hex_to_components
|
218
|
-
|
219
|
-
# @private
|
220
|
-
|
221
|
-
def self.compile_color_compare_function(color_order)
|
222
|
-
order = parse_color_order(color_order)
|
223
|
-
|
224
|
-
if hsl_order?(color_order)
|
225
|
-
proc do |color1, color2|
|
226
|
-
compare_color_components(color1.hsl, color2.hsl, order)
|
227
|
-
end
|
228
|
-
else
|
229
|
-
proc do |color1, color2|
|
230
|
-
compare_color_components(color1.rgb, color2.rgb, order)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
278
|
end
|
235
279
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'color_contrast_calc/converter'
|
4
|
+
require 'color_contrast_calc/checker'
|
5
|
+
|
6
|
+
module ColorContrastCalc
|
7
|
+
##
|
8
|
+
# Provides methods to calculate the contrast ratio between transparent colors.
|
9
|
+
#
|
10
|
+
# Colors are given as RGBA values represented as arrays of Float.
|
11
|
+
# Note that during the process of calculation, each of RGB components
|
12
|
+
# is treated as a Float, though some of them may be rounded up/down in the
|
13
|
+
# final return value.
|
14
|
+
|
15
|
+
module TransparencyCalc
|
16
|
+
include Converter::AlphaCompositing::Rgba
|
17
|
+
|
18
|
+
def self.contrast_ratio(foreground, background, base = WHITE)
|
19
|
+
colors = [foreground, background]
|
20
|
+
|
21
|
+
rgb_colors = if colors.all? {|color| opaque?(color) }
|
22
|
+
colors.map {|color| to_rgb(color) }
|
23
|
+
else
|
24
|
+
to_opaque_rgbs(foreground, background, base)
|
25
|
+
end
|
26
|
+
|
27
|
+
Checker.contrast_ratio(*rgb_colors)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.opaque?(rgba)
|
31
|
+
rgba[-1] == Utils::MAX_OPACITY
|
32
|
+
end
|
33
|
+
|
34
|
+
private_class_method :opaque?
|
35
|
+
|
36
|
+
def self.to_rgb(rgba)
|
37
|
+
rgba[0, 3]
|
38
|
+
end
|
39
|
+
|
40
|
+
private_class_method :to_rgb
|
41
|
+
|
42
|
+
def self.compose(foreground, background, base)
|
43
|
+
Converter::AlphaCompositing.compose(foreground, background, base)
|
44
|
+
end
|
45
|
+
|
46
|
+
private_class_method :compose
|
47
|
+
|
48
|
+
def self.to_opaque_rgbs(foreground, background, base)
|
49
|
+
composed = compose(foreground, background, base)
|
50
|
+
%i[foreground background].map {|key| to_rgb(composed[key]) }
|
51
|
+
end
|
52
|
+
|
53
|
+
private_class_method :to_opaque_rgbs
|
54
|
+
end
|
55
|
+
end
|