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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -0
  3. data/data/colormaps/cividis.json +258 -0
  4. data/data/colormaps/coolwarm.json +107 -0
  5. data/data/colormaps/crest.json +258 -0
  6. data/data/colormaps/flare.json +258 -0
  7. data/data/colormaps/gist_earth.json +49 -0
  8. data/data/colormaps/gist_ncar.json +55 -0
  9. data/data/colormaps/icefire.json +258 -0
  10. data/data/colormaps/inferno.json +258 -0
  11. data/data/colormaps/magma.json +258 -0
  12. data/data/colormaps/mako.json +258 -0
  13. data/data/colormaps/nipy_spectral.json +71 -0
  14. data/data/colormaps/pink.json +200 -0
  15. data/data/colormaps/plasma.json +258 -0
  16. data/data/colormaps/rocket.json +258 -0
  17. data/data/colormaps/turbo.json +258 -0
  18. data/data/colormaps/twilight.json +512 -0
  19. data/data/colormaps/viridis.json +258 -0
  20. data/data/colormaps/vlag.json +258 -0
  21. data/lib/colors.rb +17 -5
  22. data/lib/colors/abstract_color.rb +4 -0
  23. data/lib/colors/colormap.rb +143 -0
  24. data/lib/colors/colormap_data.rb +44 -0
  25. data/lib/colors/colormap_data/matplotlib_builtin.rb +990 -0
  26. data/lib/colors/colormap_data/seaborn_builtin.rb +10 -0
  27. data/lib/colors/colormap_registry.rb +62 -0
  28. data/lib/colors/convert.rb +269 -0
  29. data/lib/colors/helper.rb +2 -1
  30. data/lib/colors/husl.rb +7 -100
  31. data/lib/colors/linear_segmented_colormap.rb +137 -0
  32. data/lib/colors/listed_colormap.rb +45 -0
  33. data/lib/colors/named_colors.rb +10 -20
  34. data/lib/colors/rgb.rb +20 -10
  35. data/lib/colors/rgba.rb +14 -8
  36. data/lib/colors/utils.rb +18 -0
  37. data/lib/colors/version.rb +1 -1
  38. data/lib/colors/xterm256.rb +56 -0
  39. data/lib/colors/xyy.rb +48 -0
  40. data/lib/colors/xyz.rb +2 -55
  41. data/red-colors.gemspec +3 -1
  42. data/test/test-husl.rb +45 -53
  43. data/test/test-linear-segmented-colormap.rb +138 -0
  44. data/test/test-listed-colormap.rb +134 -0
  45. data/test/test-rgb.rb +76 -1
  46. data/test/test-xterm256.rb +76 -0
  47. data/test/test-xyz.rb +1 -1
  48. 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
@@ -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
- if cache.has_key?(name)
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(color)
26
- orig_color = color
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
- color = color.to_s
24
+ name = name.to_s
34
25
  else
35
- color = color.to_str
26
+ name = name.to_str
36
27
  end
37
- color = @mapping.fetch(color, color)
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
- case hex_string.to_str.match(/\A#(\h+)\z/) { $1 }.length
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 = hex_string.scan(/\h/).map {|h| h.hex * 17 }
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 = hex_string.scan(/\h{2}/).map(&:hex)
18
+ r, g, b = hexes.scan(/\h{2}/).map(&:hex)
14
19
  new(r, g, b)
15
20
  else
16
- raise ArgumentError, "Invalid hex string: #{hex_string.inspect}"
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
- HUSL.from_rgb(r, g, b)
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.from_rgb(r, g, b)
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
- case hex_string.to_str.match(/\A#(\h+)\z/) { $1 }.length
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 = hex_string.scan(/\h/).map {|h| h.hex * 17 }
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 = hex_string.scan(/\h{2}/).map(&:hex)
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 = hex_string.scan(/\h/).map {|h| h.hex * 17 }
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 = hex_string.scan(/\h{2}/).map(&:hex)
23
+ r, g, b, a = hexes.scan(/\h{2}/).map(&:hex)
16
24
  new(r, g, b, a)
17
25
  else
18
- raise ArgumentError, "Invalid hex string: #{hex_string.inspect}"
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)
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Colors
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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