decolmor 1.0.0 → 1.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/NEWS.md +21 -0
- data/README.md +72 -22
- data/decolmor.gemspec +4 -4
- data/lib/decolmor/main.rb +246 -220
- data/lib/decolmor/version.rb +1 -1
- data/lib/decolmor.rb +0 -14
- data/spec/decolmor_spec.rb +137 -74
- data/spec/factories/alpha.rb +1 -1
- metadata +7 -7
data/lib/decolmor/main.rb
CHANGED
@@ -1,276 +1,302 @@
|
|
1
1
|
module Decolmor
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
def self.hex_to_rgb(hex)
|
6
|
-
rgb = hex.gsub('#','').scan(/../).map(&:hex).map(&:to_i)
|
7
|
-
rgb.size == 4 ? rgb + [(rgb.delete_at(3) / 255.to_f).round(5)] : rgb
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
8
5
|
end
|
9
6
|
|
10
|
-
|
11
|
-
template = rgb.size == 3 ? "#%02X%02X%02X" : "#%02X%02X%02X%02X"
|
12
|
-
rgb = rgb[0..2] + [(rgb[3] * 255).round] if rgb.size == 4
|
13
|
-
template % rgb
|
14
|
-
end
|
7
|
+
#========= Set default rounding for HSL/HSV/HSB/CMYK conversion ========
|
15
8
|
|
16
|
-
|
9
|
+
# round 1 enough for lossless conversion RGB -> HSL/HSV/HSB -> RGB
|
10
|
+
# for lossless conversion HSL <==> HSV (HSB) better to use round 2
|
11
|
+
#
|
12
|
+
HSX_ROUND = 1
|
17
13
|
|
18
|
-
|
19
|
-
def self.new_rgb(red: nil, green: nil, blue: nil, alpha: nil)
|
20
|
-
range = 0..255
|
21
|
-
rgb = [red, green, blue].map { |channel| channel || rand(range) }
|
22
|
-
alpha.nil? ? rgb : rgb + [alpha]
|
23
|
-
end
|
14
|
+
module ClassMethods
|
24
15
|
|
25
|
-
|
16
|
+
attr_writer :hsx_round
|
26
17
|
|
27
|
-
|
28
|
-
|
29
|
-
|
18
|
+
def hsx_round
|
19
|
+
@hsx_round ||= HSX_ROUND
|
20
|
+
end
|
30
21
|
|
31
|
-
|
32
|
-
|
22
|
+
#========= HEX <==> RGB(A) =============================================
|
23
|
+
|
24
|
+
def hex_to_rgb(hex, alpha_round = 3, alpha_255: false)
|
25
|
+
hex = hex.gsub('#','')
|
26
|
+
hex = if [3, 4].include? hex.length
|
27
|
+
hex.chars.map{ |char| char * 2 }
|
28
|
+
else
|
29
|
+
hex.scan(/../)
|
30
|
+
end
|
31
|
+
rgb = hex.map(&:hex)
|
32
|
+
if rgb.size == 4
|
33
|
+
rgb[3] = (rgb[3] / 255.to_f).round(alpha_round) unless alpha_255
|
34
|
+
end
|
35
|
+
|
36
|
+
rgb
|
37
|
+
end
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
39
|
+
def rgb_to_hex(rgb, alpha_255: false)
|
40
|
+
if rgb.size == 3
|
41
|
+
"#%02X%02X%02X" % rgb
|
42
|
+
else
|
43
|
+
rgb[3] = (rgb[3] * 255).round unless alpha_255
|
44
|
+
"#%02X%02X%02X%02X" % rgb
|
45
|
+
end
|
46
|
+
end
|
38
47
|
|
39
|
-
|
40
|
-
saturation *= 100
|
41
|
-
lightness *= 100
|
48
|
+
#=======================================================================
|
42
49
|
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
50
|
+
# simple generator RGB, you can set any channel(s)
|
51
|
+
def new_rgb(red: nil, green: nil, blue: nil, alpha: nil)
|
52
|
+
range = 0..255
|
53
|
+
rgb = [red, green, blue].map { |channel| channel || rand(range) }
|
54
|
+
alpha.nil? ? rgb : rgb + [alpha]
|
55
|
+
end
|
47
56
|
|
48
|
-
|
49
|
-
# scaling RGB values into range 0..1
|
50
|
-
red, green, blue, alpha = rgb_arr.map { |color| color / 255.to_f }
|
57
|
+
#========= RGB(A) to HSL/HSV/HSB =======================================
|
51
58
|
|
52
|
-
|
53
|
-
|
59
|
+
def rgb_to_hsl(rgb_arr, rounding = hsx_round)
|
60
|
+
# scaling RGB values into range 0..1
|
61
|
+
red, green, blue, alpha = rgb_arr.map { |color| color / 255.to_f }
|
54
62
|
|
55
|
-
|
56
|
-
|
57
|
-
saturation = chroma == 0 ? 0 : chroma / cmax
|
58
|
-
value = cmax
|
63
|
+
# calculation intermediate values
|
64
|
+
cmin, cmax, chroma = get_min_max_chroma(red, green, blue)
|
59
65
|
|
60
|
-
|
61
|
-
|
62
|
-
|
66
|
+
# calculation HSL values
|
67
|
+
hue = get_hue(red, green, blue)
|
68
|
+
lightness = (cmax + cmin) / 2
|
69
|
+
saturation = chroma == 0 ? 0 : chroma / (1 - (2 * lightness - 1).abs)
|
63
70
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
71
|
+
# scaling values to fill 0..100 interval
|
72
|
+
saturation *= 100
|
73
|
+
lightness *= 100
|
68
74
|
|
75
|
+
# rounding, drop Alpha if not set (nil)
|
76
|
+
hsl = [hue, saturation, lightness].map { |x| x.round(rounding) }
|
77
|
+
alpha.nil? ? hsl : hsl + [alpha * 255]
|
78
|
+
end
|
69
79
|
|
70
|
-
|
80
|
+
def rgb_to_hsv(rgb_arr, rounding = hsx_round)
|
81
|
+
# scaling RGB values into range 0..1
|
82
|
+
red, green, blue, alpha = rgb_arr.map { |color| color / 255.to_f }
|
71
83
|
|
72
|
-
|
84
|
+
# calculation intermediate values
|
85
|
+
_cmin, cmax, chroma = get_min_max_chroma(red, green, blue)
|
73
86
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
lightness /= 100
|
87
|
+
# calculation HSV values
|
88
|
+
hue = get_hue(red, green, blue)
|
89
|
+
saturation = chroma == 0 ? 0 : chroma / cmax
|
90
|
+
value = cmax
|
79
91
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
92
|
+
# scaling values into range 0..100
|
93
|
+
saturation *= 100
|
94
|
+
value *= 100
|
95
|
+
|
96
|
+
# rounding
|
97
|
+
hsv = [hue, saturation, value].map { |x| x.round(rounding) }
|
98
|
+
alpha.nil? ? hsv : hsv + [alpha * 255]
|
85
99
|
end
|
86
100
|
|
87
|
-
|
88
|
-
rgb = [0, 8, 4]
|
89
|
-
rgb.map! { |channel| (converter.call(channel) * 255).round }
|
90
|
-
alpha.nil? ? rgb : rgb + [alpha]
|
91
|
-
end
|
101
|
+
alias_method :rgb_to_hsb, :rgb_to_hsv
|
92
102
|
|
93
|
-
|
94
|
-
hue, saturation, value, alpha = hsv_arr.map(&:to_f)
|
95
|
-
# scaling values into range 0..1
|
96
|
-
saturation /= 100
|
97
|
-
value /= 100
|
103
|
+
#========= HSL/HSV/HSB to RGB(A) =======================================
|
98
104
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
105
|
+
def hsl_to_rgb(hsl_arr)
|
106
|
+
hue, saturation, lightness, alpha = hsl_arr.map(&:to_f)
|
107
|
+
# scaling values into range 0..1
|
108
|
+
saturation /= 100
|
109
|
+
lightness /= 100
|
104
110
|
|
105
|
-
|
106
|
-
|
107
|
-
rgb.map! { |channel| (converter.call(channel) * 255).round }
|
108
|
-
alpha.nil? ? rgb : rgb + [alpha]
|
109
|
-
end
|
111
|
+
# calculation intermediate values
|
112
|
+
a = saturation * [lightness, 1 - lightness].min
|
110
113
|
|
111
|
-
|
114
|
+
# calculation rgb & scaling into range 0..255
|
115
|
+
rgb = [0, 8, 4]
|
116
|
+
rgb.map! do |channel|
|
117
|
+
k = (channel + hue / 30) % 12
|
118
|
+
channel = lightness - a * [-1, [k - 3, 9 - k, 1].min].max
|
119
|
+
(channel * 255).round
|
120
|
+
end
|
121
|
+
alpha.nil? ? rgb : rgb + [alpha]
|
122
|
+
end
|
112
123
|
|
113
|
-
|
124
|
+
def hsv_to_rgb(hsv_arr)
|
125
|
+
hue, saturation, value, alpha = hsv_arr.map(&:to_f)
|
126
|
+
# scaling values into range 0..1
|
127
|
+
saturation /= 100
|
128
|
+
value /= 100
|
129
|
+
|
130
|
+
# calculation rgb & scaling into range 0..255
|
131
|
+
rgb = [5, 3, 1]
|
132
|
+
rgb.map! do |channel|
|
133
|
+
k = (channel + hue / 60) % 6
|
134
|
+
channel = value - value * saturation * [0, [k, 4 - k, 1].min].max
|
135
|
+
(channel * 255).round
|
136
|
+
end
|
137
|
+
alpha.nil? ? rgb : rgb + [alpha]
|
138
|
+
end
|
114
139
|
|
115
|
-
|
116
|
-
hue, saturation, lightness, alpha = hsl_arr.map(&:to_f)
|
117
|
-
# scaling values into range 0..1
|
118
|
-
saturation /= 100
|
119
|
-
lightness /= 100
|
140
|
+
alias_method :hsb_to_rgb, :hsv_to_rgb
|
120
141
|
|
121
|
-
|
122
|
-
chroma = (1 - (2 * lightness - 1).abs) * saturation
|
123
|
-
hue /= 60
|
124
|
-
x = chroma * (1 - (hue % 2 - 1).abs)
|
142
|
+
#========= Alternative implementation HSL/HSV/HSB to RGB(A) ============
|
125
143
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
[x, 0, chroma],
|
132
|
-
[chroma, 0, x]]
|
133
|
-
# point selection based on entering HUE input in range
|
134
|
-
point = points.each_with_index.detect { |rgb_, n| (n..n + 1).include? hue }&.first
|
135
|
-
# if point == nil (hue undefined)
|
136
|
-
rgb = point || [0, 0, 0]
|
137
|
-
|
138
|
-
# calculation rgb & scaling into range 0..255
|
139
|
-
m = lightness - chroma / 2
|
140
|
-
rgb.map! { |channel| ((channel + m) * 255).round }
|
141
|
-
alpha.nil? ? rgb : rgb + [alpha]
|
142
|
-
end
|
144
|
+
def hsl_to_rgb_alt(hsl_arr)
|
145
|
+
hue, saturation, lightness, alpha = hsl_arr.map(&:to_f)
|
146
|
+
# scaling values into range 0..1
|
147
|
+
saturation /= 100
|
148
|
+
lightness /= 100
|
143
149
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
150
|
+
# calculation chroma & intermediate values
|
151
|
+
chroma = (1 - (2 * lightness - 1).abs) * saturation
|
152
|
+
hue /= 60
|
153
|
+
x = chroma * (1 - (hue % 2 - 1).abs)
|
154
|
+
point = get_rgb_point(hue, chroma, x)
|
149
155
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
156
|
+
# calculation rgb & scaling into range 0..255
|
157
|
+
m = lightness - chroma / 2
|
158
|
+
rgb = point.map { |channel| ((channel + m) * 255).round }
|
159
|
+
alpha.nil? ? rgb : rgb + [alpha]
|
160
|
+
end
|
154
161
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
162
|
+
def hsv_to_rgb_alt(hsv_arr)
|
163
|
+
hue, saturation, value, alpha = hsv_arr.map(&:to_f)
|
164
|
+
# scaling values into range 0..1
|
165
|
+
saturation /= 100
|
166
|
+
value /= 100
|
167
|
+
|
168
|
+
# calculation chroma & intermediate values
|
169
|
+
chroma = value * saturation
|
170
|
+
hue /= 60
|
171
|
+
x = chroma * (1 - (hue % 2 - 1).abs)
|
172
|
+
point = get_rgb_point(hue, chroma, x)
|
173
|
+
|
174
|
+
# calculation rgb & scaling into range 0..255
|
175
|
+
m = value - chroma
|
176
|
+
rgb = point.map { |channel| ((channel + m) * 255).round }
|
177
|
+
alpha.nil? ? rgb : rgb + [alpha]
|
178
|
+
end
|
172
179
|
|
173
|
-
|
180
|
+
alias_method :hsb_to_rgb_alt, :hsv_to_rgb_alt
|
174
181
|
|
175
|
-
|
182
|
+
#========= HSL <==> HSV (HSB) ==========================================
|
176
183
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
184
|
+
def hsl_to_hsv(hsl_arr, rounding = hsx_round)
|
185
|
+
hue, saturation, lightness, alpha = hsl_arr.map(&:to_f)
|
186
|
+
# scaling values into range 0..1
|
187
|
+
saturation /= 100
|
188
|
+
lightness /= 100
|
182
189
|
|
183
|
-
|
184
|
-
|
185
|
-
|
190
|
+
# calculation value & saturation HSV
|
191
|
+
value = lightness + saturation * [lightness, 1 - lightness].min
|
192
|
+
saturation_hsv = lightness == 0 ? 0 : 2 * (1 - lightness / value)
|
186
193
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
194
|
+
# scaling HSV values & rounding
|
195
|
+
hsv = [hue, saturation_hsv * 100, value * 100].map { |x| x.round(rounding) }
|
196
|
+
alpha.nil? ? hsv : hsv + [alpha]
|
197
|
+
end
|
191
198
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
199
|
+
alias_method :hsl_to_hsb, :hsl_to_hsv
|
200
|
+
|
201
|
+
def hsv_to_hsl(hsv_arr, rounding = hsx_round)
|
202
|
+
hue, saturation, value, alpha = hsv_arr.map(&:to_f)
|
203
|
+
# scaling values into range 0..1
|
204
|
+
saturation /= 100
|
205
|
+
value /= 100
|
206
|
+
|
207
|
+
# calculation lightness & saturation HSL
|
208
|
+
lightness = value * (1 - saturation / 2)
|
209
|
+
saturation_hsl = if [0, 1].any? { |v| v == lightness }
|
210
|
+
0
|
211
|
+
else
|
212
|
+
(value - lightness) / [lightness, 1 - lightness].min
|
213
|
+
end
|
214
|
+
|
215
|
+
# scaling HSL values & rounding
|
216
|
+
hsl = [hue, saturation_hsl * 100, lightness * 100].map { |x| x.round(rounding) }
|
217
|
+
alpha.nil? ? hsl : hsl + [alpha]
|
218
|
+
end
|
219
|
+
|
220
|
+
alias_method :hsb_to_hsl, :hsv_to_hsl
|
212
221
|
|
213
|
-
|
222
|
+
#========= RGB(A) <==> CMYK ============================================
|
214
223
|
|
215
|
-
|
224
|
+
def rgb_to_cmyk(rgb_arr, rounding = hsx_round)
|
225
|
+
# scaling RGB values into range 0..1
|
226
|
+
rgb = rgb_arr[0..2].map { |color| color / 255.to_f }
|
227
|
+
k = 1 - rgb.max
|
228
|
+
converter = proc do |color|
|
229
|
+
(1 - k) == 0 ? 0 : (1 - color - k) / (1 - k)
|
230
|
+
end
|
216
231
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
converter = proc do |color|
|
222
|
-
(1 - k) == 0 ? 0 : (1 - color - k) / (1 - k)
|
232
|
+
# calculation CMYK & scaling into percentages & rounding
|
233
|
+
c, m, y = rgb.map { |color| converter.call(color) || 0 }
|
234
|
+
cmyk = [c, m, y, k].map { |x| (x * 100).round(rounding) }
|
235
|
+
rgb_arr.size == 4 ? cmyk + [rgb_arr.last] : cmyk
|
223
236
|
end
|
224
237
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
238
|
+
def cmyk_to_rgb(cmyk_arr)
|
239
|
+
c, m, y, k = cmyk_arr[0..3].map { |color| color / 100.to_f }
|
240
|
+
converter = proc do |channel|
|
241
|
+
255 * (1 - channel) * (1 - k)
|
242
|
+
end
|
230
243
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
255 * (1 - channel) * (1 - k)
|
244
|
+
# calculation RGB & rounding
|
245
|
+
rgb = [c, m, y].map { |channel| converter.call(channel).round }
|
246
|
+
cmyk_arr.size == 5 ? rgb + [cmyk_arr.last] : rgb
|
235
247
|
end
|
236
248
|
|
237
|
-
|
238
|
-
rgb = [c, m, y].map { |channel| converter.call(channel).round }
|
239
|
-
cmyk_arr.size == 5 ? rgb + [cmyk_arr.last] : rgb
|
240
|
-
end
|
249
|
+
private
|
241
250
|
|
242
|
-
|
251
|
+
#========= helper methods for RGB to HSL/HSB/HSV =======================
|
243
252
|
|
244
|
-
|
253
|
+
# find greatest and smallest channel values and chroma from RGB
|
254
|
+
def get_min_max_chroma(red, green, blue)
|
255
|
+
cmin = [red, green, blue].min
|
256
|
+
cmax = [red, green, blue].max
|
257
|
+
# calculation chroma
|
258
|
+
chroma = cmax - cmin
|
245
259
|
|
246
|
-
|
247
|
-
|
248
|
-
cmin = [red, green, blue].min
|
249
|
-
cmax = [red, green, blue].max
|
250
|
-
# calculation chroma
|
251
|
-
chroma = cmax - cmin
|
260
|
+
[cmin, cmax, chroma]
|
261
|
+
end
|
252
262
|
|
253
|
-
|
254
|
-
|
263
|
+
# calculation HUE from RGB
|
264
|
+
def get_hue(red, green, blue)
|
265
|
+
_cmin, cmax, chroma = get_min_max_chroma(red, green, blue)
|
266
|
+
|
267
|
+
hue = if chroma == 0
|
268
|
+
0
|
269
|
+
elsif cmax == red
|
270
|
+
# red is max
|
271
|
+
((green - blue) / chroma) % 6
|
272
|
+
elsif cmax == green
|
273
|
+
# green is max
|
274
|
+
(blue - red) / chroma + 2
|
275
|
+
else
|
276
|
+
# blue is max
|
277
|
+
(red - green) / chroma + 4
|
278
|
+
end
|
279
|
+
hue * 60
|
280
|
+
|
281
|
+
# HUE will never leave the 0..360 range when RGB is within 0..255
|
282
|
+
# make negative HUEs positive
|
283
|
+
# 0 <= hue ? hue : hue + 360
|
284
|
+
end
|
255
285
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
# blue is max
|
270
|
-
(red - green) / chroma + 4
|
271
|
-
end
|
272
|
-
hue *= 60
|
273
|
-
# make negative HUEs positive behind 360°
|
274
|
-
0 <= hue ? hue : hue + 360
|
286
|
+
# possible RGB points
|
287
|
+
# point selection based on entering HUE input in range
|
288
|
+
def get_rgb_point(hue, chroma, x)
|
289
|
+
case hue
|
290
|
+
when 0...1 then [chroma, x, 0]
|
291
|
+
when 1...2 then [x, chroma, 0]
|
292
|
+
when 2...3 then [0, chroma, x]
|
293
|
+
when 3...4 then [0, x, chroma]
|
294
|
+
when 4...5 then [x, 0, chroma]
|
295
|
+
when 5...6 then [chroma, 0, x]
|
296
|
+
else [0, 0, 0]
|
297
|
+
end
|
298
|
+
end
|
275
299
|
end
|
300
|
+
|
301
|
+
extend ClassMethods
|
276
302
|
end
|
data/lib/decolmor/version.rb
CHANGED
data/lib/decolmor.rb
CHANGED
@@ -2,18 +2,4 @@ require 'decolmor/main'
|
|
2
2
|
require 'decolmor/version'
|
3
3
|
|
4
4
|
module Decolmor
|
5
|
-
#========= Set default rounding for HSL/HSV/HSB/CMYK conversion ========
|
6
|
-
|
7
|
-
# round 1 enough for lossless conversion RGB -> HSL/HSV/HSB -> RGB
|
8
|
-
# for lossless conversion HSL <==> HSV (HSB) better to use round 2
|
9
|
-
#
|
10
|
-
HSX_ROUND = 1
|
11
|
-
|
12
|
-
class << self
|
13
|
-
attr_writer :hsx_round
|
14
|
-
|
15
|
-
def hsx_round
|
16
|
-
@hsx_round ||= HSX_ROUND
|
17
|
-
end
|
18
|
-
end
|
19
5
|
end
|