color_converters 0.1.2 → 0.1.4
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/README.md +190 -161
- data/lib/color_converters/base_converter.rb +184 -163
- data/lib/color_converters/color.rb +18 -16
- data/lib/color_converters/converters/cielab_converter.rb +95 -88
- data/lib/color_converters/converters/cielch_converter.rb +61 -52
- data/lib/color_converters/converters/cmyk_converter.rb +59 -56
- data/lib/color_converters/converters/hex_converter.rb +45 -32
- data/lib/color_converters/converters/hsl_converter.rb +68 -64
- data/lib/color_converters/converters/hsl_string_converter.rb +46 -25
- data/lib/color_converters/converters/hsv_converter.rb +60 -58
- data/lib/color_converters/converters/name_converter.rb +67 -183
- data/lib/color_converters/converters/null_converter.rb +19 -17
- data/lib/color_converters/converters/oklab_converter.rb +102 -0
- data/lib/color_converters/converters/oklch_converter.rb +61 -0
- data/lib/color_converters/converters/rgb_converter.rb +109 -29
- data/lib/color_converters/converters/rgb_string_converter.rb +49 -32
- data/lib/color_converters/converters/xyz_converter.rb +110 -127
- data/lib/color_converters/version.rb +1 -1
- data/lib/color_converters.rb +31 -28
- metadata +34 -4
@@ -1,52 +1,61 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class CielchConverter < BaseConverter
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
7
|
+
|
8
|
+
colour_input.keys - [:l, :c, :h, :space] == [] && colour_input[:space].to_s == 'cie'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
{ l: [0.0, 100.0], c: [0.0, 150.0], h: [0.0, 360.0] }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
CielchConverter.bounds.collect do |key, range|
|
19
|
+
"#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
|
20
|
+
end.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def input_to_rgba(colour_input)
|
24
|
+
l, a, b = CielchConverter.cielch_to_cielab(colour_input)
|
25
|
+
x, y, z = CielabConverter.cielab_to_xyz({ l: l, a: a, b: b })
|
26
|
+
r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
|
27
|
+
|
28
|
+
[r, g, b, 1.0]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.cielch_to_cielab(colour_input)
|
32
|
+
l = colour_input[:l].to_d
|
33
|
+
c = colour_input[:c].to_d
|
34
|
+
h = colour_input[:h].to_d
|
35
|
+
|
36
|
+
h_rad = h * (Math::PI.to_d / 180.0.to_d)
|
37
|
+
|
38
|
+
a = c * Math.cos(h_rad).to_d
|
39
|
+
b = c * Math.sin(h_rad).to_d
|
40
|
+
|
41
|
+
[l, a, b]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.cielab_to_cielch(lab_array)
|
45
|
+
l, aa, bb = lab_array.map(&:to_d)
|
46
|
+
|
47
|
+
e = 0.0015.to_d; # if chroma is smaller than this, set hue to 0 [https://www.w3.org/TR/css-color-4/#color-conversion-code]
|
48
|
+
|
49
|
+
c = ((aa**2.to_d) + (bb**2.to_d))**0.5.to_d
|
50
|
+
|
51
|
+
h_rad = Math.atan2(bb, aa).to_d
|
52
|
+
h = h_rad * (180.0.to_d / Math::PI.to_d)
|
53
|
+
|
54
|
+
h %= 360
|
55
|
+
|
56
|
+
h = 0 if c < e
|
57
|
+
|
58
|
+
[l, c, h]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,56 +1,59 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class CmykConverter < BaseConverter
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
7
|
+
|
8
|
+
colour_input.keys - [:c, :m, :y, :k] == []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
{ c: [0.0, 100.0], m: [0.0, 100.0], y: [0.0, 100.0], k: [0.0, 100.0] }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
CmykConverter.bounds.collect do |key, range|
|
19
|
+
"#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
|
20
|
+
end.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def input_to_rgba(colour_input)
|
24
|
+
c = colour_input[:c].to_f
|
25
|
+
m = colour_input[:m].to_f
|
26
|
+
y = colour_input[:y].to_f
|
27
|
+
k = colour_input[:k].to_f
|
28
|
+
|
29
|
+
c /= 100.0
|
30
|
+
m /= 100.0
|
31
|
+
y /= 100.0
|
32
|
+
k /= 100.0
|
33
|
+
|
34
|
+
r = (255 * (1.0 - [1.0, c * (1.0 - k) + k].min))
|
35
|
+
g = (255 * (1.0 - [1.0, m * (1.0 - k) + k].min))
|
36
|
+
b = (255 * (1.0 - [1.0, y * (1.0 - k) + k].min))
|
37
|
+
|
38
|
+
[r, g, b, 1.0]
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.rgb_to_cmyk(rgb_array_frac)
|
42
|
+
r, g, b = rgb_array_frac
|
43
|
+
|
44
|
+
k = (1.0 - [r, g, b].max)
|
45
|
+
k_frac = k == 1.0 ? 1.0 : 1.0 - k
|
46
|
+
|
47
|
+
c = (1.0 - r - k) / k_frac
|
48
|
+
m = (1.0 - g - k) / k_frac
|
49
|
+
y = (1.0 - b - k) / k_frac
|
50
|
+
|
51
|
+
c *= 100
|
52
|
+
m *= 100
|
53
|
+
y *= 100
|
54
|
+
k *= 100
|
55
|
+
|
56
|
+
[c, m, y, k]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,32 +1,45 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class HexConverter < BaseConverter
|
5
|
+
def self.matches?(colour)
|
6
|
+
return false unless colour.is_a?(String)
|
7
|
+
|
8
|
+
colour.include?('#') && [4, 7, 9].include?(colour.length)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def validate_input(_colour_input)
|
14
|
+
# TODO
|
15
|
+
[] # to return no errors
|
16
|
+
end
|
17
|
+
|
18
|
+
def input_to_rgba(colour_input)
|
19
|
+
HexConverter.hex_to_rgba(colour_input)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.hex_to_rgba(hex_input)
|
23
|
+
hex_input = self.normalize_hex(hex_input)
|
24
|
+
|
25
|
+
r = hex_input[0, 2].hex
|
26
|
+
g = hex_input[2, 2].hex
|
27
|
+
b = hex_input[4, 2].hex
|
28
|
+
a = hex_input.length == 8 ? hex[6, 2].hex : 1.0
|
29
|
+
|
30
|
+
[r, g, b, a]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.rgb_to_hex(rgb_array)
|
34
|
+
r, g, b = rgb_array
|
35
|
+
|
36
|
+
format('#%<r>02x%<g>02x%<b>02x', r: r, g: g, b: b)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.normalize_hex(hex_input)
|
40
|
+
hex_input = hex_input.gsub('#', '')
|
41
|
+
|
42
|
+
(hex_input.length == 3 ? hex_input[0, 1] * 2 + hex_input[1, 1] * 2 + hex_input[2, 1] * 2 : hex_input).downcase
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,64 +1,68 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class HslConverter < BaseConverter
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
7
|
+
|
8
|
+
colour_input.keys - [:h, :s, :l] == [] || colour_input.keys - [:h, :s, :l, :a] == []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
{ h: [0.0, 360.0], s: [0.0, 100.0], l: [0.0, 100.0], a: [0.0, 1.0] }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
HslConverter.bounds.collect do |key, range|
|
19
|
+
"#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
|
20
|
+
end.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def input_to_rgba(colour_input)
|
24
|
+
h = colour_input[:h].to_s.gsub(/[^0-9.]/, '').to_f / 360.0
|
25
|
+
s = colour_input[:s].to_s.gsub(/[^0-9.]/, '').to_f / 100.0
|
26
|
+
l = colour_input[:l].to_s.gsub(/[^0-9.]/, '').to_f / 100.0
|
27
|
+
a = colour_input[:a] ? colour_input[:a].to_s.gsub(/[^0-9.]/, '').to_f : 1.0
|
28
|
+
|
29
|
+
return greyscale(l, a) if s.zero?
|
30
|
+
|
31
|
+
t2 = if l < 0.5
|
32
|
+
l * (1 + s)
|
33
|
+
else
|
34
|
+
l + s - l * s
|
35
|
+
end
|
36
|
+
|
37
|
+
t1 = 2 * l - t2
|
38
|
+
|
39
|
+
rgb = [0, 0, 0]
|
40
|
+
|
41
|
+
(0..2).each do |i|
|
42
|
+
t3 = h + 1 / 3.0 * - (i - 1)
|
43
|
+
t3.negative? && t3 += 1
|
44
|
+
t3 > 1 && t3 -= 1
|
45
|
+
|
46
|
+
val = if 6 * t3 < 1
|
47
|
+
t1 + (t2 - t1) * 6 * t3
|
48
|
+
elsif 2 * t3 < 1
|
49
|
+
t2
|
50
|
+
elsif 3 * t3 < 2
|
51
|
+
t1 + (t2 - t1) * (2 / 3.0 - t3) * 6
|
52
|
+
else
|
53
|
+
t1
|
54
|
+
end
|
55
|
+
|
56
|
+
rgb[i] = (val * 255)
|
57
|
+
end
|
58
|
+
|
59
|
+
[rgb[0], rgb[1], rgb[2], a]
|
60
|
+
end
|
61
|
+
|
62
|
+
def greyscale(luminosity, alpha)
|
63
|
+
rgb_equal_value = (luminosity * 255).round(IMPORT_DP)
|
64
|
+
|
65
|
+
[rgb_equal_value, rgb_equal_value, rgb_equal_value, alpha]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,25 +1,46 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class HslStringConverter < BaseConverter
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(String)
|
7
|
+
|
8
|
+
colour_input.include?('hsl(') || colour_input.include?('hsla(')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
HslConverter.bounds
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
keys = colour_input.include?('hsla(') ? [:h, :s, :l, :a] : [:h, :s, :l]
|
19
|
+
colour_input = HslStringConverter.sanitize_input(colour_input)
|
20
|
+
|
21
|
+
errors = keys.collect do |key|
|
22
|
+
"#{key} must be present" if colour_input[key].blank?
|
23
|
+
end.compact
|
24
|
+
|
25
|
+
return errors if errors.present?
|
26
|
+
|
27
|
+
HslStringConverter.bounds.collect do |key, range|
|
28
|
+
"#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def input_to_rgba(colour_input)
|
33
|
+
colour_input = HslStringConverter.sanitize_input(colour_input)
|
34
|
+
|
35
|
+
rgba = HslConverter.new(h: colour_input[:h], s: colour_input[:s], l: colour_input[:l], a: colour_input[:a]).rgba
|
36
|
+
|
37
|
+
[rgba[:r], rgba[:g], rgba[:b], rgba[:a]]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.sanitize_input(colour_input)
|
41
|
+
matches = colour_input.match(/hsla?\(([0-9.,%\s]+)\)/) || []
|
42
|
+
h, s, l, a = matches[1]&.split(',')&.map(&:strip)
|
43
|
+
{ h: h, s: s, l: l, a: a }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,58 +1,60 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
when
|
46
|
-
|
47
|
-
when
|
48
|
-
|
49
|
-
when
|
50
|
-
|
51
|
-
when
|
52
|
-
|
53
|
-
when
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class HsvConverter < BaseConverter
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
7
|
+
|
8
|
+
colour_input.keys - [:h, :s, :v] == [] || colour_input.keys - [:h, :s, :b] == []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
{ h: [0.0, 360.0], s: [-128.0, 127.0], v: [-128.0, 127.0], b: [-128.0, 127.0] }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
HsvConverter.bounds.collect do |key, range|
|
19
|
+
"#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].blank? || colour_input[key].to_f.between?(*range)
|
20
|
+
end.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def input_to_rgba(colour_input)
|
24
|
+
h = colour_input[:h].to_f
|
25
|
+
s = colour_input[:s].to_f
|
26
|
+
v = (colour_input[:v] || colour_input[:b]).to_f
|
27
|
+
|
28
|
+
h /= 360
|
29
|
+
s /= 100
|
30
|
+
v /= 100
|
31
|
+
|
32
|
+
i = (h * 6).floor
|
33
|
+
f = h * 6 - i
|
34
|
+
|
35
|
+
p = v * (1 - s)
|
36
|
+
q = v * (1 - f * s)
|
37
|
+
t = v * (1 - (1 - f) * s)
|
38
|
+
|
39
|
+
p = (p * 255).round(IMPORT_DP)
|
40
|
+
q = (q * 255).round(IMPORT_DP)
|
41
|
+
t = (t * 255).round(IMPORT_DP)
|
42
|
+
v = (v * 255).round(IMPORT_DP)
|
43
|
+
|
44
|
+
case i % 6
|
45
|
+
when 0
|
46
|
+
[v, t, p, 1.0]
|
47
|
+
when 1
|
48
|
+
[q, v, p, 1.0]
|
49
|
+
when 2
|
50
|
+
[p, v, t, 1.0]
|
51
|
+
when 3
|
52
|
+
[p, q, v, 1.0]
|
53
|
+
when 4
|
54
|
+
[t, p, v, 1.0]
|
55
|
+
when 5
|
56
|
+
[v, p, q, 1.0]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|