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,183 +1,67 @@
|
|
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
|
-
def
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
darkslateblue: [72, 61, 139],
|
69
|
-
darkslategray: [47, 79, 79],
|
70
|
-
darkslategrey: [47, 79, 79],
|
71
|
-
darkturquoise: [0, 206, 209],
|
72
|
-
darkviolet: [148, 0, 211],
|
73
|
-
deeppink: [255, 20, 147],
|
74
|
-
deepskyblue: [0, 191, 255],
|
75
|
-
dimgray: [105, 105, 105],
|
76
|
-
dimgrey: [105, 105, 105],
|
77
|
-
dodgerblue: [30, 144, 255],
|
78
|
-
firebrick: [178, 34, 34],
|
79
|
-
floralwhite: [255, 250, 240],
|
80
|
-
forestgreen: [34, 139, 34],
|
81
|
-
fuchsia: [255, 0, 255],
|
82
|
-
gainsboro: [220, 220, 220],
|
83
|
-
ghostwhite: [248, 248, 255],
|
84
|
-
gold: [255, 215, 0],
|
85
|
-
goldenrod: [218, 165, 32],
|
86
|
-
gray: [128, 128, 128],
|
87
|
-
green: [0, 128, 0],
|
88
|
-
greenyellow: [173, 255, 47],
|
89
|
-
grey: [128, 128, 128],
|
90
|
-
honeydew: [240, 255, 240],
|
91
|
-
hotpink: [255, 105, 180],
|
92
|
-
indianred: [205, 92, 92],
|
93
|
-
indigo: [75, 0, 130],
|
94
|
-
ivory: [255, 255, 240],
|
95
|
-
khaki: [240, 230, 140],
|
96
|
-
lavender: [230, 230, 250],
|
97
|
-
lavenderblush: [255, 240, 245],
|
98
|
-
lawngreen: [124, 252, 0],
|
99
|
-
lemonchiffon: [255, 250, 205],
|
100
|
-
lightblue: [173, 216, 230],
|
101
|
-
lightcoral: [240, 128, 128],
|
102
|
-
lightcyan: [224, 255, 255],
|
103
|
-
lightgoldenrodyellow: [250, 250, 210],
|
104
|
-
lightgray: [211, 211, 211],
|
105
|
-
lightgreen: [144, 238, 144],
|
106
|
-
lightgrey: [211, 211, 211],
|
107
|
-
lightpink: [255, 182, 193],
|
108
|
-
lightsalmon: [255, 160, 122],
|
109
|
-
lightseagreen: [32, 178, 170],
|
110
|
-
lightskyblue: [135, 206, 250],
|
111
|
-
lightslategray: [119, 136, 153],
|
112
|
-
lightslategrey: [119, 136, 153],
|
113
|
-
lightsteelblue: [176, 196, 222],
|
114
|
-
lightyellow: [255, 255, 224],
|
115
|
-
lime: [0, 255, 0],
|
116
|
-
limegreen: [50, 205, 50],
|
117
|
-
linen: [250, 240, 230],
|
118
|
-
magenta: [255, 0, 255],
|
119
|
-
maroon: [128, 0, 0],
|
120
|
-
mediumaquamarine: [102, 205, 170],
|
121
|
-
mediumblue: [0, 0, 205],
|
122
|
-
mediumorchid: [186, 85, 211],
|
123
|
-
mediumpurple: [147, 112, 219],
|
124
|
-
mediumseagreen: [60, 179, 113],
|
125
|
-
mediumslateblue: [123, 104, 238],
|
126
|
-
mediumspringgreen: [0, 250, 154],
|
127
|
-
mediumturquoise: [72, 209, 204],
|
128
|
-
mediumvioletred: [199, 21, 133],
|
129
|
-
midnightblue: [25, 25, 112],
|
130
|
-
mintcream: [245, 255, 250],
|
131
|
-
mistyrose: [255, 228, 225],
|
132
|
-
moccasin: [255, 228, 181],
|
133
|
-
navajowhite: [255, 222, 173],
|
134
|
-
navy: [0, 0, 128],
|
135
|
-
oldlace: [253, 245, 230],
|
136
|
-
olive: [128, 128, 0],
|
137
|
-
olivedrab: [107, 142, 35],
|
138
|
-
orange: [255, 165, 0],
|
139
|
-
orangered: [255, 69, 0],
|
140
|
-
orchid: [218, 112, 214],
|
141
|
-
palegoldenrod: [238, 232, 170],
|
142
|
-
palegreen: [152, 251, 152],
|
143
|
-
paleturquoise: [175, 238, 238],
|
144
|
-
palevioletred: [219, 112, 147],
|
145
|
-
papayawhip: [255, 239, 213],
|
146
|
-
peachpuff: [255, 218, 185],
|
147
|
-
peru: [205, 133, 63],
|
148
|
-
pink: [255, 192, 203],
|
149
|
-
plum: [221, 160, 221],
|
150
|
-
powderblue: [176, 224, 230],
|
151
|
-
purple: [128, 0, 128],
|
152
|
-
red: [255, 0, 0],
|
153
|
-
rosybrown: [188, 143, 143],
|
154
|
-
royalblue: [65, 105, 225],
|
155
|
-
saddlebrown: [139, 69, 19],
|
156
|
-
salmon: [250, 128, 114],
|
157
|
-
sandybrown: [244, 164, 96],
|
158
|
-
seagreen: [46, 139, 87],
|
159
|
-
seashell: [255, 245, 238],
|
160
|
-
sienna: [160, 82, 45],
|
161
|
-
silver: [192, 192, 192],
|
162
|
-
skyblue: [135, 206, 235],
|
163
|
-
slateblue: [106, 90, 205],
|
164
|
-
slategray: [112, 128, 144],
|
165
|
-
slategrey: [112, 128, 144],
|
166
|
-
snow: [255, 250, 250],
|
167
|
-
springgreen: [0, 255, 127],
|
168
|
-
steelblue: [70, 130, 180],
|
169
|
-
tan: [210, 180, 140],
|
170
|
-
teal: [0, 128, 128],
|
171
|
-
thistle: [216, 191, 216],
|
172
|
-
tomato: [255, 99, 71],
|
173
|
-
turquoise: [64, 224, 208],
|
174
|
-
violet: [238, 130, 238],
|
175
|
-
wheat: [245, 222, 179],
|
176
|
-
white: [255, 255, 255],
|
177
|
-
whitesmoke: [245, 245, 245],
|
178
|
-
yellow: [255, 255, 0],
|
179
|
-
yellowgreen: [154, 205, 50]
|
180
|
-
}
|
181
|
-
end
|
182
|
-
end
|
183
|
-
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 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,17 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
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 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
|
@@ -0,0 +1,102 @@
|
|
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
|
@@ -0,0 +1,61 @@
|
|
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,29 +1,109 @@
|
|
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
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class RgbConverter < BaseConverter
|
5
|
+
def self.matches?(colour_input)
|
6
|
+
return false unless colour_input.is_a?(Hash)
|
7
|
+
|
8
|
+
colour_input.keys - [:r, :g, :b] == [] || colour_input.keys - [:r, :g, :b, :a] == []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
{ r: [0.0, 255.0], g: [0.0, 255.0], b: [0.0, 255.0], a: [0.0, 1.0] }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
RgbConverter.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
|
+
r = colour_input[:r].to_f
|
25
|
+
g = colour_input[:g].to_f
|
26
|
+
b = colour_input[:b].to_f
|
27
|
+
a = (colour_input[:a] || 1.0).to_f
|
28
|
+
|
29
|
+
[r, g, b, a]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.rgb_to_lrgb(rgb_array_frac)
|
33
|
+
# [0, 1]
|
34
|
+
r, g, b = rgb_array_frac
|
35
|
+
|
36
|
+
# Inverse sRGB companding. Linearizes RGB channels with respect to energy.
|
37
|
+
# Assumption that r, g, b are always positive
|
38
|
+
rr, gg, bb = [r, g, b].map do
|
39
|
+
if _1.to_d <= 0.04045.to_d
|
40
|
+
_1.to_d / 12.92.to_d
|
41
|
+
else
|
42
|
+
# sRGB Inverse Companding (Non-linear to Linear RGB)
|
43
|
+
# The sRGB specification (IEC 61966-2-1) defines the exponent as 2.4.
|
44
|
+
#
|
45
|
+
(((_1.to_d + 0.055.to_d) / 1.055.to_d)**2.4.to_d)
|
46
|
+
|
47
|
+
# IMPORTANT NUMERICAL NOTE:
|
48
|
+
# On this specific system (and confirmed by Wolfram Alpha for direct calculation),
|
49
|
+
# 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).
|
50
|
+
#
|
51
|
+
# To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
|
52
|
+
# an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
|
53
|
+
#
|
54
|
+
# Choose 2.4 for strict adherence to the standard's definition (knowing your results may slightly deviate from common calculators),
|
55
|
+
# or choose 2.5 to ensure your calculated linear RGB values (and thus CIELAB) match authoritative external tools on this system.
|
56
|
+
#
|
57
|
+
# ((_1 + 0.055) / 1.055)**2.5
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# [0, 1]
|
62
|
+
[rr, gg, bb]
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.lrgb_to_rgb(lrgb_array)
|
66
|
+
rr, gg, bb = lrgb_array
|
67
|
+
|
68
|
+
# Apply sRGB Companding (gamma correction) to convert from Linear RGB to non-linear sRGB.
|
69
|
+
# This is defined by the sRGB specification (IEC 61966-2-1).
|
70
|
+
# The exponent for the non-linear segment is 1/2.4 (approximately 0.41666...).
|
71
|
+
# Assumption that rr, gg, bb are always positive
|
72
|
+
r, g, b = [rr, gg, bb].map do
|
73
|
+
if _1.to_d <= 0.0031308.to_d
|
74
|
+
# Linear portion of the sRGB curve
|
75
|
+
_1.to_d * 12.92.to_d
|
76
|
+
else
|
77
|
+
# Non-linear (gamma-corrected) portion of the sRGB curve
|
78
|
+
# The sRGB specification uses an exponent of 1/2.4.
|
79
|
+
#
|
80
|
+
(1.055.to_d * (_1.to_d**(1.0.to_d / 2.4.to_d))) - 0.055.to_d
|
81
|
+
|
82
|
+
# IMPORTANT NUMERICAL NOTE:
|
83
|
+
# On this specific system (and confirmed by Wolfram Alpha for direct calculation),
|
84
|
+
# 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).
|
85
|
+
#
|
86
|
+
# To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
|
87
|
+
# an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
|
88
|
+
#
|
89
|
+
# Choose 1/2.4 for strict adherence to the standard's definition (knowing your results may slightly deviate from common calculators),
|
90
|
+
# or choose 1/2.5 to ensure your calculated linear RGB values (and thus CIELAB) match authoritative external tools on this system.
|
91
|
+
#
|
92
|
+
# (1.055 * (_1**(1.0 / 2.5))) - 0.055
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Scale the 0-1 sRGB value to the 0-255 range for 8-bit colour components.
|
97
|
+
r *= 255.0.to_d
|
98
|
+
g *= 255.0.to_d
|
99
|
+
b *= 255.0.to_d
|
100
|
+
|
101
|
+
# Clamping RGB values to prevent out-of-gamut issues and numerical errors and ensures these values stay within the valid and expected range.
|
102
|
+
r = r.clamp(0.0..255.0)
|
103
|
+
g = g.clamp(0.0..255.0)
|
104
|
+
b = b.clamp(0.0..255.0)
|
105
|
+
|
106
|
+
[r, g, b]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -1,32 +1,49 @@
|
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColorConverters
|
4
|
+
class RgbStringConverter < BaseConverter
|
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
|
10
|
+
|
11
|
+
def self.bounds
|
12
|
+
RgbConverter.bounds
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_input(colour_input)
|
18
|
+
keys = colour_input.include?('rgba(') ? [:r, :g, :b, :a] : [:r, :g, :b]
|
19
|
+
colour_input = RgbStringConverter.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
|
+
RgbStringConverter.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 = RgbStringConverter.sanitize_input(colour_input)
|
34
|
+
|
35
|
+
r = colour_input[:r].to_f
|
36
|
+
g = colour_input[:g].to_f
|
37
|
+
b = colour_input[:b].to_f
|
38
|
+
a = (colour_input[:a] || 1.0).to_f
|
39
|
+
|
40
|
+
[r, g, b, a]
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.sanitize_input(colour_input)
|
44
|
+
matches = colour_input.match(/rgba?\(([0-9.,%\s]+)\)/) || []
|
45
|
+
r, g, b, a = matches[1]&.split(',')&.map(&:strip)
|
46
|
+
{ r: r, g: g, b: b, a: a }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|