color_converters 0.1.4 → 0.1.5

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.
@@ -1,60 +1,64 @@
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
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 clamp_input(colour_input)
18
+ # colour_input.each { |key, value| colour_input[key] = value.clamp(*HsvConverter.bounds[key]) }
19
+ # end
20
+
21
+ def validate_input(colour_input)
22
+ HsvConverter.bounds.collect do |key, range|
23
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].blank? || colour_input[key].to_f.between?(*range)
24
+ end.compact
25
+ end
26
+
27
+ def input_to_rgba(colour_input)
28
+ h = colour_input[:h].to_f
29
+ s = colour_input[:s].to_f
30
+ v = (colour_input[:v] || colour_input[:b]).to_f
31
+
32
+ h /= 360
33
+ s /= 100
34
+ v /= 100
35
+
36
+ i = (h * 6).floor
37
+ f = h * 6 - i
38
+
39
+ p = v * (1 - s)
40
+ q = v * (1 - f * s)
41
+ t = v * (1 - (1 - f) * s)
42
+
43
+ p = (p * 255).round(IMPORT_DP)
44
+ q = (q * 255).round(IMPORT_DP)
45
+ t = (t * 255).round(IMPORT_DP)
46
+ v = (v * 255).round(IMPORT_DP)
47
+
48
+ case i % 6
49
+ when 0
50
+ [v, t, p, 1.0]
51
+ when 1
52
+ [q, v, p, 1.0]
53
+ when 2
54
+ [p, v, t, 1.0]
55
+ when 3
56
+ [p, q, v, 1.0]
57
+ when 4
58
+ [t, p, v, 1.0]
59
+ when 5
60
+ [v, p, q, 1.0]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,67 +1,69 @@
1
- # frozen_string_literal: true
2
-
3
- require 'color_swatch_collection'
4
-
5
- module ColorConverters
6
- class NameConverter < BaseConverter
7
- def self.matches?(colour_input)
8
- return false unless colour_input.is_a?(String)
9
-
10
- !colour_input.include?('#') && !colour_input.include?('rgb') && !colour_input.include?('hsl')
11
- end
12
-
13
- def self.rgb_to_name(rgb_array, fuzzy = false)
14
- if fuzzy
15
- source_colour = ColorConverters::Color.new({ r: rgb_array[0], g: rgb_array[1], b: rgb_array[2] })
16
-
17
- collection_colours = []
18
-
19
- ::ColorSwatchCollection.list_collections.each do |collection_name|
20
- collection_colours += self.add_colour_distances_to_collection(Object.const_get("::ColorSwatchCollection::#{collection_name.capitalize}").colours, source_colour)
21
- end
22
-
23
- collection_colours.min_by { |swatch| swatch[:distance] }.dig(:name)
24
- else
25
- ::ColorSwatchCollection.get_from_hex(HexConverter.rgb_to_hex(rgb_array)).dig(:name)
26
- end
27
- end
28
-
29
- private
30
-
31
- def validate_input(colour_input)
32
- self.class.match_name_from_palettes(colour_input).present? ? [] : ['name could not be found across colour collections']
33
- end
34
-
35
- def input_to_rgba(colour_input)
36
- found_colour = self.class.match_name_from_palettes(colour_input)
37
-
38
- raise InvalidColorError unless found_colour.present?
39
-
40
- HexConverter.hex_to_rgba(found_colour)
41
- end
42
-
43
- # this is a checking for a direct naming match against the ColorSwatchCollection
44
- def self.match_name_from_palettes(colour_name)
45
- ::ColorSwatchCollection.get_from_name(colour_name).dig(:hex)
46
- end
47
-
48
- def self.add_colour_distances_to_collection(collection_colours, source_colour)
49
- collection_colours.map do |swatch|
50
- swatch[:distance] = self.distance_between_colours(ColorConverters::Color.new(swatch.dig(:hex)), source_colour)
51
- end
52
-
53
- collection_colours
54
- end
55
-
56
- def self.distance_between_colours(comparison_colour, source_colour)
57
- # https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions
58
- # https://www.baeldung.com/cs/compute-similarity-of-colours
59
- # TODO: allow the type of matching to be set via config. Use HSL for now as it's far faster than CIELab
60
- conversion_1 = comparison_colour.hsl
61
- conversion_2 = source_colour.hsl
62
-
63
- keys = conversion_1.keys
64
- Math.sqrt((conversion_1[keys[0]] - conversion_2[keys[0]])**2 + (conversion_1[keys[1]] - conversion_2[keys[1]])**2 + (conversion_1[keys[2]] - conversion_2[keys[2]])**2)
65
- end
66
- end
67
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_swatch_collection'
4
+
5
+ module ColorConverters
6
+ class NameConverter < BaseConverter
7
+ def self.matches?(colour_input)
8
+ return false unless colour_input.is_a?(String)
9
+
10
+ !colour_input.include?('#') && !colour_input.include?('rgb') && !colour_input.include?('hsl')
11
+ end
12
+
13
+ def self.rgb_to_name(rgb_array, fuzzy = false)
14
+ if fuzzy
15
+ source_colour = ColorConverters::Color.new({ r: rgb_array[0], g: rgb_array[1], b: rgb_array[2] })
16
+
17
+ collection_colours = []
18
+
19
+ ::ColorSwatchCollection.list_collections.each do |collection_name|
20
+ collection_colours += self.add_colour_distances_to_collection(Object.const_get("::ColorSwatchCollection::#{collection_name.capitalize}").colours, source_colour)
21
+ end
22
+
23
+ collection_colours.min_by { |swatch| swatch[:distance] }.dig(:name)
24
+ else
25
+ ::ColorSwatchCollection.get_from_hex(HexConverter.rgb_to_hex(rgb_array)).dig(:name)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # def clamp_input(colour_input)
32
+ # colour_input.each { |key, value| colour_input[key] = value.clamp(*CielchConverter.bounds[key]) }
33
+ # end
34
+
35
+ def validate_input(colour_input)
36
+ self.class.match_name_from_palettes(colour_input).present? ? [] : ['name could not be found across colour collections']
37
+ end
38
+
39
+ def input_to_rgba(colour_input)
40
+ found_colour = self.class.match_name_from_palettes(colour_input) || ''
41
+
42
+ HexConverter.hex_to_rgba(found_colour) if found_colour.present?
43
+ end
44
+
45
+ # this is a checking for a direct naming match against the ColorSwatchCollection
46
+ def self.match_name_from_palettes(colour_name)
47
+ ::ColorSwatchCollection.get_from_name(colour_name).dig(:hex)
48
+ end
49
+
50
+ def self.add_colour_distances_to_collection(collection_colours, source_colour)
51
+ collection_colours.map do |swatch|
52
+ swatch[:distance] = self.distance_between_colours(ColorConverters::Color.new(swatch.dig(:hex)), source_colour)
53
+ end
54
+
55
+ collection_colours
56
+ end
57
+
58
+ def self.distance_between_colours(comparison_colour, source_colour)
59
+ # https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions
60
+ # https://www.baeldung.com/cs/compute-similarity-of-colours
61
+ # TODO: allow the type of matching to be set via config. Use HSL for now as it's far faster than CIELab
62
+ conversion_1 = comparison_colour.hsl
63
+ conversion_2 = source_colour.hsl
64
+
65
+ keys = conversion_1.keys
66
+ Math.sqrt((conversion_1[keys[0]] - conversion_2[keys[0]])**2 + (conversion_1[keys[1]] - conversion_2[keys[1]])**2 + (conversion_1[keys[2]] - conversion_2[keys[2]])**2)
67
+ end
68
+ end
69
+ end
@@ -1,19 +1,23 @@
1
- # frozen_string_literal: true
2
-
3
- module ColorConverters
4
- class NullConverter < BaseConverter
5
- def self.matches?(_colour_input)
6
- true
7
- end
8
-
9
- private
10
-
11
- def validate_input(_colour_input)
12
- ['did not recognise colour input']
13
- end
14
-
15
- def input_to_rgba(_colour_input)
16
- raise InvalidColorError
17
- end
18
- end
19
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class NullConverter < BaseConverter
5
+ def self.matches?(_colour_input)
6
+ true
7
+ end
8
+
9
+ private
10
+
11
+ # def clamp_input(colour_input)
12
+ # colour_input
13
+ # end
14
+
15
+ def validate_input(_colour_input)
16
+ ['did not recognise colour input']
17
+ end
18
+
19
+ def input_to_rgba(_colour_input)
20
+ raise InvalidColorError
21
+ end
22
+ end
23
+ end
@@ -1,102 +1,106 @@
1
- # frozen_string_literal: true
2
-
3
- module ColorConverters
4
- class OklabConverter < BaseConverter
5
- def self.matches?(colour_input)
6
- return false unless colour_input.is_a?(Hash)
7
-
8
- colour_input.keys - [:l, :a, :b, :space] == [] && colour_input[:space].to_s == 'ok'
9
- end
10
-
11
- def self.bounds
12
- { l: [0.0, 100.0], a: [-0.5, 0.5], b: [-0.5, 0.5] }
13
- end
14
-
15
- private
16
-
17
- def validate_input(colour_input)
18
- OklabConverter.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
- x, y, z = OklabConverter.oklab_to_xyz(colour_input)
25
- r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
26
-
27
- [r, g, b, 1.0]
28
- end
29
-
30
- def self.oklab_to_xyz(colour_input)
31
- l = colour_input[:l].to_d
32
- a = colour_input[:a].to_d
33
- b = colour_input[:b].to_d
34
-
35
- # Now, scale l to a decimal
36
- l /= 100.0.to_d
37
-
38
- # Convert Oklab (L*a*b*) to LMS'
39
- lab_to_lms_matrix = ::Matrix[
40
- [1.0000000000000000.to_d, 0.3963377773761749.to_d, 0.2158037573099136.to_d],
41
- [1.0000000000000000.to_d, -0.1055613458156586.to_d, -0.0638541728258133.to_d],
42
- [1.0000000000000000.to_d, -0.0894841775298119.to_d, -1.2914855480194092.to_d]
43
- ]
44
-
45
- l_lms, m_lms, s_lms = (lab_to_lms_matrix * ::Matrix.column_vector([l, a, b])).to_a.flatten
46
-
47
- l_lms **= 3.0.to_d
48
- m_lms **= 3.0.to_d
49
- s_lms **= 3.0.to_d
50
-
51
- lms_to_xyz_matrix = ::Matrix[
52
- [1.2268798758459243.to_d, -0.5578149944602171.to_d, 0.2813910456659647.to_d],
53
- [-0.0405757452148008.to_d, 1.1122868032803170.to_d, -0.0717110580655164.to_d],
54
- [-0.0763729366746601.to_d, -0.4214933324022432.to_d, 1.5869240198367816.to_d]
55
- ]
56
-
57
- x, y, z = (lms_to_xyz_matrix * ::Matrix.column_vector([l_lms, m_lms, s_lms])).to_a.flatten
58
-
59
- x *= 100.0.to_d
60
- y *= 100.0.to_d
61
- z *= 100.0.to_d
62
-
63
- [x, y, z]
64
- end
65
-
66
- def self.xyz_to_oklab(xyz_array)
67
- x, y, z = xyz_array.map(&:to_d)
68
-
69
- # The transformation matrix expects normalised X, Y, Z values.
70
- x /= 100.0.to_d
71
- y /= 100.0.to_d
72
- z /= 100.0.to_d
73
-
74
- # Given XYZ relative to D65, convert to OKLab
75
- xyz_to_lms_matrix = ::Matrix[
76
- [0.8190224379967030.to_d, 0.3619062600528904.to_d, -0.1288737815209879.to_d],
77
- [0.0329836539323885.to_d, 0.9292868615863434.to_d, 0.0361446663506424.to_d],
78
- [0.0481771893596242.to_d, 0.2642395317527308.to_d, 0.6335478284694309.to_d]
79
- ]
80
-
81
- l_lms, m_lms, s_lms = (xyz_to_lms_matrix * ::Matrix.column_vector([x, y, z])).to_a.flatten
82
-
83
- cube_root = (1.0.to_d / 3.0.to_d)
84
- l_lms **= cube_root
85
- m_lms **= cube_root
86
- s_lms **= cube_root
87
-
88
- lms_to_lab_matrix = ::Matrix[
89
- [0.2104542683093140.to_d, 0.7936177747023054.to_d, -0.0040720430116193.to_d],
90
- [1.9779985324311684.to_d, -2.4285922420485799.to_d, 0.4505937096174110.to_d],
91
- [0.0259040424655478.to_d, 0.7827717124575296.to_d, -0.8086757549230774.to_d]
92
- ]
93
-
94
- l_lab, a_lab, b_lab = (lms_to_lab_matrix * ::Matrix.column_vector([l_lms, m_lms, s_lms])).to_a.flatten
95
-
96
- # Now, scale l to a percentage
97
- l_lab *= 100.0.to_d
98
-
99
- [l_lab, a_lab, b_lab]
100
- end
101
- end
102
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class OklabConverter < BaseConverter
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
7
+
8
+ colour_input.keys - [:l, :a, :b, :space] == [] && colour_input[:space].to_s == 'ok'
9
+ end
10
+
11
+ def self.bounds
12
+ { l: [0.0, 100.0], a: [-0.5, 0.5], b: [-0.5, 0.5] }
13
+ end
14
+
15
+ private
16
+
17
+ # def clamp_input(colour_input)
18
+ # colour_input.each { |key, value| colour_input[key] = value.clamp(*OklabConverter.bounds[key]) }
19
+ # end
20
+
21
+ def validate_input(colour_input)
22
+ OklabConverter.bounds.collect do |key, range|
23
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
24
+ end.compact
25
+ end
26
+
27
+ def input_to_rgba(colour_input)
28
+ x, y, z = OklabConverter.oklab_to_xyz(colour_input)
29
+ r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
30
+
31
+ [r, g, b, 1.0]
32
+ end
33
+
34
+ def self.oklab_to_xyz(colour_input)
35
+ l = colour_input[:l].to_d
36
+ a = colour_input[:a].to_d
37
+ b = colour_input[:b].to_d
38
+
39
+ # Now, scale l to a decimal
40
+ l /= 100.0.to_d
41
+
42
+ # Convert Oklab (L*a*b*) to LMS'
43
+ lab_to_lms_matrix = ::Matrix[
44
+ [1.0000000000000000.to_d, 0.3963377773761749.to_d, 0.2158037573099136.to_d],
45
+ [1.0000000000000000.to_d, -0.1055613458156586.to_d, -0.0638541728258133.to_d],
46
+ [1.0000000000000000.to_d, -0.0894841775298119.to_d, -1.2914855480194092.to_d]
47
+ ]
48
+
49
+ l_lms, m_lms, s_lms = (lab_to_lms_matrix * ::Matrix.column_vector([l, a, b])).to_a.flatten
50
+
51
+ l_lms **= 3.0.to_d
52
+ m_lms **= 3.0.to_d
53
+ s_lms **= 3.0.to_d
54
+
55
+ lms_to_xyz_matrix = ::Matrix[
56
+ [1.2268798758459243.to_d, -0.5578149944602171.to_d, 0.2813910456659647.to_d],
57
+ [-0.0405757452148008.to_d, 1.1122868032803170.to_d, -0.0717110580655164.to_d],
58
+ [-0.0763729366746601.to_d, -0.4214933324022432.to_d, 1.5869240198367816.to_d]
59
+ ]
60
+
61
+ x, y, z = (lms_to_xyz_matrix * ::Matrix.column_vector([l_lms, m_lms, s_lms])).to_a.flatten
62
+
63
+ x *= 100.0.to_d
64
+ y *= 100.0.to_d
65
+ z *= 100.0.to_d
66
+
67
+ [x, y, z]
68
+ end
69
+
70
+ def self.xyz_to_oklab(xyz_array)
71
+ x, y, z = xyz_array.map(&:to_d)
72
+
73
+ # The transformation matrix expects normalised X, Y, Z values.
74
+ x /= 100.0.to_d
75
+ y /= 100.0.to_d
76
+ z /= 100.0.to_d
77
+
78
+ # Given XYZ relative to D65, convert to OKLab
79
+ xyz_to_lms_matrix = ::Matrix[
80
+ [0.8190224379967030.to_d, 0.3619062600528904.to_d, -0.1288737815209879.to_d],
81
+ [0.0329836539323885.to_d, 0.9292868615863434.to_d, 0.0361446663506424.to_d],
82
+ [0.0481771893596242.to_d, 0.2642395317527308.to_d, 0.6335478284694309.to_d]
83
+ ]
84
+
85
+ l_lms, m_lms, s_lms = (xyz_to_lms_matrix * ::Matrix.column_vector([x, y, z])).to_a.flatten
86
+
87
+ cube_root = (1.0.to_d / 3.0.to_d)
88
+ l_lms **= cube_root
89
+ m_lms **= cube_root
90
+ s_lms **= cube_root
91
+
92
+ lms_to_lab_matrix = ::Matrix[
93
+ [0.2104542683093140.to_d, 0.7936177747023054.to_d, -0.0040720430116193.to_d],
94
+ [1.9779985324311684.to_d, -2.4285922420485799.to_d, 0.4505937096174110.to_d],
95
+ [0.0259040424655478.to_d, 0.7827717124575296.to_d, -0.8086757549230774.to_d]
96
+ ]
97
+
98
+ l_lab, a_lab, b_lab = (lms_to_lab_matrix * ::Matrix.column_vector([l_lms, m_lms, s_lms])).to_a.flatten
99
+
100
+ # Now, scale l to a percentage
101
+ l_lab *= 100.0.to_d
102
+
103
+ [l_lab, a_lab, b_lab]
104
+ end
105
+ end
106
+ end
@@ -1,61 +1,65 @@
1
- # frozen_string_literal: true
2
-
3
- module ColorConverters
4
- class OklchConverter < 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 == 'ok'
9
- end
10
-
11
- def self.bounds
12
- { l: [0.0, 100.0], c: [0.0, 500.0], h: [0.0, 360.0] }
13
- end
14
-
15
- private
16
-
17
- def validate_input(colour_input)
18
- OklchConverter.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 = OklchConverter.oklch_to_oklab(colour_input)
25
- x, y, z = OklabConverter.oklab_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.oklch_to_oklab(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.oklab_to_oklch(lab_array)
45
- l, aa, bb = lab_array.map(&:to_d)
46
-
47
- e = 0.000015.to_d; # if chroma is smaller than this, set hue to 0 similar to CIELch
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
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class OklchConverter < 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 == 'ok'
9
+ end
10
+
11
+ def self.bounds
12
+ { l: [0.0, 100.0], c: [0.0, 500.0], h: [0.0, 360.0] }
13
+ end
14
+
15
+ private
16
+
17
+ # def clamp_input(colour_input)
18
+ # colour_input.each { |key, value| colour_input[key] = value.clamp(*OklchConverter.bounds[key]) }
19
+ # end
20
+
21
+ def validate_input(colour_input)
22
+ OklchConverter.bounds.collect do |key, range|
23
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
24
+ end.compact
25
+ end
26
+
27
+ def input_to_rgba(colour_input)
28
+ l, a, b = OklchConverter.oklch_to_oklab(colour_input)
29
+ x, y, z = OklabConverter.oklab_to_xyz({ l: l, a: a, b: b })
30
+ r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
31
+
32
+ [r, g, b, 1.0]
33
+ end
34
+
35
+ def self.oklch_to_oklab(colour_input)
36
+ l = colour_input[:l].to_d
37
+ c = colour_input[:c].to_d
38
+ h = colour_input[:h].to_d
39
+
40
+ h_rad = h * (Math::PI.to_d / 180.0.to_d)
41
+
42
+ a = c * Math.cos(h_rad).to_d
43
+ b = c * Math.sin(h_rad).to_d
44
+
45
+ [l, a, b]
46
+ end
47
+
48
+ def self.oklab_to_oklch(lab_array)
49
+ l, aa, bb = lab_array.map(&:to_d)
50
+
51
+ e = 0.000015.to_d; # if chroma is smaller than this, set hue to 0 similar to CIELch
52
+
53
+ c = ((aa**2.to_d) + (bb**2.to_d))**0.5.to_d
54
+
55
+ h_rad = Math.atan2(bb, aa).to_d
56
+ h = h_rad * (180.0.to_d / Math::PI.to_d)
57
+
58
+ h %= 360
59
+
60
+ h = 0 if c < e
61
+
62
+ [l, c, h]
63
+ end
64
+ end
65
+ end