Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/gtn/hsluv.rb
1677 views
1
# frozen_string_literal: true
2
3
# The MIT License (MIT)
4
#
5
# Copyright (c) 2015 Radu-Bogdan Croitoru
6
# Copyright (c) 2016 Alexei Boronine
7
#
8
# Permission is hereby granted, free of charge, to any person obtaining a copy
9
# of this software and associated documentation files (the "Software"), to deal
10
# in the Software without restriction, including without limitation the rights
11
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
# copies of the Software, and to permit persons to whom the Software is
13
# furnished to do so, subject to the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included in
16
# all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
# THE SOFTWARE.
25
#
26
# https://github.com/hsluv/hsluv-ruby
27
28
# Converts between RGB, HSLUV
29
module Hsluv
30
module_function
31
32
M = [
33
[3.240969941904521, -1.537383177570093, -0.498610760293],
34
[-0.96924363628087, 1.87596750150772, 0.041555057407175],
35
[0.055630079696993, -0.20397695888897, 1.056971514242878]
36
].freeze
37
38
M_INV = [
39
[0.41239079926595, 0.35758433938387, 0.18048078840183],
40
[0.21263900587151, 0.71516867876775, 0.072192315360733],
41
[0.019330818715591, 0.11919477979462, 0.95053215224966]
42
].freeze
43
44
REF_X = 0.95045592705167
45
REF_Y = 1.0
46
REF_Z = 1.089057750759878
47
REF_U = 0.19783000664283
48
REF_V = 0.46831999493879
49
KAPPA = 903.2962962
50
EPSILON = 0.0088564516
51
52
###
53
54
def hsluv_to_hex(h, s, l)
55
rgb_to_hex(*hsluv_to_rgb(h, s, l))
56
end
57
58
def hpluv_to_hex(h, s, l)
59
rgb_to_hex(*hpluv_to_rgb(h, s, l))
60
end
61
62
def hex_to_hsluv(hex)
63
rgb_to_hsluv(*hex_to_rgb(hex))
64
end
65
66
def hex_to_hpluv(hex)
67
rgb_to_hpluv(*hex_to_rgb(hex))
68
end
69
70
def hsluv_to_rgb(h, s, l)
71
xyz_to_rgb(luv_to_xyz(lch_to_luv(hsluv_to_lch([h, s, l]))))
72
end
73
74
def rgb_to_hsluv(r, g, b)
75
lch_to_hsluv(rgb_to_lch(r, g, b))
76
end
77
78
def hpluv_to_rgb(h, s, l)
79
lch_to_rgb(*hpluv_to_lch([h, s, l]))
80
end
81
82
def rgb_to_hpluv(r, g, b)
83
lch_to_hpluv(rgb_to_lch(r, g, b))
84
end
85
86
def lch_to_rgb(l, c, h)
87
xyz_to_rgb(luv_to_xyz(lch_to_luv([l, c, h])))
88
end
89
90
def rgb_to_lch(r, g, b)
91
luv_to_lch(xyz_to_luv(rgb_to_xyz([r, g, b])))
92
end
93
94
def rgb_to_hex(r, g, b)
95
'#%02x%02x%02x' % rgb_prepare([r, g, b])
96
end
97
98
def hex_to_rgb(hex)
99
hex = hex.tr('#', '')
100
[].tap { |arr| hex.chars.each_slice(2) { |block| arr << (block.join.to_i(16) / 255.0) } }
101
end
102
103
###
104
105
def rgb_to_xyz(arr)
106
rgbl = arr.map { |val| to_linear(val) }
107
M_INV.map { |i| dot_product(i, rgbl) }
108
end
109
110
def xyz_to_luv(arr)
111
x, y, z = arr
112
l = f(y)
113
114
return [0.0, 0.0, 0.0] if [x, y, z, 0.0].uniq.length == 1 || l.zero?
115
116
var_u = (4.0 * x) / (x + (15.0 * y) + (3.0 * z))
117
var_v = (9.0 * y) / (x + (15.0 * y) + (3.0 * z))
118
u = 13.0 * l * (var_u - REF_U)
119
v = 13.0 * l * (var_v - REF_V)
120
121
[l, u, v]
122
end
123
124
def luv_to_lch(arr)
125
l, u, v = arr
126
c = ((u**2) + (v**2))**(1 / 2.0)
127
hrad = Math.atan2(v, u)
128
h = radians_to_degrees(hrad)
129
h += 360.0 if h < 0.0
130
[l, c, h]
131
end
132
133
def lch_to_hsluv(arr)
134
l, c, h = arr
135
return [h, 0.0, 100.0] if l > 99.9999999
136
return [h, 0.0, 0.0] if l < 0.00000001
137
138
mx = max_chroma_for(l, h)
139
s = c / mx * 100.0
140
141
[h, s, l]
142
end
143
144
def lch_to_hpluv(arr)
145
l, c, h = arr
146
147
return [h, 0.0, 100.0] if l > 99.9999999
148
return [h, 0.0, 0.0] if l < 0.00000001
149
150
mx = max_safe_chroma_for(l)
151
s = c / mx * 100.0
152
153
[h, s, l]
154
end
155
156
###
157
158
def xyz_to_rgb(arr)
159
xyz = M.map { |i| dot_product(i, arr) }
160
xyz.map { |i| from_linear(i) }
161
end
162
163
def luv_to_xyz(arr)
164
l, u, v = arr
165
166
return [0.0, 0.0, 0.0] if l.zero?
167
168
var_y = f_inv(l)
169
var_u = (u / (13.0 * l)) + REF_U
170
var_v = (v / (13.0 * l)) + REF_V
171
172
y = var_y * REF_Y
173
x = 0.0 - ((9.0 * y * var_u) / (((var_u - 4.0) * var_v) - (var_u * var_v)))
174
z = ((9.0 * y) - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v)
175
176
[x, y, z]
177
end
178
179
def lch_to_luv(arr)
180
l, c, h = arr
181
182
hrad = degrees_to_radians(h)
183
u = Math.cos(hrad) * c
184
v = Math.sin(hrad) * c
185
186
[l, u, v]
187
end
188
189
def hsluv_to_lch(arr)
190
h, s, l = arr
191
192
return [100, 0.0, h] if l > 99.9999999
193
return [0.0, 0.0, h] if l < 0.00000001
194
195
mx = max_chroma_for(l, h)
196
c = mx / 100.0 * s
197
198
[l, c, h]
199
end
200
201
def hpluv_to_lch(arr)
202
h, s, l = arr
203
204
return [100, 0.0, h] if l > 99.9999999
205
return [0.0, 0.0, h] if l < 0.00000001
206
207
mx = max_safe_chroma_for(l)
208
c = mx / 100.0 * s
209
210
[l, c, h]
211
end
212
213
###
214
215
def radians_to_degrees(rad)
216
rad * 180.0 / Math::PI
217
end
218
219
def degrees_to_radians(degrees)
220
degrees * Math::PI / 180.0
221
end
222
223
def max_chroma_for(l, h)
224
hrad = h / 360.0 * Math::PI * 2.0
225
lengths = []
226
227
get_bounds(l).each do |line|
228
l = length_of_ray_until_intersect(hrad, line)
229
lengths << l if l
230
end
231
232
lengths.min
233
end
234
235
def max_safe_chroma_for(l)
236
lengths = []
237
238
get_bounds(l).each do |m1, b1|
239
x = intersect_line_line([m1, b1], [-1.0 / m1, 0.0])
240
lengths << distance_from_pole([x, b1 + (x * m1)])
241
end
242
243
lengths.min
244
end
245
246
def get_bounds(l)
247
sub1 = ((l + 16.0)**3.0) / 1_560_896.0
248
sub2 = sub1 > EPSILON ? sub1 : l / KAPPA
249
ret = []
250
251
M.each do |m1, m2, m3|
252
[0, 1].each do |t|
253
top1 = ((284_517.0 * m1) - (94_839.0 * m3)) * sub2
254
top2 = (((838_422.0 * m3) + (769_860.0 * m2) + (731_718.0 * m1)) * l * sub2) - (769_860.0 * t * l)
255
bottom = (((632_260.0 * m3) - (126_452.0 * m2)) * sub2) + (126_452.0 * t)
256
ret << [top1 / bottom, top2 / bottom]
257
end
258
end
259
260
ret
261
end
262
263
def length_of_ray_until_intersect(theta, line)
264
m1, b1 = line
265
length = b1 / (Math.sin(theta) - (m1 * Math.cos(theta)))
266
return nil if length.negative?
267
268
length
269
end
270
271
def intersect_line_line(line1, line2)
272
(line1[1] - line2[1]) / (line2[0] - line1[0])
273
end
274
275
def distance_from_pole(point)
276
Math.sqrt((point[0]**2) + (point[1]**2))
277
end
278
279
def f(t)
280
t > EPSILON ? (116 * ((t / REF_Y)**(1.0 / 3.0))) - 16.0 : t / REF_Y * KAPPA
281
end
282
283
def f_inv(t)
284
t > 8 ? REF_Y * (((t + 16.0) / 116.0)**3.0) : REF_Y * t / KAPPA
285
end
286
287
def to_linear(c)
288
c > 0.04045 ? ((c + 0.055) / 1.055)**2.4 : c / 12.92
289
end
290
291
def from_linear(c)
292
c <= 0.0031308 ? 12.92 * c : ((1.055 * (c**(1.0 / 2.4))) - 0.055)
293
end
294
295
def dot_product(a, b)
296
a.zip(b).map { |i, j| i * j }.inject(:+)
297
end
298
299
def rgb_prepare(arr)
300
arr.map! do |ch|
301
ch = ch.round(3)
302
ch = [0, ch].max
303
ch = [1, ch].min
304
(ch * 255).round
305
end
306
end
307
end
308
309