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.
- checksums.yaml +4 -4
- data/abachrome.gemspec +1 -0
- data/devenv.nix +1 -1
- data/lib/abachrome/abc_decimal.rb +38 -35
- data/lib/abachrome/color.rb +37 -10
- data/lib/abachrome/color_mixins/blend.rb +7 -5
- data/lib/abachrome/color_mixins/lighten.rb +8 -6
- data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
- data/lib/abachrome/color_mixins/to_colorspace.rb +10 -8
- data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
- data/lib/abachrome/color_mixins/to_lrgb.rb +14 -12
- data/lib/abachrome/color_mixins/to_oklab.rb +16 -14
- data/lib/abachrome/color_mixins/to_oklch.rb +12 -10
- data/lib/abachrome/color_mixins/to_srgb.rb +17 -15
- data/lib/abachrome/color_models/cmyk.rb +159 -0
- data/lib/abachrome/color_models/hsv.rb +5 -3
- data/lib/abachrome/color_models/lms.rb +3 -1
- data/lib/abachrome/color_models/oklab.rb +3 -1
- data/lib/abachrome/color_models/oklch.rb +6 -4
- data/lib/abachrome/color_models/rgb.rb +4 -2
- data/lib/abachrome/color_models/xyz.rb +3 -1
- data/lib/abachrome/color_models/yiq.rb +37 -0
- data/lib/abachrome/color_space.rb +28 -14
- data/lib/abachrome/converter.rb +10 -8
- data/lib/abachrome/converters/base.rb +13 -11
- data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
- data/lib/abachrome/converters/lms_to_lrgb.rb +5 -3
- data/lib/abachrome/converters/lms_to_srgb.rb +6 -4
- data/lib/abachrome/converters/lms_to_xyz.rb +5 -3
- data/lib/abachrome/converters/lrgb_to_lms.rb +3 -1
- data/lib/abachrome/converters/lrgb_to_oklab.rb +5 -3
- data/lib/abachrome/converters/lrgb_to_srgb.rb +6 -4
- data/lib/abachrome/converters/lrgb_to_xyz.rb +5 -3
- data/lib/abachrome/converters/oklab_to_lms.rb +9 -7
- data/lib/abachrome/converters/oklab_to_lrgb.rb +7 -7
- data/lib/abachrome/converters/oklab_to_oklch.rb +4 -2
- data/lib/abachrome/converters/oklab_to_srgb.rb +4 -2
- data/lib/abachrome/converters/oklch_to_lrgb.rb +5 -3
- data/lib/abachrome/converters/oklch_to_oklab.rb +5 -3
- data/lib/abachrome/converters/oklch_to_srgb.rb +6 -4
- data/lib/abachrome/converters/oklch_to_xyz.rb +6 -4
- data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
- data/lib/abachrome/converters/srgb_to_lrgb.rb +5 -3
- data/lib/abachrome/converters/srgb_to_oklab.rb +4 -2
- data/lib/abachrome/converters/srgb_to_oklch.rb +5 -3
- data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
- data/lib/abachrome/converters/xyz_to_lms.rb +5 -3
- data/lib/abachrome/converters/xyz_to_oklab.rb +5 -3
- data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
- data/lib/abachrome/gamut/base.rb +3 -3
- data/lib/abachrome/gamut/p3.rb +3 -3
- data/lib/abachrome/gamut/rec2020.rb +2 -2
- data/lib/abachrome/gamut/srgb.rb +4 -2
- data/lib/abachrome/illuminants/base.rb +2 -2
- data/lib/abachrome/illuminants/d50.rb +2 -2
- data/lib/abachrome/illuminants/d55.rb +2 -2
- data/lib/abachrome/illuminants/d65.rb +2 -2
- data/lib/abachrome/illuminants/d75.rb +2 -2
- data/lib/abachrome/named/css.rb +149 -149
- data/lib/abachrome/named/tailwind.rb +265 -265
- data/lib/abachrome/outputs/css.rb +2 -2
- data/lib/abachrome/palette.rb +26 -25
- data/lib/abachrome/palette_mixins/interpolate.rb +3 -1
- data/lib/abachrome/palette_mixins/resample.rb +2 -2
- data/lib/abachrome/palette_mixins/stretch_luminance.rb +2 -2
- data/lib/abachrome/parsers/css.rb +86 -71
- data/lib/abachrome/parsers/hex.rb +2 -2
- data/lib/abachrome/parsers/tailwind.rb +8 -8
- data/lib/abachrome/spectral.rb +277 -0
- data/lib/abachrome/to_abcd.rb +4 -4
- data/lib/abachrome/version.rb +2 -2
- data/lib/abachrome.rb +66 -10
- 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
|
data/lib/abachrome/to_abcd.rb
CHANGED
|
@@ -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.
|
data/lib/abachrome/version.rb
CHANGED
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.
|
|
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-
|
|
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.
|
|
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: []
|