abachrome-float 0.1.6
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 +7 -0
- data/.envrc +3 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +21 -0
- data/CLA.md +45 -0
- data/CODE-OF-CONDUCT.md +9 -0
- data/LICENSE +19 -0
- data/README.md +315 -0
- data/Rakefile +15 -0
- data/SECURITY.md +94 -0
- data/abachrome-float.gemspec +36 -0
- data/demos/ncurses/plasma.rb +124 -0
- data/devenv.lock +171 -0
- data/devenv.nix +52 -0
- data/devenv.yaml +8 -0
- data/lib/abachrome/color.rb +197 -0
- data/lib/abachrome/color_mixins/blend.rb +100 -0
- data/lib/abachrome/color_mixins/lighten.rb +90 -0
- data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
- data/lib/abachrome/color_mixins/to_colorspace.rb +107 -0
- data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
- data/lib/abachrome/color_mixins/to_lrgb.rb +121 -0
- data/lib/abachrome/color_mixins/to_oklab.rb +117 -0
- data/lib/abachrome/color_mixins/to_oklch.rb +110 -0
- data/lib/abachrome/color_mixins/to_srgb.rb +142 -0
- data/lib/abachrome/color_models/cmyk.rb +159 -0
- data/lib/abachrome/color_models/hsv.rb +49 -0
- data/lib/abachrome/color_models/lms.rb +38 -0
- data/lib/abachrome/color_models/oklab.rb +37 -0
- data/lib/abachrome/color_models/oklch.rb +91 -0
- data/lib/abachrome/color_models/rgb.rb +58 -0
- data/lib/abachrome/color_models/xyz.rb +31 -0
- data/lib/abachrome/color_models/yiq.rb +37 -0
- data/lib/abachrome/color_space.rb +199 -0
- data/lib/abachrome/converter.rb +117 -0
- data/lib/abachrome/converters/base.rb +128 -0
- data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
- data/lib/abachrome/converters/lms_to_lrgb.rb +40 -0
- data/lib/abachrome/converters/lms_to_srgb.rb +27 -0
- data/lib/abachrome/converters/lms_to_xyz.rb +34 -0
- data/lib/abachrome/converters/lrgb_to_lms.rb +3 -0
- data/lib/abachrome/converters/lrgb_to_oklab.rb +57 -0
- data/lib/abachrome/converters/lrgb_to_srgb.rb +59 -0
- data/lib/abachrome/converters/lrgb_to_xyz.rb +33 -0
- data/lib/abachrome/converters/oklab_to_lms.rb +44 -0
- data/lib/abachrome/converters/oklab_to_lrgb.rb +71 -0
- data/lib/abachrome/converters/oklab_to_oklch.rb +56 -0
- data/lib/abachrome/converters/oklab_to_srgb.rb +46 -0
- data/lib/abachrome/converters/oklch_to_lrgb.rb +79 -0
- data/lib/abachrome/converters/oklch_to_oklab.rb +52 -0
- data/lib/abachrome/converters/oklch_to_srgb.rb +46 -0
- data/lib/abachrome/converters/oklch_to_xyz.rb +70 -0
- data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
- data/lib/abachrome/converters/srgb_to_lrgb.rb +55 -0
- data/lib/abachrome/converters/srgb_to_oklab.rb +45 -0
- data/lib/abachrome/converters/srgb_to_oklch.rb +47 -0
- data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
- data/lib/abachrome/converters/xyz_to_lms.rb +34 -0
- data/lib/abachrome/converters/xyz_to_oklab.rb +42 -0
- data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
- data/lib/abachrome/gamut/base.rb +74 -0
- data/lib/abachrome/gamut/p3.rb +27 -0
- data/lib/abachrome/gamut/rec2020.rb +25 -0
- data/lib/abachrome/gamut/srgb.rb +49 -0
- data/lib/abachrome/illuminants/base.rb +35 -0
- data/lib/abachrome/illuminants/d50.rb +33 -0
- data/lib/abachrome/illuminants/d55.rb +29 -0
- data/lib/abachrome/illuminants/d65.rb +37 -0
- data/lib/abachrome/illuminants/d75.rb +29 -0
- data/lib/abachrome/named/css.rb +157 -0
- data/lib/abachrome/named/tailwind.rb +301 -0
- data/lib/abachrome/outputs/css.rb +119 -0
- data/lib/abachrome/palette.rb +244 -0
- data/lib/abachrome/palette_mixins/interpolate.rb +53 -0
- data/lib/abachrome/palette_mixins/resample.rb +61 -0
- data/lib/abachrome/palette_mixins/stretch_luminance.rb +72 -0
- data/lib/abachrome/parsers/css.rb +452 -0
- data/lib/abachrome/parsers/hex.rb +52 -0
- data/lib/abachrome/parsers/tailwind.rb +45 -0
- data/lib/abachrome/spectral.rb +276 -0
- data/lib/abachrome/to_abcd.rb +23 -0
- data/lib/abachrome/version.rb +7 -0
- data/lib/abachrome.rb +242 -0
- data/logo.png +0 -0
- data/logo.webp +0 -0
- data/security/assesments/2025-10-12-SECURITY_ASSESSMENT.md +53 -0
- data/security/vex.json +21 -0
- metadata +146 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Abachrome::Parsers::CSS - CSS color format parser
|
|
5
|
+
#
|
|
6
|
+
# This parser handles various CSS color formats including:
|
|
7
|
+
# - Named colors (red, blue, etc.)
|
|
8
|
+
# - Hex colors (#rgb, #rrggbb, #rgba, #rrggbbaa)
|
|
9
|
+
# - rgb() and rgba() functions
|
|
10
|
+
# - hsl() and hsla() functions
|
|
11
|
+
# - hwb() function
|
|
12
|
+
# - lab() and lch() functions
|
|
13
|
+
# - oklab() and oklch() functions
|
|
14
|
+
# - color() function
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
require_relative "hex"
|
|
18
|
+
require_relative "../named/css"
|
|
19
|
+
require_relative "../color"
|
|
20
|
+
|
|
21
|
+
module Abachrome
|
|
22
|
+
module Parsers
|
|
23
|
+
class CSS
|
|
24
|
+
def self.parse(input)
|
|
25
|
+
return nil unless input.is_a?(String)
|
|
26
|
+
|
|
27
|
+
input = input.strip.downcase
|
|
28
|
+
|
|
29
|
+
# Try named colors first
|
|
30
|
+
named_color = parse_named_color(input)
|
|
31
|
+
return named_color if named_color
|
|
32
|
+
|
|
33
|
+
# Try hex colors
|
|
34
|
+
hex_color = Hex.parse(input)
|
|
35
|
+
return hex_color if hex_color
|
|
36
|
+
|
|
37
|
+
# Try functional notation
|
|
38
|
+
parse_functional_color(input)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.parse_named_color(input)
|
|
42
|
+
# Check if input matches a named color
|
|
43
|
+
rgb_values = Named::CSS.method(input.to_sym)&.call
|
|
44
|
+
return nil unless rgb_values
|
|
45
|
+
|
|
46
|
+
# Convert 0-255 RGB values to 0-1 range
|
|
47
|
+
r, g, b = rgb_values.map { |v| v / 255.0 }
|
|
48
|
+
Color.from_rgb(r, g, b)
|
|
49
|
+
rescue NameError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.parse_functional_color(input)
|
|
54
|
+
case input
|
|
55
|
+
when /^rgb\((.+)\)$/
|
|
56
|
+
parse_rgb(::Regexp.last_match(1))
|
|
57
|
+
when /^rgba\((.+)\)$/
|
|
58
|
+
parse_rgba(::Regexp.last_match(1))
|
|
59
|
+
when /^hsl\((.+)\)$/
|
|
60
|
+
parse_hsl(::Regexp.last_match(1))
|
|
61
|
+
when /^hsla\((.+)\)$/
|
|
62
|
+
parse_hsla(::Regexp.last_match(1))
|
|
63
|
+
when /^hwb\((.+)\)$/
|
|
64
|
+
parse_hwb(::Regexp.last_match(1))
|
|
65
|
+
when /^lab\((.+)\)$/
|
|
66
|
+
parse_lab(::Regexp.last_match(1))
|
|
67
|
+
when /^lch\((.+)\)$/
|
|
68
|
+
parse_lch(::Regexp.last_match(1))
|
|
69
|
+
when /^oklab\((.+)\)$/
|
|
70
|
+
parse_oklab(::Regexp.last_match(1))
|
|
71
|
+
when /^oklch\((.+)\)$/
|
|
72
|
+
parse_oklch(::Regexp.last_match(1))
|
|
73
|
+
when /^color\((.+)\)$/
|
|
74
|
+
parse_color_function(::Regexp.last_match(1))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.parse_rgb(params)
|
|
79
|
+
values = parse_color_values(params, 3)
|
|
80
|
+
return nil unless values
|
|
81
|
+
|
|
82
|
+
r, g, b = values
|
|
83
|
+
Color.from_rgb(r, g, b)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.parse_rgba(params)
|
|
87
|
+
values = parse_color_values(params, 4)
|
|
88
|
+
return nil unless values
|
|
89
|
+
|
|
90
|
+
r, g, b, a = values
|
|
91
|
+
Color.from_rgb(r, g, b, a)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.parse_hsl(params)
|
|
95
|
+
values = parse_hsl_values(params, 3)
|
|
96
|
+
return nil unless values
|
|
97
|
+
|
|
98
|
+
h, s, l = values
|
|
99
|
+
rgb = hsl_to_rgb(h, s, l)
|
|
100
|
+
Color.from_rgb(*rgb)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.parse_hsla(params)
|
|
104
|
+
values = parse_hsl_values(params, 4)
|
|
105
|
+
return nil unless values
|
|
106
|
+
|
|
107
|
+
h, s, l, a = values
|
|
108
|
+
rgb = hsl_to_rgb(h, s, l)
|
|
109
|
+
Color.from_rgb(*rgb, a)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.parse_hwb(params)
|
|
113
|
+
values = parse_hwb_values(params)
|
|
114
|
+
return nil unless values
|
|
115
|
+
|
|
116
|
+
h, w, b, a = values
|
|
117
|
+
rgb = hwb_to_rgb(h, w, b)
|
|
118
|
+
Color.from_rgb(*rgb, a)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.parse_lab(params)
|
|
122
|
+
values = parse_lab_values(params, 3)
|
|
123
|
+
return nil unless values
|
|
124
|
+
|
|
125
|
+
l, a, b = values
|
|
126
|
+
# Convert CIELAB to XYZ, then to sRGB
|
|
127
|
+
xyz = lab_to_xyz(l, a, b)
|
|
128
|
+
rgb = xyz_to_rgb(*xyz)
|
|
129
|
+
Color.from_rgb(*rgb)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.parse_lch(params)
|
|
133
|
+
values = parse_lch_values(params, 3)
|
|
134
|
+
return nil unless values
|
|
135
|
+
|
|
136
|
+
l, c, h = values
|
|
137
|
+
# Convert CIELCH to CIELAB, then to XYZ, then to sRGB
|
|
138
|
+
lab = lch_to_lab(l, c, h)
|
|
139
|
+
xyz = lab_to_xyz(*lab)
|
|
140
|
+
rgb = xyz_to_rgb(*xyz)
|
|
141
|
+
Color.from_rgb(*rgb)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.parse_oklab(params)
|
|
145
|
+
values = parse_oklab_values(params, 3)
|
|
146
|
+
return nil unless values
|
|
147
|
+
|
|
148
|
+
l, a, b = values
|
|
149
|
+
Color.from_oklab(l, a, b)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.parse_oklch(params)
|
|
153
|
+
values = parse_oklch_values(params, 3)
|
|
154
|
+
return nil unless values
|
|
155
|
+
|
|
156
|
+
l, c, h = values
|
|
157
|
+
Color.from_oklch(l, c, h)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.parse_color_function(params)
|
|
161
|
+
# Parse color(space values...)
|
|
162
|
+
parts = params.split(/\s+/, 2)
|
|
163
|
+
return nil unless parts.length == 2
|
|
164
|
+
|
|
165
|
+
space = parts[0]
|
|
166
|
+
values_str = parts[1]
|
|
167
|
+
|
|
168
|
+
case space
|
|
169
|
+
when "srgb"
|
|
170
|
+
values = parse_color_values(values_str, 3)
|
|
171
|
+
return nil unless values
|
|
172
|
+
|
|
173
|
+
r, g, b = values
|
|
174
|
+
Color.from_rgb(r, g, b)
|
|
175
|
+
when "srgb-linear"
|
|
176
|
+
values = parse_color_values(values_str, 3)
|
|
177
|
+
return nil unless values
|
|
178
|
+
|
|
179
|
+
r, g, b = values
|
|
180
|
+
Color.from_lrgb(r, g, b)
|
|
181
|
+
when "display-p3"
|
|
182
|
+
# For now, approximate as sRGB
|
|
183
|
+
values = parse_color_values(values_str, 3)
|
|
184
|
+
return nil unless values
|
|
185
|
+
|
|
186
|
+
r, g, b = values
|
|
187
|
+
Color.from_rgb(r, g, b)
|
|
188
|
+
when "a98-rgb"
|
|
189
|
+
# For now, approximate as sRGB
|
|
190
|
+
values = parse_color_values(values_str, 3)
|
|
191
|
+
return nil unless values
|
|
192
|
+
|
|
193
|
+
r, g, b = values
|
|
194
|
+
Color.from_rgb(r, g, b)
|
|
195
|
+
when "prophoto-rgb"
|
|
196
|
+
# For now, approximate as sRGB
|
|
197
|
+
values = parse_color_values(values_str, 3)
|
|
198
|
+
return nil unless values
|
|
199
|
+
|
|
200
|
+
r, g, b = values
|
|
201
|
+
Color.from_rgb(r, g, b)
|
|
202
|
+
when "rec2020"
|
|
203
|
+
# For now, approximate as sRGB
|
|
204
|
+
values = parse_color_values(values_str, 3)
|
|
205
|
+
return nil unless values
|
|
206
|
+
|
|
207
|
+
r, g, b = values
|
|
208
|
+
Color.from_rgb(r, g, b)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Helper methods for parsing values
|
|
213
|
+
|
|
214
|
+
def self.parse_color_values(str, expected_count)
|
|
215
|
+
values = str.split(/\s*,\s*/).map(&:strip)
|
|
216
|
+
return nil unless values.length == expected_count
|
|
217
|
+
|
|
218
|
+
values.map do |v|
|
|
219
|
+
parse_numeric_value(v)
|
|
220
|
+
end.compact
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.parse_hsl_values(str, expected_count)
|
|
224
|
+
values = str.split(/\s*,\s*/).map(&:strip)
|
|
225
|
+
return nil unless values.length == expected_count
|
|
226
|
+
|
|
227
|
+
parsed = []
|
|
228
|
+
values.each_with_index do |v, i|
|
|
229
|
+
val = if i.zero? # Hue
|
|
230
|
+
parse_angle_value(v)
|
|
231
|
+
|
|
232
|
+
else # Saturation, Lightness, Alpha
|
|
233
|
+
parse_percentage_or_number(v)
|
|
234
|
+
|
|
235
|
+
end
|
|
236
|
+
return nil unless val
|
|
237
|
+
|
|
238
|
+
parsed << val
|
|
239
|
+
end
|
|
240
|
+
parsed
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.parse_hwb_values(str)
|
|
244
|
+
values = str.split(/\s*,\s*/).map(&:strip)
|
|
245
|
+
return nil unless values.length >= 3
|
|
246
|
+
|
|
247
|
+
h = parse_angle_value(values[0])
|
|
248
|
+
w = parse_percentage_or_number(values[1])
|
|
249
|
+
b = parse_percentage_or_number(values[2])
|
|
250
|
+
a = values[3] ? parse_numeric_value(values[3]) : 1.0
|
|
251
|
+
|
|
252
|
+
return nil unless h && w && b && a
|
|
253
|
+
|
|
254
|
+
[h, w, b, a]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.parse_lab_values(str, expected_count)
|
|
258
|
+
values = str.split(/\s+/, expected_count).map(&:strip)
|
|
259
|
+
return nil unless values.length == expected_count
|
|
260
|
+
|
|
261
|
+
l = parse_percentage_or_number(values[0])
|
|
262
|
+
a = parse_numeric_value(values[1])
|
|
263
|
+
b = parse_numeric_value(values[2])
|
|
264
|
+
|
|
265
|
+
return nil unless l && a && b
|
|
266
|
+
|
|
267
|
+
[l, a, b]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def self.parse_lch_values(str, expected_count)
|
|
271
|
+
values = str.split(/\s+/, expected_count).map(&:strip)
|
|
272
|
+
return nil unless values.length == expected_count
|
|
273
|
+
|
|
274
|
+
l = parse_percentage_or_number(values[0])
|
|
275
|
+
c = parse_numeric_value(values[1])
|
|
276
|
+
h = parse_angle_value(values[2])
|
|
277
|
+
|
|
278
|
+
return nil unless l && c && h
|
|
279
|
+
|
|
280
|
+
[l, c, h]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def self.parse_oklab_values(str, expected_count)
|
|
284
|
+
values = str.split(/\s+/, expected_count).map(&:strip)
|
|
285
|
+
return nil unless values.length == expected_count
|
|
286
|
+
|
|
287
|
+
l = parse_percentage_or_number(values[0])
|
|
288
|
+
a = parse_numeric_value(values[1])
|
|
289
|
+
b = parse_numeric_value(values[2])
|
|
290
|
+
|
|
291
|
+
return nil unless l && a && b
|
|
292
|
+
|
|
293
|
+
[l, a, b]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def self.parse_oklch_values(str, expected_count)
|
|
297
|
+
values = str.split(/\s+/, expected_count).map(&:strip)
|
|
298
|
+
return nil unless values.length == expected_count
|
|
299
|
+
|
|
300
|
+
l = parse_percentage_or_number(values[0])
|
|
301
|
+
c = parse_numeric_value(values[1])
|
|
302
|
+
h = parse_angle_value(values[2])
|
|
303
|
+
|
|
304
|
+
return nil unless l && c && h
|
|
305
|
+
|
|
306
|
+
[l, c, h]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def self.parse_numeric_value(str)
|
|
310
|
+
return nil unless str
|
|
311
|
+
|
|
312
|
+
if str.end_with?("%")
|
|
313
|
+
(str.chomp("%").to_f / 100.0)
|
|
314
|
+
else
|
|
315
|
+
str.to_f
|
|
316
|
+
end
|
|
317
|
+
rescue StandardError
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def self.parse_percentage_or_number(str)
|
|
322
|
+
return nil unless str
|
|
323
|
+
|
|
324
|
+
if str.end_with?("%")
|
|
325
|
+
str.chomp("%").to_f / 100.0
|
|
326
|
+
else
|
|
327
|
+
str.to_f
|
|
328
|
+
end
|
|
329
|
+
rescue StandardError
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def self.parse_angle_value(str)
|
|
334
|
+
return nil unless str
|
|
335
|
+
|
|
336
|
+
if str.end_with?("deg")
|
|
337
|
+
str.chomp("deg").to_f
|
|
338
|
+
elsif str.end_with?("rad")
|
|
339
|
+
str.chomp("rad").to_f * 180.0 / Math::PI
|
|
340
|
+
elsif str.end_with?("grad")
|
|
341
|
+
str.chomp("grad").to_f * 0.9
|
|
342
|
+
elsif str.end_with?("turn")
|
|
343
|
+
str.chomp("turn").to_f * 360.0
|
|
344
|
+
else
|
|
345
|
+
str.to_f # Assume degrees
|
|
346
|
+
end
|
|
347
|
+
rescue StandardError
|
|
348
|
+
nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Color space conversion functions
|
|
352
|
+
|
|
353
|
+
def self.hsl_to_rgb(h, s, l)
|
|
354
|
+
h /= 360.0 # Normalize hue to 0-1
|
|
355
|
+
|
|
356
|
+
c = (1 - ((2 * l) - 1).abs) * s
|
|
357
|
+
x = c * (1 - (((h * 6) % 2) - 1).abs)
|
|
358
|
+
m = l - (c / 2)
|
|
359
|
+
|
|
360
|
+
if h < 1.0 / 6
|
|
361
|
+
r = c
|
|
362
|
+
g = x
|
|
363
|
+
b = 0
|
|
364
|
+
elsif h < 2.0 / 6
|
|
365
|
+
r = x
|
|
366
|
+
g = c
|
|
367
|
+
b = 0
|
|
368
|
+
elsif h < 3.0 / 6
|
|
369
|
+
r = 0
|
|
370
|
+
g = c
|
|
371
|
+
b = x
|
|
372
|
+
elsif h < 4.0 / 6
|
|
373
|
+
r = 0
|
|
374
|
+
g = x
|
|
375
|
+
b = c
|
|
376
|
+
elsif h < 5.0 / 6
|
|
377
|
+
r = x
|
|
378
|
+
g = 0
|
|
379
|
+
b = c
|
|
380
|
+
else
|
|
381
|
+
r = c
|
|
382
|
+
g = 0
|
|
383
|
+
b = x
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
[r + m, g + m, b + m]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def self.hwb_to_rgb(h, w, b)
|
|
390
|
+
# Normalize values
|
|
391
|
+
h /= 360.0
|
|
392
|
+
|
|
393
|
+
# Calculate RGB from HSL equivalent
|
|
394
|
+
if w + b >= 1
|
|
395
|
+
gray = w / (w + b)
|
|
396
|
+
[gray, gray, gray]
|
|
397
|
+
else
|
|
398
|
+
rgb = hsl_to_rgb(h * 360, 1, 0.5)
|
|
399
|
+
r, g, b_rgb = rgb
|
|
400
|
+
|
|
401
|
+
# Apply whiteness and blackness
|
|
402
|
+
r = (r * (1 - w - b)) + w
|
|
403
|
+
g = (g * (1 - w - b)) + w
|
|
404
|
+
b_rgb = (b_rgb * (1 - w - b)) + w
|
|
405
|
+
|
|
406
|
+
[r, g, b_rgb]
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def self.lab_to_xyz(l, a, b)
|
|
411
|
+
# CIELAB to XYZ conversion (D65 white point)
|
|
412
|
+
y = (l + 16) / 116
|
|
413
|
+
x = (a / 500) + y
|
|
414
|
+
z = y - (b / 200)
|
|
415
|
+
|
|
416
|
+
x = x**3 > 0.008856 ? x**3 : (x - (16 / 116)) / 7.787
|
|
417
|
+
y = y**3 > 0.008856 ? y**3 : (y - (16 / 116)) / 7.787
|
|
418
|
+
z = z**3 > 0.008856 ? z**3 : (z - (16 / 116)) / 7.787
|
|
419
|
+
|
|
420
|
+
# D65 white point
|
|
421
|
+
x *= 0.95047
|
|
422
|
+
y *= 1.0
|
|
423
|
+
z *= 1.08883
|
|
424
|
+
|
|
425
|
+
[x, y, z]
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def self.lch_to_lab(l, c, h)
|
|
429
|
+
h_rad = h * Math::PI / 180.0
|
|
430
|
+
a = c * Math.cos(h_rad)
|
|
431
|
+
b = c * Math.sin(h_rad)
|
|
432
|
+
[l, a, b]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def self.xyz_to_rgb(x, y, z)
|
|
436
|
+
# XYZ to linear RGB
|
|
437
|
+
r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986)
|
|
438
|
+
g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415)
|
|
439
|
+
b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570)
|
|
440
|
+
|
|
441
|
+
# Linear RGB to sRGB
|
|
442
|
+
[r, g, b].map do |v|
|
|
443
|
+
if v > 0.0031308
|
|
444
|
+
(1.055 * (v**(1 / 2.4))) - 0.055
|
|
445
|
+
else
|
|
446
|
+
12.92 * v
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Parsers
|
|
5
|
+
class Hex
|
|
6
|
+
HEX_PATTERN = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{8})$/
|
|
7
|
+
|
|
8
|
+
def self.parse(input)
|
|
9
|
+
hex = input.gsub(/^#/, "")
|
|
10
|
+
return nil unless hex.match?(HEX_PATTERN)
|
|
11
|
+
|
|
12
|
+
case hex.length
|
|
13
|
+
when 3
|
|
14
|
+
parse_short_hex(hex)
|
|
15
|
+
when 4
|
|
16
|
+
parse_short_hex_with_alpha(hex)
|
|
17
|
+
when 6
|
|
18
|
+
parse_full_hex(hex)
|
|
19
|
+
when 8
|
|
20
|
+
parse_full_hex_with_alpha(hex)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.parse_short_hex(hex)
|
|
25
|
+
r, g, b = hex.chars.map { |c| (c + c).to_i(16) }
|
|
26
|
+
Color.from_rgb(r / 255.0, g / 255.0, b / 255.0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.parse_short_hex_with_alpha(hex)
|
|
30
|
+
r, g, b, a = hex.chars.map { |c| (c + c).to_i(16) }
|
|
31
|
+
Color.from_rgb(r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.parse_full_hex(hex)
|
|
35
|
+
r = hex[0, 2].to_i(16)
|
|
36
|
+
g = hex[2, 2].to_i(16)
|
|
37
|
+
b = hex[4, 2].to_i(16)
|
|
38
|
+
Color.from_rgb(r / 255.0, g / 255.0, b / 255.0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.parse_full_hex_with_alpha(hex)
|
|
42
|
+
r = hex[0, 2].to_i(16)
|
|
43
|
+
g = hex[2, 2].to_i(16)
|
|
44
|
+
b = hex[4, 2].to_i(16)
|
|
45
|
+
a = hex[6, 2].to_i(16)
|
|
46
|
+
Color.from_rgb(r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Parsers
|
|
5
|
+
class Tailwind
|
|
6
|
+
# Matches Tailwind color patterns like:
|
|
7
|
+
# - gray-400
|
|
8
|
+
# - blue-900/20 (with opacity)
|
|
9
|
+
# - slate-50
|
|
10
|
+
TAILWIND_PATTERN = %r{^([a-z]+)-(\d+)(?:/(\d+(?:\.\d+)?))?$}
|
|
11
|
+
|
|
12
|
+
def self.parse(input)
|
|
13
|
+
match = input.match(TAILWIND_PATTERN)
|
|
14
|
+
return nil unless match
|
|
15
|
+
|
|
16
|
+
color_name = match[1]
|
|
17
|
+
shade = match[2]
|
|
18
|
+
opacity = match[3]
|
|
19
|
+
|
|
20
|
+
# Look up the color in the Tailwind color palette
|
|
21
|
+
color_shades = Named::Tailwind::COLORS[color_name]
|
|
22
|
+
return nil unless color_shades
|
|
23
|
+
|
|
24
|
+
rgb_values = color_shades[shade]
|
|
25
|
+
return nil unless rgb_values
|
|
26
|
+
|
|
27
|
+
# Convert RGB values to 0-1 range
|
|
28
|
+
r, g, b = rgb_values.map { |v| v / 255.0 }
|
|
29
|
+
|
|
30
|
+
# Calculate alpha from opacity percentage if provided
|
|
31
|
+
alpha = if opacity
|
|
32
|
+
opacity_value = opacity.to_f
|
|
33
|
+
# Opacity in Tailwind is a percentage (0-100)
|
|
34
|
+
opacity_value / 100.0
|
|
35
|
+
else
|
|
36
|
+
1.0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Color.from_rgb(r, g, b, alpha)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|