red-colors 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -0
- data/data/colormaps/cividis.json +258 -0
- data/data/colormaps/coolwarm.json +107 -0
- data/data/colormaps/crest.json +258 -0
- data/data/colormaps/flare.json +258 -0
- data/data/colormaps/gist_earth.json +49 -0
- data/data/colormaps/gist_ncar.json +55 -0
- data/data/colormaps/icefire.json +258 -0
- data/data/colormaps/inferno.json +258 -0
- data/data/colormaps/magma.json +258 -0
- data/data/colormaps/mako.json +258 -0
- data/data/colormaps/nipy_spectral.json +71 -0
- data/data/colormaps/pink.json +200 -0
- data/data/colormaps/plasma.json +258 -0
- data/data/colormaps/rocket.json +258 -0
- data/data/colormaps/turbo.json +258 -0
- data/data/colormaps/twilight.json +512 -0
- data/data/colormaps/viridis.json +258 -0
- data/data/colormaps/vlag.json +258 -0
- data/lib/colors.rb +17 -5
- data/lib/colors/abstract_color.rb +4 -0
- data/lib/colors/colormap.rb +143 -0
- data/lib/colors/colormap_data.rb +44 -0
- data/lib/colors/colormap_data/matplotlib_builtin.rb +990 -0
- data/lib/colors/colormap_data/seaborn_builtin.rb +10 -0
- data/lib/colors/colormap_registry.rb +62 -0
- data/lib/colors/convert.rb +269 -0
- data/lib/colors/helper.rb +2 -1
- data/lib/colors/husl.rb +7 -100
- data/lib/colors/linear_segmented_colormap.rb +137 -0
- data/lib/colors/listed_colormap.rb +45 -0
- data/lib/colors/named_colors.rb +10 -20
- data/lib/colors/rgb.rb +20 -10
- data/lib/colors/rgba.rb +14 -8
- data/lib/colors/utils.rb +18 -0
- data/lib/colors/version.rb +1 -1
- data/lib/colors/xterm256.rb +56 -0
- data/lib/colors/xyy.rb +48 -0
- data/lib/colors/xyz.rb +2 -55
- data/red-colors.gemspec +3 -1
- data/test/test-husl.rb +45 -53
- data/test/test-linear-segmented-colormap.rb +138 -0
- data/test/test-listed-colormap.rb +134 -0
- data/test/test-rgb.rb +76 -1
- data/test/test-xterm256.rb +76 -0
- data/test/test-xyz.rb +1 -1
- metadata +50 -15
@@ -0,0 +1,137 @@
|
|
1
|
+
module Colors
|
2
|
+
class LinearSegmentedColormap < Colormap
|
3
|
+
def initialize(name, segmented_data, n_colors: 256, gamma: 1.0)
|
4
|
+
super(name, n_colors)
|
5
|
+
|
6
|
+
@monochrome = false
|
7
|
+
@segmented_data = segmented_data
|
8
|
+
@gamma = gamma
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :segmented_data, :gamma
|
12
|
+
|
13
|
+
def self.new_from_list(name, colors, n_colors: 256, gamma: 1.0)
|
14
|
+
case colors
|
15
|
+
when Enumerable
|
16
|
+
colors = colors.to_a
|
17
|
+
else
|
18
|
+
ary = Array.try_convert(colors)
|
19
|
+
if ary.nil?
|
20
|
+
raise ArgumentError, "colors must be convertible to an array: %p for %s" % [colors, name]
|
21
|
+
else
|
22
|
+
colors = ary
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
case colors[0]
|
27
|
+
when Array
|
28
|
+
unless colors.all? {|a| a.length == 2 }
|
29
|
+
raise ArgumentError, "colors array has invalid items"
|
30
|
+
end
|
31
|
+
vals, colors = colors.transpose
|
32
|
+
else
|
33
|
+
vals = Utils.linspace(0r, 1r, colors.length)
|
34
|
+
end
|
35
|
+
|
36
|
+
r, g, b, a = colors.map { |c|
|
37
|
+
Utils.make_color(c).to_rgba.components
|
38
|
+
}.transpose
|
39
|
+
|
40
|
+
segmented_data = {
|
41
|
+
red: [vals, r, r].transpose,
|
42
|
+
green: [vals, g, g].transpose,
|
43
|
+
blue: [vals, b, b].transpose,
|
44
|
+
alpha: [vals, a, a].transpose
|
45
|
+
}
|
46
|
+
|
47
|
+
new(name, segmented_data, n_colors: n_colors, gamma: gamma)
|
48
|
+
end
|
49
|
+
|
50
|
+
def gamma=(val)
|
51
|
+
@gamma = val
|
52
|
+
@initialized = false
|
53
|
+
end
|
54
|
+
|
55
|
+
private def init_colormap
|
56
|
+
red = create_lookup_table(self.n_colors, @segmented_data[:red], @gamma)
|
57
|
+
green = create_lookup_table(self.n_colors, @segmented_data[:green], @gamma)
|
58
|
+
blue = create_lookup_table(self.n_colors, @segmented_data[:blue], @gamma)
|
59
|
+
alpha = if @segmented_data.key?(:alpha)
|
60
|
+
create_lookup_table(self.n_colors, @segmented_data[:alpha], @gamma)
|
61
|
+
end
|
62
|
+
@lookup_table = Array.new(self.n_colors) do |i|
|
63
|
+
Colors::RGBA.new(red[i], green[i], blue[i], alpha ? alpha[i] : 1r)
|
64
|
+
end
|
65
|
+
@initialized = true
|
66
|
+
update_extreme_colors
|
67
|
+
end
|
68
|
+
|
69
|
+
private def create_lookup_table(n, data, gamma=1.0)
|
70
|
+
if data.respond_to?(:call)
|
71
|
+
xind = Utils.linspace(0r, 1r, n).map {|x| x ** gamma }
|
72
|
+
lut = xind.map {|i| data.(i).clamp(0, 1).to_f }
|
73
|
+
return lut
|
74
|
+
end
|
75
|
+
|
76
|
+
ary = Array.try_convert(data)
|
77
|
+
if ary.nil?
|
78
|
+
raise ArgumentError, "data must be convertible to an array"
|
79
|
+
elsif ary.any? {|sub| sub.length != 3 }
|
80
|
+
raise ArgumentError, "data array must consist of 3-length arrays"
|
81
|
+
end
|
82
|
+
|
83
|
+
shape = [ary.length, 3]
|
84
|
+
|
85
|
+
x, y0, y1 = ary.transpose
|
86
|
+
|
87
|
+
if x[0] != 0.0 || x[-1] != 1.0
|
88
|
+
raise ArgumentError,
|
89
|
+
"data mapping points must start with x=0 and end with x=1"
|
90
|
+
end
|
91
|
+
|
92
|
+
unless x.each_cons(2).all? {|a, b| a < b }
|
93
|
+
raise ArgumentError,
|
94
|
+
"data mapping points must have x in increasing order"
|
95
|
+
end
|
96
|
+
|
97
|
+
if n == 1
|
98
|
+
# Use the `y = f(x=1)` value for a 1-element lookup table
|
99
|
+
lut = [y0[-1]]
|
100
|
+
else
|
101
|
+
x.map! {|v| v.to_f * (n - 1) }
|
102
|
+
xind = Utils.linspace(0r, 1r, n).map {|i| (n - 1) * i ** gamma }
|
103
|
+
ind = (0 ... n).map {|i| x.find_index {|v| xind[i] < v } }[1 ... -1]
|
104
|
+
|
105
|
+
distance = ind.map.with_index do |i, j|
|
106
|
+
(xind[j+1] - x[i - 1]) / (x[i] - x[i - 1])
|
107
|
+
end
|
108
|
+
|
109
|
+
lut = [
|
110
|
+
y1[0],
|
111
|
+
*ind.map.with_index {|i, j| distance[j] * (y0[i] - y1[i - 1]) + y1[i - 1] },
|
112
|
+
y0[-1]
|
113
|
+
]
|
114
|
+
end
|
115
|
+
|
116
|
+
return lut.map {|v| v.clamp(0, 1).to_f }
|
117
|
+
end
|
118
|
+
|
119
|
+
private def make_reverse_colormap(name)
|
120
|
+
segmented_data_r = self.segmented_data.map { |key, data|
|
121
|
+
data_r = if data.respond_to?(:call)
|
122
|
+
make_inverse_func(data)
|
123
|
+
else
|
124
|
+
data.reverse_each.map do |x, y0, y1|
|
125
|
+
[1r - x, y1, y0]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
[key, data_r]
|
129
|
+
}.to_h
|
130
|
+
LinearSegmentedColormap.new(name, segmented_data_r, n_colors: self.n_colors, gamma: self.gamma)
|
131
|
+
end
|
132
|
+
|
133
|
+
private def make_inverse_func(f)
|
134
|
+
->(x) { f(1 - x) }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Colors
|
2
|
+
class ListedColormap < Colormap
|
3
|
+
def initialize(colors, name: :from_list, n_colors: nil)
|
4
|
+
@monochrome = false
|
5
|
+
if n_colors.nil?
|
6
|
+
@colors = Array.try_convert(colors)
|
7
|
+
n_colors = @colors.length
|
8
|
+
else
|
9
|
+
case colors
|
10
|
+
when String, Symbol
|
11
|
+
@colors = Array.new(n_colors) { colors }
|
12
|
+
@monochrome = true
|
13
|
+
when Enumerable
|
14
|
+
@colors = colors.cycle.take(n_colors)
|
15
|
+
@monochrome = @colors.all? {|x| x == @colors[0] }
|
16
|
+
else
|
17
|
+
begin
|
18
|
+
gray = Float(colors)
|
19
|
+
rescue TypeError, ArgumentError
|
20
|
+
raise ArgumentError,
|
21
|
+
"invalid value for `colors` (%p)" % colors
|
22
|
+
else
|
23
|
+
@colors = Array.new(n_colors) { gray }
|
24
|
+
end
|
25
|
+
@monochrome = true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@colors.freeze
|
29
|
+
|
30
|
+
super(name, n_colors)
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :colors
|
34
|
+
|
35
|
+
private def init_colormap
|
36
|
+
@lookup_table = self.colors.map {|color| Utils.make_color(color).to_rgba }
|
37
|
+
@initialized = true
|
38
|
+
update_extreme_colors
|
39
|
+
end
|
40
|
+
|
41
|
+
private def make_reverse_colormap(name)
|
42
|
+
ListedColormap.new(self.colors.reverse, name: name, n_colors: self.n_colors)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/colors/named_colors.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require_relative 'color_data'
|
2
|
-
|
3
1
|
module Colors
|
4
2
|
module NamedColors
|
5
3
|
class Mapping
|
@@ -8,33 +6,26 @@ module Colors
|
|
8
6
|
@cache = {}
|
9
7
|
end
|
10
8
|
|
11
|
-
attr_reader :cache
|
12
|
-
|
13
9
|
def [](name)
|
14
10
|
if NamedColors.nth_color?(name)
|
15
11
|
cycle = ColorData::DEFAULT_COLOR_CYCLE
|
16
12
|
name = cycle[name[1..-1].to_i % cycle.length]
|
17
13
|
end
|
18
|
-
|
19
|
-
cache[name]
|
20
|
-
else
|
21
|
-
cache[name] = lookup_no_color_cycle(name)
|
22
|
-
end
|
14
|
+
@cache[name] ||= lookup_no_color_cycle(name)
|
23
15
|
end
|
24
16
|
|
25
|
-
private def lookup_no_color_cycle(
|
26
|
-
|
27
|
-
case color
|
17
|
+
private def lookup_no_color_cycle(name)
|
18
|
+
case name
|
28
19
|
when /\Anone\z/i
|
29
20
|
return RGBA.new(0, 0, 0, 0)
|
30
21
|
when String
|
31
22
|
# nothing to do
|
32
23
|
when Symbol
|
33
|
-
|
24
|
+
name = name.to_s
|
34
25
|
else
|
35
|
-
|
26
|
+
name = name.to_str
|
36
27
|
end
|
37
|
-
color = @mapping.fetch(
|
28
|
+
color = @mapping.fetch(name, name)
|
38
29
|
case color
|
39
30
|
when /\A#\h+\z/
|
40
31
|
case color.length - 1
|
@@ -64,19 +55,19 @@ module Colors
|
|
64
55
|
def []=(name, value)
|
65
56
|
@mapping[name] = value
|
66
57
|
ensure
|
67
|
-
cache.clear
|
58
|
+
@cache.clear
|
68
59
|
end
|
69
60
|
|
70
61
|
def delete(name)
|
71
62
|
@mapping.delete(name)
|
72
63
|
ensure
|
73
|
-
cache.clear
|
64
|
+
@cache.clear
|
74
65
|
end
|
75
66
|
|
76
67
|
def update(other)
|
77
68
|
@mapping.update(other)
|
78
69
|
ensure
|
79
|
-
cache.clear
|
70
|
+
@cache.clear
|
80
71
|
end
|
81
72
|
end
|
82
73
|
|
@@ -104,11 +95,10 @@ module Colors
|
|
104
95
|
when Symbol
|
105
96
|
name = name.to_s
|
106
97
|
else
|
98
|
+
return false unless name.respond_to?(:to_str)
|
107
99
|
name = name.to_str
|
108
100
|
end
|
109
101
|
name.match?(/\AC\d+\z/)
|
110
|
-
rescue NoMethodError, TypeError
|
111
|
-
false
|
112
102
|
end
|
113
103
|
end
|
114
104
|
end
|
data/lib/colors/rgb.rb
CHANGED
@@ -1,22 +1,25 @@
|
|
1
|
-
require_relative 'helper'
|
2
|
-
|
3
1
|
module Colors
|
4
2
|
class RGB < AbstractColor
|
5
3
|
include Helper
|
6
4
|
|
7
5
|
def self.parse(hex_string)
|
8
|
-
|
6
|
+
error_message = "must be a hexadecimal string of `#rrggbb` or `#rgb` form"
|
7
|
+
unless hex_string.respond_to?(:to_str)
|
8
|
+
raise ArgumentError, "#{error_message}: #{hex_string.inspect}"
|
9
|
+
end
|
10
|
+
|
11
|
+
hex_string = hex_string.to_str
|
12
|
+
hexes = hex_string.match(/\A#(\h+)\z/) { $1 }
|
13
|
+
case hexes&.length
|
9
14
|
when 3 # rgb
|
10
|
-
r, g, b =
|
15
|
+
r, g, b = hexes.scan(/\h/).map {|h| h.hex * 17 }
|
11
16
|
new(r, g, b)
|
12
17
|
when 6 # rrggbb
|
13
|
-
r, g, b =
|
18
|
+
r, g, b = hexes.scan(/\h{2}/).map(&:hex)
|
14
19
|
new(r, g, b)
|
15
20
|
else
|
16
|
-
raise ArgumentError, "
|
21
|
+
raise ArgumentError, "#{error_message}: #{hex_string.inspect}"
|
17
22
|
end
|
18
|
-
rescue NoMethodError
|
19
|
-
raise ArgumentError, "hex_string must be a hexadecimal string of `#rrggbb` or `#rgb` form"
|
20
23
|
end
|
21
24
|
|
22
25
|
def initialize(r, g, b)
|
@@ -112,11 +115,18 @@ module Colors
|
|
112
115
|
end
|
113
116
|
|
114
117
|
def to_husl
|
115
|
-
|
118
|
+
c = RGB.new(r, g, b).to_xyz
|
119
|
+
l, u, v = c.luv_components(WHITE_POINT_D65)
|
120
|
+
h, s, l = Convert.luv_to_husl(l, u, v)
|
121
|
+
HUSL.new(h, s.clamp(0r, 1r).to_r, l.clamp(0r, 1r).to_r)
|
116
122
|
end
|
117
123
|
|
118
124
|
def to_xyz
|
119
|
-
XYZ.
|
125
|
+
XYZ.new(*Convert.rgb_to_xyz(r, g, b))
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_xterm256
|
129
|
+
Xterm256.new(*Convert.rgb_to_xterm256(r, g, b))
|
120
130
|
end
|
121
131
|
|
122
132
|
private def canonicalize(r, g, b)
|
data/lib/colors/rgba.rb
CHANGED
@@ -1,24 +1,30 @@
|
|
1
1
|
module Colors
|
2
2
|
class RGBA < RGB
|
3
3
|
def self.parse(hex_string)
|
4
|
-
|
4
|
+
error_message = "must be a hexadecimal string of " +
|
5
|
+
"`#rrggbbaa`, `#rgba`, `#rrggbb` or `#rgb` form"
|
6
|
+
unless hex_string.respond_to?(:to_str)
|
7
|
+
raise ArgumentError, "#{error_message}: #{hex_string.inspect}"
|
8
|
+
end
|
9
|
+
|
10
|
+
hex_string = hex_string.to_str
|
11
|
+
hexes = hex_string.match(/\A#(\h+)\z/) { $1 }
|
12
|
+
case hexes&.length
|
5
13
|
when 3 # rgb
|
6
|
-
r, g, b =
|
14
|
+
r, g, b = hexes.scan(/\h/).map {|h| h.hex * 17 }
|
7
15
|
new(r, g, b, 255)
|
8
16
|
when 6 # rrggbb
|
9
|
-
r, g, b =
|
17
|
+
r, g, b = hexes.scan(/\h{2}/).map(&:hex)
|
10
18
|
new(r, g, b, 255)
|
11
19
|
when 4 # rgba
|
12
|
-
r, g, b, a =
|
20
|
+
r, g, b, a = hexes.scan(/\h/).map {|h| h.hex * 17 }
|
13
21
|
new(r, g, b, a)
|
14
22
|
when 8 # rrggbbaa
|
15
|
-
r, g, b, a =
|
23
|
+
r, g, b, a = hexes.scan(/\h{2}/).map(&:hex)
|
16
24
|
new(r, g, b, a)
|
17
25
|
else
|
18
|
-
raise ArgumentError, "
|
26
|
+
raise ArgumentError, "#{error_message}: #{hex_string.inspect}"
|
19
27
|
end
|
20
|
-
rescue NoMethodError
|
21
|
-
raise ArgumentError, "hex_string must be a hexadecimal string of `#rrggbb` or `#rgb` form"
|
22
28
|
end
|
23
29
|
|
24
30
|
def initialize(r, g, b, a)
|
data/lib/colors/utils.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Colors
|
2
|
+
module Utils
|
3
|
+
module_function def linspace(x0, x1, n)
|
4
|
+
Array.new(n) { |i|
|
5
|
+
x0 + i*(x1 - x0)/(n-1r)
|
6
|
+
}
|
7
|
+
end
|
8
|
+
|
9
|
+
module_function def make_color(value)
|
10
|
+
case value
|
11
|
+
when Colors::AbstractColor
|
12
|
+
value
|
13
|
+
else
|
14
|
+
Colors[value]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/colors/version.rb
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Colors
|
2
|
+
class Xterm256 < AbstractColor
|
3
|
+
include Helper
|
4
|
+
|
5
|
+
def initialize(code)
|
6
|
+
unless 16 <= code && code <= 255
|
7
|
+
raise ArgumentError, "code should be in 16..255, but #{code} is given"
|
8
|
+
end
|
9
|
+
@code = code
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :code
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
case other
|
16
|
+
when Xterm256
|
17
|
+
code == other.code
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_rgb_components
|
24
|
+
if code < 232
|
25
|
+
x = code - 16
|
26
|
+
x, b = x.divmod(6)
|
27
|
+
r, g = x.divmod(6)
|
28
|
+
r = 40*r + 55 if r > 0
|
29
|
+
g = 40*g + 55 if g > 0
|
30
|
+
b = 40*b + 55 if b > 0
|
31
|
+
[
|
32
|
+
canonicalize_component_from_integer(r, :r),
|
33
|
+
canonicalize_component_from_integer(g, :r),
|
34
|
+
canonicalize_component_from_integer(b, :r)
|
35
|
+
]
|
36
|
+
else
|
37
|
+
grey = to_grey_level
|
38
|
+
[grey, grey, grey]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_grey_level
|
43
|
+
if code < 232
|
44
|
+
r, g, b = to_rgb_components
|
45
|
+
x, y, z = Convet.rgb_to_xyz(r, g, b)
|
46
|
+
else
|
47
|
+
grey = 10*(code - 232) + 8
|
48
|
+
canonicalize_component_from_integer(grey, :grey)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_rgb
|
53
|
+
RGB.new(*to_rgb_components)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/colors/xyy.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Colors
|
2
|
+
class XYY < AbstractColor
|
3
|
+
include Helper
|
4
|
+
|
5
|
+
def initialize(x, y, large_y)
|
6
|
+
@x, @y, @large_y = canonicalize(x, y, large_y)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :x, :y, :large_y
|
10
|
+
|
11
|
+
def components
|
12
|
+
[x, y, large_y]
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
case other
|
17
|
+
when XYY
|
18
|
+
x == other.x && y == other.y && large_y == other.large_y
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_rgb
|
25
|
+
to_xyz.to_rgb
|
26
|
+
end
|
27
|
+
|
28
|
+
def rgb_components
|
29
|
+
to_xyz.rgb_components
|
30
|
+
end
|
31
|
+
|
32
|
+
def luv_components(wp)
|
33
|
+
to_xyz.luv_components(wp)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_xyz
|
37
|
+
XYZ.new(*Convert.xyy_to_xyz(*components))
|
38
|
+
end
|
39
|
+
|
40
|
+
private def canonicalize(x, y, large_y)
|
41
|
+
[
|
42
|
+
Rational(x),
|
43
|
+
Rational(y),
|
44
|
+
Rational(large_y)
|
45
|
+
]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|