color_contrast_calc 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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/