red-colors 0.1.0 → 0.3.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 +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
|