color_converters 0.1.3 → 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,27 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class HslStringConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(String)
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
5
10
 
6
- color_input.include?('hsl(') || color_input.include?('hsla(')
11
+ def self.bounds
12
+ HslConverter.bounds
7
13
  end
8
14
 
9
15
  private
10
16
 
11
- def validate_input(_color_input)
12
- true
13
- end
17
+ # def clamp_input(colour_input)
18
+ # colour_input = HslStringConverter.sanitize_input(colour_input)
19
+ # colour_input.each { |key, value| colour_input[key] = value.to_f.clamp(*HslConverter.bounds[key]) }
20
+ # HslStringConverter.rgb_to_rgbstring([colour_input[:h], colour_input[:s], colour_input[:l]], colour_input[:a])
21
+ # end
22
+
23
+ def validate_input(colour_input)
24
+ keys = colour_input.include?('hsla(') ? [:h, :s, :l, :a] : [:h, :s, :l]
25
+ colour_input = HslStringConverter.sanitize_input(colour_input)
14
26
 
15
- def input_to_rgba(color_input)
16
- matches = color_input.match(/hsla?\(([0-9.,%\s]+)\)/)
17
- raise InvalidColorError unless matches
27
+ errors = keys.collect do |key|
28
+ "#{key} must be present" if colour_input[key].blank?
29
+ end.compact
18
30
 
19
- h, s, l, a = matches[1].split(',').map(&:strip)
20
- raise InvalidColorError unless h.present? && s.present? && l.present?
31
+ return errors if errors.present?
21
32
 
22
- rgba = HslConverter.new(h: h, s: s, l: l, a: a).rgba
33
+ HslStringConverter.bounds.collect do |key, range|
34
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
35
+ end.compact
36
+ end
37
+
38
+ def input_to_rgba(colour_input)
39
+ colour_input = HslStringConverter.sanitize_input(colour_input)
40
+
41
+ rgba = HslConverter.new({ h: colour_input[:h], s: colour_input[:s], l: colour_input[:l], a: colour_input[:a] }, limit_override: true).rgba
23
42
 
24
43
  [rgba[:r], rgba[:g], rgba[:b], rgba[:a]]
25
44
  end
45
+
46
+ def self.sanitize_input(colour_input)
47
+ matches = colour_input.match(/hsla?\(([0-9.,%\s]+)\)/) || []
48
+ h, s, l, a = matches[1]&.split(',')&.map(&:strip)
49
+ { h: h, s: s, l: l, a: a }
50
+ end
51
+
52
+ # def self.rgb_to_hslstring(rgb_array_frac, alpha)
53
+ # r, g, b = rgb_array_frac
54
+ # if alpha == 1.0
55
+ # "hsl(#{[r, g, b].join(', ')})"
56
+ # else
57
+ # "hsla(#{[r, g, b, alpha].join(', ')})"
58
+ # end
59
+ # end
26
60
  end
27
61
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class HsvConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(Hash)
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
5
7
 
6
- color_input.keys - [:h, :s, :v] == [] || color_input.keys - [:h, :s, :b] == []
8
+ colour_input.keys - [:h, :s, :v] == [] || colour_input.keys - [:h, :s, :b] == []
7
9
  end
8
10
 
9
11
  def self.bounds
@@ -12,16 +14,20 @@ module ColorConverters
12
14
 
13
15
  private
14
16
 
15
- def validate_input(color_input)
16
- bounds = HsvConverter.bounds
17
- light = (color_input[:v].present? && color_input[:v].to_f.between?(*bounds[:v])) || (color_input[:b].present? && color_input[:b].to_f.between?(*bounds[:v]))
18
- color_input[:h].to_f.between?(*bounds[:h]) && color_input[:s].to_f.between?(*bounds[:s]) && light
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
19
25
  end
20
26
 
21
- def input_to_rgba(color_input)
22
- h = color_input[:h].to_f
23
- s = color_input[:s].to_f
24
- v = (color_input[:v] || color_input[:b]).to_f
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
25
31
 
26
32
  h /= 360
27
33
  s /= 100
@@ -1,185 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_swatch_collection'
4
+
1
5
  module ColorConverters
2
6
  class NameConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(String)
7
+ def self.matches?(colour_input)
8
+ return false unless colour_input.is_a?(String)
5
9
 
6
- self.color_names.include?(color_input.downcase.to_sym)
10
+ !colour_input.include?('#') && !colour_input.include?('rgb') && !colour_input.include?('hsl')
7
11
  end
8
12
 
9
- def self.rgb_to_name(rgb_array)
10
- r, g, b = rgb_array
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 = []
11
18
 
12
- name = self.color_names.find { |_k, v| v == [r.round, g.round, b.round] }
13
- name.present? ? name[0].to_s : nil
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
14
27
  end
15
28
 
16
29
  private
17
30
 
18
- def validate_input(_color_input)
19
- # TODO: validate against list of keys?
20
- true
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']
21
37
  end
22
38
 
23
- def input_to_rgba(color_input)
24
- found_colour = self.class.color_names[color_input.downcase.to_sym]
25
- raise InvalidColorError unless found_colour.present?
39
+ def input_to_rgba(colour_input)
40
+ found_colour = self.class.match_name_from_palettes(colour_input) || ''
26
41
 
27
- r, g, b = found_colour
42
+ HexConverter.hex_to_rgba(found_colour) if found_colour.present?
43
+ end
28
44
 
29
- [r, g, b, 1.0]
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)
30
48
  end
31
49
 
32
- # TODO: use color_namer_ruby gem instead, but that gem also uses this gem
33
- def self.color_names
34
- {
35
- aliceblue: [240, 248, 255],
36
- antiquewhite: [250, 235, 215],
37
- aqua: [0, 255, 255],
38
- aquamarine: [127, 255, 212],
39
- azure: [240, 255, 255],
40
- beige: [245, 245, 220],
41
- bisque: [255, 228, 196],
42
- black: [0, 0, 0],
43
- blanchedalmond: [255, 235, 205],
44
- blue: [0, 0, 255],
45
- blueviolet: [138, 43, 226],
46
- brown: [165, 42, 42],
47
- burlywood: [222, 184, 135],
48
- cadetblue: [95, 158, 160],
49
- chartreuse: [127, 255, 0],
50
- chocolate: [210, 105, 30],
51
- coral: [255, 127, 80],
52
- cornflowerblue: [100, 149, 237],
53
- cornsilk: [255, 248, 220],
54
- crimson: [220, 20, 60],
55
- cyan: [0, 255, 255],
56
- darkblue: [0, 0, 139],
57
- darkcyan: [0, 139, 139],
58
- darkgoldenrod: [184, 134, 11],
59
- darkgray: [169, 169, 169],
60
- darkgreen: [0, 100, 0],
61
- darkgrey: [169, 169, 169],
62
- darkkhaki: [189, 183, 107],
63
- darkmagenta: [139, 0, 139],
64
- darkolivegreen: [85, 107, 47],
65
- darkorange: [255, 140, 0],
66
- darkorchid: [153, 50, 204],
67
- darkred: [139, 0, 0],
68
- darksalmon: [233, 150, 122],
69
- darkseagreen: [143, 188, 143],
70
- darkslateblue: [72, 61, 139],
71
- darkslategray: [47, 79, 79],
72
- darkslategrey: [47, 79, 79],
73
- darkturquoise: [0, 206, 209],
74
- darkviolet: [148, 0, 211],
75
- deeppink: [255, 20, 147],
76
- deepskyblue: [0, 191, 255],
77
- dimgray: [105, 105, 105],
78
- dimgrey: [105, 105, 105],
79
- dodgerblue: [30, 144, 255],
80
- firebrick: [178, 34, 34],
81
- floralwhite: [255, 250, 240],
82
- forestgreen: [34, 139, 34],
83
- fuchsia: [255, 0, 255],
84
- gainsboro: [220, 220, 220],
85
- ghostwhite: [248, 248, 255],
86
- gold: [255, 215, 0],
87
- goldenrod: [218, 165, 32],
88
- gray: [128, 128, 128],
89
- green: [0, 128, 0],
90
- greenyellow: [173, 255, 47],
91
- grey: [128, 128, 128],
92
- honeydew: [240, 255, 240],
93
- hotpink: [255, 105, 180],
94
- indianred: [205, 92, 92],
95
- indigo: [75, 0, 130],
96
- ivory: [255, 255, 240],
97
- khaki: [240, 230, 140],
98
- lavender: [230, 230, 250],
99
- lavenderblush: [255, 240, 245],
100
- lawngreen: [124, 252, 0],
101
- lemonchiffon: [255, 250, 205],
102
- lightblue: [173, 216, 230],
103
- lightcoral: [240, 128, 128],
104
- lightcyan: [224, 255, 255],
105
- lightgoldenrodyellow: [250, 250, 210],
106
- lightgray: [211, 211, 211],
107
- lightgreen: [144, 238, 144],
108
- lightgrey: [211, 211, 211],
109
- lightpink: [255, 182, 193],
110
- lightsalmon: [255, 160, 122],
111
- lightseagreen: [32, 178, 170],
112
- lightskyblue: [135, 206, 250],
113
- lightslategray: [119, 136, 153],
114
- lightslategrey: [119, 136, 153],
115
- lightsteelblue: [176, 196, 222],
116
- lightyellow: [255, 255, 224],
117
- lime: [0, 255, 0],
118
- limegreen: [50, 205, 50],
119
- linen: [250, 240, 230],
120
- magenta: [255, 0, 255],
121
- maroon: [128, 0, 0],
122
- mediumaquamarine: [102, 205, 170],
123
- mediumblue: [0, 0, 205],
124
- mediumorchid: [186, 85, 211],
125
- mediumpurple: [147, 112, 219],
126
- mediumseagreen: [60, 179, 113],
127
- mediumslateblue: [123, 104, 238],
128
- mediumspringgreen: [0, 250, 154],
129
- mediumturquoise: [72, 209, 204],
130
- mediumvioletred: [199, 21, 133],
131
- midnightblue: [25, 25, 112],
132
- mintcream: [245, 255, 250],
133
- mistyrose: [255, 228, 225],
134
- moccasin: [255, 228, 181],
135
- navajowhite: [255, 222, 173],
136
- navy: [0, 0, 128],
137
- oldlace: [253, 245, 230],
138
- olive: [128, 128, 0],
139
- olivedrab: [107, 142, 35],
140
- orange: [255, 165, 0],
141
- orangered: [255, 69, 0],
142
- orchid: [218, 112, 214],
143
- palegoldenrod: [238, 232, 170],
144
- palegreen: [152, 251, 152],
145
- paleturquoise: [175, 238, 238],
146
- palevioletred: [219, 112, 147],
147
- papayawhip: [255, 239, 213],
148
- peachpuff: [255, 218, 185],
149
- peru: [205, 133, 63],
150
- pink: [255, 192, 203],
151
- plum: [221, 160, 221],
152
- powderblue: [176, 224, 230],
153
- purple: [128, 0, 128],
154
- red: [255, 0, 0],
155
- rosybrown: [188, 143, 143],
156
- royalblue: [65, 105, 225],
157
- saddlebrown: [139, 69, 19],
158
- salmon: [250, 128, 114],
159
- sandybrown: [244, 164, 96],
160
- seagreen: [46, 139, 87],
161
- seashell: [255, 245, 238],
162
- sienna: [160, 82, 45],
163
- silver: [192, 192, 192],
164
- skyblue: [135, 206, 235],
165
- slateblue: [106, 90, 205],
166
- slategray: [112, 128, 144],
167
- slategrey: [112, 128, 144],
168
- snow: [255, 250, 250],
169
- springgreen: [0, 255, 127],
170
- steelblue: [70, 130, 180],
171
- tan: [210, 180, 140],
172
- teal: [0, 128, 128],
173
- thistle: [216, 191, 216],
174
- tomato: [255, 99, 71],
175
- turquoise: [64, 224, 208],
176
- violet: [238, 130, 238],
177
- wheat: [245, 222, 179],
178
- white: [255, 255, 255],
179
- whitesmoke: [245, 245, 245],
180
- yellow: [255, 255, 0],
181
- yellowgreen: [154, 205, 50]
182
- }
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)
183
67
  end
184
68
  end
185
69
  end
@@ -1,16 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class NullConverter < BaseConverter
3
- def self.matches?(_color_input)
5
+ def self.matches?(_colour_input)
4
6
  true
5
7
  end
6
8
 
7
9
  private
8
10
 
9
- def validate_input(_color_input)
10
- false
11
+ # def clamp_input(colour_input)
12
+ # colour_input
13
+ # end
14
+
15
+ def validate_input(_colour_input)
16
+ ['did not recognise colour input']
11
17
  end
12
18
 
13
- def input_to_rgba(_color_input)
19
+ def input_to_rgba(_colour_input)
14
20
  raise InvalidColorError
15
21
  end
16
22
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class OklabConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(Hash)
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
5
7
 
6
- color_input.keys - [:l, :a, :b, :space] == [] && color_input[:space].to_s == 'ok'
8
+ colour_input.keys - [:l, :a, :b, :space] == [] && colour_input[:space].to_s == 'ok'
7
9
  end
8
10
 
9
11
  def self.bounds
@@ -12,22 +14,27 @@ module ColorConverters
12
14
 
13
15
  private
14
16
 
15
- def validate_input(color_input)
16
- bounds = OklabConverter.bounds
17
- color_input[:l].to_f.between?(*bounds[:l]) && color_input[:a].to_f.between?(*bounds[:a]) && color_input[:b].to_f.between?(*bounds[:b])
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
18
25
  end
19
26
 
20
- def input_to_rgba(color_input)
21
- x, y, z = OklabConverter.oklab_to_xyz(color_input)
27
+ def input_to_rgba(colour_input)
28
+ x, y, z = OklabConverter.oklab_to_xyz(colour_input)
22
29
  r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
23
30
 
24
31
  [r, g, b, 1.0]
25
32
  end
26
33
 
27
- def self.oklab_to_xyz(color_input)
28
- l = color_input[:l].to_d
29
- a = color_input[:a].to_d
30
- b = color_input[:b].to_d
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
31
38
 
32
39
  # Now, scale l to a decimal
33
40
  l /= 100.0.to_d
@@ -41,9 +48,9 @@ module ColorConverters
41
48
 
42
49
  l_lms, m_lms, s_lms = (lab_to_lms_matrix * ::Matrix.column_vector([l, a, b])).to_a.flatten
43
50
 
44
- l_lms **= 3.to_d
45
- m_lms **= 3.to_d
46
- s_lms **= 3.to_d
51
+ l_lms **= 3.0.to_d
52
+ m_lms **= 3.0.to_d
53
+ s_lms **= 3.0.to_d
47
54
 
48
55
  lms_to_xyz_matrix = ::Matrix[
49
56
  [1.2268798758459243.to_d, -0.5578149944602171.to_d, 0.2813910456659647.to_d],
@@ -77,9 +84,10 @@ module ColorConverters
77
84
 
78
85
  l_lms, m_lms, s_lms = (xyz_to_lms_matrix * ::Matrix.column_vector([x, y, z])).to_a.flatten
79
86
 
80
- l_lms **= (1.0.to_d / 3.0.to_d)
81
- m_lms **= (1.0.to_d / 3.0.to_d)
82
- s_lms **= (1.0.to_d / 3.0.to_d)
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
83
91
 
84
92
  lms_to_lab_matrix = ::Matrix[
85
93
  [0.2104542683093140.to_d, 0.7936177747023054.to_d, -0.0040720430116193.to_d],
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class OklchConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(Hash)
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
5
7
 
6
- color_input.keys - [:l, :c, :h, :space] == [] && color_input[:space].to_s == 'ok'
8
+ colour_input.keys - [:l, :c, :h, :space] == [] && colour_input[:space].to_s == 'ok'
7
9
  end
8
10
 
9
11
  def self.bounds
@@ -12,23 +14,28 @@ module ColorConverters
12
14
 
13
15
  private
14
16
 
15
- def validate_input(color_input)
16
- bounds = OklchConverter.bounds
17
- color_input[:l].to_f.between?(*bounds[:l]) && color_input[:c].to_f.between?(*bounds[:c]) && color_input[:h].to_f.between?(*bounds[:h])
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
18
25
  end
19
26
 
20
- def input_to_rgba(color_input)
21
- l, a, b = OklchConverter.oklch_to_oklab(color_input)
27
+ def input_to_rgba(colour_input)
28
+ l, a, b = OklchConverter.oklch_to_oklab(colour_input)
22
29
  x, y, z = OklabConverter.oklab_to_xyz({ l: l, a: a, b: b })
23
30
  r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
24
31
 
25
32
  [r, g, b, 1.0]
26
33
  end
27
34
 
28
- def self.oklch_to_oklab(color_input)
29
- l = color_input[:l].to_d
30
- c = color_input[:c].to_d
31
- h = color_input[:h].to_d
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
32
39
 
33
40
  h_rad = h * (Math::PI.to_d / 180.0.to_d)
34
41
 
@@ -1,27 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class RgbConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(Hash)
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
5
7
 
6
- color_input.keys - [:r, :g, :b] == [] || color_input.keys - [:r, :g, :b, :a] == []
8
+ colour_input.keys - [:r, :g, :b] == [] || colour_input.keys - [:r, :g, :b, :a] == []
7
9
  end
8
10
 
9
11
  def self.bounds
10
- { r: [0.0, 255.0], g: [0.0, 255.0], b: [0.0, 255.0] }
12
+ { r: [0.0, 255.0], g: [0.0, 255.0], b: [0.0, 255.0], a: [0.0, 1.0] }
11
13
  end
12
14
 
13
15
  private
14
16
 
15
- def validate_input(color_input)
16
- bounds = RgbConverter.bounds
17
- color_input[:r].to_f.between?(*bounds[:r]) && color_input[:g].to_f.between?(*bounds[:g]) && color_input[:b].to_f.between?(*bounds[:b])
17
+ # def clamp_input(colour_input)
18
+ # colour_input.each { |key, value| colour_input[key] = value.clamp(*RgbConverter.bounds[key]) }
19
+ # end
20
+
21
+ def validate_input(colour_input)
22
+ RgbConverter.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
18
25
  end
19
26
 
20
- def input_to_rgba(color_input)
21
- r = color_input[:r].to_f
22
- g = color_input[:g].to_f
23
- b = color_input[:b].to_f
24
- a = (color_input[:a] || 1.0).to_f
27
+ def input_to_rgba(colour_input)
28
+ r = colour_input[:r].to_f
29
+ g = colour_input[:g].to_f
30
+ b = colour_input[:b].to_f
31
+ a = (colour_input[:a] || 1.0).to_f
25
32
 
26
33
  [r, g, b, a]
27
34
  end
@@ -43,7 +50,7 @@ module ColorConverters
43
50
 
44
51
  # IMPORTANT NUMERICAL NOTE:
45
52
  # On this specific system (and confirmed by Wolfram Alpha for direct calculation),
46
- # the power function for val**2.4 yields a result that deviates from the value expected by widely-used color science libraries (like Bruce Lindbloom's).
53
+ # the power function for val**2.4 yields a result that deviates from the value expected by widely-used colour science libraries (like Bruce Lindbloom's).
47
54
  #
48
55
  # To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
49
56
  # an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
@@ -78,7 +85,7 @@ module ColorConverters
78
85
 
79
86
  # IMPORTANT NUMERICAL NOTE:
80
87
  # On this specific system (and confirmed by Wolfram Alpha for direct calculation),
81
- # the inverse power function for val**2.4 yields a result that deviates from the value expected by widely-used color science libraries (like Bruce Lindbloom's).
88
+ # the inverse power function for val**2.4 yields a result that deviates from the value expected by widely-used colour science libraries (like Bruce Lindbloom's).
82
89
  #
83
90
  # To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
84
91
  # an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
@@ -90,7 +97,7 @@ module ColorConverters
90
97
  end
91
98
  end
92
99
 
93
- # Scale the 0-1 sRGB value to the 0-255 range for 8-bit color components.
100
+ # Scale the 0-1 sRGB value to the 0-255 range for 8-bit colour components.
94
101
  r *= 255.0.to_d
95
102
  g *= 255.0.to_d
96
103
  b *= 255.0.to_d
@@ -1,32 +1,64 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorConverters
2
4
  class RgbStringConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(String)
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(String)
7
+
8
+ colour_input.include?('rgb(') || colour_input.include?('rgba(')
9
+ end
5
10
 
6
- color_input.include?('rgb(') || color_input.include?('rgba(')
11
+ def self.bounds
12
+ RgbConverter.bounds
7
13
  end
8
14
 
9
15
  private
10
16
 
11
- def validate_input(_color_input)
12
- true
13
- end
17
+ # def clamp_input(colour_input)
18
+ # colour_input = RgbStringConverter.sanitize_input(colour_input)
19
+ # colour_input.each { |key, value| colour_input[key] = value.to_f.clamp(*RgbConverter.bounds[key]) }
20
+ # RgbStringConverter.rgb_to_rgbstring([colour_input[:r], colour_input[:g], colour_input[:b]], colour_input[:a])
21
+ # end
22
+
23
+ def validate_input(colour_input)
24
+ keys = colour_input.include?('rgba(') ? [:r, :g, :b, :a] : [:r, :g, :b]
25
+ colour_input = RgbStringConverter.sanitize_input(colour_input)
14
26
 
15
- def input_to_rgba(color_input)
16
- matches = color_input.match(/rgba?\(([0-9.,\s]+)\)/)
17
- raise InvalidColorError unless matches
27
+ errors = keys.collect do |key|
28
+ "#{key} must be present" if colour_input[key].blank?
29
+ end.compact
18
30
 
19
- r, g, b, a = matches[1].split(',').map(&:strip)
20
- raise InvalidColorError unless r.present? && g.present? && b.present?
31
+ return errors if errors.present?
21
32
 
22
- a ||= 1.0
33
+ RgbStringConverter.bounds.collect do |key, range|
34
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
35
+ end.compact
36
+ end
37
+
38
+ def input_to_rgba(colour_input)
39
+ colour_input = RgbStringConverter.sanitize_input(colour_input)
23
40
 
24
- r = r.to_f
25
- g = g.to_f
26
- b = b.to_f
27
- a = a.to_f
41
+ r = colour_input[:r].to_f
42
+ g = colour_input[:g].to_f
43
+ b = colour_input[:b].to_f
44
+ a = (colour_input[:a] || 1.0).to_f
28
45
 
29
46
  [r, g, b, a]
30
47
  end
48
+
49
+ def self.sanitize_input(colour_input)
50
+ matches = colour_input.match(/rgba?\(([0-9.,%\s]+)\)/) || []
51
+ r, g, b, a = matches[1]&.split(',')&.map(&:strip)
52
+ { r: r, g: g, b: b, a: a }
53
+ end
54
+
55
+ # def self.rgb_to_rgbstring(rgb_array_frac, alpha)
56
+ # r, g, b = rgb_array_frac
57
+ # if alpha == 1.0
58
+ # "rgb(#{[r, g, b].join(', ')})"
59
+ # else
60
+ # "rgba(#{[r, g, b, alpha].join(', ')})"
61
+ # end
62
+ # end
31
63
  end
32
64
  end