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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +9 -0
- data/.yardopts +4 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.ja.md +296 -0
- data/README.md +295 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/color_contrast_calc.gemspec +30 -0
- data/examples/color_instance.rb +12 -0
- data/examples/color_lists.rb +26 -0
- data/examples/grayscale.rb +10 -0
- data/examples/sort_colors.rb +30 -0
- data/examples/yellow_black_contrast.rb +12 -0
- data/examples/yellow_orange_contrast.rb +32 -0
- data/exe/color_contrast_calc +4 -0
- data/lib/color_contrast_calc.rb +108 -0
- data/lib/color_contrast_calc/checker.rb +126 -0
- data/lib/color_contrast_calc/color.rb +409 -0
- data/lib/color_contrast_calc/converter.rb +191 -0
- data/lib/color_contrast_calc/data/color_keywords.json +149 -0
- data/lib/color_contrast_calc/shim.rb +32 -0
- data/lib/color_contrast_calc/sorter.rb +227 -0
- data/lib/color_contrast_calc/threshold_finder.rb +276 -0
- data/lib/color_contrast_calc/utils.rb +240 -0
- data/lib/color_contrast_calc/version.rb +5 -0
- metadata +146 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
@@ -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,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
|