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