color_contrast_calc 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eca556d535a8e8eda4ad0bf84e1891447b7065823b18cce4b4695f5a7e2beb6
4
- data.tar.gz: 2548294c153d699792adceb71818b4a670da7f5faca6e48d67a1f511ae389f91
3
+ metadata.gz: 599083b4c4ea4f652dd649dfc78bf9b15e7322496fa757830e04cbfff1618897
4
+ data.tar.gz: 1a551dd81a0042938f7afbd6e6df33f435a09cfc6990a77711258e90d3b93e02
5
5
  SHA512:
6
- metadata.gz: cf7971d631606c8cd89f2c23661cb873f73874877c2668e742683c044f634b508262aca2745077831104bdaeeefb123ee30ca303c9cffbef2bc4298bb7518b35
7
- data.tar.gz: 67851e719c99d0c047e86f8634c4c7b5ad6511c924b80f2246197bd01e7781c0ff6959dd05f16dba44a43792c916857c4fd698db4081bb6a06a242af5b0dbd1c
6
+ metadata.gz: 77b7e9bc36cef897660f4dda625e10c658fb48b91608ea13a68f9d6c0b395e668f08d2e4af812f58146724ad4846c479b1bc833bbd338def9549dc4113c44ff2
7
+ data.tar.gz: 777fbeed77cbc3b645be5e0a28ae5db2c9c7e1ea7dd38e2a582ca3d9133d01f0b1017b1e253c0171f333ab5b43d5e2933d57c3a696f6be297826ca20b724e7e1
@@ -157,6 +157,29 @@ Contrast ratio between yellow and black: 19.555999999999997
157
157
  Contrast level: AAA
158
158
  ```
159
159
 
160
+ #### 1.4: [実験的対応] 透明色間のコントラスト比の計算
161
+
162
+ 透明色間の計算のために``ColorContrastCalc.contrast_ratio_with_opacity()``が
163
+ 提供されています。
164
+
165
+ このメソッドは3つの引数として、前景色と背景色、省略可能な基調色、を取ります。
166
+
167
+ 3番目の基調色は他の2色の下に配置され、完全に不透明なことが期待される点にご注意下さい。
168
+
169
+ 例:
170
+
171
+ ```bash
172
+ irb -r color_contrast_calc
173
+ irb(main):001:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0 / 1.0)', 'rgb(0 255 0 / 0.5)', 'white')
174
+ => 1.1828076947731336
175
+ irb(main):002:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0 / 1.0)', 'rgb(0 255 0 / 0.5)') # 3番目の色のデフォルト値は白です。
176
+ => 1.1828076947731336
177
+ irb(main):003:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0 / 1.0)', 'rgb(0 255 0 / 0.5)', 'black')
178
+ => 4.78414518008597
179
+ irb(main):004:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0)', 'rgb(0 255 0 / 0.5)', 'black') # 完全に不透明な色について不透明度を指定する必要はありません。
180
+ => 4.78414518008597
181
+ ```
182
+
160
183
  ### 例2: ある色に対し十分なコントラスト比のある色を見つける
161
184
 
162
185
  2色の組み合わせのうち、一方の色のbrightness/lightnessを変化させることで十分な
data/README.md CHANGED
@@ -157,6 +157,32 @@ Contrast ratio between yellow and black: 19.555999999999997
157
157
  Contrast level: AAA
158
158
  ```
159
159
 
160
+
161
+ #### 1.4: [Experimental] Calculate the contrast ratio between transparent colors
162
+
163
+ ``ColorContrastCalc.contrast_ratio_with_opacity()`` is provided for the
164
+ calculation.
165
+
166
+ The method takes three arguments, foreground color, background color and an
167
+ optional base color.
168
+
169
+ Please note that the third color is placed below the other two colors and
170
+ expects to be fully opaque.
171
+
172
+ For example:
173
+
174
+ ```bash
175
+ irb -r color_contrast_calc
176
+ irb(main):001:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0 / 1.0)', 'rgb(0 255 0 / 0.5)', 'white')
177
+ => 1.1828076947731336
178
+ irb(main):002:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0 / 1.0)', 'rgb(0 255 0 / 0.5)') # The default value for the third parameter is white.
179
+ => 1.1828076947731336
180
+ irb(main):003:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0 / 1.0)', 'rgb(0 255 0 / 0.5)', 'black')
181
+ => 4.78414518008597
182
+ irb(main):004:0> ColorContrastCalc.contrast_ratio_with_opacity('rgb(255 255 0)', 'rgb(0 255 0 / 0.5)', 'black') # For a fully opaque color, you don't need to specify the opacity.
183
+ => 4.78414518008597
184
+ ```
185
+
160
186
  ### Example 2: Find colors that have enough contrast ratio with a given color
161
187
 
162
188
  If you want to find a combination of colors with sufficient contrast
@@ -24,8 +24,8 @@ Gem::Specification.new do |spec|
24
24
  spec.require_paths = ['lib']
25
25
 
26
26
  spec.add_development_dependency 'bundler', '~> 2.0'
27
- spec.add_development_dependency 'rake', '~> 10.0'
28
- spec.add_development_dependency 'rspec', '~> 3.0'
29
- spec.add_development_dependency 'rubocop', '~> 0.79'
30
- spec.add_development_dependency 'yard', '~> 0.9.9'
27
+ spec.add_development_dependency 'rake', '~> 13.0'
28
+ spec.add_development_dependency 'rspec', '~> 3.9'
29
+ spec.add_development_dependency 'rubocop', '~> 0.80'
30
+ spec.add_development_dependency 'yard', '~> 0.9.24'
31
31
  end
@@ -5,6 +5,7 @@ require 'color_contrast_calc/utils'
5
5
  require 'color_contrast_calc/converter'
6
6
  require 'color_contrast_calc/checker'
7
7
  require 'color_contrast_calc/threshold_finder'
8
+ require 'color_contrast_calc/transparency_calc'
8
9
  require 'color_contrast_calc/color'
9
10
  require 'color_contrast_calc/sorter'
10
11
 
@@ -63,18 +64,44 @@ module ColorContrastCalc
63
64
  # Please note that this method may be slow, as it internally creates
64
65
  # Color instances.
65
66
  #
66
- # @param color1 [String, Array<Integer>] RGB color given as a string or
67
- # an array of integers. Yellow, for example, can be given as "#ffff00",
68
- # "#ff0", "rgb(255, 255, 0)", "hsl(60deg, 100%, 50%)", "hwb(60deg 0% 0%)"
69
- # or [255, 255, 0].
70
- # @param color2 [String, Array<Integer>] RGB color given as a string or
71
- # an array of integers.
67
+ # @param color1 [String, Array<Integer>, Color] color given as a string,
68
+ # an array of integers or a Color instance. Yellow, for example, can be
69
+ # given as "#ffff00", "#ff0", "rgb(255, 255, 0)", "hsl(60deg, 100%, 50%)",
70
+ # "hwb(60deg 0% 0%)" or [255, 255, 0].
71
+ # @param color2 [String, Array<Integer>, Color] color given as a string,
72
+ # an array of integers or a Color instance.
72
73
  # @return [Float] Contrast ratio
73
74
 
74
75
  def self.contrast_ratio(color1, color2)
75
76
  Color.as_color(color1).contrast_ratio_against(Color.as_color(color2))
76
77
  end
77
78
 
79
+ ##
80
+ # Calculate the contrast ratio of transparent colors.
81
+ #
82
+ # For the calculation, you have to specify three colors because
83
+ # when both of two colors to be compared are transparent,
84
+ # the third color put under them filters through them.
85
+ #
86
+ # @param foreground [String, Array<Integer>, Color] The uppermost
87
+ # color such as "rgb(255, 255, 0, 0.5)" or "hsl(60 100% 50% / 50%)"
88
+ # @param background [String, Array<Integer>, Color] The color placed
89
+ # between the others
90
+ # @param base [String, Array<Integer>, Color] The color placed in
91
+ # the bottom. When the backgound is completely opaque, this color
92
+ # is ignored.
93
+ # @return [Float] Contrast ratio
94
+
95
+ def self.contrast_ratio_with_opacity(foreground, background,
96
+ base = Color::WHITE)
97
+ params = [foreground, background, base].map do |c|
98
+ color = Color.as_color(c)
99
+ color.rgb + [color.opacity]
100
+ end
101
+
102
+ TransparencyCalc.contrast_ratio(*params)
103
+ end
104
+
78
105
  ##
79
106
  # Select from two colors the one of which the contrast ratio is higher
80
107
  # than the other's, against a given color.
@@ -70,7 +70,12 @@ module ColorContrastCalc
70
70
  # @return [Color] Instance of Color
71
71
 
72
72
  def from_hsl(hsl, name = nil)
73
- rgb = Utils.hsl_to_rgb(hsl)
73
+ if hsl.length == 4
74
+ rgb = Utils.hsl_to_rgb(hsl[0, 3])
75
+ return Color.new(rgb.push(hsl.last), name) unless opaque?(hsl)
76
+ end
77
+
78
+ rgb ||= Utils.hsl_to_rgb(hsl)
74
79
  !name && List::HEX_TO_COLOR[Utils.rgb_to_hex(rgb)] ||
75
80
  Color.new(rgb, name)
76
81
  end
@@ -96,14 +101,21 @@ module ColorContrastCalc
96
101
  raise InvalidColorRepresentationError.from_value(color_value)
97
102
  end
98
103
 
99
- return color_from_rgb(color_value, name) if color_value.is_a?(Array)
100
-
101
- if /\A(?:rgb|hsl|hwb)/i =~ color_value
102
- return color_from_func(color_value, name)
104
+ if color_value.is_a?(Array)
105
+ return color_from_rgba(color_value, name) if color_value.length == 4
106
+ return color_from_rgb(color_value, name)
103
107
  end
108
+
109
+ return color_from_func(color_value, name) if function?(color_value)
104
110
  color_from_str(color_value, name)
105
111
  end
106
112
 
113
+ def function?(color_value)
114
+ /\A(?:rgba?|hsla?|hwb)/i =~ color_value
115
+ end
116
+
117
+ private :function?
118
+
107
119
  ##
108
120
  # Return an instance of Color.
109
121
  #
@@ -128,6 +140,24 @@ module ColorContrastCalc
128
140
  color_from(color_value, name)
129
141
  end
130
142
 
143
+ def opaque?(color_value)
144
+ color_value[-1] == 1.0
145
+ end
146
+
147
+ private :opaque?
148
+
149
+ def color_from_rgba(rgba_value, name = nil)
150
+ unless Utils.valid_rgb?(rgba_value[0, 3])
151
+ raise Invalidcolorrepresentationerror.from_value(rgb_value)
152
+ end
153
+
154
+ return color_from_rgb(rgba_value[0, 3], name) if opaque?(rgba_value)
155
+
156
+ Color.new(rgba_value, name)
157
+ end
158
+
159
+ private :color_from_rgba
160
+
131
161
  def color_from_rgb(rgb_value, name = nil)
132
162
  unless Utils.valid_rgb?(rgb_value)
133
163
  raise InvalidColorRepresentationError.from_value(rgb_value)
@@ -141,11 +171,15 @@ module ColorContrastCalc
141
171
 
142
172
  def color_from_func(color_value, name = nil)
143
173
  conv = ColorFunctionParser.parse(color_value)
144
- if conv.scheme == ColorFunctionParser::Scheme::HSL
174
+ if conv.scheme == ColorFunctionParser::Scheme::HSL ||
175
+ conv.scheme == ColorFunctionParser::Scheme::HSLA
145
176
  return from_hsl(conv.to_a, name || color_value)
146
177
  end
147
178
 
148
- color_from_rgb(conv.rgb, name || color_value)
179
+ name ||= color_value
180
+
181
+ return color_from_rgb(conv.rgb, name) if conv.opaque?
182
+ color_from_rgba(conv.rgba, name)
149
183
  end
150
184
 
151
185
  private :color_from_func
@@ -180,7 +214,7 @@ module ColorContrastCalc
180
214
  # @!attribute [r] relative_luminance
181
215
  # @return [Float] Relative luminance of the color
182
216
 
183
- attr_reader :rgb, :hex, :name, :relative_luminance
217
+ attr_reader :rgb, :hex, :name, :relative_luminance, :opacity
184
218
 
185
219
  ##
186
220
  # Create a new instance of Color.
@@ -193,7 +227,8 @@ module ColorContrastCalc
193
227
  # @return [Color] New instance of Color
194
228
 
195
229
  def initialize(rgb, name = nil)
196
- @rgb = rgb.is_a?(String) ? Utils.hex_to_rgb(rgb) : rgb
230
+ @rgb = rgb.is_a?(String) ? Utils.hex_to_rgb(rgb) : rgb.dup
231
+ @opacity = @rgb.length == 4 ? @rgb.pop : 1.0
197
232
  @hex = Utils.rgb_to_hex(@rgb)
198
233
  @name = name || common_name
199
234
  @relative_luminance = Checker.relative_luminance(@rgb)
@@ -15,7 +15,9 @@ module ColorContrastCalc
15
15
 
16
16
  module Scheme
17
17
  RGB = 'rgb'
18
+ RGBA = 'rgba'
18
19
  HSL = 'hsl'
20
+ HSLA = 'hsla'
19
21
  HWB = 'hwb'
20
22
  end
21
23
 
@@ -68,10 +70,10 @@ module ColorContrastCalc
68
70
 
69
71
  # @private
70
72
  def validate_units(parameters, original_value = nil)
71
- @config[:units].each_with_index do |unit, i|
72
- passed_unit = parameters[i][:unit]
73
+ parameters.each_with_index do |param, i|
74
+ passed_unit = param[:unit]
73
75
 
74
- unless unit.include? passed_unit
76
+ unless @config[:units][i].include? passed_unit
75
77
  raise InvalidColorRepresentationError,
76
78
  error_message(parameters, passed_unit, i, original_value)
77
79
  end
@@ -85,6 +87,7 @@ module ColorContrastCalc
85
87
  {
86
88
  scheme: Scheme::RGB,
87
89
  units: [
90
+ [nil, PERCENT],
88
91
  [nil, PERCENT],
89
92
  [nil, PERCENT],
90
93
  [nil, PERCENT]
@@ -99,7 +102,8 @@ module ColorContrastCalc
99
102
  units: [
100
103
  [nil, DEG, GRAD, RAD, TURN],
101
104
  [PERCENT],
102
- [PERCENT]
105
+ [PERCENT],
106
+ [nil, PERCENT]
103
107
  ]
104
108
  }
105
109
  end
@@ -111,14 +115,17 @@ module ColorContrastCalc
111
115
  units: [
112
116
  [nil, DEG, GRAD, RAD, TURN],
113
117
  [PERCENT],
114
- [PERCENT]
118
+ [PERCENT],
119
+ [nil, PERCENT]
115
120
  ]
116
121
  }
117
122
  end
118
123
 
119
124
  VALIDATORS = {
120
125
  Scheme::RGB => RGB,
126
+ Scheme::RGBA => RGB,
121
127
  Scheme::HSL => HSL,
128
+ Scheme::HSLA => HSL,
122
129
  Scheme::HWB => HWB
123
130
  }.freeze
124
131
 
@@ -138,7 +145,7 @@ module ColorContrastCalc
138
145
  # so do not rely on the current class name and its interfaces.
139
146
  # They may change in the future.
140
147
 
141
- class Converter
148
+ class ColorFunction
142
149
  UNIT_CONV = {
143
150
  Unit::PERCENT => proc do |n, base|
144
151
  if base == 255
@@ -174,19 +181,44 @@ module ColorContrastCalc
174
181
  # ColorFunctionParser.parse() and the manual creation of
175
182
  # instances of this class by end users is not expected.
176
183
 
177
- def initialize(parsed_value, original_value)
184
+ def initialize(parsed_value)
178
185
  @scheme = parsed_value[:scheme]
179
186
  @params = parsed_value[:parameters]
180
- @source = original_value
187
+ @source = parsed_value[:source]
181
188
  @normalized = normalize_params
189
+ normalize_opacity!(@normalized)
190
+ end
191
+
192
+ def convert_unit(param, base = nil)
193
+ UNIT_CONV[param[:unit]][param[:number], base]
182
194
  end
183
195
 
196
+ private :convert_unit
197
+
184
198
  def normalize_params
185
199
  raise NotImplementedError, 'Overwrite the method in a subclass'
186
200
  end
187
201
 
188
202
  private :normalize_params
189
203
 
204
+ def color_components
205
+ return @normalized if @normalized.length == 3
206
+ @normalized[0, 3]
207
+ end
208
+
209
+ private :color_components
210
+
211
+ def normalize_opacity!(normalized)
212
+ return unless @params.length == 4
213
+
214
+ param = @params.last
215
+ n = param[:number]
216
+ base = param[:unit] == Unit::PERCENT ? 100 : 1
217
+ normalized[-1] = n.to_f / base
218
+ end
219
+
220
+ private :normalize_opacity!
221
+
190
222
  ##
191
223
  # Return the RGB value gained from a RGB/HSL/HWB function.
192
224
  #
@@ -208,40 +240,66 @@ module ColorContrastCalc
208
240
  @normalized
209
241
  end
210
242
 
243
+ ##
244
+ # Return the opacity of a color presented as a RGB/HSL/HWB
245
+ # function. The returned value is normalized to a floating number
246
+ # between 0 and 1.
247
+ #
248
+ # @return [Float] Normalized opacity
249
+
250
+ def opacity
251
+ @opacity ||= @normalized.length == 3 ? 1.0 : @normalized.last
252
+ end
253
+
254
+ ##
255
+ # Return the RGBA value gained from a RGB/HSL/HWB function.
256
+ # The opacity is normalized to a floating number between 0 and 1.
257
+ #
258
+ # @return [Array<Integer, Float>] RGBA value represented as an array.
259
+
260
+ def rgba
261
+ rgb + [opacity]
262
+ end
263
+
264
+ ##
265
+ # Return true when the Color is completely opaque.
266
+ #
267
+ # @return [true, false] return true when the opacity equals 1.0
268
+
269
+ def opaque?
270
+ opacity == 1.0
271
+ end
272
+
211
273
  # @private
212
274
  class Rgb < self
213
275
  def normalize_params
214
- @params.map do |param|
215
- UNIT_CONV[param[:unit]][param[:number], 255]
216
- end
276
+ @params.map {|param| convert_unit(param, 255) }
217
277
  end
218
278
 
219
- alias rgb to_a
279
+ alias rgb color_components
280
+
281
+ public :rgb
220
282
  end
221
283
 
222
284
  # @private
223
285
  class Hsl < self
224
286
  def normalize_params
225
- @params.map do |param|
226
- UNIT_CONV[param[:unit]][param[:number]]
227
- end
287
+ @params.map {|param| convert_unit(param) }
228
288
  end
229
289
 
230
290
  def rgb
231
- Utils.hsl_to_rgb(to_a)
291
+ Utils.hsl_to_rgb(color_components)
232
292
  end
233
293
  end
234
294
 
235
295
  # @private
236
296
  class Hwb < self
237
297
  def normalize_params
238
- @params.map do |param|
239
- UNIT_CONV[param[:unit]][param[:number]]
240
- end
298
+ @params.map {|param| convert_unit(param) }
241
299
  end
242
300
 
243
301
  def rgb
244
- Utils.hwb_to_rgb(to_a)
302
+ Utils.hwb_to_rgb(color_components)
245
303
  end
246
304
  end
247
305
 
@@ -249,12 +307,12 @@ module ColorContrastCalc
249
307
  def self.create(parsed_value, original_value)
250
308
  Validator.validate(parsed_value, original_value)
251
309
  case parsed_value[:scheme]
252
- when Scheme::RGB
253
- Rgb.new(parsed_value, original_value)
254
- when Scheme::HSL
255
- Hsl.new(parsed_value, original_value)
310
+ when Scheme::RGB, Scheme::RGBA
311
+ Rgb.new(parsed_value)
312
+ when Scheme::HSL, Scheme::HSLA
313
+ Hsl.new(parsed_value)
256
314
  when Scheme::HWB
257
- Hwb.new(parsed_value, original_value)
315
+ Hwb.new(parsed_value)
258
316
  end
259
317
  end
260
318
  end
@@ -262,12 +320,50 @@ module ColorContrastCalc
262
320
  # @private
263
321
  module TokenRe
264
322
  SPACES = /\s+/.freeze
265
- SCHEME = /(rgb|hsl|hwb)/i.freeze
323
+ SCHEME = /rgba?|hsla?|hwb/i.freeze
266
324
  OPEN_PAREN = /\(/.freeze
267
325
  CLOSE_PAREN = /\)/.freeze
268
326
  COMMA = /,/.freeze
269
- NUMBER = /(\d+)(:?\.\d+)?/.freeze
270
- UNIT = /(%|deg|grad|rad|turn)/.freeze
327
+ SLASH = %r{/}.freeze
328
+ NUMBER = /(?:\d+)(?:\.\d+)?|\.\d+/.freeze
329
+ UNIT = /%|deg|grad|rad|turn/.freeze
330
+ end
331
+
332
+ # @private
333
+ module ErrorReporter
334
+ MAX_SOURCE_LENGTH = 60
335
+
336
+ private_constant :MAX_SOURCE_LENGTH
337
+
338
+ def self.format_error_message(scanner, re)
339
+ out = StringIO.new
340
+ color_value = sanitized_source(scanner)
341
+
342
+ out.print format('"%s" is not a valid code. ', color_value)
343
+ print_error_pos!(out, color_value, scanner.charpos)
344
+ out.puts " while searching with #{re}"
345
+
346
+ out.string
347
+ end
348
+
349
+ def self.print_error_pos!(out, color_value, pos)
350
+ out.puts 'An error occurred at:'
351
+ out.puts color_value
352
+ out.print "#{' ' * pos}^"
353
+ end
354
+
355
+ def self.sanitized_source(scanner)
356
+ src = scanner.string
357
+ parsed = src[0, scanner.charpos]
358
+ max_src = src[0, MAX_SOURCE_LENGTH]
359
+
360
+ return max_src if /\A[[:ascii:]&&[:^cntrl:]]+\Z/.match(max_src)
361
+
362
+ suspicious_chars = max_src[parsed.length, MAX_SOURCE_LENGTH]
363
+ "#{parsed}#{suspicious_chars.inspect[1..-2]}"
364
+ end
365
+
366
+ private_class_method :sanitized_source
271
367
  end
272
368
 
273
369
  class Parser
@@ -295,25 +391,23 @@ module ColorContrastCalc
295
391
  end
296
392
 
297
393
  def format_error_message(scanner, re)
298
- out = StringIO.new
299
- color_value = scanner.string
394
+ ErrorReporter.format_error_message(scanner, re)
395
+ end
300
396
 
301
- out.print format('"%s" is not a valid code. ', color_value)
302
- print_error_pos!(out, color_value, scanner.charpos)
303
- out.puts " while searching with #{re}"
397
+ private :format_error_message
304
398
 
305
- out.string
399
+ def source_until_current_pos(scanner)
400
+ scanner.string[0, scanner.charpos]
306
401
  end
307
402
 
308
- private :format_error_message
403
+ private :source_until_current_pos
309
404
 
310
- def print_error_pos!(out, color_value, pos)
311
- out.puts 'An error occurred at:'
312
- out.puts color_value
313
- out.print "#{' ' * pos}^"
405
+ def fix_value!(parsed_value, scanner)
406
+ parsed_value[:source] = source_until_current_pos(scanner).strip
407
+ parsed_value
314
408
  end
315
409
 
316
- private :print_error_pos!
410
+ private :fix_value!
317
411
 
318
412
  def read_token!(scanner, re)
319
413
  skip_spaces!(scanner)
@@ -362,11 +456,36 @@ module ColorContrastCalc
362
456
 
363
457
  parsed_value[:parameters].last[:unit] = unit if unit
364
458
 
365
- read_comma!(scanner, parsed_value)
459
+ read_separator!(scanner, parsed_value)
366
460
  end
367
461
 
368
462
  private :read_unit!
369
463
 
464
+ def read_separator!(scanner, parsed_value)
465
+ if next_spaces_as_separator?(scanner)
466
+ return read_number!(scanner, parsed_value)
467
+ end
468
+
469
+ if parsed_value[:parameters].length == 3 &&
470
+ check_next_token(scanner, TokenRe::SLASH)
471
+ return read_opacity!(scanner, parsed_value)
472
+ end
473
+
474
+ read_comma!(scanner, parsed_value)
475
+ end
476
+
477
+ private :read_separator!
478
+
479
+ def check_next_token(scanner, re)
480
+ cur_pos = scanner.pos
481
+ skip_spaces!(scanner)
482
+ result = scanner.check(re)
483
+ scanner.pos = cur_pos
484
+ result
485
+ end
486
+
487
+ private :check_next_token
488
+
370
489
  def next_spaces_as_separator?(scanner)
371
490
  cur_pos = scanner.pos
372
491
  spaces = skip_spaces!(scanner)
@@ -377,14 +496,15 @@ module ColorContrastCalc
377
496
 
378
497
  private :next_spaces_as_separator?
379
498
 
380
- def read_comma!(scanner, parsed_value)
381
- if next_spaces_as_separator?(scanner)
382
- return read_number!(scanner, parsed_value)
383
- end
499
+ def read_opacity!(scanner, parsed_value)
500
+ read_token!(scanner, TokenRe::SLASH)
501
+ read_number!(scanner, parsed_value)
502
+ end
384
503
 
504
+ def read_comma!(scanner, parsed_value)
385
505
  skip_spaces!(scanner)
386
506
 
387
- return parsed_value if read_close_paren!(scanner)
507
+ return fix_value!(parsed_value, scanner) if read_close_paren!(scanner)
388
508
 
389
509
  read_token!(scanner, TokenRe::COMMA)
390
510
  read_number!(scanner, parsed_value)
@@ -395,17 +515,13 @@ module ColorContrastCalc
395
515
 
396
516
  class FunctionParser < Parser
397
517
  def read_comma!(scanner, parsed_value)
398
- if next_spaces_as_separator?(scanner)
399
- return read_number!(scanner, parsed_value)
400
- end
401
-
402
518
  skip_spaces!(scanner)
403
519
 
404
520
  if scanner.check(TokenRe::COMMA)
405
521
  wrong_separator_error(scanner, parsed_value)
406
522
  end
407
523
 
408
- return parsed_value if read_close_paren!(scanner)
524
+ return fix_value!(parsed_value, scanner) if read_close_paren!(scanner)
409
525
 
410
526
  read_number!(scanner, parsed_value)
411
527
  end
@@ -417,7 +533,7 @@ module ColorContrastCalc
417
533
  # The trailing space after the first message is intentional,
418
534
  # because it is immediately followed by another message.
419
535
  out.print "\",\" is not a valid separator for #{scheme} functions. "
420
- print_error_pos!(out, color_value, scanner.charpos)
536
+ ErrorReporter.print_error_pos!(out, color_value, scanner.charpos)
421
537
  out.puts
422
538
  out.string
423
539
  end
@@ -440,15 +556,15 @@ module ColorContrastCalc
440
556
 
441
557
  ##
442
558
  # Parse an RGB/HSL/HWB function and store the result as an instance of
443
- # ColorFunctionParser::Converter.
559
+ # ColorFunctionParser::ColorFunction.
444
560
  #
445
561
  # @param color_value [String] RGB/HSL/HWB function defined at
446
562
  # https://www.w3.org/TR/2019/WD-css-color-4-20191105/
447
- # @return [Converter] An instance of ColorFunctionParser::Converter
563
+ # @return [ColorFunction] An instance of ColorFunctionParser::ColorFunction
448
564
 
449
565
  def self.parse(color_value)
450
566
  parsed_value = MAIN_PARSER.read_scheme!(StringScanner.new(color_value))
451
- Converter.create(parsed_value, color_value)
567
+ ColorFunction.create(parsed_value, color_value)
452
568
  end
453
569
 
454
570
  ##
@@ -459,5 +575,15 @@ module ColorContrastCalc
459
575
  def self.to_rgb(color_value)
460
576
  parse(color_value).rgb
461
577
  end
578
+
579
+ ##
580
+ # Return An RGBA value gained from an RGB/HSL/HWB function.
581
+ # The opacity is normalized to a floating number between 0 and 1.
582
+ #
583
+ # @return [Array<Integer, Float>] RGBA value represented as an array
584
+
585
+ def self.to_rgba(color_value)
586
+ parse(color_value).rgba
587
+ end
462
588
  end
463
589
  end
@@ -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,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] == 1.0
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ColorContrastCalc
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: color_contrast_calc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - HASHIMOTO, Naoki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-13 00:00:00.000000000 Z
11
+ date: 2020-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -30,56 +30,56 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
47
+ version: '3.9'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '3.9'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.79'
61
+ version: '0.80'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.79'
68
+ version: '0.80'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: yard
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 0.9.9
75
+ version: 0.9.24
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 0.9.9
82
+ version: 0.9.24
83
83
  description:
84
84
  email:
85
85
  - hashimoto.naoki@gmail.com
@@ -120,6 +120,7 @@ files:
120
120
  - lib/color_contrast_calc/shim.rb
121
121
  - lib/color_contrast_calc/sorter.rb
122
122
  - lib/color_contrast_calc/threshold_finder.rb
123
+ - lib/color_contrast_calc/transparency_calc.rb
123
124
  - lib/color_contrast_calc/utils.rb
124
125
  - lib/color_contrast_calc/version.rb
125
126
  homepage: https://github.com/nico-hn/color_contrast_calc_rb/