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.
- checksums.yaml +4 -4
- data/README.md +70 -47
- data/Rakefile +8 -8
- data/lib/color_converters/base_converter.rb +17 -14
- data/lib/color_converters/color.rb +17 -2
- data/lib/color_converters/converters/cielab_converter.rb +19 -12
- data/lib/color_converters/converters/cielch_converter.rb +19 -12
- data/lib/color_converters/converters/cmyk_converter.rb +18 -11
- data/lib/color_converters/converters/hex_converter.rb +29 -12
- data/lib/color_converters/converters/hsl_converter.rb +20 -13
- data/lib/color_converters/converters/hsl_string_converter.rb +46 -12
- data/lib/color_converters/converters/hsv_converter.rb +17 -11
- data/lib/color_converters/converters/name_converter.rb +50 -166
- data/lib/color_converters/converters/null_converter.rb +10 -4
- data/lib/color_converters/converters/oklab_converter.rb +26 -18
- data/lib/color_converters/converters/oklch_converter.rb +19 -12
- data/lib/color_converters/converters/rgb_converter.rb +22 -15
- data/lib/color_converters/converters/rgb_string_converter.rb +48 -16
- data/lib/color_converters/converters/xyz_converter.rb +19 -12
- data/lib/color_converters/version.rb +1 -1
- data/lib/color_converters.rb +31 -30
- metadata +18 -4
@@ -1,27 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ColorConverters
|
2
4
|
class HslStringConverter < BaseConverter
|
3
|
-
def self.matches?(
|
4
|
-
return false unless
|
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
|
-
|
11
|
+
def self.bounds
|
12
|
+
HslConverter.bounds
|
7
13
|
end
|
8
14
|
|
9
15
|
private
|
10
16
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
27
|
+
errors = keys.collect do |key|
|
28
|
+
"#{key} must be present" if colour_input[key].blank?
|
29
|
+
end.compact
|
18
30
|
|
19
|
-
|
20
|
-
raise InvalidColorError unless h.present? && s.present? && l.present?
|
31
|
+
return errors if errors.present?
|
21
32
|
|
22
|
-
|
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?(
|
4
|
-
return false unless
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
5
7
|
|
6
|
-
|
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
|
16
|
-
|
17
|
-
|
18
|
-
|
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(
|
22
|
-
h =
|
23
|
-
s =
|
24
|
-
v = (
|
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?(
|
4
|
-
return false unless
|
7
|
+
def self.matches?(colour_input)
|
8
|
+
return false unless colour_input.is_a?(String)
|
5
9
|
|
6
|
-
|
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
|
-
|
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
|
-
|
13
|
-
|
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
|
19
|
-
|
20
|
-
|
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(
|
24
|
-
found_colour = self.class.
|
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
|
-
|
42
|
+
HexConverter.hex_to_rgba(found_colour) if found_colour.present?
|
43
|
+
end
|
28
44
|
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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?(
|
5
|
+
def self.matches?(_colour_input)
|
4
6
|
true
|
5
7
|
end
|
6
8
|
|
7
9
|
private
|
8
10
|
|
9
|
-
def
|
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']
|
11
17
|
end
|
12
18
|
|
13
|
-
def input_to_rgba(
|
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?(
|
4
|
-
return false unless
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
5
7
|
|
6
|
-
|
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
|
16
|
-
|
17
|
-
|
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(
|
21
|
-
x, y, z = OklabConverter.oklab_to_xyz(
|
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(
|
28
|
-
l =
|
29
|
-
a =
|
30
|
-
b =
|
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
|
-
|
81
|
-
|
82
|
-
|
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?(
|
4
|
-
return false unless
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
5
7
|
|
6
|
-
|
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
|
16
|
-
|
17
|
-
|
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(
|
21
|
-
l, a, b = OklchConverter.oklch_to_oklab(
|
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(
|
29
|
-
l =
|
30
|
-
c =
|
31
|
-
h =
|
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?(
|
4
|
-
return false unless
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
5
7
|
|
6
|
-
|
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
|
16
|
-
|
17
|
-
|
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(
|
21
|
-
r =
|
22
|
-
g =
|
23
|
-
b =
|
24
|
-
a = (
|
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
|
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
|
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
|
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?(
|
4
|
-
return false unless
|
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
|
-
|
11
|
+
def self.bounds
|
12
|
+
RgbConverter.bounds
|
7
13
|
end
|
8
14
|
|
9
15
|
private
|
10
16
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
27
|
+
errors = keys.collect do |key|
|
28
|
+
"#{key} must be present" if colour_input[key].blank?
|
29
|
+
end.compact
|
18
30
|
|
19
|
-
|
20
|
-
raise InvalidColorError unless r.present? && g.present? && b.present?
|
31
|
+
return errors if errors.present?
|
21
32
|
|
22
|
-
|
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
|