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.
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