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 +4 -4
- data/README.ja.md +23 -0
- data/README.md +26 -0
- data/color_contrast_calc.gemspec +4 -4
- data/lib/color_contrast_calc.rb +33 -6
- data/lib/color_contrast_calc/color.rb +44 -9
- data/lib/color_contrast_calc/color_function_parser.rb +181 -55
- data/lib/color_contrast_calc/converter.rb +52 -0
- data/lib/color_contrast_calc/transparency_calc.rb +55 -0
- data/lib/color_contrast_calc/version.rb +1 -1
- metadata +11 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 599083b4c4ea4f652dd649dfc78bf9b15e7322496fa757830e04cbfff1618897
|
4
|
+
data.tar.gz: 1a551dd81a0042938f7afbd6e6df33f435a09cfc6990a77711258e90d3b93e02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77b7e9bc36cef897660f4dda625e10c658fb48b91608ea13a68f9d6c0b395e668f08d2e4af812f58146724ad4846c479b1bc833bbd338def9549dc4113c44ff2
|
7
|
+
data.tar.gz: 777fbeed77cbc3b645be5e0a28ae5db2c9c7e1ea7dd38e2a582ca3d9133d01f0b1017b1e253c0171f333ab5b43d5e2933d57c3a696f6be297826ca20b724e7e1
|
data/README.ja.md
CHANGED
@@ -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
|
data/color_contrast_calc.gemspec
CHANGED
@@ -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', '~>
|
28
|
-
spec.add_development_dependency 'rspec', '~> 3.
|
29
|
-
spec.add_development_dependency 'rubocop', '~> 0.
|
30
|
-
spec.add_development_dependency 'yard', '~> 0.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
|
data/lib/color_contrast_calc.rb
CHANGED
@@ -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
|
67
|
-
# an array of integers. Yellow, for example, can be
|
68
|
-
# "#ff0", "rgb(255, 255, 0)", "hsl(60deg, 100%, 50%)",
|
69
|
-
# or [255, 255, 0].
|
70
|
-
# @param color2 [String, Array<Integer
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
|
72
|
-
passed_unit =
|
73
|
+
parameters.each_with_index do |param, i|
|
74
|
+
passed_unit = param[:unit]
|
73
75
|
|
74
|
-
unless
|
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
|
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
|
184
|
+
def initialize(parsed_value)
|
178
185
|
@scheme = parsed_value[:scheme]
|
179
186
|
@params = parsed_value[:parameters]
|
180
|
-
@source =
|
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
|
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
|
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
|
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(
|
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
|
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(
|
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
|
254
|
-
when Scheme::HSL
|
255
|
-
Hsl.new(parsed_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
|
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 = /
|
323
|
+
SCHEME = /rgba?|hsla?|hwb/i.freeze
|
266
324
|
OPEN_PAREN = /\(/.freeze
|
267
325
|
CLOSE_PAREN = /\)/.freeze
|
268
326
|
COMMA = /,/.freeze
|
269
|
-
|
270
|
-
|
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
|
-
|
299
|
-
|
394
|
+
ErrorReporter.format_error_message(scanner, re)
|
395
|
+
end
|
300
396
|
|
301
|
-
|
302
|
-
print_error_pos!(out, color_value, scanner.charpos)
|
303
|
-
out.puts " while searching with #{re}"
|
397
|
+
private :format_error_message
|
304
398
|
|
305
|
-
|
399
|
+
def source_until_current_pos(scanner)
|
400
|
+
scanner.string[0, scanner.charpos]
|
306
401
|
end
|
307
402
|
|
308
|
-
private :
|
403
|
+
private :source_until_current_pos
|
309
404
|
|
310
|
-
def
|
311
|
-
|
312
|
-
|
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 :
|
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
|
-
|
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
|
381
|
-
|
382
|
-
|
383
|
-
|
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::
|
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 [
|
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
|
-
|
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
|
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.
|
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-
|
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: '
|
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: '
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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/
|