red-colors 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/.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
|