color_contrast_calc 0.5.0 → 0.9.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 +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
|