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.
@@ -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.
@@ -118,6 +118,7 @@
118
118
  ["plum", "#dda0dd"],
119
119
  ["powderblue", "#b0e0e6"],
120
120
  ["purple", "#800080"],
121
+ ["rebeccapurple", "#663399"],
121
122
  ["red", "#ff0000"],
122
123
  ["rosybrown", "#bc8f8f"],
123
124
  ["royalblue", "#4169e1"],
@@ -6,7 +6,7 @@ module ColorContrastCalc
6
6
 
7
7
  module Deprecated
8
8
  def self.warn(old_method, new_method)
9
- STDERR.puts "##{old_method} is deprecated. Use ##{new_method} instead"
9
+ Kernel.warn "##{old_method} is deprecated. Use ##{new_method} instead"
10
10
  end
11
11
 
12
12
  module 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
- # @private shorthands for Utils.hex_to_rgb() and .hex_to_hsl()
70
- HEX_TO_COMPONENTS = {
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
- case key_type
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