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,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'color_contrast_calc'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require 'pry'
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'color_contrast_calc/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'color_contrast_calc'
9
+ spec.version = ColorContrastCalc::VERSION
10
+ spec.authors = ['HASHIMOTO, Naoki']
11
+ spec.email = ['hashimoto.naoki@gmail.com']
12
+
13
+ spec.summary = 'Utility that helps you choose colors with sufficient contrast, WCAG 2.0 in mind'
14
+ # spec.description = %q(TODO: Write a longer description or delete this line.)
15
+ spec.homepage = 'https://github.com/nico-hn/color_contrast_calc_rb/'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = 'exe'
22
+ spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.15'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'rubocop', '~> 0.49.1'
29
+ spec.add_development_dependency 'yard', '~> 0.9.9'
30
+ end
@@ -0,0 +1,12 @@
1
+ require 'color_contrast_calc'
2
+
3
+ require 'color_contrast_calc'
4
+
5
+ # Create an instance of Color from a hex code
6
+ # (You can pass 'red' or [255, 0, 0] instead of '#ff0000')
7
+ red = ColorContrastCalc.color_from('#ff0000')
8
+ puts red.class
9
+ puts red.name
10
+ puts red.hex
11
+ puts red.rgb.to_s
12
+ puts red.hsl.to_s
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'color_contrast_calc'
4
+
5
+ # Named colors
6
+ named_colors = ColorContrastCalc.named_colors
7
+
8
+ puts("The number of named colors: #{named_colors.size}")
9
+ puts("The first of named colors: #{named_colors[0].name}")
10
+ puts("The last of named colors: #{named_colors[-1].name}")
11
+
12
+ # Web safe colors
13
+ web_safe_colors = ColorContrastCalc.web_safe_colors
14
+
15
+ puts("The number of web safe colors: #{web_safe_colors.size}")
16
+ puts("The first of web safe colors: #{web_safe_colors[0].name}")
17
+ puts("The last of web safe colors: #{web_safe_colors[-1].name}")
18
+
19
+ # HSL colors
20
+ hsl_colors = ColorContrastCalc.hsl_colors
21
+
22
+ puts("The number of HSL colors: #{hsl_colors.size}")
23
+ puts("The first of HSL colors: #{hsl_colors[0].name}")
24
+ puts("The 60th of HSL colors: #{hsl_colors[60].name}")
25
+ puts("The 120th of HSL colors: #{hsl_colors[120].name}")
26
+ puts("The last of HSL colors: #{hsl_colors[-1].name}")
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'color_contrast_calc'
4
+
5
+ yellow = ColorContrastCalc.color_from('yellow')
6
+ orange = ColorContrastCalc.color_from('orange')
7
+
8
+ report = 'The grayscale of %s is %s.'
9
+ puts(format(report, yellow.hex, yellow.new_grayscale_color))
10
+ puts(format(report, orange.hex, orange.new_grayscale_color))
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'color_contrast_calc'
4
+
5
+ color_names = ['red', 'yellow', 'lime', 'cyan', 'fuchsia', 'blue']
6
+ colors = color_names.map {|c| ColorContrastCalc.color_from(c) }
7
+
8
+ # sort by hSL order. An uppercase for a component of color means
9
+ # that component should be sorted in descending order.
10
+
11
+ hsl_ordered = ColorContrastCalc::Sorter.sort(colors, 'hSL')
12
+ puts("Colors sorted in the order of hSL: #{hsl_ordered.map(&:name)}")
13
+
14
+ # sort by RGB order.
15
+
16
+ rgb_ordered = ColorContrastCalc::Sorter.sort(colors, 'RGB')
17
+ puts("Colors sorted in the order of RGB: #{rgb_ordered.map(&:name)}")
18
+
19
+ # You can also change the precedence of components.
20
+
21
+ grb_ordered = ColorContrastCalc::Sorter.sort(colors, 'GRB')
22
+ puts("Colors sorted in the order of GRB: #{grb_ordered.map(&:name)}")
23
+
24
+ # And you can directly sort hex color codes.
25
+
26
+ ## Hex color codes that correspond to the color_names given above.
27
+ hex_codes = ['#ff0000', '#ff0', '#00ff00', '#0ff', '#f0f', '#0000FF']
28
+
29
+ hsl_ordered = ColorContrastCalc::Sorter.sort(hex_codes, 'hSL')
30
+ puts("Colors sorted in the order of hSL: #{hsl_ordered}")
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'color_contrast_calc'
4
+
5
+ yellow = ColorContrastCalc.color_from('yellow')
6
+ black = ColorContrastCalc.color_from('black')
7
+
8
+ contrast_ratio = yellow.contrast_ratio_against(black)
9
+
10
+ report = 'The contrast ratio between %s and %s is %2.4f'
11
+ puts(format(report, yellow.name, black.name, contrast_ratio))
12
+ puts(format(report, yellow.hex, black.hex, contrast_ratio))
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'color_contrast_calc'
4
+
5
+ yellow = ColorContrastCalc.color_from('yellow')
6
+ orange = ColorContrastCalc.color_from('orange')
7
+
8
+ report = 'The contrast ratio between %s and %s is %2.4f'
9
+
10
+ # Find brightness adjusted colors.
11
+
12
+ a_orange = yellow.find_brightness_threshold(orange, 'A')
13
+ a_contrast_ratio = yellow.contrast_ratio_against(a_orange)
14
+
15
+ aa_orange = yellow.find_brightness_threshold(orange, 'AA')
16
+ aa_contrast_ratio = yellow.contrast_ratio_against(aa_orange)
17
+
18
+ puts('# Brightness adjusted colors')
19
+ puts(format(report, yellow.hex, a_orange.hex, a_contrast_ratio))
20
+ puts(format(report, yellow.hex, aa_orange.hex, aa_contrast_ratio))
21
+
22
+ # Find lightness adjusted colors.
23
+
24
+ a_orange = yellow.find_lightness_threshold(orange, 'A')
25
+ a_contrast_ratio = yellow.contrast_ratio_against(a_orange)
26
+
27
+ aa_orange = yellow.find_lightness_threshold(orange, 'AA')
28
+ aa_contrast_ratio = yellow.contrast_ratio_against(aa_orange)
29
+
30
+ puts('# Lightness adjusted colors')
31
+ puts(format(report, yellow.hex, a_orange.hex, a_contrast_ratio))
32
+ puts(format(report, yellow.hex, aa_orange.hex, aa_contrast_ratio))
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'color_contrast_calc'
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_contrast_calc/version'
4
+ require 'color_contrast_calc/utils'
5
+ require 'color_contrast_calc/converter'
6
+ require 'color_contrast_calc/checker'
7
+ require 'color_contrast_calc/threshold_finder'
8
+ require 'color_contrast_calc/color'
9
+ require 'color_contrast_calc/sorter'
10
+
11
+ module ColorContrastCalc
12
+ ##
13
+ # Error raised if creating a Color instance with invalid value.
14
+
15
+ class InvalidColorRepresentationError < StandardError; end
16
+
17
+ ##
18
+ # Return an instance of Color.
19
+ #
20
+ # As +color_value+, you can pass a predefined color name, or an
21
+ # RGB value represented as an array of integers or a hex code such
22
+ # as [255, 255, 0] or "#ffff00". +name+ is assigned to the returned
23
+ # instance if it does not have a name already assigned.
24
+ # @param color_value [String, Array<Integer>] Name of a predefined
25
+ # color or RGB value
26
+ # @param name [String] Unless the instance has predefined name, the
27
+ # name passed to the method is set to self.name
28
+ # @return [Color] Instance of Color
29
+
30
+ def self.color_from(color_value, name = nil)
31
+ error_message = 'A color should be given as an array or string.'
32
+
33
+ if !color_value.is_a?(String) && !color_value.is_a?(Array)
34
+ raise InvalidColorRepresentationError, error_message
35
+ end
36
+
37
+ return color_from_rgb(color_value, name) if color_value.is_a?(Array)
38
+ color_from_str(color_value, name)
39
+ end
40
+
41
+ ##
42
+ # Return an array of named colors.
43
+ #
44
+ # You can find the color names at
45
+ # https://www.w3.org/TR/SVG/types.html#ColorKeywords
46
+ # @param frozen [true|false] Set to false if you want an unfrozen array.
47
+ # @return [Array<Color>] Named colors
48
+
49
+ def self.named_colors(frozen: true)
50
+ named_colors = Color::List::NAMED_COLORS
51
+ frozen ? named_colors : named_colors.dup
52
+ end
53
+
54
+ ##
55
+ # Return an array of web safe colors.
56
+ #
57
+ # @param frozen [true|false] Set to false if you want an unfrozen array.
58
+ # @return [Array<Color>] Web safe colors
59
+
60
+ def self.web_safe_colors(frozen: true)
61
+ colors = Color::List::WEB_SAFE_COLORS
62
+ frozen ? colors : colors.dup
63
+ end
64
+
65
+ ##
66
+ # Return a list of colors which share the same saturation and lightness.
67
+ #
68
+ # By default, so-called pure colors are returned.
69
+ # @param s 100 [Float] Ratio of saturation in percentage
70
+ # @param l 50 [Float] Ratio of lightness in percentage
71
+ # @param h_interval 1 [Integer] Interval of hues in degrees.
72
+ # By default, the method returns 360 hues beginning from red.
73
+ # @return [Array<Color>] Array of colors which share the same
74
+ # saturation and lightness
75
+
76
+ def self.hsl_colors(s: 100, l: 50, h_interval: 1)
77
+ Color::List.hsl_colors(s: s, l: l, h_interval: h_interval)
78
+ end
79
+
80
+ def self.color_from_rgb(color_value, name = nil)
81
+ error_message = 'An RGB value should be given in form of [r, g, b].'
82
+
83
+ unless Utils.valid_rgb?(color_value)
84
+ raise InvalidColorRepresentationError, error_message
85
+ end
86
+
87
+ hex_code = Utils.rgb_to_hex(color_value)
88
+ Color::List::HEX_TO_COLOR[hex_code] || Color.new(color_value, name)
89
+ end
90
+
91
+ private_class_method :color_from_rgb
92
+
93
+ def self.color_from_str(color_value, name = nil)
94
+ error_message = 'A hex code is in form of "#xxxxxx" where 0 <= x <= f.'
95
+
96
+ named_color = Color::List::NAME_TO_COLOR[color_value]
97
+ return named_color if named_color
98
+
99
+ unless Utils.valid_hex?(color_value)
100
+ raise InvalidColorRepresentationError, error_message
101
+ end
102
+
103
+ hex_code = Utils.normalize_hex(color_value)
104
+ Color::List::HEX_TO_COLOR[hex_code] || Color.new(hex_code, name)
105
+ end
106
+
107
+ private_class_method :color_from_str
108
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_contrast_calc/utils'
4
+
5
+ module ColorContrastCalc
6
+ ##
7
+ # Utility to check properties of given colors.
8
+ #
9
+ # This module provides functions that check the relative luminance and
10
+ # contrast ratio of colors. A color is given as RGB value (represented
11
+ # as a tuple of integers) or a hex color code such "#ffff00".
12
+
13
+ module Checker
14
+ ##
15
+ # Collection of constants that correspond to the three levels of success
16
+ # criteria defined in WCAG 2.0. You will find more information at
17
+ # https://www.w3.org/TR/UNDERSTANDING-WCAG20/conformance.html#uc-conf-req1-head
18
+
19
+ module Level
20
+ # The minimum level of Conformance
21
+ A = 'A'
22
+ # Organizations in the public sector generally adopt this level.
23
+ AA = 'AA'
24
+ # This level seems to be hard to satisfy.
25
+ AAA = 'AAA'
26
+ end
27
+
28
+ LEVEL_TO_RATIO = {
29
+ Level::AAA => 7,
30
+ Level::AA => 4.5,
31
+ Level::A => 3,
32
+ 3 => 7,
33
+ 2 => 4.5,
34
+ 1 => 3
35
+ }.freeze
36
+
37
+ private_constant :LEVEL_TO_RATIO
38
+
39
+ ##
40
+ # Calculate the relative luminance of a RGB color.
41
+ #
42
+ # The definition of relative luminance is given at
43
+ # {https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef}
44
+ # @param rgb [String, Array<Integer>] RGB color given as a string or
45
+ # an array of integers. Yellow, for example, can be given as "#ffff00"
46
+ # or [255, 255, 0].
47
+ # @return [Float] Relative luminance of the passed color.
48
+
49
+ def self.relative_luminance(rgb = [255, 255, 255])
50
+ # https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
51
+
52
+ rgb = Utils.hex_to_rgb(rgb) if rgb.is_a? String
53
+ r, g, b = rgb.map {|c| tristimulus_value(c) }
54
+ r * 0.2126 + g * 0.7152 + b * 0.0722
55
+ end
56
+
57
+ ##
58
+ # Calculate the contrast ratio of given colors.
59
+ #
60
+ # The definition of contrast ratio is given at
61
+ # {https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef}
62
+ # @param color1 [String, Array<Integer>] RGB color given as a string or
63
+ # an array of integers. Yellow, for example, can be given as "#ffff00"
64
+ # or [255, 255, 0].
65
+ # @param color2 [String, Array<Integer>] RGB color given as a string or
66
+ # an array of integers. Yellow, for example, can be given as "#ffff00"
67
+ # or [255, 255, 0].
68
+ # @return [Float] Contrast ratio
69
+
70
+ def self.contrast_ratio(color1, color2)
71
+ # https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
72
+
73
+ luminance_to_contrast_ratio(relative_luminance(color1),
74
+ relative_luminance(color2))
75
+ end
76
+
77
+ ##
78
+ # Calculate contrast ratio from a pair of relative luminance.
79
+ #
80
+ # @param luminance1 [Float] Relative luminance
81
+ # @param luminance2 [Float] Relative luminance
82
+ # @return [Float] Contrast ratio
83
+
84
+ def self.luminance_to_contrast_ratio(luminance1, luminance2)
85
+ l1, l2 = *([luminance1, luminance2].sort {|c1, c2| c2 <=> c1 })
86
+ (l1 + 0.05) / (l2 + 0.05)
87
+ end
88
+
89
+ def self.tristimulus_value(primary_color, base = 255)
90
+ s = primary_color.to_f / base
91
+ s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055)**2.4
92
+ end
93
+
94
+ private_class_method :tristimulus_value
95
+
96
+ ##
97
+ # Rate a given contrast ratio according to the WCAG 2.0 criteria.
98
+ #
99
+ # The success criteria are given at
100
+ # * {https://www.w3.org/TR/WCAG20/#visual-audio-contrast}
101
+ # * {https://www.w3.org/TR/WCAG20-TECHS/G183.html}
102
+ #
103
+ # N.B. The size of text is not taken into consideration.
104
+ # @param ratio [Float] Contrast Ratio
105
+ # @return [String] If one of criteria is satisfied, "A", "AA" or "AAA",
106
+ # otherwise "-"
107
+
108
+ def self.ratio_to_level(ratio)
109
+ return Level::AAA if ratio >= 7
110
+ return Level::AA if ratio >= 4.5
111
+ return Level::A if ratio >= 3
112
+ '-'
113
+ end
114
+
115
+ ##
116
+ # Return a contrast ratio required to meet a given WCAG 2.0 level.
117
+ #
118
+ # N.B. The size of text is not taken into consideration.
119
+ # @param level [String] "A", "AA" or "AAA"
120
+ # @return [Float] Contrast ratio
121
+
122
+ def self.level_to_ratio(level)
123
+ LEVEL_TO_RATIO[level]
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,409 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_contrast_calc/utils'
4
+ require 'color_contrast_calc/checker'
5
+ require 'color_contrast_calc/threshold_finder'
6
+ require 'json'
7
+
8
+ module ColorContrastCalc
9
+ ##
10
+ # Represent specific colors.
11
+ #
12
+ # This class also provides lists of predefined colors represented as
13
+ # instances of Color class.
14
+
15
+ class Color
16
+ # @private
17
+ RGB_LIMITS = [0, 255].freeze
18
+
19
+ ##
20
+ # @!attribute [r] rgb
21
+ # @return [Array<Integer>] RGB value of the color
22
+ # @!attribute [r] hex
23
+ # @return [String] Hex color code of the color
24
+ # @!attribute [r] name
25
+ # @return [String] Name of the color
26
+ # @!attribute [r] relative_luminance
27
+ # @return [Float] Relative luminance of the color
28
+
29
+ attr_reader :rgb, :hex, :name, :relative_luminance
30
+
31
+ ##
32
+ # Return an instance of Color for a predefined color name.
33
+ #
34
+ # Color names are defined at
35
+ # * {https://www.w3.org/TR/SVG/types.html#ColorKeywords}
36
+ # @param name [String] Name of color
37
+ # @return [Color] Instance of Color
38
+
39
+ def self.from_name(name)
40
+ List::NAME_TO_COLOR[name.downcase]
41
+ end
42
+
43
+ ##
44
+ # Return an instance of Color for a hex color code.
45
+ #
46
+ # @param hex [String] Hex color code such as "#ffff00"
47
+ # @return [Color] Instance of Color
48
+
49
+ def self.from_hex(hex)
50
+ normalized_hex = Utils.normalize_hex(hex)
51
+ List::HEX_TO_COLOR[normalized_hex] || Color.new(normalized_hex)
52
+ end
53
+
54
+ ##
55
+ # Create an instance of Color from an HSL value.
56
+ #
57
+ # @param hsl [Float] HSL value represented as an array of numbers
58
+ # @param name [String] You can name the color to be created
59
+ # @return [Color] Instance of Color
60
+
61
+ def self.new_from_hsl(hsl, name = nil)
62
+ new(Utils.hsl_to_rgb(hsl), name)
63
+ end
64
+
65
+ ##
66
+ # Create a new instance of Color.
67
+ #
68
+ # @param rgb [Array<Integer>, String] RGB value represented as an array
69
+ # of integers or hex color code such as [255, 255, 0] or "#ffff00".
70
+ # @param name [String] You can name the color to be created.
71
+ # Without this option, the value of normalized hex color code is
72
+ # assigned instead.
73
+ # @return [Color] New instance of Color
74
+
75
+ def initialize(rgb, name = nil)
76
+ @rgb = rgb.is_a?(String) ? Utils.hex_to_rgb(rgb) : rgb
77
+ @hex = Utils.rgb_to_hex(@rgb)
78
+ @name = name || @hex
79
+ @relative_luminance = Checker.relative_luminance(@rgb)
80
+ end
81
+
82
+ ##
83
+ # Return HSL value of the color.
84
+ #
85
+ # The value is calculated from the RGB value, so if you create
86
+ # the instance by Color.new_from_hsl method, the value used to
87
+ # create the color does not necessarily correspond to the value
88
+ # of this property.
89
+ #
90
+ # @return [Array<Float>] HSL value represented as an array of numbers
91
+
92
+ def hsl
93
+ @hsl ||= Utils.rgb_to_hsl(@rgb)
94
+ end
95
+
96
+ ##
97
+ # Return a new instance of Color with adjusted contrast.
98
+ #
99
+ # @param ratio [Float] Adjustment ratio in percentage
100
+ # @param name [String] You can name the color to be created.
101
+ # Without this option, the value of normalized hex color
102
+ # code is assigned instead.
103
+ # @return [Color] New color with adjusted contrast
104
+
105
+ def new_contrast_color(ratio, name = nil)
106
+ generate_new_color(Converter::Contrast, ratio, name)
107
+ end
108
+
109
+ ##
110
+ # Return a new instance of Color with adjusted brightness.
111
+ #
112
+ # @param ratio [Float] Adjustment ratio in percentage
113
+ # @param name [String] You can name the color to be created.
114
+ # Without this option, the value of normalized hex color
115
+ # code is assigned instead.
116
+ # @return [Color] New color with adjusted brightness
117
+
118
+ def new_brightness_color(ratio, name = nil)
119
+ generate_new_color(Converter::Brightness, ratio, name)
120
+ end
121
+
122
+ ##
123
+ # Return an inverted color as an instance of Color.
124
+ #
125
+ # @param ratio [Float] Proportion of the conversion in percentage
126
+ # @param name [String] You can name the color to be created.
127
+ # Without this option, the value of normalized hex color
128
+ # code is assigned instead.
129
+ # @return [Color] New inverted color
130
+
131
+ def new_invert_color(ratio = 100, name = nil)
132
+ generate_new_color(Converter::Invert, ratio, name)
133
+ end
134
+
135
+ ##
136
+ # Return a hue rotation applied color as an instance of Color.
137
+ #
138
+ # @param degree [Float] Degrees of rotation (0 to 360)
139
+ # @param name [String] You can name the color to be created.
140
+ # Without this option, the value of normalized hex color
141
+ # code is assigned instead.
142
+ # @return [Color] New hue rotation applied color
143
+
144
+ def new_hue_rotate_color(degree, name = nil)
145
+ generate_new_color(Converter::HueRotate, degree, name)
146
+ end
147
+
148
+ ##
149
+ # Return a saturated color as an instance of Color.
150
+ #
151
+ # @param ratio [Float] Proprtion of the conversion in percentage
152
+ # @param name [String] You can name the color to be created.
153
+ # Without this option, the value of normalized hex color
154
+ # code is assigned instead.
155
+ # @return [Color] New saturated color
156
+
157
+ def new_saturate_color(ratio, name = nil)
158
+ generate_new_color(Converter::Saturate, ratio, name)
159
+ end
160
+
161
+ ##
162
+ # Return a grayscale of the original color.
163
+ #
164
+ # @param ratio [Float] Conversion ratio in percentage
165
+ # @param name [String] You can name the color to be created.
166
+ # Without this option, the value of normalized hex color
167
+ # code is assigned instead.
168
+ # @return [Color] New grayscale color
169
+
170
+ def new_grayscale_color(ratio = 100, name = nil)
171
+ generate_new_color(Converter::Grayscale, ratio, name)
172
+ end
173
+
174
+ ##
175
+ # Try to find a color who has a satisfying contrast ratio.
176
+ #
177
+ # The returned color is gained by modifying the brightness of
178
+ # another color. Even when a color that satisfies the specified
179
+ # level is not found, it returns a new color anyway.
180
+ # @param other_color [Color, Array<Integer>, String] Color before
181
+ # the adjustment of brightness
182
+ # @param level [String] "A", "AA" or "AAA"
183
+ # @return [Color] New color whose brightness is adjusted from that
184
+ # of +other_color+
185
+
186
+ def find_brightness_threshold(other_color, level = Checker::Level::AA)
187
+ other_color = Color.new(other_color) unless other_color.is_a? Color
188
+ ThresholdFinder::Brightness.find(self, other_color, level)
189
+ end
190
+
191
+ ##
192
+ # Try to find a color who has a satisfying contrast ratio.
193
+ #
194
+ # The returned color is gained by modifying the lightness of
195
+ # another color. Even when a color that satisfies the specified
196
+ # level is not found, it returns a new color anyway.
197
+ # @param other_color [Color, Array<Integer>, String] Color before
198
+ # the adjustment of lightness
199
+ # @param level [String] "A", "AA" or "AAA"
200
+ # @return [Color] New color whose brightness is adjusted from that
201
+ # of +other_color+
202
+
203
+ def find_lightness_threshold(other_color, level = Checker::Level::AA)
204
+ other_color = Color.new(other_color) unless other_color.is_a? Color
205
+ ThresholdFinder::Lightness.find(self, other_color, level)
206
+ end
207
+
208
+ ##
209
+ # Calculate the contrast ratio against another color.
210
+ #
211
+ # @param other_color [Color, Array<Integer>, String] Another instance
212
+ # of Color, RGB value or hex color code
213
+ # @return [Float] Contrast ratio
214
+
215
+ def contrast_ratio_against(other_color)
216
+ unless other_color.is_a? Color
217
+ return Checker.contrast_ratio(rgb, other_color)
218
+ end
219
+
220
+ Checker.luminance_to_contrast_ratio(relative_luminance,
221
+ other_color.relative_luminance)
222
+ end
223
+
224
+ ##
225
+ # Return the level of contrast ratio defined by WCAG 2.0.
226
+ #
227
+ # @param other_color [Color, Array<Integer>, String] Another instance
228
+ # of Color, RGB value or hex color code
229
+ # @return [String] "A", "AA" or "AAA" if the contrast ratio meets the
230
+ # criteria of WCAG 2.0, otherwise "-"
231
+
232
+ def contrast_level(other_color)
233
+ Checker.ratio_to_level(contrast_ratio_against(other_color))
234
+ end
235
+
236
+ ##
237
+ # Return a string representation of the color.
238
+ #
239
+ # @param base [Ingeger, nil] 16, 10 or nil. when +base+ = 16,
240
+ # a hex color code such as "#ffff00" is returned, and when
241
+ # +base+ = 10, a code in RGB notation such as "rgb(255, 255, 0)"
242
+ # @return [String] String representation of the color
243
+
244
+ def to_s(base = 16)
245
+ case base
246
+ when 16
247
+ hex
248
+ when 10
249
+ @rgb_code ||= format('rgb(%d,%d,%d)', *rgb)
250
+ else
251
+ name
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Check if the contrast ratio with another color meets a
257
+ # WCAG 2.0 criterion.
258
+ #
259
+ # @param other_color [Color, Array<Integer>, String] Another instance
260
+ # of Color, RGB value or hex color code
261
+ # @param level [String] "A", "AA" or "AAA"
262
+ # @return [Boolean] true if the contrast ratio meets the specified level
263
+
264
+ def sufficient_contrast?(other_color, level = Checker::Level::AA)
265
+ ratio = Checker.level_to_ratio(level)
266
+ contrast_ratio_against(other_color) >= ratio
267
+ end
268
+
269
+ ##
270
+ # Check it two colors have the same RGB value.
271
+ #
272
+ # @param other_color [Color, Array<Integer>, String] Another instance
273
+ # of Color, RGB value or hex color code
274
+ # @return [Boolean] true if other_color has the same RGB value
275
+
276
+ def same_color?(other_color)
277
+ case other_color
278
+ when Color
279
+ hex == other_color.hex
280
+ when Array
281
+ hex == Utils.rgb_to_hex(other_color)
282
+ when String
283
+ hex == Utils.normalize_hex(other_color)
284
+ end
285
+ end
286
+
287
+ ##
288
+ # Check if the color reachs already the max contrast.
289
+ #
290
+ # The max contrast in this context means that of colors modified
291
+ # by the operation defined at
292
+ # * {https://www.w3.org/TR/filter-effects/#funcdef-contrast}
293
+ # @return [Boolean] true if self.new_contrast_color(r) where r is
294
+ # greater than 100 returns the same color as self.
295
+
296
+ def max_contrast?
297
+ rgb.all? {|c| RGB_LIMITS.include? c }
298
+ end
299
+
300
+ ##
301
+ # Check if the color reachs already the min contrast.
302
+ #
303
+ # The min contrast in this context means that of colors modified
304
+ # by the operation defined at
305
+ # * {https://www.w3.org/TR/filter-effects/#funcdef-contrast}
306
+ # @return [Boolean] true if self is the same color as "#808080"
307
+
308
+ def min_contrast?
309
+ rgb == GRAY.rgb
310
+ end
311
+
312
+ ##
313
+ # Check if the color has higher luminance than another color.
314
+ #
315
+ # @param other_color [Color] Another color
316
+ # @return [Boolean] true if the relative luminance of self is higher
317
+ # than that of other_color
318
+
319
+ def higher_luminance_than?(other_color)
320
+ relative_luminance > other_color.relative_luminance
321
+ end
322
+
323
+ ##
324
+ # Check if two colors has the same relative luminance.
325
+ #
326
+ # @param other_color [Color] Another color
327
+ # @return [Boolean] true if the relative luminance of self
328
+ # and other_color are same.
329
+
330
+ def same_luminance_as?(other_color)
331
+ relative_luminance == other_color.relative_luminance
332
+ end
333
+
334
+ ##
335
+ # Check if the contrast ratio against black is higher than against white.
336
+ #
337
+ # @return [Boolean] true if the contrast ratio against white is qual to
338
+ # or less than the ratio against black
339
+
340
+ def light_color?
341
+ contrast_ratio_against(WHITE.rgb) <= contrast_ratio_against(BLACK.rgb)
342
+ end
343
+
344
+ def generate_new_color(calc, ratio, name = nil)
345
+ new_rgb = calc.calc_rgb(rgb, ratio)
346
+ self.class.new(new_rgb, name)
347
+ end
348
+
349
+ private :generate_new_color
350
+
351
+ ##
352
+ # Provide predefined lists of Color instances.
353
+
354
+ module List
355
+ # named colors: https://www.w3.org/TR/SVG/types.html#ColorKeywords
356
+ keywords_file = "#{__dir__}/data/color_keywords.json"
357
+ keywords = JSON.parse(File.read(keywords_file))
358
+
359
+ ##
360
+ # Predefined list of named colors.
361
+ #
362
+ # You can find the color names at
363
+ # https://www.w3.org/TR/SVG/types.html#ColorKeywords
364
+ # @return [Array<Color>] Named colors
365
+
366
+ NAMED_COLORS = keywords.map {|name, hex| Color.new(hex, name) }.freeze
367
+
368
+ # @private
369
+ NAME_TO_COLOR = NAMED_COLORS.map {|color| [color.name, color] }.to_h
370
+
371
+ # @private
372
+ HEX_TO_COLOR = NAMED_COLORS.map {|color| [color.hex, color] }.to_h
373
+
374
+ def self.generate_web_safe_colors
375
+ 0.step(15, 3).to_a.repeated_permutation(3).sort.map do |rgb|
376
+ hex_code = Utils.rgb_to_hex(rgb.map {|c| c * 17 })
377
+ HEX_TO_COLOR[hex_code] || Color.new(hex_code)
378
+ end
379
+ end
380
+
381
+ private_class_method :generate_web_safe_colors
382
+
383
+ ##
384
+ # Predefined list of web safe colors.
385
+ #
386
+ # @return [Array<Color>] Web safe colors
387
+
388
+ WEB_SAFE_COLORS = generate_web_safe_colors.freeze
389
+
390
+ ##
391
+ # Return a list of colors which share the same saturation and
392
+ # lightness.
393
+ #
394
+ # By default, so-called pure colors are returned.
395
+ # @param s 100 [Float] Ratio of saturation in percentage
396
+ # @param l 50 [Float] Ratio of lightness in percentage
397
+ # @param h_interval 1 [Integer] Interval of hues in degrees.
398
+ # By default, the method returns 360 hues beginning from red.
399
+ # @return [Array<Color>] Array of colors which share the same
400
+ # saturation and lightness
401
+
402
+ def self.hsl_colors(s: 100, l: 50, h_interval: 1)
403
+ 0.step(360, h_interval).map {|h| Color.new_from_hsl([h, s, l]) }.freeze
404
+ end
405
+ end
406
+
407
+ WHITE, GRAY, BLACK = %w[white gray black].map {|n| List::NAME_TO_COLOR[n] }
408
+ end
409
+ end