color_contrast_calc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix'
4
+
5
+ module ColorContrastCalc
6
+ ##
7
+ # Collection of modules that implement the main logic of instance
8
+ # methods of Color, +Color#new_*_color()+.
9
+
10
+ module Converter
11
+ using Shim unless 0.respond_to? :clamp
12
+
13
+ ##
14
+ # Values given as an array are passed to a given block, and new values
15
+ # returned from the block are rounded and clamped between 0 and 255,
16
+ # so that the resulting values can be treated as an RGB value.
17
+ #
18
+ # @param vals [Array<Numeric>] An array of three numbers
19
+ # @return [Array<Integer>] Value that can possibly be an RGB value
20
+
21
+ def self.rgb_map(vals)
22
+ if block_given?
23
+ return vals.map do |val|
24
+ new_val = yield val
25
+ new_val.round.clamp(0, 255)
26
+ end
27
+ end
28
+
29
+ vals.map {|val| val.round.clamp(0, 255) }
30
+ end
31
+
32
+ module Contrast
33
+ ##
34
+ # Return contrast adjusted RGB value of passed color.
35
+ #
36
+ # THe calculation is based on the definition found at
37
+ # https://www.w3.org/TR/filter-effects/#funcdef-contrast
38
+ # https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
39
+ # @param rgb [Array<Integer>] The Original RGB value before the adjustment
40
+ # @param ratio [Float] Adjustment ratio in percentage
41
+ # @return [Array<Integer>] Contrast adjusted RGB value
42
+
43
+ def self.calc_rgb(rgb, ratio = 100)
44
+ r = ratio.to_f
45
+ Converter.rgb_map(rgb) {|c| (c * r + 255 * (50 - r / 2)) / 100 }
46
+ end
47
+ end
48
+
49
+ module Brightness
50
+ ##
51
+ # Return brightness adjusted RGB value of passed color.
52
+ #
53
+ # THe calculation is based on the definition found at
54
+ # https://www.w3.org/TR/filter-effects/#funcdef-brightness
55
+ # https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
56
+ # @param rgb [Array<Integer>] The Original RGB value before the adjustment
57
+ # @param ratio [Float] Adjustment ratio in percentage
58
+ # @return [Array<Integer>] Brightness adjusted RGB value
59
+
60
+ def self.calc_rgb(rgb, ratio = 100)
61
+ r = ratio.to_f
62
+ Converter.rgb_map(rgb) {|c| c * r / 100 }
63
+ end
64
+ end
65
+
66
+ module Invert
67
+ ##
68
+ # Return an inverted RGB value of passed color.
69
+ #
70
+ # The calculation is based on the definition found at
71
+ # https://www.w3.org/TR/filter-effects/#funcdef-invert
72
+ # https://www.w3.org/TR/filter-effects-1/#invertEquivalent
73
+ # https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
74
+ # @param rgb [Array<Integer>] The Original RGB value before the inversion
75
+ # @param ratio [Float] Proportion of the conversion in percentage
76
+ # @return [Array<Integer>] Inverted RGB value
77
+
78
+ def self.calc_rgb(rgb, ratio)
79
+ r = ratio.to_f
80
+ rgb.map {|c| ((100 * c - 2 * c * r + 255 * r) / 100).round }
81
+ end
82
+ end
83
+
84
+ module HueRotate
85
+ # @private
86
+ CONST_PART = Matrix[[0.213, 0.715, 0.072],
87
+ [0.213, 0.715, 0.072],
88
+ [0.213, 0.715, 0.072]]
89
+
90
+ # @private
91
+ COS_PART = Matrix[[0.787, -0.715, -0.072],
92
+ [-0.213, 0.285, -0.072],
93
+ [-0.213, -0.715, 0.928]]
94
+
95
+ # @private
96
+ SIN_PART = Matrix[[-0.213, -0.715, 0.928],
97
+ [0.143, 0.140, -0.283],
98
+ [-0.787, 0.715, 0.072]]
99
+
100
+ ##
101
+ # Return a hue rotation applied RGB value of passed color.
102
+ #
103
+ # THe calculation is based on the definition found at
104
+ # https://www.w3.org/TR/filter-effects/#funcdef-hue-rotate
105
+ # https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
106
+ # @param rgb [Array<Integer>] The Original RGB value before the rotation
107
+ # @param deg [Float] Degrees of rotation (0 to 360)
108
+ # @return [Array<Integer>] Hue rotation applied RGB value
109
+
110
+ def self.calc_rgb(rgb, deg)
111
+ Converter.rgb_map((calc_rotation(deg) * Vector[*rgb]).to_a)
112
+ end
113
+
114
+ def self.deg_to_rad(deg)
115
+ Math::PI * deg / 180
116
+ end
117
+
118
+ private_class_method :deg_to_rad
119
+
120
+ def self.calc_rotation(deg)
121
+ rad = deg_to_rad(deg)
122
+ cos_part = COS_PART * Math.cos(rad)
123
+ sin_part = SIN_PART * Math.sin(rad)
124
+ CONST_PART + cos_part + sin_part
125
+ end
126
+
127
+ private_class_method :calc_rotation
128
+ end
129
+
130
+ module Saturate
131
+ # @private
132
+ CONST_PART = HueRotate::CONST_PART
133
+ # @private
134
+ SATURATE_PART = HueRotate::COS_PART
135
+
136
+ ##
137
+ # Return a saturated RGB value of passed color.
138
+ #
139
+ # The calculation is based on the definition found at
140
+ # https://www.w3.org/TR/filter-effects/#funcdef-saturate
141
+ # https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
142
+ # @param rgb [Array<Integer>] The Original RGB value before the saturation
143
+ # @param s [Float] Proprtion of the conversion in percentage
144
+ # @return [Array<Integer>] Saturated RGB value
145
+
146
+ def self.calc_rgb(rgb, s)
147
+ Converter.rgb_map((calc_saturation(s) * Vector[*rgb]).to_a)
148
+ end
149
+
150
+ def self.calc_saturation(s)
151
+ CONST_PART + SATURATE_PART * (s.to_f / 100)
152
+ end
153
+
154
+ private_class_method :calc_saturation
155
+ end
156
+
157
+ module Grayscale
158
+ # @private
159
+ CONST_PART = Matrix[[0.2126, 0.7152, 0.0722],
160
+ [0.2126, 0.7152, 0.0722],
161
+ [0.2126, 0.7152, 0.0722]]
162
+
163
+ # @private
164
+ RATIO_PART = Matrix[[0.7874, -0.7152, -0.0722],
165
+ [-0.2126, 0.2848, -0.0722],
166
+ [-0.2126, -0.7152, 0.9278]]
167
+
168
+ ##
169
+ # Convert passed a passed color to grayscale.
170
+ #
171
+ # The calculation is based on the definition found at
172
+ # https://www.w3.org/TR/filter-effects/#funcdef-grayscale
173
+ # https://www.w3.org/TR/filter-effects/#grayscaleEquivalent
174
+ # https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
175
+ # @param rgb [Array<Integer>] The Original RGB value before the conversion
176
+ # @param s [Float] Conversion ratio in percentage
177
+ # @return [Array<Integer>] RGB value of grayscale color
178
+
179
+ def self.calc_rgb(rgb, s)
180
+ Converter.rgb_map((calc_grayscale(s) * Vector[*rgb]).to_a)
181
+ end
182
+
183
+ def self.calc_grayscale(s)
184
+ r = 1 - [100, s].min.to_f / 100
185
+ CONST_PART + RATIO_PART * r
186
+ end
187
+
188
+ private_class_method :calc_grayscale
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,149 @@
1
+ [
2
+ ["aliceblue", "#f0f8ff"],
3
+ ["antiquewhite", "#faebd7"],
4
+ ["aqua", "#00ffff"],
5
+ ["aquamarine", "#7fffd4"],
6
+ ["azure", "#f0ffff"],
7
+ ["beige", "#f5f5dc"],
8
+ ["bisque", "#ffe4c4"],
9
+ ["black", "#000000"],
10
+ ["blanchedalmond", "#ffebcd"],
11
+ ["blue", "#0000ff"],
12
+ ["blueviolet", "#8a2be2"],
13
+ ["brown", "#a52a2a"],
14
+ ["burlywood", "#deb887"],
15
+ ["cadetblue", "#5f9ea0"],
16
+ ["chartreuse", "#7fff00"],
17
+ ["chocolate", "#d2691e"],
18
+ ["coral", "#ff7f50"],
19
+ ["cornflowerblue", "#6495ed"],
20
+ ["cornsilk", "#fff8dc"],
21
+ ["crimson", "#dc143c"],
22
+ ["cyan", "#00ffff"],
23
+ ["darkblue", "#00008b"],
24
+ ["darkcyan", "#008b8b"],
25
+ ["darkgoldenrod", "#b8860b"],
26
+ ["darkgray", "#a9a9a9"],
27
+ ["darkgreen", "#006400"],
28
+ ["darkgrey", "#a9a9a9"],
29
+ ["darkkhaki", "#bdb76b"],
30
+ ["darkmagenta", "#8b008b"],
31
+ ["darkolivegreen", "#556b2f"],
32
+ ["darkorange", "#ff8c00"],
33
+ ["darkorchid", "#9932cc"],
34
+ ["darkred", "#8b0000"],
35
+ ["darksalmon", "#e9967a"],
36
+ ["darkseagreen", "#8fbc8f"],
37
+ ["darkslateblue", "#483d8b"],
38
+ ["darkslategray", "#2f4f4f"],
39
+ ["darkslategrey", "#2f4f4f"],
40
+ ["darkturquoise", "#00ced1"],
41
+ ["darkviolet", "#9400d3"],
42
+ ["deeppink", "#ff1493"],
43
+ ["deepskyblue", "#00bfff"],
44
+ ["dimgray", "#696969"],
45
+ ["dimgrey", "#696969"],
46
+ ["dodgerblue", "#1e90ff"],
47
+ ["firebrick", "#b22222"],
48
+ ["floralwhite", "#fffaf0"],
49
+ ["forestgreen", "#228b22"],
50
+ ["fuchsia", "#ff00ff"],
51
+ ["gainsboro", "#dcdcdc"],
52
+ ["ghostwhite", "#f8f8ff"],
53
+ ["gold", "#ffd700"],
54
+ ["goldenrod", "#daa520"],
55
+ ["gray", "#808080"],
56
+ ["green", "#008000"],
57
+ ["greenyellow", "#adff2f"],
58
+ ["grey", "#808080"],
59
+ ["honeydew", "#f0fff0"],
60
+ ["hotpink", "#ff69b4"],
61
+ ["indianred", "#cd5c5c"],
62
+ ["indigo", "#4b0082"],
63
+ ["ivory", "#fffff0"],
64
+ ["khaki", "#f0e68c"],
65
+ ["lavender", "#e6e6fa"],
66
+ ["lavenderblush", "#fff0f5"],
67
+ ["lawngreen", "#7cfc00"],
68
+ ["lemonchiffon", "#fffacd"],
69
+ ["lightblue", "#add8e6"],
70
+ ["lightcoral", "#f08080"],
71
+ ["lightcyan", "#e0ffff"],
72
+ ["lightgoldenrodyellow", "#fafad2"],
73
+ ["lightgray", "#d3d3d3"],
74
+ ["lightgreen", "#90ee90"],
75
+ ["lightgrey", "#d3d3d3"],
76
+ ["lightpink", "#ffb6c1"],
77
+ ["lightsalmon", "#ffa07a"],
78
+ ["lightseagreen", "#20b2aa"],
79
+ ["lightskyblue", "#87cefa"],
80
+ ["lightslategray", "#778899"],
81
+ ["lightslategrey", "#778899"],
82
+ ["lightsteelblue", "#b0c4de"],
83
+ ["lightyellow", "#ffffe0"],
84
+ ["lime", "#00ff00"],
85
+ ["limegreen", "#32cd32"],
86
+ ["linen", "#faf0e6"],
87
+ ["magenta", "#ff00ff"],
88
+ ["maroon", "#800000"],
89
+ ["mediumaquamarine", "#66cdaa"],
90
+ ["mediumblue", "#0000cd"],
91
+ ["mediumorchid", "#ba55d3"],
92
+ ["mediumpurple", "#9370db"],
93
+ ["mediumseagreen", "#3cb371"],
94
+ ["mediumslateblue", "#7b68ee"],
95
+ ["mediumspringgreen", "#00fa9a"],
96
+ ["mediumturquoise", "#48d1cc"],
97
+ ["mediumvioletred", "#c71585"],
98
+ ["midnightblue", "#191970"],
99
+ ["mintcream", "#f5fffa"],
100
+ ["mistyrose", "#ffe4e1"],
101
+ ["moccasin", "#ffe4b5"],
102
+ ["navajowhite", "#ffdead"],
103
+ ["navy", "#000080"],
104
+ ["oldlace", "#fdf5e6"],
105
+ ["olive", "#808000"],
106
+ ["olivedrab", "#6b8e23"],
107
+ ["orange", "#ffa500"],
108
+ ["orangered", "#ff4500"],
109
+ ["orchid", "#da70d6"],
110
+ ["palegoldenrod", "#eee8aa"],
111
+ ["palegreen", "#98fb98"],
112
+ ["paleturquoise", "#afeeee"],
113
+ ["palevioletred", "#db7093"],
114
+ ["papayawhip", "#ffefd5"],
115
+ ["peachpuff", "#ffdab9"],
116
+ ["peru", "#cd853f"],
117
+ ["pink", "#ffc0cb"],
118
+ ["plum", "#dda0dd"],
119
+ ["powderblue", "#b0e0e6"],
120
+ ["purple", "#800080"],
121
+ ["red", "#ff0000"],
122
+ ["rosybrown", "#bc8f8f"],
123
+ ["royalblue", "#4169e1"],
124
+ ["saddlebrown", "#8b4513"],
125
+ ["salmon", "#fa8072"],
126
+ ["sandybrown", "#f4a460"],
127
+ ["seagreen", "#2e8b57"],
128
+ ["seashell", "#fff5ee"],
129
+ ["sienna", "#a0522d"],
130
+ ["silver", "#c0c0c0"],
131
+ ["skyblue", "#87ceeb"],
132
+ ["slateblue", "#6a5acd"],
133
+ ["slategray", "#708090"],
134
+ ["slategrey", "#708090"],
135
+ ["snow", "#fffafa"],
136
+ ["springgreen", "#00ff7f"],
137
+ ["steelblue", "#4682b4"],
138
+ ["tan", "#d2b48c"],
139
+ ["teal", "#008080"],
140
+ ["thistle", "#d8bfd8"],
141
+ ["tomato", "#ff6347"],
142
+ ["turquoise", "#40e0d0"],
143
+ ["violet", "#ee82ee"],
144
+ ["wheat", "#f5deb3"],
145
+ ["white", "#ffffff"],
146
+ ["whitesmoke", "#f5f5f5"],
147
+ ["yellow", "#ffff00"],
148
+ ["yellowgreen", "#9acd32"]
149
+ ]
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ColorContrastCalc
4
+ ##
5
+ # Provide methods that are not availabe in older versions of Ruby.
6
+
7
+ module Shim
8
+ refine Regexp do
9
+ ##
10
+ # Regexp.match?() is available for Ruby >= 2.4,
11
+ # and the following implementation does not satisfy
12
+ # the full specification of the original method.
13
+
14
+ def match?(str)
15
+ self === str
16
+ end
17
+ end
18
+
19
+ refine Numeric do
20
+ ##
21
+ # Comparable#clamp() is available for Ruby >= 2.4,
22
+ # and the following implementation does not satisfy
23
+ # the full specification of the original method.
24
+
25
+ def clamp(lower_bound, upper_bound)
26
+ return lower_bound if self < lower_bound
27
+ return upper_bound if self > upper_bound
28
+ self
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_contrast_calc/color'
4
+
5
+ module ColorContrastCalc
6
+ ##
7
+ # Provide two methods sort() and compile_compare_function()
8
+ #
9
+ # The other methods defined in this module should not be considered
10
+ # as stable interfaces.
11
+
12
+ module Sorter
13
+ using Shim unless //.respond_to? :match?
14
+
15
+ # @private The visitiblity ot this module is not changed just
16
+ # because it is used in unit tests.
17
+
18
+ module ColorComponent
19
+ RGB = 'rgb'.chars
20
+ HSL = 'hsl'.chars
21
+ end
22
+
23
+ module CompFunc
24
+ # @private
25
+ ASCEND = proc {|x, y| x <=> y }
26
+ # @private
27
+ DESCEND = proc {|x, y| y <=> x }
28
+ end
29
+
30
+ private_constant :CompFunc
31
+
32
+ ##
33
+ # Constants used as a second argeument of Sorter.compile_compare_function()
34
+ #
35
+ # The constants COLOR, COMPONENTS and HEX are expected to be used as a
36
+ # second argument of Sorter.compile_compare_function()
37
+
38
+ module KeyTypes
39
+ # The function returned by Sorter.compile_compare_function() expects
40
+ # instances of Color as key values when this constants is specified.
41
+ COLOR = :color
42
+ # The function returned by Sorter.compile_compare_function() expects
43
+ # RGB or HSL values as key values when this constants is specified.
44
+ COMPONENTS = :components
45
+ # The function returned by Sorter.compile_compare_function() expects
46
+ # hex color codes as key values when this constants is specified.
47
+ HEX = :hex
48
+ # @private
49
+ CLASS_TO_TYPE = {
50
+ Color => COLOR,
51
+ Array => COMPONENTS,
52
+ String => HEX
53
+ }.freeze
54
+
55
+ ##
56
+ # Returns COLOR, COMPONENTS or HEX when a possible key value is passed.
57
+ #
58
+ # @param color [Color, Array<Numeric>, String] Possible key value
59
+ # @param key_mapper [Proc] Function which retrieves a key value from
60
+ # +color+, that means <tt>key_mapper[color]</tt> returns a key value
61
+ # @return [:color, :components, :hex] Symbol that represents a key type
62
+
63
+ def self.guess(color, key_mapper = nil)
64
+ key = key_mapper ? key_mapper[color] : color
65
+ CLASS_TO_TYPE[key.class]
66
+ end
67
+ end
68
+
69
+ # @private shorthands for Utils.hex_to_rgb() and .hex_to_hsl()
70
+ HEX_TO_COMPONENTS = {
71
+ rgb: Utils.method(:hex_to_rgb),
72
+ hsl: Utils.method(:hex_to_hsl)
73
+ }.freeze
74
+
75
+ ##
76
+ # Sort colors in the order specified by +color_order+.
77
+ #
78
+ # Sort colors given as a list or tuple of Color instances or hex
79
+ # color codes.
80
+ #
81
+ # You can specify sorting order by giving a +color_order+ tring, such
82
+ # as "HSL" or "RGB". A component of +color_order+ on the left side
83
+ # has a higher sorting precedence, and an uppercase letter means
84
+ # descending order.
85
+ # @param colors [Array<Color>, Array<String>] Array of Color instances
86
+ # or items from which color hex codes can be retrieved.
87
+ # @param color_order [String] String such as "HSL", "RGB" or "lsH"
88
+ # @param key_mapper [Proc, nil] Proc object used to retrive key values
89
+ # from items to be sorted
90
+ # @return [Array<Color>, Array<String>] Array of of sorted colors
91
+
92
+ def self.sort(colors, color_order = 'hSL', key_mapper = nil)
93
+ key_type = KeyTypes.guess(colors[0], key_mapper)
94
+ compare = compile_compare_function(color_order, key_type, key_mapper)
95
+
96
+ colors.sort(&compare)
97
+ end
98
+
99
+ ##
100
+ # Return a Proc object to be passed to Array#sort().
101
+ #
102
+ # @param color_order [String] String such as "HSL", "RGB" or "lsH"
103
+ # @param key_type [Symbol] +:color+, +:components+ or +:hex+
104
+ # @param key_mapper [Proc, nil] Proc object to be used to retrive
105
+ # key values from items to be sorted.
106
+ # @return [Proc] Proc object to be passed to Array#sort()
107
+
108
+ def self.compile_compare_function(color_order, key_type, key_mapper = nil)
109
+ case key_type
110
+ when KeyTypes::COLOR
111
+ compare = compile_color_compare_function(color_order)
112
+ when KeyTypes::COMPONENTS
113
+ compare = compile_components_compare_function(color_order)
114
+ when KeyTypes::HEX
115
+ compare = compile_hex_compare_function(color_order)
116
+ end
117
+
118
+ compose_function(compare, key_mapper)
119
+ end
120
+
121
+ # @private
122
+
123
+ def self.compose_function(compare_function, key_mapper = nil)
124
+ return compare_function unless key_mapper
125
+
126
+ proc do |color1, color2|
127
+ compare_function[key_mapper[color1], key_mapper[color2]]
128
+ end
129
+ end
130
+
131
+ # @private
132
+
133
+ def self.color_component_pos(color_order, ordered_components)
134
+ color_order.downcase.chars.map do |component|
135
+ ordered_components.index(component)
136
+ end
137
+ end
138
+
139
+ # @private
140
+
141
+ def self.parse_color_order(color_order)
142
+ ordered_components = ColorComponent::RGB
143
+ ordered_components = ColorComponent::HSL if hsl_order?(color_order)
144
+ pos = color_component_pos(color_order, ordered_components)
145
+ funcs = []
146
+ pos.each_with_index do |ci, i|
147
+ c = color_order[i]
148
+ funcs[ci] = Utils.uppercase?(c) ? CompFunc::DESCEND : CompFunc::ASCEND
149
+ end
150
+ { pos: pos, funcs: funcs }
151
+ end
152
+
153
+ # @private
154
+
155
+ def self.hsl_order?(color_order)
156
+ /[hsl]{3}/i.match?(color_order)
157
+ end
158
+
159
+ # @private
160
+
161
+ def self.compare_color_components(color1, color2, order)
162
+ funcs = order[:funcs]
163
+ order[:pos].each do |i|
164
+ result = funcs[i][color1[i], color2[i]]
165
+ return result unless result.zero?
166
+ end
167
+
168
+ 0
169
+ end
170
+
171
+ # @private
172
+
173
+ def self.compile_components_compare_function(color_order)
174
+ order = parse_color_order(color_order)
175
+
176
+ proc do |color1, color2|
177
+ compare_color_components(color1, color2, order)
178
+ end
179
+ end
180
+
181
+ # @private
182
+
183
+ def self.compile_hex_compare_function(color_order)
184
+ order = parse_color_order(color_order)
185
+ converter = HEX_TO_COMPONENTS[:rgb]
186
+ converter = HEX_TO_COMPONENTS[:hsl] if hsl_order?(color_order)
187
+ cache = {}
188
+
189
+ proc do |hex1, hex2|
190
+ color1 = hex_to_components(hex1, converter, cache)
191
+ color2 = hex_to_components(hex2, converter, cache)
192
+
193
+ compare_color_components(color1, color2, order)
194
+ end
195
+ end
196
+
197
+ # @private
198
+
199
+ def self.hex_to_components(hex, converter, cache)
200
+ cached_components = cache[hex]
201
+ return cached_components if cached_components
202
+
203
+ components = converter[hex]
204
+ cache[hex] = components
205
+
206
+ components
207
+ end
208
+
209
+ private_class_method :hex_to_components
210
+
211
+ # @private
212
+
213
+ def self.compile_color_compare_function(color_order)
214
+ order = parse_color_order(color_order)
215
+
216
+ if hsl_order?(color_order)
217
+ proc do |color1, color2|
218
+ compare_color_components(color1.hsl, color2.hsl, order)
219
+ end
220
+ else
221
+ proc do |color1, color2|
222
+ compare_color_components(color1.rgb, color2.rgb, order)
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end