abachrome 0.1.5 → 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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/abachrome.gemspec +1 -0
  3. data/devenv.nix +1 -1
  4. data/lib/abachrome/abc_decimal.rb +38 -35
  5. data/lib/abachrome/color.rb +37 -10
  6. data/lib/abachrome/color_mixins/blend.rb +7 -5
  7. data/lib/abachrome/color_mixins/lighten.rb +8 -6
  8. data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
  9. data/lib/abachrome/color_mixins/to_colorspace.rb +10 -8
  10. data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
  11. data/lib/abachrome/color_mixins/to_lrgb.rb +14 -12
  12. data/lib/abachrome/color_mixins/to_oklab.rb +16 -14
  13. data/lib/abachrome/color_mixins/to_oklch.rb +12 -10
  14. data/lib/abachrome/color_mixins/to_srgb.rb +17 -15
  15. data/lib/abachrome/color_models/cmyk.rb +159 -0
  16. data/lib/abachrome/color_models/hsv.rb +5 -3
  17. data/lib/abachrome/color_models/lms.rb +3 -1
  18. data/lib/abachrome/color_models/oklab.rb +3 -1
  19. data/lib/abachrome/color_models/oklch.rb +6 -4
  20. data/lib/abachrome/color_models/rgb.rb +4 -2
  21. data/lib/abachrome/color_models/xyz.rb +3 -1
  22. data/lib/abachrome/color_models/yiq.rb +37 -0
  23. data/lib/abachrome/color_space.rb +28 -14
  24. data/lib/abachrome/converter.rb +10 -8
  25. data/lib/abachrome/converters/base.rb +13 -11
  26. data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
  27. data/lib/abachrome/converters/lms_to_lrgb.rb +5 -3
  28. data/lib/abachrome/converters/lms_to_srgb.rb +6 -4
  29. data/lib/abachrome/converters/lms_to_xyz.rb +5 -3
  30. data/lib/abachrome/converters/lrgb_to_lms.rb +3 -1
  31. data/lib/abachrome/converters/lrgb_to_oklab.rb +5 -3
  32. data/lib/abachrome/converters/lrgb_to_srgb.rb +6 -4
  33. data/lib/abachrome/converters/lrgb_to_xyz.rb +5 -3
  34. data/lib/abachrome/converters/oklab_to_lms.rb +9 -7
  35. data/lib/abachrome/converters/oklab_to_lrgb.rb +7 -7
  36. data/lib/abachrome/converters/oklab_to_oklch.rb +4 -2
  37. data/lib/abachrome/converters/oklab_to_srgb.rb +4 -2
  38. data/lib/abachrome/converters/oklch_to_lrgb.rb +5 -3
  39. data/lib/abachrome/converters/oklch_to_oklab.rb +5 -3
  40. data/lib/abachrome/converters/oklch_to_srgb.rb +6 -4
  41. data/lib/abachrome/converters/oklch_to_xyz.rb +6 -4
  42. data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
  43. data/lib/abachrome/converters/srgb_to_lrgb.rb +5 -3
  44. data/lib/abachrome/converters/srgb_to_oklab.rb +4 -2
  45. data/lib/abachrome/converters/srgb_to_oklch.rb +5 -3
  46. data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
  47. data/lib/abachrome/converters/xyz_to_lms.rb +5 -3
  48. data/lib/abachrome/converters/xyz_to_oklab.rb +5 -3
  49. data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
  50. data/lib/abachrome/gamut/base.rb +3 -3
  51. data/lib/abachrome/gamut/p3.rb +3 -3
  52. data/lib/abachrome/gamut/rec2020.rb +2 -2
  53. data/lib/abachrome/gamut/srgb.rb +4 -2
  54. data/lib/abachrome/illuminants/base.rb +2 -2
  55. data/lib/abachrome/illuminants/d50.rb +2 -2
  56. data/lib/abachrome/illuminants/d55.rb +2 -2
  57. data/lib/abachrome/illuminants/d65.rb +2 -2
  58. data/lib/abachrome/illuminants/d75.rb +2 -2
  59. data/lib/abachrome/named/css.rb +149 -149
  60. data/lib/abachrome/named/tailwind.rb +265 -265
  61. data/lib/abachrome/outputs/css.rb +2 -2
  62. data/lib/abachrome/palette.rb +26 -25
  63. data/lib/abachrome/palette_mixins/interpolate.rb +3 -1
  64. data/lib/abachrome/palette_mixins/resample.rb +2 -2
  65. data/lib/abachrome/palette_mixins/stretch_luminance.rb +2 -2
  66. data/lib/abachrome/parsers/css.rb +86 -71
  67. data/lib/abachrome/parsers/hex.rb +2 -2
  68. data/lib/abachrome/parsers/tailwind.rb +8 -8
  69. data/lib/abachrome/spectral.rb +277 -0
  70. data/lib/abachrome/to_abcd.rb +4 -4
  71. data/lib/abachrome/version.rb +2 -2
  72. data/lib/abachrome.rb +66 -10
  73. metadata +29 -3
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::Spectral - Kubelka-Munk spectral color mixing
4
+ #
5
+ # This module implements the Kubelka-Munk theory for realistic paint-like color mixing.
6
+ # Based on spectral.js by Ronald van Wijnen (https://github.com/rvanwijnen/spectral.js)
7
+ #
8
+ # The Kubelka-Munk model simulates how real pigments absorb and scatter light by:
9
+ # 1. Converting RGB colors to spectral reflectance curves (38 wavelength samples)
10
+ # 2. Computing absorption/scattering coefficients (KS values)
11
+ # 3. Mixing colors by weighted averaging of KS values
12
+ # 4. Converting back to RGB via XYZ color space
13
+ #
14
+ # This produces more realistic color mixing than simple RGB interpolation,
15
+ # avoiding issues like muddy browns when mixing complementary colors.
16
+
17
+ require_relative "abc_decimal"
18
+
19
+ module Abachrome
20
+ module Spectral
21
+ module_function
22
+
23
+ # Number of wavelength samples in spectral curves
24
+ SIZE = 38
25
+
26
+ # Gamma value for sRGB companding
27
+ GAMMA = 2.4
28
+
29
+ # Base spectral reflectance curves for White, Cyan, Magenta, Yellow, Red, Green, Blue
30
+ # These are the fundamental building blocks for converting RGB to spectral data
31
+ BASE_SPECTRA = {
32
+ W: [
33
+ 1.00116072718764, 1.00116065159728, 1.00116031922747, 1.00115867270789, 1.00115259844552, 1.00113252528998, 1.00108500663327, 1.00099687889453, 1.00086525152274,
34
+ 1.0006962900094, 1.00050496114888, 1.00030808187992, 1.00011966602013, 0.999952765968407, 0.999821836899297, 0.999738609557593, 0.999709551639612, 0.999731930210627,
35
+ 0.999799436346195, 0.999900330316671, 1.00002040652611, 1.00014478793658, 1.00025997903412, 1.00035579697089, 1.00042753780269, 1.00047623344888, 1.00050720967508,
36
+ 1.00052519156373, 1.00053509606896, 1.00054022097482, 1.00054272816784, 1.00054389569087, 1.00054448212151, 1.00054476959992, 1.00054489887762, 1.00054496254689,
37
+ 1.00054498927058, 1.000544996993
38
+ ].freeze,
39
+ C: [
40
+ 0.970585001322962, 0.970592498143425, 0.970625348729891, 0.970786806119017, 0.971368673228248, 0.973163230621252, 0.976740223158765, 0.981587605491377, 0.986280265652949,
41
+ 0.989949147689134, 0.99249270153842, 0.994145680405256, 0.995183975033212, 0.995756750110818, 0.99591281828671, 0.995606157834528, 0.994597600961854, 0.99221571549237,
42
+ 0.986236452783249, 0.967943337264541, 0.891285004244943, 0.536202477862053, 0.154108119001878, 0.0574575093228929, 0.0315349873107007, 0.0222633920086335, 0.0182022841492439,
43
+ 0.016299055973264, 0.0153656239334613, 0.0149111568733976, 0.0146954339898235, 0.0145964146717719, 0.0145470156699655, 0.0145228771899495, 0.0145120341118965,
44
+ 0.0145066940939832, 0.0145044507314479, 0.0145038009464639
45
+ ].freeze,
46
+ M: [
47
+ 0.990673557319988, 0.990671524961979, 0.990662582353421, 0.990618107644795, 0.99045148087871, 0.989871081400204, 0.98828660875964, 0.984290692797504, 0.973934905625306,
48
+ 0.941817838460145, 0.817390326195156, 0.432472805065729, 0.13845397825887, 0.0537347216940033, 0.0292174996673231, 0.021313651750859, 0.0201349530181136, 0.0241323096280662,
49
+ 0.0372236145223627, 0.0760506552706601, 0.205375471942399, 0.541268903460439, 0.815841685086486, 0.912817704123976, 0.946339830166962, 0.959927696331991, 0.966260595230312,
50
+ 0.969325970058424, 0.970854536721399, 0.971605066528128, 0.971962769757392, 0.972127272274509, 0.972209417745812, 0.972249577678424, 0.972267621998742, 0.97227650946215,
51
+ 0.972280243306874, 0.97228132482656
52
+ ].freeze,
53
+ Y: [
54
+ 0.0210523371789306, 0.0210564627517414, 0.0210746178695038, 0.0211649058448753, 0.0215027957272504, 0.0226738799041561, 0.0258235649693629, 0.0334879385639851,
55
+ 0.0519069663740307, 0.100749014833473, 0.239129899706847, 0.534804312272748, 0.79780757864303, 0.911449894067384, 0.953797963004507, 0.971241615465429, 0.979303123807588,
56
+ 0.983380119507575, 0.985461246567755, 0.986435046976605, 0.986738250670141, 0.986617882445032, 0.986277776758643, 0.985860592444056, 0.98547492767621, 0.985176934765558,
57
+ 0.984971574014181, 0.984846303415712, 0.984775351811199, 0.984738066625265, 0.984719648311765, 0.984711023391939, 0.984706683300676, 0.984704554393091, 0.98470359630937,
58
+ 0.984703124077552, 0.98470292561509, 0.984702868122795
59
+ ].freeze,
60
+ R: [
61
+ 0.0315605737777207, 0.0315520718330149, 0.0315148215513658, 0.0313318044982702, 0.0306729857725527, 0.0286480476989607, 0.0246450407045709, 0.0192960753663651,
62
+ 0.0142066612220556, 0.0102942608878609, 0.0076191460521811, 0.005898041083542, 0.0048233247781713, 0.0042298748350633, 0.0040599171299341, 0.0043533695594676,
63
+ 0.0053434425970201, 0.0076917201010463, 0.0135969795736536, 0.0316975442661115, 0.107861196355249, 0.463812603168704, 0.847055405272011, 0.943185409393918, 0.968862150696558,
64
+ 0.978030667473603, 0.982043643854306, 0.983923623718707, 0.984845484154382, 0.985294275814596, 0.985507295219825, 0.985605071539837, 0.985653849933578, 0.985677685033883,
65
+ 0.985688391806122, 0.985693664690031, 0.985695879848205, 0.985696521463762
66
+ ].freeze,
67
+ G: [
68
+ 0.0095560747554212, 0.0095581580120851, 0.0095673245444588, 0.0096129126297349, 0.0097837090401843, 0.010378622705871, 0.0120026452378567, 0.0160977721473922,
69
+ 0.026706190223168, 0.0595555440185881, 0.186039826532826, 0.570579820116159, 0.861467768400292, 0.945879089767658, 0.970465486474305, 0.97841363028445, 0.979589031411224,
70
+ 0.975533536908632, 0.962288755397813, 0.92312157451312, 0.793434018943111, 0.459270135902429, 0.185574103666303, 0.0881774959955372, 0.05436302287667, 0.0406288447060719,
71
+ 0.034221520431697, 0.0311185790956966, 0.0295708898336134, 0.0288108739348928, 0.0284486271324597, 0.0282820301724731, 0.0281988376490237, 0.0281581655342037,
72
+ 0.0281398910216386, 0.0281308901665811, 0.0281271086805816, 0.0281260133612096
73
+ ].freeze,
74
+ B: [
75
+ 0.979404752502014, 0.97940070684313, 0.979382903470261, 0.979294364945594, 0.97896301460857, 0.977814466694043, 0.974724321133836, 0.967198482343973, 0.949079657530575,
76
+ 0.900850128940977, 0.76315044546224, 0.465922171649319, 0.201263280451005, 0.0877524413419623, 0.0457176793291679, 0.0284706050521843, 0.020527176756985, 0.0165302792310211,
77
+ 0.0145135107212858, 0.0136003508637687, 0.0133604258769571, 0.013548894314568, 0.0139594356366992, 0.014443425575357, 0.0148854440621406, 0.0152254296999746,
78
+ 0.0154592848180209, 0.0156018026485961, 0.0156824871281936, 0.0157248764360615, 0.0157458108784121, 0.0157556123350225, 0.0157605443964911, 0.0157629637515278,
79
+ 0.0157640525629106, 0.015764589232951, 0.0157648147772649, 0.0157648801149616
80
+ ].freeze
81
+ }.freeze
82
+
83
+ # CIE 1931 Color Matching Functions weighted by D65 Standard Illuminant
84
+ # Used to convert spectral reflectance to XYZ tristimulus values
85
+ CMF = [
86
+ [
87
+ 0.0000646919989576, 0.0002194098998132, 0.0011205743509343, 0.0037666134117111, 0.011880553603799, 0.0232864424191771, 0.0345594181969747, 0.0372237901162006,
88
+ 0.0324183761091486, 0.021233205609381, 0.0104909907685421, 0.0032958375797931, 0.0005070351633801, 0.0009486742057141, 0.0062737180998318, 0.0168646241897775,
89
+ 0.028689649025981, 0.0426748124691731, 0.0562547481311377, 0.0694703972677158, 0.0830531516998291, 0.0861260963002257, 0.0904661376847769, 0.0850038650591277,
90
+ 0.0709066691074488, 0.0506288916373645, 0.035473961885264, 0.0214682102597065, 0.0125164567619117, 0.0068045816390165, 0.0034645657946526, 0.0014976097506959,
91
+ 0.000769700480928, 0.0004073680581315, 0.0001690104031614, 0.0000952245150365, 0.0000490309872958, 0.0000199961492222
92
+ ].freeze,
93
+ [
94
+ 0.000001844289444, 0.0000062053235865, 0.0000310096046799, 0.0001047483849269, 0.0003536405299538, 0.0009514714056444, 0.0022822631748318, 0.004207329043473,
95
+ 0.0066887983719014, 0.0098883960193565, 0.0152494514496311, 0.0214183109449723, 0.0334229301575068, 0.0513100134918512, 0.070402083939949, 0.0878387072603517,
96
+ 0.0942490536184085, 0.0979566702718931, 0.0941521856862608, 0.0867810237486753, 0.0788565338632013, 0.0635267026203555, 0.05374141675682, 0.042646064357412,
97
+ 0.0316173492792708, 0.020885205921391, 0.0138601101360152, 0.0081026402038399, 0.004630102258803, 0.0024913800051319, 0.0012593033677378, 0.000541646522168,
98
+ 0.0002779528920067, 0.0001471080673854, 0.0000610327472927, 0.0000343873229523, 0.0000177059860053, 0.000007220974913
99
+ ].freeze,
100
+ [
101
+ 0.000305017147638, 0.0010368066663574, 0.0053131363323992, 0.0179543925899536, 0.0570775815345485, 0.113651618936287, 0.17335872618355, 0.196206575558657,
102
+ 0.186082370706296, 0.139950475383207, 0.0891745294268649, 0.0478962113517075, 0.0281456253957952, 0.0161376622950514, 0.0077591019215214, 0.0042961483736618,
103
+ 0.0020055092122156, 0.0008614711098802, 0.0003690387177652, 0.0001914287288574, 0.0001495555858975, 0.0000923109285104, 0.0000681349182337, 0.0000288263655696,
104
+ 0.0000157671820553, 0.0000039406041027, 0.000001584012587, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
105
+ ].freeze
106
+ ].freeze
107
+
108
+ # sRGB to XYZ transformation matrix (D65 illuminant)
109
+ RGB_TO_XYZ = [
110
+ [0.41239079926595934, 0.357584339383878, 0.1804807884018343],
111
+ [0.21263900587151027, 0.715168678767756, 0.07219231536073371],
112
+ [0.01933081871559182, 0.11919477979462598, 0.9505321522496607]
113
+ ].freeze
114
+
115
+ # XYZ to sRGB transformation matrix (D65 illuminant)
116
+ XYZ_TO_RGB = [
117
+ [3.2409699419045226, -1.537383177570094, -0.4986107602930034],
118
+ [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
119
+ [0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
120
+ ].freeze
121
+
122
+ # Inverse companding: sRGB to linear RGB
123
+ def uncompand(x)
124
+ x = x.to_f
125
+ x > 0.04045 ? ((x + 0.055) / 1.055)**GAMMA : x / 12.92
126
+ end
127
+
128
+ # Companding: linear RGB to sRGB
129
+ def compand(x)
130
+ x = x.to_f
131
+ x > 0.0031308 ? (1.055 * (x**(1.0 / GAMMA))) - 0.055 : x * 12.92
132
+ end
133
+
134
+ # Convert sRGB [0-1] to linear RGB
135
+ def srgb_to_lrgb(srgb)
136
+ srgb.map { |x| uncompand(x) }
137
+ end
138
+
139
+ # Convert linear RGB to sRGB [0-1]
140
+ def lrgb_to_srgb(lrgb)
141
+ lrgb.map { |x| compand(x) }
142
+ end
143
+
144
+ # Matrix-vector multiplication
145
+ def mul_mat_vec(matrix, vector)
146
+ matrix.map do |row|
147
+ row.zip(vector).map { |a, b| a * b }.sum
148
+ end
149
+ end
150
+
151
+ # Convert linear RGB to spectral reflectance curve
152
+ # Uses the seven primary spectral curves (W, C, M, Y, R, G, B)
153
+ def lrgb_to_reflectance(lrgb)
154
+ r, g, b = lrgb
155
+
156
+ # Extract white component
157
+ w = [r, g, b].min
158
+ r -= w
159
+ g -= w
160
+ b -= w
161
+
162
+ # Extract CMY components
163
+ c = [g, b].min
164
+ m = [r, b].min
165
+ y = [r, g].min
166
+
167
+ # Extract pure RGB components
168
+ r_pure = [0, [r - b, r - g].min].max
169
+ g_pure = [0, [g - b, g - r].min].max
170
+ b_pure = [0, [b - g, b - r].min].max
171
+
172
+ # Combine spectral curves
173
+ Array.new(SIZE) do |i|
174
+ [
175
+ Float::EPSILON,
176
+ (w * BASE_SPECTRA[:W][i]) +
177
+ (c * BASE_SPECTRA[:C][i]) +
178
+ (m * BASE_SPECTRA[:M][i]) +
179
+ (y * BASE_SPECTRA[:Y][i]) +
180
+ (r_pure * BASE_SPECTRA[:R][i]) +
181
+ (g_pure * BASE_SPECTRA[:G][i]) +
182
+ (b_pure * BASE_SPECTRA[:B][i])
183
+ ].max
184
+ end
185
+ end
186
+
187
+ # Convert spectral reflectance to XYZ using CIE color matching functions
188
+ def reflectance_to_xyz(reflectance)
189
+ mul_mat_vec(CMF, reflectance)
190
+ end
191
+
192
+ # Convert XYZ to linear RGB
193
+ def xyz_to_lrgb(xyz)
194
+ mul_mat_vec(XYZ_TO_RGB, xyz)
195
+ end
196
+
197
+ # Convert linear RGB to XYZ
198
+ def lrgb_to_xyz(lrgb)
199
+ mul_mat_vec(RGB_TO_XYZ, lrgb)
200
+ end
201
+
202
+ # Kubelka-Munk absorption/scattering parameter
203
+ # Converts reflectance R to absorption/scattering coefficient KS
204
+ def ks_from_reflectance(r)
205
+ ((1.0 - r)**2) / (2.0 * r)
206
+ end
207
+
208
+ # Inverse Kubelka-Munk function
209
+ # Converts KS back to reflectance
210
+ def reflectance_from_ks(ks)
211
+ 1.0 + ks - Math.sqrt((ks**2) + (2.0 * ks))
212
+ end
213
+
214
+ # Calculate luminance from XYZ (Y component)
215
+ def luminance_from_xyz(xyz)
216
+ [Float::EPSILON, xyz[1]].max
217
+ end
218
+
219
+ # Mix colors using Kubelka-Munk theory
220
+ #
221
+ # @param colors [Array<Hash>] Array of hashes with :color (Abachrome::Color) and :weight (Numeric)
222
+ # @param tinting_strengths [Hash] Optional hash mapping colors to tinting strengths (default: 1.0)
223
+ # @return [Abachrome::Color] The mixed color
224
+ #
225
+ # @example
226
+ # red = Abachrome.from_rgb(1, 0, 0)
227
+ # blue = Abachrome.from_rgb(0, 0, 1)
228
+ # mixed = Abachrome::Spectral.mix([{color: red, weight: 1}, {color: blue, weight: 1}])
229
+ def mix(colors, tinting_strengths: {})
230
+ # Convert colors to linear RGB and then to spectral reflectance
231
+ spectral_data = colors.map do |data|
232
+ color = data[:color]
233
+ weight = data[:weight].to_f
234
+ tinting_strength = tinting_strengths[color] || 1.0
235
+
236
+ # Convert to linear RGB
237
+ lrgb_color = color.to_color_space(:lrgb)
238
+ lrgb = lrgb_color.coordinates.map(&:to_f)
239
+
240
+ # Get spectral reflectance and KS values
241
+ reflectance = lrgb_to_reflectance(lrgb)
242
+ ks_values = reflectance.map { |r| ks_from_reflectance(r) }
243
+
244
+ # Calculate XYZ for luminance
245
+ xyz = reflectance_to_xyz(reflectance)
246
+ luminance = luminance_from_xyz(xyz)
247
+
248
+ # Calculate effective concentration
249
+ # Note: weight is already normalized (sums to 1), so we don't square it
250
+ # spectral.js squares 'factor' because it uses unnormalized values
251
+ concentration = weight * (tinting_strength**2) * luminance
252
+
253
+ {
254
+ ks_values: ks_values,
255
+ concentration: concentration
256
+ }
257
+ end
258
+
259
+ # Mix KS values using weighted average
260
+ total_concentration = spectral_data.sum { |d| d[:concentration] }
261
+
262
+ mixed_reflectance = Array.new(SIZE) do |i|
263
+ ks_mix = spectral_data.sum { |d| d[:ks_values][i] * d[:concentration] }
264
+ ks_mix /= total_concentration
265
+ reflectance_from_ks(ks_mix)
266
+ end
267
+
268
+ # Convert back to RGB
269
+ xyz = reflectance_to_xyz(mixed_reflectance)
270
+ lrgb = xyz_to_lrgb(xyz)
271
+ srgb = lrgb_to_srgb(lrgb)
272
+
273
+ # Return as Color object
274
+ Abachrome::Color.from_rgb(*srgb)
275
+ end
276
+ end
277
+ end
@@ -1,13 +1,13 @@
1
- #
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Abachrome
4
4
  module ToAbcd
5
5
  # Converts the receiver to an AbcDecimal object.
6
- #
6
+ #
7
7
  # This method converts the receiver (typically a numeric value) to an AbcDecimal
8
8
  # instance, which provides high precision decimal arithmetic capabilities for
9
9
  # color space calculations.
10
- #
10
+ #
11
11
  # @return [Abachrome::AbcDecimal] a new AbcDecimal instance representing the
12
12
  # same numeric value as the receiver
13
13
  def to_abcd
@@ -20,4 +20,4 @@ end
20
20
  klass.include(Abachrome::ToAbcd)
21
21
  end
22
22
 
23
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
23
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,7 +1,7 @@
1
- #
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Abachrome
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
6
6
 
7
7
  # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
data/lib/abachrome.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome - A Ruby color manipulation library
2
4
  #
3
5
  # This is the main entry point for the Abachrome library, providing color creation,
@@ -25,13 +27,16 @@ module Abachrome
25
27
  autoload :ColorSpace, "abachrome/color_space"
26
28
  autoload :Converter, "abachrome/converter"
27
29
  autoload :Gamut, "abachrome/gamut/base"
30
+ autoload :Spectral, "abachrome/spectral"
28
31
  autoload :ToAbcd, "abachrome/to_abcd"
29
32
  autoload :VERSION, "abachrome/version"
30
33
 
31
34
  module ColorModels
35
+ autoload :CMYK, "abachrome/color_models/cmyk"
32
36
  autoload :HSV, "abachrome/color_models/hsv"
33
37
  autoload :Oklab, "abachrome/color_models/oklab"
34
38
  autoload :RGB, "abachrome/color_models/rgb"
39
+ autoload :YIQ, "abachrome/color_models/yiq"
35
40
  end
36
41
 
37
42
  module ColorMixins
@@ -72,7 +77,7 @@ module Abachrome
72
77
  end
73
78
 
74
79
  # Creates a new color in the specified color space with given coordinates and alpha value.
75
- #
80
+ #
76
81
  # @param space_name [Symbol, String] The name of the color space (e.g., :srgb, :oklch)
77
82
  # @param coordinates [Array<Numeric>] The color coordinates in the specified color space
78
83
  # @param alpha [Float] The alpha (opacity) value of the color, defaults to 1.0 (fully opaque)
@@ -83,7 +88,7 @@ module Abachrome
83
88
  end
84
89
 
85
90
  # Creates a color object from RGB values.
86
- #
91
+ #
87
92
  # @param r [Numeric] The red component value (typically 0-255 or 0.0-1.0)
88
93
  # @param g [Numeric] The green component value (typically 0-255 or 0.0-1.0)
89
94
  # @param b [Numeric] The blue component value (typically 0-255 or 0.0-1.0)
@@ -94,7 +99,7 @@ module Abachrome
94
99
  end
95
100
 
96
101
  # Creates a color in the OKLAB color space.
97
- #
102
+ #
98
103
  # @param l [Numeric] The lightness component (L) in the OKLAB color space, typically in range 0 to 1
99
104
  # @param a [Numeric] The green-red component (a) in the OKLAB color space
100
105
  # @param b [Numeric] The blue-yellow component (b) in the OKLAB color space
@@ -105,7 +110,7 @@ module Abachrome
105
110
  end
106
111
 
107
112
  # Creates a new color from OKLCH color space values.
108
- #
113
+ #
109
114
  # @param l [Numeric] The lightness value, typically in range 0-1
110
115
  # @param a [Numeric] The chroma (colorfulness) value
111
116
  # @param b [Numeric] The hue angle value in degrees (0-360)
@@ -116,7 +121,7 @@ module Abachrome
116
121
  end
117
122
 
118
123
  # Creates a color object from a hexadecimal color code string.
119
- #
124
+ #
120
125
  # @param hex_str [String] The hexadecimal color code string to parse. Can be in formats like
121
126
  # "#RGB", "#RRGGBB", "RGB", or "RRGGBB", with or without the leading "#" character.
122
127
  # @return [Abachrome::Color] A new Color object representing the parsed hexadecimal color.
@@ -141,6 +146,57 @@ module Abachrome
141
146
  from_rgb(*rgb_values.map { |v| v / 255.0 })
142
147
  end
143
148
 
149
+ # Creates a color in the YIQ color space.
150
+ #
151
+ # @param y [Numeric] The luma (brightness) component, typically in range 0 to 1
152
+ # @param i [Numeric] The in-phase component (orange-blue), typically in range -0.5957 to 0.5957
153
+ # @param q [Numeric] The quadrature component (purple-green), typically in range -0.5226 to 0.5226
154
+ # @param alpha [Float] The alpha (opacity) value, ranging from 0.0 (transparent) to 1.0 (opaque), defaults to 1.0
155
+ # @return [Abachrome::Color] A new Color object in the YIQ color space
156
+ def from_yiq(y, i, q, alpha = 1.0)
157
+ Color.from_yiq(y, i, q, alpha)
158
+ end
159
+
160
+ # Creates a color in the CMYK color space.
161
+ #
162
+ # @param c [Numeric] The cyan component, typically in range 0 to 1 (or 0 to 100 for percentages)
163
+ # @param m [Numeric] The magenta component, typically in range 0 to 1 (or 0 to 100 for percentages)
164
+ # @param y [Numeric] The yellow component, typically in range 0 to 1 (or 0 to 100 for percentages)
165
+ # @param k [Numeric] The key/black component, typically in range 0 to 1 (or 0 to 100 for percentages)
166
+ # @param alpha [Float] The alpha (opacity) value, ranging from 0.0 (transparent) to 1.0 (opaque), defaults to 1.0
167
+ # @return [Abachrome::Color] A new Color object in the CMYK color space
168
+ def from_cmyk(c, m, y, k, alpha = 1.0)
169
+ Color.from_cmyk(c, m, y, k, alpha)
170
+ end
171
+
172
+ # Mix multiple colors using Kubelka-Munk spectral mixing.
173
+ #
174
+ # This function produces more realistic color mixing than simple RGB or LAB interpolation
175
+ # by simulating how real pigments absorb and scatter light. Based on spectral.js by
176
+ # Ronald van Wijnen (https://github.com/rvanwijnen/spectral.js)
177
+ #
178
+ # @param colors [Array<Hash>] Array of hashes with :color (Abachrome::Color) and :weight (Numeric)
179
+ # @param tinting_strengths [Hash] Optional hash mapping colors to tinting strengths (default: 1.0)
180
+ # @return [Abachrome::Color] The mixed color
181
+ #
182
+ # @example Mix red and blue with equal weights
183
+ # red = Abachrome.from_rgb(1, 0, 0)
184
+ # blue = Abachrome.from_rgb(0, 0, 1)
185
+ # purple = Abachrome.spectral_mix([
186
+ # {color: red, weight: 1},
187
+ # {color: blue, weight: 1}
188
+ # ])
189
+ #
190
+ # @example Mix three colors with different weights
191
+ # mixed = Abachrome.spectral_mix([
192
+ # {color: red, weight: 2},
193
+ # {color: green, weight: 1},
194
+ # {color: blue, weight: 1}
195
+ # ])
196
+ def spectral_mix(colors, tinting_strengths: {})
197
+ Spectral.mix(colors, tinting_strengths: tinting_strengths)
198
+ end
199
+
144
200
  # Parses a CSS color string and returns a Color object.
145
201
  #
146
202
  # @param css_string [String] The CSS color string to parse (e.g., "#ff0000", "rgb(255, 0, 0)", "red")
@@ -151,7 +207,7 @@ module Abachrome
151
207
  end
152
208
 
153
209
  # Convert a color from its current color space to another color space.
154
- #
210
+ #
155
211
  # @param color [Abachrome::Color] The color object to convert
156
212
  # @param to_space [Symbol, String] The destination color space identifier (e.g. :srgb, :oklch)
157
213
  # @return [Abachrome::Color] A new color object in the specified color space
@@ -160,7 +216,7 @@ module Abachrome
160
216
  end
161
217
 
162
218
  # Register a new color space with the Abachrome library.
163
- #
219
+ #
164
220
  # @param name [Symbol, String] The identifier for the color space being registered
165
221
  # @param block [Proc] A block that defines the color space properties and conversion rules
166
222
  # @return [Abachrome::ColorSpace] The newly registered color space object
@@ -169,11 +225,11 @@ module Abachrome
169
225
  end
170
226
 
171
227
  # Register a new color space converter in the Abachrome system.
172
- #
228
+ #
173
229
  # This method allows registering custom converters between color spaces.
174
230
  # Converters are used to transform color representations from one color
175
231
  # space to another.
176
- #
232
+ #
177
233
  # @param from_space [Symbol, String] The source color space identifier
178
234
  # @param to_space [Symbol, String] The destination color space identifier
179
235
  # @param converter [#call] An object responding to #call that performs the conversion
@@ -183,4 +239,4 @@ module Abachrome
183
239
  end
184
240
  end
185
241
 
186
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
242
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
metadata CHANGED
@@ -1,14 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abachrome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Durable Programming
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 1980-01-01 00:00:00.000000000 Z
11
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
12
27
  - !ruby/object:Gem::Dependency
13
28
  name: dry-inflector
14
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +67,25 @@ files:
52
67
  - lib/abachrome/color.rb
53
68
  - lib/abachrome/color_mixins/blend.rb
54
69
  - lib/abachrome/color_mixins/lighten.rb
70
+ - lib/abachrome/color_mixins/spectral_mix.rb
55
71
  - lib/abachrome/color_mixins/to_colorspace.rb
72
+ - lib/abachrome/color_mixins/to_grayscale.rb
56
73
  - lib/abachrome/color_mixins/to_lrgb.rb
57
74
  - lib/abachrome/color_mixins/to_oklab.rb
58
75
  - lib/abachrome/color_mixins/to_oklch.rb
59
76
  - lib/abachrome/color_mixins/to_srgb.rb
77
+ - lib/abachrome/color_models/cmyk.rb
60
78
  - lib/abachrome/color_models/hsv.rb
61
79
  - lib/abachrome/color_models/lms.rb
62
80
  - lib/abachrome/color_models/oklab.rb
63
81
  - lib/abachrome/color_models/oklch.rb
64
82
  - lib/abachrome/color_models/rgb.rb
65
83
  - lib/abachrome/color_models/xyz.rb
84
+ - lib/abachrome/color_models/yiq.rb
66
85
  - lib/abachrome/color_space.rb
67
86
  - lib/abachrome/converter.rb
68
87
  - lib/abachrome/converters/base.rb
88
+ - lib/abachrome/converters/cmyk_to_srgb.rb
69
89
  - lib/abachrome/converters/lms_to_lrgb.rb
70
90
  - lib/abachrome/converters/lms_to_srgb.rb
71
91
  - lib/abachrome/converters/lms_to_xyz.rb
@@ -81,11 +101,14 @@ files:
81
101
  - lib/abachrome/converters/oklch_to_oklab.rb
82
102
  - lib/abachrome/converters/oklch_to_srgb.rb
83
103
  - lib/abachrome/converters/oklch_to_xyz.rb
104
+ - lib/abachrome/converters/srgb_to_cmyk.rb
84
105
  - lib/abachrome/converters/srgb_to_lrgb.rb
85
106
  - lib/abachrome/converters/srgb_to_oklab.rb
86
107
  - lib/abachrome/converters/srgb_to_oklch.rb
108
+ - lib/abachrome/converters/srgb_to_yiq.rb
87
109
  - lib/abachrome/converters/xyz_to_lms.rb
88
110
  - lib/abachrome/converters/xyz_to_oklab.rb
111
+ - lib/abachrome/converters/yiq_to_srgb.rb
89
112
  - lib/abachrome/gamut/base.rb
90
113
  - lib/abachrome/gamut/p3.rb
91
114
  - lib/abachrome/gamut/rec2020.rb
@@ -105,6 +128,7 @@ files:
105
128
  - lib/abachrome/parsers/css.rb
106
129
  - lib/abachrome/parsers/hex.rb
107
130
  - lib/abachrome/parsers/tailwind.rb
131
+ - lib/abachrome/spectral.rb
108
132
  - lib/abachrome/to_abcd.rb
109
133
  - lib/abachrome/version.rb
110
134
  - logo.png
@@ -118,6 +142,7 @@ metadata:
118
142
  homepage_uri: https://github.com/durableprogramming/abachrome
119
143
  source_code_uri: https://github.com/durableprogramming/abachrome
120
144
  changelog_uri: https://github.com/durableprogramming/abachrome/blob/main/CHANGELOG.md
145
+ post_install_message:
121
146
  rdoc_options: []
122
147
  require_paths:
123
148
  - lib
@@ -132,7 +157,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
157
  - !ruby/object:Gem::Version
133
158
  version: '0'
134
159
  requirements: []
135
- rubygems_version: 3.7.1
160
+ rubygems_version: 3.5.9
161
+ signing_key:
136
162
  specification_version: 4
137
163
  summary: A Ruby gem for parsing, manipulating, and managing colors
138
164
  test_files: []