red-colors 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +6 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +7 -0
- data/README.md +31 -0
- data/Rakefile +19 -0
- data/lib/colors.rb +30 -0
- data/lib/colors/abstract_color.rb +28 -0
- data/lib/colors/alpha_component.rb +13 -0
- data/lib/colors/color_data.rb +1146 -0
- data/lib/colors/helper.rb +17 -0
- data/lib/colors/hsl.rb +128 -0
- data/lib/colors/hsla.rb +81 -0
- data/lib/colors/husl.rb +144 -0
- data/lib/colors/named_colors.rb +114 -0
- data/lib/colors/rgb.rb +145 -0
- data/lib/colors/rgba.rb +101 -0
- data/lib/colors/version.rb +3 -0
- data/lib/colors/xyz.rb +123 -0
- data/red-colors.gemspec +41 -0
- data/test/helper.rb +13 -0
- data/test/run.rb +17 -0
- data/test/test-hsl.rb +267 -0
- data/test/test-hsla.rb +309 -0
- data/test/test-husl.rb +241 -0
- data/test/test-named-color.rb +43 -0
- data/test/test-rgb.rb +294 -0
- data/test/test-rgba.rb +332 -0
- data/test/test-xyz.rb +10 -0
- metadata +165 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Colors
|
2
|
+
module Helper
|
3
|
+
private def check_type(obj, type, name)
|
4
|
+
return obj if obj.instance_of?(type)
|
5
|
+
check_fail TypeError, "#{name} must be a #{type}, but #{obj.class} is given"
|
6
|
+
end
|
7
|
+
|
8
|
+
private def check_range(value, range, name)
|
9
|
+
return value if range.cover?(value)
|
10
|
+
check_fail ArgumentError, "#{name} must be in #{range}, but #{value} is given"
|
11
|
+
end
|
12
|
+
|
13
|
+
private def check_fail(exc_class, message)
|
14
|
+
raise exc_class, message, caller(2)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/colors/hsl.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
module Colors
|
2
|
+
class HSL < AbstractColor
|
3
|
+
include Helper
|
4
|
+
|
5
|
+
def initialize(h, s, l)
|
6
|
+
@h, @s, @l = canonicalize(h, s, l)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :h, :s, :l
|
10
|
+
|
11
|
+
def components
|
12
|
+
[h, s, l]
|
13
|
+
end
|
14
|
+
|
15
|
+
alias hsl_components components
|
16
|
+
|
17
|
+
def h=(h)
|
18
|
+
@h = Rational(h) % 360
|
19
|
+
end
|
20
|
+
|
21
|
+
def s=(s)
|
22
|
+
@s = if s.instance_of?(Integer)
|
23
|
+
check_range(s, 0..255, :s) / 255r
|
24
|
+
else
|
25
|
+
Rational(check_range(s, 0..1, :s))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def l=(l)
|
30
|
+
@l = if l.instance_of?(Integer)
|
31
|
+
check_range(l, 0..255, :l) / 255r
|
32
|
+
else
|
33
|
+
Rational(check_range(l, 0..1, :l))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
alias hue h
|
38
|
+
alias saturation s
|
39
|
+
alias lightness l
|
40
|
+
|
41
|
+
alias hue= h=
|
42
|
+
alias saturation= s=
|
43
|
+
alias lightness= l=
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
case other
|
47
|
+
when HSLA
|
48
|
+
other == self
|
49
|
+
when HSL
|
50
|
+
h == other.h && s == other.s && l == other.l
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def desaturate(factor)
|
57
|
+
HSL.new(h, s*factor, l)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_hsl
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_hsla(alpha: 1.0)
|
65
|
+
alpha = canonicalize_component(alpha, :alpha)
|
66
|
+
HSLA.new(h, s, l, alpha)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_rgb
|
70
|
+
RGB.new(*rgb_components)
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_rgba(alpha: 1.0)
|
74
|
+
alpha = canonicalize_component(alpha, :alpha)
|
75
|
+
RGBA.new(*rgb_components, alpha)
|
76
|
+
end
|
77
|
+
|
78
|
+
def rgb_components
|
79
|
+
t2 = if l <= 0.5r
|
80
|
+
l * (s + 1r)
|
81
|
+
else
|
82
|
+
l + s - l * s
|
83
|
+
end
|
84
|
+
t1 = l * 2r - t2
|
85
|
+
hh = h/60r
|
86
|
+
r = hue_to_rgb(t1, t2, hh + 2)
|
87
|
+
g = hue_to_rgb(t1, t2, hh)
|
88
|
+
b = hue_to_rgb(t1, t2, hh - 2)
|
89
|
+
[r, g, b]
|
90
|
+
end
|
91
|
+
|
92
|
+
private def hue_to_rgb(t1, t2, h)
|
93
|
+
h += 6r if h < 0
|
94
|
+
h -= 6r if h >= 6
|
95
|
+
if h < 1
|
96
|
+
(t2 - t1) * h + t1
|
97
|
+
elsif h < 3
|
98
|
+
t2
|
99
|
+
elsif h < 4
|
100
|
+
(t2 - t1) * (4r - h) + t1
|
101
|
+
else
|
102
|
+
t1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private def canonicalize(h, s, l)
|
107
|
+
if [s, l].map(&:class) == [Integer, Integer]
|
108
|
+
canonicalize_from_integer(h, s, l)
|
109
|
+
else
|
110
|
+
[
|
111
|
+
Rational(h) % 360,
|
112
|
+
canonicalize_component_to_rational(s, :s),
|
113
|
+
canonicalize_component_to_rational(l, :l)
|
114
|
+
]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private def canonicalize_from_integer(h, s, l)
|
119
|
+
check_type(s, Integer, :s)
|
120
|
+
check_type(l, Integer, :l)
|
121
|
+
[
|
122
|
+
Rational(h) % 360,
|
123
|
+
canonicalize_component_from_integer(s, :s),
|
124
|
+
canonicalize_component_from_integer(l, :l)
|
125
|
+
]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/colors/hsla.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Colors
|
2
|
+
class HSLA < HSL
|
3
|
+
def initialize(h, s, l, a)
|
4
|
+
@h, @s, @l, @a = canonicalize(h, s, l, a)
|
5
|
+
end
|
6
|
+
|
7
|
+
include AlphaComponent
|
8
|
+
|
9
|
+
def components
|
10
|
+
[h, s, l, a]
|
11
|
+
end
|
12
|
+
|
13
|
+
alias hsla_components components
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
case other
|
17
|
+
when HSLA
|
18
|
+
h == other.h && s == other.s && l == other.l && a == other.a
|
19
|
+
when HSL
|
20
|
+
h == other.h && s == other.s && l == other.l && a == 1r
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def desaturate(factor)
|
27
|
+
HSLA.new(h, s*factor, l, a)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_hsla
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_rgba
|
35
|
+
RGBA.new(*rgb_components, a)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_hsl
|
39
|
+
if a == 1r
|
40
|
+
super
|
41
|
+
else
|
42
|
+
raise NotImplementedError,
|
43
|
+
"Unable to convert non-opaque HSLA to HSL"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_rgb
|
48
|
+
if a == 1r
|
49
|
+
super
|
50
|
+
else
|
51
|
+
raise NotImplementedError,
|
52
|
+
"Unable to convert non-opaque HSLA to RGB"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private def canonicalize(h, s, l, a)
|
57
|
+
if [s, l, a].map(&:class) == [Integer, Integer, Integer]
|
58
|
+
canonicalize_from_integer(h, s, l, a)
|
59
|
+
else
|
60
|
+
[
|
61
|
+
Rational(h) % 360,
|
62
|
+
canonicalize_component_to_rational(s, :s),
|
63
|
+
canonicalize_component_to_rational(l, :l),
|
64
|
+
canonicalize_component_to_rational(a, :a)
|
65
|
+
]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private def canonicalize_from_integer(h, s, l, a)
|
70
|
+
check_type(s, Integer, :s)
|
71
|
+
check_type(l, Integer, :l)
|
72
|
+
check_type(a, Integer, :a)
|
73
|
+
[
|
74
|
+
Rational(h) % 360,
|
75
|
+
canonicalize_component_from_integer(s, :s),
|
76
|
+
canonicalize_component_from_integer(l, :l),
|
77
|
+
canonicalize_component_from_integer(a, :a)
|
78
|
+
]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/colors/husl.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require "numo/narray"
|
2
|
+
|
3
|
+
module Colors
|
4
|
+
# Human-friendly alternative to HSL color space.
|
5
|
+
# The definition of HUSL is provided in <http://www.hsluv.org>.
|
6
|
+
class HUSL < HSL
|
7
|
+
DEG2RAD = 0.01745329251994329577r # 2 * pi / 360
|
8
|
+
|
9
|
+
def self.from_rgb(r, g, b)
|
10
|
+
c = XYZ.from_rgb(r, g, b)
|
11
|
+
l, u, v = c.luv_components(WHITE_POINT_D65)
|
12
|
+
l, c, h = convert_luv_to_lch(l, u, v)
|
13
|
+
h, s, l = convert_lch_to_husl(l, c, h)
|
14
|
+
new(h, s.to_r.clamp(0r, 1r), l.to_r.clamp(0r, 1r))
|
15
|
+
end
|
16
|
+
|
17
|
+
private_class_method def self.convert_luv_to_lch(l, u, v)
|
18
|
+
c = Math.sqrt(u*u + v*v).to_r
|
19
|
+
|
20
|
+
if c < 1e-8
|
21
|
+
h = 0r
|
22
|
+
else
|
23
|
+
h = Math.atan2(v, u).to_r * 180/Math::PI.to_r
|
24
|
+
h += 360r if h < 0
|
25
|
+
end
|
26
|
+
|
27
|
+
[l, c, h]
|
28
|
+
end
|
29
|
+
|
30
|
+
private_class_method def self.convert_lch_to_husl(l, c, h)
|
31
|
+
if l > 99.9999999 || l < 1e-8
|
32
|
+
s = 0r
|
33
|
+
else
|
34
|
+
mx = max_chroma(l, h)
|
35
|
+
s = c / mx * 100r
|
36
|
+
end
|
37
|
+
|
38
|
+
h = 0r if c < 1e-8
|
39
|
+
|
40
|
+
[h, s/100r, l/100r]
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
case other
|
45
|
+
when HUSL
|
46
|
+
h == other.h && s == other.s && l == other.l
|
47
|
+
else
|
48
|
+
other == self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def desaturate(factor)
|
53
|
+
to_rgb.desaturate(factor).to_husl
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_husl
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_rgb
|
61
|
+
RGB.new(*rgb_components)
|
62
|
+
end
|
63
|
+
|
64
|
+
def rgb_components
|
65
|
+
l, u, v = convert_lch_to_luv(*lch_components)
|
66
|
+
x, y, z = convert_luv_to_xyz(l, u, v)
|
67
|
+
XYZ.new(x, y, z).rgb_components
|
68
|
+
end
|
69
|
+
|
70
|
+
def lch_components
|
71
|
+
l = self.l * 100r
|
72
|
+
s = self.s * 100r
|
73
|
+
|
74
|
+
if l > 99.9999999 || l < 1e-8
|
75
|
+
c = 0r
|
76
|
+
else
|
77
|
+
mx = self.class.max_chroma(l, h)
|
78
|
+
c = mx / 100r * s
|
79
|
+
end
|
80
|
+
|
81
|
+
h = s < 1e-8 ? 0r : self.h
|
82
|
+
|
83
|
+
[l, c, h]
|
84
|
+
end
|
85
|
+
|
86
|
+
private def convert_lch_to_luv(l, c, h)
|
87
|
+
h_rad = h * DEG2RAD
|
88
|
+
u = Math.cos(h_rad).to_r * c
|
89
|
+
v = Math.sin(h_rad).to_r * c
|
90
|
+
[l, u, v]
|
91
|
+
end
|
92
|
+
|
93
|
+
private def convert_luv_to_xyz(l, u, v)
|
94
|
+
return [0r, 0r, 0r] if l <= 1e-8
|
95
|
+
|
96
|
+
wp_u, wp_v = WHITE_POINT_D65.uv_values
|
97
|
+
var_u = u / (13 * l) + wp_u
|
98
|
+
var_v = v / (13 * l) + wp_v
|
99
|
+
y = if l < 8
|
100
|
+
l / XYZ::KAPPA
|
101
|
+
else
|
102
|
+
((l + 16r) / 116r)**3
|
103
|
+
end
|
104
|
+
x = -(9 * y * var_u) / ((var_u - 4) * var_v - var_u * var_v)
|
105
|
+
z = (9 * y - (15 * var_v * y) - (var_v * x)) / (3 * var_v)
|
106
|
+
[x, y, z]
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.max_chroma(l, h)
|
110
|
+
h_rad = h * DEG2RAD
|
111
|
+
sin_h = Math.sin(h_rad).to_r
|
112
|
+
cos_h = Math.cos(h_rad).to_r
|
113
|
+
|
114
|
+
result = Float::INFINITY
|
115
|
+
get_bounds(l).each do |line|
|
116
|
+
len = line[1] / (sin_h - line[0] * cos_h)
|
117
|
+
result = len if 0 <= len && len < result
|
118
|
+
end
|
119
|
+
result
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.get_bounds(l)
|
123
|
+
sub1 = (l + 16)**3 / 1560896r
|
124
|
+
sub2 = sub1 > XYZ::EPSILON ? sub1 : l/XYZ::KAPPA
|
125
|
+
|
126
|
+
bounds = Array.new(6) { [0r, 0r] }
|
127
|
+
0.upto(2) do |ch|
|
128
|
+
m1 = XYZ2RGB[ch, 0].to_r
|
129
|
+
m2 = XYZ2RGB[ch, 1].to_r
|
130
|
+
m3 = XYZ2RGB[ch, 2].to_r
|
131
|
+
|
132
|
+
[0, 1].each do |t|
|
133
|
+
top1 = (284517r * m1 - 94839r * m3) * sub2
|
134
|
+
top2 = (838422r * m3 + 769860r * m2 + 731718r * m1) * l * sub2 - 769860r * t * l
|
135
|
+
bottom = (632260r * m3 - 126452r * m2) * sub2 + 126452r * t
|
136
|
+
|
137
|
+
bounds[ch*2 + t][0] = top1 / bottom
|
138
|
+
bounds[ch*2 + t][1] = top2 / bottom
|
139
|
+
end
|
140
|
+
end
|
141
|
+
bounds
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require_relative 'color_data'
|
2
|
+
|
3
|
+
module Colors
|
4
|
+
module NamedColors
|
5
|
+
class Mapping
|
6
|
+
def initialize
|
7
|
+
@mapping = {}
|
8
|
+
@cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :cache
|
12
|
+
|
13
|
+
def [](name)
|
14
|
+
if NamedColors.nth_color?(name)
|
15
|
+
cycle = ColorData::DEFAULT_COLOR_CYCLE
|
16
|
+
name = cycle[name[1..-1].to_i % cycle.length]
|
17
|
+
end
|
18
|
+
if cache.has_key?(name)
|
19
|
+
cache[name]
|
20
|
+
else
|
21
|
+
cache[name] = lookup_no_color_cycle(name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private def lookup_no_color_cycle(color)
|
26
|
+
orig_color = color
|
27
|
+
case color
|
28
|
+
when /\Anone\z/i
|
29
|
+
return RGBA.new(0, 0, 0, 0)
|
30
|
+
when String
|
31
|
+
# nothing to do
|
32
|
+
when Symbol
|
33
|
+
color = color.to_s
|
34
|
+
else
|
35
|
+
color = color.to_str
|
36
|
+
end
|
37
|
+
color = @mapping.fetch(color, color)
|
38
|
+
case color
|
39
|
+
when /\A#\h+\z/
|
40
|
+
case color.length - 1
|
41
|
+
when 3, 6
|
42
|
+
RGB.parse(color)
|
43
|
+
when 4, 8
|
44
|
+
RGBA.parse(color)
|
45
|
+
else
|
46
|
+
raise RuntimeError,
|
47
|
+
"[BUG] Invalid hex string form #{color.inspect} for #{name.inspect}"
|
48
|
+
end
|
49
|
+
when Array
|
50
|
+
case color.length
|
51
|
+
when 3
|
52
|
+
RGB.new(*color)
|
53
|
+
when 4
|
54
|
+
RGBA.new(*color)
|
55
|
+
else
|
56
|
+
raise RuntimeError,
|
57
|
+
"[BUG] Invalid number of color components #{color} for #{name.inspect}"
|
58
|
+
end
|
59
|
+
else
|
60
|
+
color
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def []=(name, value)
|
65
|
+
@mapping[name] = value
|
66
|
+
ensure
|
67
|
+
cache.clear
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete(name)
|
71
|
+
@mapping.delete(name)
|
72
|
+
ensure
|
73
|
+
cache.clear
|
74
|
+
end
|
75
|
+
|
76
|
+
def update(other)
|
77
|
+
@mapping.update(other)
|
78
|
+
ensure
|
79
|
+
cache.clear
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
MAPPING = Mapping.new
|
84
|
+
MAPPING.update(ColorData::XKCD_COLORS)
|
85
|
+
ColorData::XKCD_COLORS.each do |key, value|
|
86
|
+
MAPPING[key.sub("grey", "gray")] = value if key.include? "grey"
|
87
|
+
end
|
88
|
+
MAPPING.update(ColorData::CSS4_COLORS)
|
89
|
+
MAPPING.update(ColorData::TABLEAU_COLORS)
|
90
|
+
ColorData::TABLEAU_COLORS.each do |key, value|
|
91
|
+
MAPPING[key.sub("gray", "grey")] = value if key.include? "gray"
|
92
|
+
end
|
93
|
+
MAPPING.update(ColorData::BASE_COLORS)
|
94
|
+
|
95
|
+
def self.[](name)
|
96
|
+
MAPPING[name]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return whether `name` is an item in the color cycle.
|
100
|
+
def self.nth_color?(name)
|
101
|
+
case name
|
102
|
+
when String
|
103
|
+
# do nothing
|
104
|
+
when Symbol
|
105
|
+
name = name.to_s
|
106
|
+
else
|
107
|
+
name = name.to_str
|
108
|
+
end
|
109
|
+
name.match?(/\AC\d+\z/)
|
110
|
+
rescue NoMethodError, TypeError
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|