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.
@@ -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