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,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