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,10 @@
1
+ module Colors
2
+ module ColormapRegistry
3
+ register_listed_colormap("rocket")
4
+ register_listed_colormap("mako")
5
+ register_listed_colormap("icefire")
6
+ register_listed_colormap("vlag")
7
+ register_listed_colormap("flare")
8
+ register_listed_colormap("crest")
9
+ end
10
+ end
@@ -0,0 +1,62 @@
1
+ module Colors
2
+ module ColormapRegistry
3
+ @registry = {}
4
+
5
+ def self.[](name)
6
+ return name if name.is_a?(Colormap)
7
+
8
+ name = String.try_convert(name)
9
+ if @registry.key?(name)
10
+ return @registry[name]
11
+ else
12
+ raise ArgumentError, "Unknown colormap name: %p" % name
13
+ end
14
+ end
15
+
16
+ def self.register(cmap, name: nil, override_builtin: false)
17
+ case name
18
+ when String, Symbol
19
+ name = name.to_s
20
+ when nil
21
+ name = cmap.name
22
+ if name.nil?
23
+ raise ArgumentError, "`name` cannot be omitted for unnamed colormaps"
24
+ end
25
+ else
26
+ name = String.try_convert(name)
27
+ if name.nil?
28
+ raise ArgumentError, "`name` must be convertible to a String by to_str"
29
+ end
30
+ end
31
+
32
+ if @registry.key?(name)
33
+ existing = @registry[name]
34
+ if BUILTIN_COLORMAPS.key?(name)
35
+ unless override_builtin
36
+ raise ArgumentError,
37
+ "Trying to re-register a builtin colormap: %p" % name
38
+ end
39
+ end
40
+ warn "Trying to re-register the colormap %p which already exists" % name
41
+ end
42
+
43
+ unless cmap.is_a?(Colormap)
44
+ raise ArgumentError,
45
+ "Invalid value for registering a colormap (%p for a Colormap)" % cmap
46
+ end
47
+
48
+ @registry[name] = cmap
49
+ end
50
+
51
+ def self.unregister(name)
52
+ if @registry.key?(name)
53
+ if BUILTIN_COLORMAPS.key?(name)
54
+ raise ArgumentError,
55
+ "Unable to unregister the colormap %p which is a builtin colormap" % name
56
+ end
57
+ else
58
+ @registry.delete(name)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,269 @@
1
+ require "matrix"
2
+
3
+ module Colors
4
+ module Convert
5
+ module_function
6
+
7
+ # Sort alphabetically by FROM name such as degree, LCh, LUV and so on.
8
+
9
+ # Utilities
10
+
11
+ private def dot_product(matrix, vector)
12
+ matrix.map do |row|
13
+ product = 0.0
14
+ row.zip(vector) do |value1, value2|
15
+ product += value1 * value2
16
+ end
17
+ product
18
+ end
19
+ end
20
+
21
+ private def matrix_inv(matrix)
22
+ matrix = Matrix[*matrix]
23
+ matrix.inv.to_a
24
+ end
25
+
26
+ def max_chroma(l, h)
27
+ h_rad = degree_to_radian(h)
28
+ sin_h = Math.sin(h_rad).to_r
29
+ cos_h = Math.cos(h_rad).to_r
30
+
31
+ max = Float::INFINITY
32
+ luminance_bounds(l).each do |line|
33
+ len = line[1] / (sin_h - line[0] * cos_h)
34
+ max = len if 0 <= len && len < max
35
+ end
36
+ max
37
+ end
38
+
39
+ private def luminance_bounds(l)
40
+ sub1 = (l + 16)**3 / 1560896r
41
+ sub2 = sub1 > XYZ::EPSILON ? sub1 : l/XYZ::KAPPA
42
+
43
+ bounds = Array.new(6) { [0r, 0r] }
44
+ 0.upto(2) do |ch|
45
+ m1 = M_XYZ_RGB[ch][0].to_r
46
+ m2 = M_XYZ_RGB[ch][1].to_r
47
+ m3 = M_XYZ_RGB[ch][2].to_r
48
+
49
+ [0, 1].each do |t|
50
+ top1 = (284517r * m1 - 94839r * m3) * sub2
51
+ top2 = (838422r * m3 + 769860r * m2 + 731718r * m1) * l * sub2 - 769860r * t * l
52
+ bottom = (632260r * m3 - 126452r * m2) * sub2 + 126452r * t
53
+
54
+ bounds[ch*2 + t][0] = top1 / bottom
55
+ bounds[ch*2 + t][1] = top2 / bottom
56
+ end
57
+ end
58
+ bounds
59
+ end
60
+
61
+ # degree -> ???
62
+
63
+ DEG2RAD = 0.01745329251994329577r # 2 * pi / 360
64
+ def degree_to_radian(d)
65
+ d * DEG2RAD
66
+ end
67
+
68
+ # LCh -> ???
69
+
70
+ def lch_to_husl(l, c, h)
71
+ if l > 99.9999999 || l < 1e-8
72
+ s = 0r
73
+ else
74
+ mx = max_chroma(l, h)
75
+ s = c / mx * 100r
76
+ end
77
+
78
+ h = 0r if c < 1e-8
79
+
80
+ [h, s/100r, l/100r]
81
+ end
82
+
83
+ def lch_to_luv(l, c, h)
84
+ h_rad = degree_to_radian(h)
85
+ u = Math.cos(h_rad).to_r * c
86
+ v = Math.sin(h_rad).to_r * c
87
+ [l, u, v]
88
+ end
89
+
90
+ def lch_to_xyz(l, c, h)
91
+ luv_to_xyz(*lch_to_luv(l, c, h))
92
+ end
93
+
94
+ # linear-sRGB -> ???
95
+
96
+ def linear_srgb_to_srgb(r, g, b)
97
+ [r, g, b].map do |v|
98
+ # the following is an optimization technique for `1.055*v**(1/2.4) - 0.055`.
99
+ # x^y ~= exp(y*log(x)) ~= exp2(y*log2(y)); the middle form is faster
100
+ #
101
+ # See https://github.com/JuliaGraphics/Colors.jl/issues/351#issuecomment-532073196
102
+ # for more detail benchmark in Julia language.
103
+ if v <= 0.0031308
104
+ 12.92*v
105
+ else
106
+ 1.055 * Math.exp(1/2.4 * Math.log(v)) - 0.055
107
+ end
108
+ end
109
+ end
110
+
111
+ # Luv -> ???
112
+
113
+ def luv_to_husl(l, u, v)
114
+ lch_to_husl(*luv_to_lch(l, u, v))
115
+ end
116
+
117
+ def luv_to_lch(l, u, v)
118
+ c = Math.sqrt(u*u + v*v).to_r
119
+ hard = Math.atan2(v, u).to_r
120
+ h = hard * 180 / Math::PI.to_r
121
+ h += 360r if h < 0
122
+ [l, c, h]
123
+ end
124
+
125
+ def luv_to_xyz(l, u, v)
126
+ return [0r, 0r, 0r] if l <= 1e-8
127
+
128
+ wp_u, wp_v = WHITE_POINT_D65.uv_values
129
+ var_u = u / (13 * l) + wp_u
130
+ var_v = v / (13 * l) + wp_v
131
+ y = if l < 8
132
+ l / XYZ::KAPPA
133
+ else
134
+ ((l + 16r) / 116r)**3
135
+ end
136
+ x = -(9 * y * var_u) / ((var_u - 4) * var_v - var_u * var_v)
137
+ z = (9 * y - (15 * var_v * y) - (var_v * x)) / (3 * var_v)
138
+ [x, y, z]
139
+ end
140
+
141
+ # RGB -> ???
142
+
143
+ RGB2XYZ = [
144
+ [ 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 ],
145
+ [ 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 ],
146
+ [ 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 ]
147
+ ]
148
+
149
+ def rgb_to_xyz(r, g, b)
150
+ dot_product(RGB2XYZ, srgb_to_linear_srgb(r, g, b))
151
+ end
152
+
153
+ def rgb_to_xterm256(r, g, b)
154
+ i = closest_xterm256_rgb_index(r)
155
+ j = closest_xterm256_rgb_index(g)
156
+ k = closest_xterm256_rgb_index(b)
157
+
158
+ r0 = xterm256_rgb_index_to_rgb_value(i)
159
+ g0 = xterm256_rgb_index_to_rgb_value(j)
160
+ b0 = xterm256_rgb_index_to_rgb_value(k)
161
+ d0 = (r - r0)**2 + (g - g0)**2 + (b - b0)**2
162
+
163
+ l = closest_xterm256_gray_index(r, g, b)
164
+ gr = xterm256_gray_index_to_gray_level(l)
165
+ d1 = (r - gr)**2 + (g - gr)**2 + (b - gr)**2
166
+
167
+ if d0 > d1
168
+ xterm256_gray_index_to_code(l)
169
+ else
170
+ xterm256_rgb_indices_to_code(i, j, k)
171
+ end
172
+ end
173
+
174
+ def xterm256_rgb_index_to_rgb_value(i)
175
+ (i == 0) ? 0 : (40*i + 55)/255.0
176
+ end
177
+
178
+ def closest_xterm256_rgb_index(x)
179
+ ([x*255 - 55, 0].max / 40.0).round
180
+ end
181
+
182
+ def xterm256_gray_index_to_gray_level(i)
183
+ (10*i + 8)/255.0
184
+ end
185
+
186
+ def closest_xterm256_gray_index(r, g, b)
187
+ ((255*(r + g + b) - 24)/30.0).round.clamp(0, 23)
188
+ end
189
+
190
+ def xterm256_rgb_indices_to_code(i, j, k)
191
+ 6*(6*i + j) + k + 16
192
+ end
193
+
194
+ def xterm256_gray_index_to_code(i)
195
+ i + 232
196
+ end
197
+
198
+ # sRGB -> ???
199
+
200
+ def srgb_from_linear_srgb(r, g, b)
201
+ a = 0.055r
202
+ [r, g, b].map do |v|
203
+ if v < 0.0031308
204
+ 12.92r * v
205
+ else
206
+ (1 + a) * v**(1/2.4r) - a
207
+ end
208
+ end
209
+ end
210
+
211
+ def srgb_to_linear_srgb(r, g, b)
212
+ a = 0.055r
213
+ [r, g, b].map do |v|
214
+ if v > 0.04045
215
+ ((v + a) / (1 + a)) ** 2.4r
216
+ else
217
+ v / 12.92r
218
+ end
219
+ end
220
+ end
221
+
222
+ # xyY -> ???
223
+
224
+ def xyy_to_xyz(x, y, large_y)
225
+ large_x = large_y*x/y
226
+ large_z = large_y*(1 - x - y)/y
227
+ [large_x, large_y, large_z]
228
+ end
229
+
230
+ # XYZ -> ???
231
+
232
+ # sRGB reference points
233
+ R_xyY = [0.64r, 0.33r, 1r]
234
+ G_xyY = [0.30r, 0.60r, 1r]
235
+ B_xyY = [0.15r, 0.06r, 1r]
236
+ D65_xyY = [0.3127r, 0.3290r, 1r]
237
+
238
+ R_XYZ = xyy_to_xyz(*R_xyY)
239
+ G_XYZ = xyy_to_xyz(*G_xyY)
240
+ B_XYZ = xyy_to_xyz(*B_xyY)
241
+ D65_XYZ = xyy_to_xyz(*D65_xyY)
242
+
243
+ M_P = [
244
+ [R_XYZ[0], G_XYZ[0], B_XYZ[0]],
245
+ [R_XYZ[1], G_XYZ[1], B_XYZ[1]],
246
+ [R_XYZ[2], G_XYZ[2], B_XYZ[2]]
247
+ ]
248
+
249
+ M_S = dot_product(matrix_inv(M_P), D65_XYZ)
250
+
251
+ M_RGB_XYZ = (0 ... 3).map do |i|
252
+ (0 ... 3).map {|j| (M_S[j] * M_P[i][j]).round(4) }
253
+ end
254
+
255
+ M_XYZ_RGB = matrix_inv(M_RGB_XYZ).map do |row|
256
+ row.map {|v| v.round(4) }
257
+ end
258
+
259
+ def xyz_to_rgb(x, y, z)
260
+ r, g, b = dot_product(M_XYZ_RGB, [x, y, z])
261
+ r, g, b = srgb_from_linear_srgb(r, g, b)
262
+ [
263
+ r.clamp(0r, 1r),
264
+ g.clamp(0r, 1r),
265
+ b.clamp(0r, 1r)
266
+ ]
267
+ end
268
+ end
269
+ end
data/lib/colors/helper.rb CHANGED
@@ -7,7 +7,8 @@ module Colors
7
7
 
8
8
  private def check_range(value, range, name)
9
9
  return value if range.cover?(value)
10
- check_fail ArgumentError, "#{name} must be in #{range}, but #{value} is given"
10
+ check_fail ArgumentError,
11
+ "#{name} must be in #{range}, but %p is given" % value
11
12
  end
12
13
 
13
14
  private def check_fail(exc_class, message)
data/lib/colors/husl.rb CHANGED
@@ -1,45 +1,7 @@
1
- require "numo/narray"
2
-
3
1
  module Colors
4
2
  # Human-friendly alternative to HSL color space.
5
3
  # The definition of HUSL is provided in <http://www.hsluv.org>.
6
4
  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
5
  def ==(other)
44
6
  case other
45
7
  when HUSL
@@ -61,10 +23,13 @@ module Colors
61
23
  RGB.new(*rgb_components)
62
24
  end
63
25
 
26
+ def to_xyz
27
+ x, y, z = Convert.lch_to_xyz(*lch_components)
28
+ XYZ.new(x, y, z)
29
+ end
30
+
64
31
  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
32
+ to_xyz.rgb_components
68
33
  end
69
34
 
70
35
  def lch_components
@@ -74,7 +39,7 @@ module Colors
74
39
  if l > 99.9999999 || l < 1e-8
75
40
  c = 0r
76
41
  else
77
- mx = self.class.max_chroma(l, h)
42
+ mx = Convert.max_chroma(l, h)
78
43
  c = mx / 100r * s
79
44
  end
80
45
 
@@ -82,63 +47,5 @@ module Colors
82
47
 
83
48
  [l, c, h]
84
49
  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
50
  end
144
51
  end