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