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,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::LrgbToOklab - Linear RGB to OKLAB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the linear RGB (LRGB) color space to the OKLAB color space
|
|
6
|
+
# using the standard OKLAB transformation matrices. The conversion process applies a series of
|
|
7
|
+
# matrix transformations and non-linear operations to accurately map linear RGB coordinates to
|
|
8
|
+
# the perceptually uniform OKLAB color space.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Implements the official OKLAB transformation algorithm with high-precision matrices
|
|
12
|
+
# - Converts linear RGB values through intermediate LMS color space representation
|
|
13
|
+
# - Applies cube root transformation for perceptual uniformity in the OKLAB space
|
|
14
|
+
# - Maintains alpha channel transparency values during conversion
|
|
15
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
16
|
+
# - Validates input color space to ensure proper linear RGB source data
|
|
17
|
+
#
|
|
18
|
+
# The OKLAB color space provides better perceptual uniformity compared to traditional RGB spaces,
|
|
19
|
+
# making it ideal for color manipulation operations like blending, lightness adjustments, and
|
|
20
|
+
# gamut mapping where human visual perception accuracy is important.
|
|
21
|
+
|
|
22
|
+
module Abachrome
|
|
23
|
+
module Converters
|
|
24
|
+
class LrgbToOklab < Abachrome::Converters::Base
|
|
25
|
+
# Converts a color from linear RGB (LRGB) color space to OKLAB color space.
|
|
26
|
+
#
|
|
27
|
+
# This conversion applies a matrix transformation to the linear RGB values,
|
|
28
|
+
# followed by a non-linear transformation, then another matrix transformation
|
|
29
|
+
# to produce OKLAB coordinates.
|
|
30
|
+
#
|
|
31
|
+
# @param rgb_color [Abachrome::Color] A color in linear RGB (LRGB) color space
|
|
32
|
+
# @raise [ArgumentError] If the provided color is not in LRGB color space
|
|
33
|
+
# @return [Abachrome::Color] The converted color in OKLAB color space with the same alpha value as the input
|
|
34
|
+
def self.convert(rgb_color)
|
|
35
|
+
raise_unless rgb_color, :lrgb
|
|
36
|
+
|
|
37
|
+
r, g, b = rgb_color.coordinates.map { |_| _.to_f }
|
|
38
|
+
|
|
39
|
+
l = (AD("0.41222147079999993") * r) + (AD("0.5363325363") * g) + (AD("0.0514459929") * b)
|
|
40
|
+
m = (AD("0.2119034981999999") * r) + (AD("0.680699545099999") * g) + (AD("0.1073969566") * b)
|
|
41
|
+
s = (AD("0.08830246189999998") * r) + (AD("0.2817188376") * g) + (AD("0.6299787005000002") * b)
|
|
42
|
+
|
|
43
|
+
l_ = l.to_f**Rational(1, 3)
|
|
44
|
+
m_ = m.to_f**Rational(1, 3)
|
|
45
|
+
s_ = s.to_f**Rational(1, 3)
|
|
46
|
+
|
|
47
|
+
lightness = (AD("0.2104542553") * l_) + (AD("0.793617785") * m_) - (AD("0.0040720468") * s_)
|
|
48
|
+
a = (AD("1.9779984951") * l_) - (AD("2.4285922050") * m_) + (AD("0.4505937099") * s_)
|
|
49
|
+
b = (AD("0.0259040371") * l_) + (AD("0.7827717662") * m_) - (AD("0.8086757660") * s_)
|
|
50
|
+
|
|
51
|
+
Color.new(ColorSpace.find(:oklab), [lightness, a, b], rgb_color.alpha)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::LrgbToSrgb - Linear RGB to sRGB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the linear RGB (LRGB) color space to the standard RGB (sRGB) color space by applying gamma correction. The conversion process applies the sRGB transfer function which uses different formulas for small and large values to match the non-linear response characteristics of typical display devices.
|
|
6
|
+
#
|
|
7
|
+
# Key features:
|
|
8
|
+
# - Implements the standard sRGB gamma correction algorithm with precise threshold handling
|
|
9
|
+
# - Converts linear RGB values to gamma-corrected sRGB values for proper display representation
|
|
10
|
+
# - Applies different transformation functions based on value magnitude (linear vs power function)
|
|
11
|
+
# - Maintains alpha channel transparency values during conversion
|
|
12
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
13
|
+
# - Validates input color space to ensure proper linear RGB source data
|
|
14
|
+
#
|
|
15
|
+
# The sRGB color space is the standard RGB color space for web content and most consumer displays, providing gamma correction that better matches human visual perception and display device characteristics compared to linear RGB values.
|
|
16
|
+
|
|
17
|
+
module Abachrome
|
|
18
|
+
module Converters
|
|
19
|
+
class LrgbToSrgb < Abachrome::Converters::Base
|
|
20
|
+
# Converts a color from linear RGB to sRGB color space.
|
|
21
|
+
#
|
|
22
|
+
# @param lrgb_color [Abachrome::Color] The color in linear RGB color space to convert
|
|
23
|
+
# @return [Abachrome::Color] A new Color object in sRGB color space with the converted coordinates
|
|
24
|
+
# @raise [TypeError] If the provided color is not in linear RGB color space
|
|
25
|
+
def self.convert(lrgb_color)
|
|
26
|
+
raise_unless lrgb_color, :lrgb
|
|
27
|
+
r, g, b = lrgb_color.coordinates.map { |c| to_srgb(c.to_f) }
|
|
28
|
+
|
|
29
|
+
output_coords = [r, g, b]
|
|
30
|
+
|
|
31
|
+
Color.new(
|
|
32
|
+
ColorSpace.find(:srgb),
|
|
33
|
+
output_coords,
|
|
34
|
+
lrgb_color.alpha
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Converts a linear RGB value to standard RGB color space (sRGB) value.
|
|
39
|
+
#
|
|
40
|
+
# This method implements the standard linearization function used in the sRGB color space.
|
|
41
|
+
# For small values (≤ 0.0031308), a simple linear transformation is applied.
|
|
42
|
+
# For larger values, a power function with gamma correction is used.
|
|
43
|
+
#
|
|
44
|
+
# @param v [AbcDecimal] The linear RGB value to convert
|
|
45
|
+
# @return [AbcDecimal] The corresponding sRGB value, preserving the sign of the input
|
|
46
|
+
def self.to_srgb(v)
|
|
47
|
+
v_abs = v.abs
|
|
48
|
+
v_sign = v.negative? ? -1 : 1
|
|
49
|
+
if v_abs <= AD("0.0031308")
|
|
50
|
+
v * AD("12.92")
|
|
51
|
+
else
|
|
52
|
+
v_sign * ((AD("1.055") * (v_abs**Rational(1.0, 2.4))) - AD("0.055"))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class LrgbToXyz < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from linear RGB color space to XYZ color space.
|
|
7
|
+
#
|
|
8
|
+
# This method implements the linear RGB to XYZ transformation using the standard
|
|
9
|
+
# transformation matrix for the sRGB color space with D65 white point. The XYZ
|
|
10
|
+
# color space is the CIE 1931 color space that forms the basis for most other
|
|
11
|
+
# color space definitions and serves as a device-independent reference.
|
|
12
|
+
#
|
|
13
|
+
# @param lrgb_color [Abachrome::Color] The color in linear RGB color space
|
|
14
|
+
# @raise [ArgumentError] If the input color is not in linear RGB color space
|
|
15
|
+
# @return [Abachrome::Color] The resulting color in XYZ color space with
|
|
16
|
+
# the same alpha as the input color
|
|
17
|
+
def self.convert(lrgb_color)
|
|
18
|
+
raise_unless lrgb_color, :lrgb
|
|
19
|
+
|
|
20
|
+
r, g, b = lrgb_color.coordinates.map { |_| _.to_f }
|
|
21
|
+
|
|
22
|
+
# Linear RGB to XYZ transformation matrix (sRGB/D65)
|
|
23
|
+
x = (r * AD("0.4124564")) + (g * AD("0.3575761")) + (b * AD("0.1804375"))
|
|
24
|
+
y = (r * AD("0.2126729")) + (g * AD("0.7151522")) + (b * AD("0.0721750"))
|
|
25
|
+
z = (r * AD("0.0193339")) + (g * AD("0.1191920")) + (b * AD("0.9503041"))
|
|
26
|
+
|
|
27
|
+
Color.new(ColorSpace.find(:xyz), [x, y, z], lrgb_color.alpha)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class OklabToLms < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from OKLAB color space to LMS color space.
|
|
7
|
+
#
|
|
8
|
+
# This method implements the first part of the OKLAB to linear RGB transformation,
|
|
9
|
+
# converting OKLAB coordinates to the intermediate LMS (Long, Medium, Short) color space
|
|
10
|
+
# which represents the response of the three types of cone cells in the human eye.
|
|
11
|
+
#
|
|
12
|
+
# @param oklab_color [Abachrome::Color] The color in OKLAB color space
|
|
13
|
+
# @raise [ArgumentError] If the input color is not in OKLAB color space
|
|
14
|
+
# @return [Abachrome::Color] The resulting color in LMS color space with
|
|
15
|
+
# the same alpha as the input color
|
|
16
|
+
def self.convert(oklab_color)
|
|
17
|
+
raise_unless oklab_color, :oklab
|
|
18
|
+
|
|
19
|
+
l, a, b = oklab_color.coordinates.map { |_| _.to_f }
|
|
20
|
+
|
|
21
|
+
l_ = AbcDecimal(l +
|
|
22
|
+
(AD("0.39633779217376785678") * a) +
|
|
23
|
+
(AD("0.21580375806075880339") * b))
|
|
24
|
+
|
|
25
|
+
m_ = AbcDecimal(l -
|
|
26
|
+
(a * AD("-0.1055613423236563494")) +
|
|
27
|
+
(b * AD("-0.063854174771705903402")))
|
|
28
|
+
|
|
29
|
+
s_ = AbcDecimal(l -
|
|
30
|
+
(a * AD("-0.089484182094965759684")) +
|
|
31
|
+
(b * AD("-1.2914855378640917399")))
|
|
32
|
+
|
|
33
|
+
# Apply cubic operation to convert from L'M'S' to LMS
|
|
34
|
+
l_lms = l_.to_f**3
|
|
35
|
+
m_lms = m_.to_f**3
|
|
36
|
+
s_lms = s_.to_f**3
|
|
37
|
+
|
|
38
|
+
Color.new(ColorSpace.find(:lms), [l_lms, m_lms, s_lms], oklab_color.alpha)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::OklabToLrgb - OKLAB to Linear RGB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the OKLAB color space to the linear RGB (LRGB) color space
|
|
6
|
+
# using the standard OKLAB transformation matrices. The conversion process first transforms
|
|
7
|
+
# OKLAB coordinates to the intermediate LMS (Long, Medium, Short) color space, then applies
|
|
8
|
+
# another matrix transformation to convert LMS coordinates to linear RGB coordinates.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Implements the official OKLAB inverse transformation algorithm with high-precision matrices
|
|
12
|
+
# - Converts OKLAB coordinates through intermediate LMS color space representation
|
|
13
|
+
# - Applies cubic transformation for perceptual uniformity in the OKLAB space
|
|
14
|
+
# - Maintains alpha channel transparency values during conversion
|
|
15
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
16
|
+
# - Validates input color space to ensure proper OKLAB source data
|
|
17
|
+
#
|
|
18
|
+
# The linear RGB color space provides a linear relationship between stored numeric values and
|
|
19
|
+
# actual light intensity, making it essential for accurate color calculations and serving as
|
|
20
|
+
# an intermediate color space for many color transformations, particularly when converting
|
|
21
|
+
# between different color models or preparing colors for display on standard monitors.
|
|
22
|
+
|
|
23
|
+
module Abachrome
|
|
24
|
+
module Converters
|
|
25
|
+
class OklabToLrgb < Abachrome::Converters::Base
|
|
26
|
+
# Converts a color from OKLAB color space to linear RGB (LRGB) color space.
|
|
27
|
+
#
|
|
28
|
+
# This method performs a two-step conversion:
|
|
29
|
+
# 1. OKLAB to LMS (cone response space)
|
|
30
|
+
# 2. LMS to LRGB (linear RGB)
|
|
31
|
+
#
|
|
32
|
+
# @param oklab_color [Abachrome::Color] The color in OKLAB color space
|
|
33
|
+
# @raise [ArgumentError] If the input color is not in OKLAB color space
|
|
34
|
+
# @return [Abachrome::Color] The resulting color in linear RGB color space with
|
|
35
|
+
# the same alpha as the input color
|
|
36
|
+
def self.convert(oklab_color)
|
|
37
|
+
raise_unless oklab_color, :oklab
|
|
38
|
+
|
|
39
|
+
l_ok, a_ok, b_ok = oklab_color.coordinates.map { |_| _.to_f }
|
|
40
|
+
|
|
41
|
+
# Step 1: OKLAB to L'M'S' (cone responses, non-linear)
|
|
42
|
+
# These are the M_lms_prime_from_oklab matrix operations.
|
|
43
|
+
l_prime = l_ok + (AD("0.39633779217376785678".to_f * a_ok) + (AD("0.21580375806075880339") * b_ok))
|
|
44
|
+
m_prime = l_ok - (a_ok * AD("0.1055613423236563494".to_f) - (b_ok * AD("0.063854174771705903402"))) # NOTE: original OklabToLms had + (b * AD("-0.063..."))
|
|
45
|
+
s_prime = l_ok - (a_ok * AD("0.089484182094965759684".to_f) - (b_ok * AD("1.2914855378640917399"))) # NOTE: original OklabToLms had + (b * AD("-1.291..."))
|
|
46
|
+
|
|
47
|
+
# Step 2: L'M'S' to LMS (cubing)
|
|
48
|
+
l_lms = l_prime**3
|
|
49
|
+
m_lms = m_prime**3
|
|
50
|
+
s_lms = s_prime**3
|
|
51
|
+
|
|
52
|
+
# Step 3: LMS to LRGB
|
|
53
|
+
# Using matrix M_lrgb_from_lms (OKLAB specific)
|
|
54
|
+
r_lrgb = (l_lms * AD("4.07674166134799")) + (m_lms * AD("-3.307711590408193")) + (s_lms * AD("0.230969928729428"))
|
|
55
|
+
g_lrgb = (l_lms * AD("-1.2684380040921763")) + (m_lms * AD("2.6097574006633715")) + (s_lms * AD("-0.3413193963102197"))
|
|
56
|
+
b_lrgb = (l_lms * AD("-0.004196086541837188")) + (m_lms * AD("-0.7034186144594493")) + (s_lms * AD("1.7076147009309444"))
|
|
57
|
+
|
|
58
|
+
# Clamp LRGB values to be non-negative (as done in LmsToLrgb.rb)
|
|
59
|
+
# It's also common to clamp to [0, 1] range after conversion from a wider gamut space
|
|
60
|
+
# For LRGB, often just ensuring non-negative is done, and further clamping happens
|
|
61
|
+
# when converting to sRGB or other display spaces.
|
|
62
|
+
# Here, we'll ensure non-negative as per LmsToLrgb.
|
|
63
|
+
output_coords = [r_lrgb, g_lrgb, b_lrgb].map { |it| [it, AD(0)].max }
|
|
64
|
+
|
|
65
|
+
Color.new(ColorSpace.find(:lrgb), output_coords, oklab_color.alpha)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::OklabToOklch - OKLAB to OKLCH color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the OKLAB color space to the OKLCH color space
|
|
6
|
+
# using cylindrical coordinate conversion. The transformation converts the rectangular
|
|
7
|
+
# coordinates (L, a, b) to cylindrical coordinates (L, C, h) where lightness remains
|
|
8
|
+
# unchanged, chroma is calculated as the Euclidean distance in the a-b plane, and hue
|
|
9
|
+
# is calculated as the angle in the a-b plane expressed in degrees.
|
|
10
|
+
#
|
|
11
|
+
# Key features:
|
|
12
|
+
# - Converts OKLAB rectangular coordinates to OKLCH cylindrical coordinates
|
|
13
|
+
# - Preserves lightness component unchanged during conversion
|
|
14
|
+
# - Calculates chroma as sqrt(a² + b²) for colorfulness representation
|
|
15
|
+
# - Computes hue angle using atan2 function and normalizes to 0-360 degree range
|
|
16
|
+
# - Maintains alpha channel transparency values during conversion
|
|
17
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
18
|
+
# - Validates input color space to ensure proper OKLAB source data
|
|
19
|
+
#
|
|
20
|
+
# The OKLCH color space provides an intuitive interface for color manipulation through
|
|
21
|
+
# its cylindrical coordinate system, making it ideal for hue adjustments, saturation
|
|
22
|
+
# modifications, and other color operations that benefit from polar coordinates.
|
|
23
|
+
|
|
24
|
+
module Abachrome
|
|
25
|
+
module Converters
|
|
26
|
+
class OklabToOklch < Abachrome::Converters::Base
|
|
27
|
+
# Converts a color from OKLAB color space to OKLCH color space.
|
|
28
|
+
# The method performs a mathematical transformation from the rectangular
|
|
29
|
+
# coordinates (L, a, b) to cylindrical coordinates (L, C, h), where:
|
|
30
|
+
# - L (lightness) remains the same
|
|
31
|
+
# - C (chroma) is calculated as the Euclidean distance from the origin in the a-b plane
|
|
32
|
+
# - h (hue) is calculated as the angle in the a-b plane
|
|
33
|
+
#
|
|
34
|
+
# @param oklab_color [Abachrome::Color] A color in the OKLAB color space
|
|
35
|
+
# @raise [ArgumentError] If the provided color is not in OKLAB color space
|
|
36
|
+
# @return [Abachrome::Color] The equivalent color in OKLCH color space with the same alpha value
|
|
37
|
+
def self.convert(oklab_color)
|
|
38
|
+
raise_unless oklab_color, :oklab
|
|
39
|
+
|
|
40
|
+
l, a, b = oklab_color.coordinates.map { |_| _.to_f }
|
|
41
|
+
|
|
42
|
+
c = ((a * a) + (b * b)).sqrt
|
|
43
|
+
h = (AbcDecimal.atan2(b, a) * 180) / Math::PI
|
|
44
|
+
h += 360 if h.negative?
|
|
45
|
+
|
|
46
|
+
Color.new(
|
|
47
|
+
ColorSpace.find(:oklch),
|
|
48
|
+
[l, c, h],
|
|
49
|
+
oklab_color.alpha
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::OklabToSrgb - OKLAB to sRGB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the OKLAB color space to the standard RGB (sRGB) color space
|
|
6
|
+
# through a two-step conversion process. The transformation first converts OKLAB coordinates to
|
|
7
|
+
# linear RGB as an intermediate step, then applies gamma correction to produce the final sRGB
|
|
8
|
+
# values suitable for display on standard monitors and web applications.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Two-stage conversion pipeline: OKLAB → Linear RGB → sRGB
|
|
12
|
+
# - Leverages existing OklabToLrgb and LrgbToSrgb converters for modular transformation
|
|
13
|
+
# - Maintains alpha channel transparency values during conversion
|
|
14
|
+
# - Applies proper gamma correction for display-ready color values
|
|
15
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
16
|
+
# - Validates input color space to ensure proper OKLAB source data
|
|
17
|
+
#
|
|
18
|
+
# The sRGB color space is the standard RGB color space for web content and most consumer
|
|
19
|
+
# displays, providing gamma-corrected values that properly represent colors on typical
|
|
20
|
+
# display devices while maintaining compatibility with web standards and digital media formats.
|
|
21
|
+
|
|
22
|
+
module Abachrome
|
|
23
|
+
module Converters
|
|
24
|
+
class OklabToSrgb < Abachrome::Converters::Base
|
|
25
|
+
# Converts a color from the Oklab color space to the sRGB color space.
|
|
26
|
+
# This conversion is performed in two steps:
|
|
27
|
+
# 1. First converts from Oklab to linear RGB
|
|
28
|
+
# 2. Then converts from linear RGB to sRGB
|
|
29
|
+
#
|
|
30
|
+
# @param oklab_color [Color] A color in the Oklab color space
|
|
31
|
+
# @raise [ArgumentError] If the provided color is not in the Oklab color space
|
|
32
|
+
# @return [Color] The converted color in the sRGB color space
|
|
33
|
+
def self.convert(oklab_color)
|
|
34
|
+
raise_unless oklab_color, :oklab
|
|
35
|
+
|
|
36
|
+
# First convert Oklab to linear RGB
|
|
37
|
+
lrgb_color = OklabToLrgb.convert(oklab_color)
|
|
38
|
+
|
|
39
|
+
# Then use the LrgbToSrgb converter to go from linear RGB to sRGB
|
|
40
|
+
LrgbToSrgb.convert(lrgb_color)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::OklchToLrgb - OKLCH to Linear RGB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the OKLCH color space to the linear RGB (LRGB) color space.
|
|
6
|
+
# The conversion is performed by first transforming OKLCH's cylindrical coordinates (Lightness, Chroma, Hue)
|
|
7
|
+
# into OKLAB's rectangular coordinates (L, a, b).
|
|
8
|
+
# Then, these OKLAB coordinates are converted to LRGB. This second part involves transforming
|
|
9
|
+
# OKLAB to an intermediate non-linear cone response space (L'M'S'), then to a linear
|
|
10
|
+
# cone response space (LMS), and finally from LMS to LRGB using appropriate matrices.
|
|
11
|
+
# All these steps are combined into a single direct conversion method.
|
|
12
|
+
#
|
|
13
|
+
# Key features:
|
|
14
|
+
# - Direct conversion from OKLCH to LRGB.
|
|
15
|
+
# - Combines cylindrical to rectangular conversion (OKLCH to OKLAB)
|
|
16
|
+
# with the OKLAB to LRGB transformation pipeline (OKLAB -> L'M'S' -> LMS -> LRGB).
|
|
17
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations.
|
|
18
|
+
# - Maintains alpha channel transparency values during conversion.
|
|
19
|
+
# - Validates input color space to ensure proper OKLCH source data.
|
|
20
|
+
|
|
21
|
+
module Abachrome
|
|
22
|
+
module Converters
|
|
23
|
+
class OklchToLrgb < Abachrome::Converters::Base
|
|
24
|
+
def self.convert(oklch_color)
|
|
25
|
+
raise_unless oklch_color, :oklch
|
|
26
|
+
|
|
27
|
+
l_oklch, c_oklch, h_oklch = oklch_color.coordinates.map { |_| _.to_f }
|
|
28
|
+
alpha = oklch_color.alpha
|
|
29
|
+
|
|
30
|
+
# Step 1: OKLCH to OKLAB
|
|
31
|
+
# l_oklab is the same as l_oklch
|
|
32
|
+
l_oklab = l_oklch
|
|
33
|
+
|
|
34
|
+
# Convert hue from degrees to radians
|
|
35
|
+
# h_oklch is AbcDecimal, Math::PI is Float. AD(Math::PI) makes it AbcDecimal.
|
|
36
|
+
# Division by AD("180") ensures AbcDecimal arithmetic.
|
|
37
|
+
h_rad = (h_oklch * AD(Math::PI)) / AD("180")
|
|
38
|
+
|
|
39
|
+
# Calculate a_oklab and b_oklab
|
|
40
|
+
# Math.cos/sin take a float; .value of AbcDecimal is BigDecimal.
|
|
41
|
+
# AD(Math.cos/sin(big_decimal_value)) wraps the result back to AbcDecimal.
|
|
42
|
+
a_oklab = c_oklch * AD(Math.cos(h_rad.value))
|
|
43
|
+
b_oklab = c_oklch * AD(Math.sin(h_rad.value))
|
|
44
|
+
|
|
45
|
+
# Step 2: OKLAB to L'M'S' (cone responses, non-linear)
|
|
46
|
+
# Constants from the inverse of M2 matrix (OKLAB to L'M'S')
|
|
47
|
+
# l_oklab, a_oklab, b_oklab are already AbcDecimal.
|
|
48
|
+
l_prime = l_oklab + (AD("0.39633779217376785678") * a_oklab) + (AD("0.21580375806075880339") * b_oklab)
|
|
49
|
+
m_prime = l_oklab - (AD("0.1055613423236563494") * a_oklab) - (AD("0.063854174771705903402") * b_oklab)
|
|
50
|
+
s_prime = l_oklab - (AD("0.089484182094965759684") * a_oklab) - (AD("1.2914855378640917399") * b_oklab)
|
|
51
|
+
|
|
52
|
+
# Step 3: L'M'S' to LMS (cubing to linearize cone responses)
|
|
53
|
+
l_lms = l_prime**3
|
|
54
|
+
m_lms = m_prime**3
|
|
55
|
+
s_lms = s_prime**3
|
|
56
|
+
|
|
57
|
+
# Step 4: LMS to LRGB
|
|
58
|
+
# Using matrix M_lrgb_from_lms (OKLAB specific)
|
|
59
|
+
r_lrgb = (l_lms * AD("4.07674166134799")) + (m_lms * AD("-3.307711590408193")) + (s_lms * AD("0.230969928729428"))
|
|
60
|
+
g_lrgb = (l_lms * AD("-1.2684380040921763")) + (m_lms * AD("2.6097574006633715")) + (s_lms * AD("-0.3413193963102197"))
|
|
61
|
+
b_lrgb = (l_lms * AD("-0.004196086541837188")) + (m_lms * AD("-0.7034186144594493")) + (s_lms * AD("1.7076147009309444"))
|
|
62
|
+
|
|
63
|
+
# Clamp LRGB values to be non-negative.
|
|
64
|
+
# LRGB values can be outside [0,1] but should be >= 0.
|
|
65
|
+
# Further clamping to [0,1] typically occurs when converting to display-referred spaces like sRGB.
|
|
66
|
+
zero_ad = AD("0")
|
|
67
|
+
output_coords = [
|
|
68
|
+
[r_lrgb, zero_ad].max,
|
|
69
|
+
[g_lrgb, zero_ad].max,
|
|
70
|
+
[b_lrgb, zero_ad].max
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
Color.new(ColorSpace.find(:lrgb), output_coords, alpha)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::OklchToOklab - OKLCH to OKLAB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the OKLCH color space to the OKLAB color space
|
|
6
|
+
# using cylindrical to rectangular coordinate conversion. The transformation converts the
|
|
7
|
+
# cylindrical coordinates (L, C, h) to rectangular coordinates (L, a, b) where lightness
|
|
8
|
+
# remains unchanged, and the a and b components are calculated from chroma and hue using
|
|
9
|
+
# trigonometric functions (cosine and sine respectively).
|
|
10
|
+
#
|
|
11
|
+
# Key features:
|
|
12
|
+
# - Converts OKLCH cylindrical coordinates to OKLAB rectangular coordinates
|
|
13
|
+
# - Preserves lightness component unchanged during conversion
|
|
14
|
+
# - Calculates a component as chroma × cos(hue) for green-red axis positioning
|
|
15
|
+
# - Calculates b component as chroma × sin(hue) for blue-yellow axis positioning
|
|
16
|
+
# - Converts hue angle from degrees to radians for trigonometric calculations
|
|
17
|
+
# - Maintains alpha channel transparency values during conversion
|
|
18
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
19
|
+
# - Validates input color space to ensure proper OKLCH source data
|
|
20
|
+
#
|
|
21
|
+
# The OKLAB color space provides the foundation for further conversions to other color
|
|
22
|
+
# spaces and serves as an intermediate step in the color transformation pipeline when
|
|
23
|
+
# working with OKLCH color manipulations that need to be converted to display-ready formats.
|
|
24
|
+
|
|
25
|
+
module Abachrome
|
|
26
|
+
module Converters
|
|
27
|
+
class OklchToOklab < Abachrome::Converters::Base
|
|
28
|
+
# Converts a color from OKLCH color space to OKLAB color space.
|
|
29
|
+
#
|
|
30
|
+
# @param oklch_color [Abachrome::Color] The color in OKLCH format to convert
|
|
31
|
+
# @return [Abachrome::Color] The converted color in OKLAB format
|
|
32
|
+
# @raise [StandardError] If the provided color is not in OKLCH color space
|
|
33
|
+
def self.convert(oklch_color)
|
|
34
|
+
raise_unless oklch_color, :oklch
|
|
35
|
+
|
|
36
|
+
l, c, h = oklch_color.coordinates.map { |_| _.to_f }
|
|
37
|
+
|
|
38
|
+
h_rad = (h * Math::PI) / AD(180)
|
|
39
|
+
a = c * AD(Math.cos(h_rad.value))
|
|
40
|
+
b = c * AD(Math.sin(h_rad.value))
|
|
41
|
+
|
|
42
|
+
Color.new(
|
|
43
|
+
ColorSpace.find(:oklab),
|
|
44
|
+
[l, a, b],
|
|
45
|
+
oklch_color.alpha
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Converters::OklchToSrgb - OKLCH to sRGB color space converter
|
|
4
|
+
#
|
|
5
|
+
# This converter transforms colors from the OKLCH color space to the standard RGB (sRGB) color space
|
|
6
|
+
# through a two-step conversion process. The transformation first converts OKLCH cylindrical
|
|
7
|
+
# coordinates to OKLAB rectangular coordinates, then applies the standard OKLAB to sRGB
|
|
8
|
+
# transformation pipeline to produce the final gamma-corrected sRGB values.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Two-stage conversion pipeline: OKLCH → OKLAB → sRGB
|
|
12
|
+
# - Leverages existing OklchToOklab and OklabToSrgb converters for modular transformation
|
|
13
|
+
# - Converts cylindrical coordinates (lightness, chroma, hue) to display-ready RGB values
|
|
14
|
+
# - Maintains alpha channel transparency values during conversion
|
|
15
|
+
# - Applies proper gamma correction for display on standard monitors and web applications
|
|
16
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations
|
|
17
|
+
# - Validates input color space to ensure proper OKLCH source data
|
|
18
|
+
#
|
|
19
|
+
# The sRGB color space is the standard RGB color space for web content and most consumer
|
|
20
|
+
# displays, providing gamma-corrected values that properly represent colors on typical
|
|
21
|
+
# display devices while maintaining compatibility with web standards and digital media formats.
|
|
22
|
+
|
|
23
|
+
require_relative "oklch_to_oklab"
|
|
24
|
+
require_relative "oklab_to_srgb"
|
|
25
|
+
|
|
26
|
+
module Abachrome
|
|
27
|
+
module Converters
|
|
28
|
+
class OklchToSrgb < Abachrome::Converters::Base
|
|
29
|
+
# Converts a color from OKLCH color space to sRGB color space.
|
|
30
|
+
# This is done by first converting from OKLCH to OKLAB,
|
|
31
|
+
# then from OKLAB to sRGB.
|
|
32
|
+
#
|
|
33
|
+
# @param oklch_color [Abachrome::Color] Color in OKLCH color space
|
|
34
|
+
# @return [Abachrome::Color] The converted color in sRGB color space
|
|
35
|
+
def self.convert(oklch_color)
|
|
36
|
+
# Convert OKLCh to OKLab first
|
|
37
|
+
oklab_color = OklchToOklab.convert(oklch_color)
|
|
38
|
+
|
|
39
|
+
# Then convert OKLab to sRGB
|
|
40
|
+
OklabToSrgb.convert(oklab_color)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class OklchToXyz < Abachrome::Converters::Base
|
|
6
|
+
def self.convert(oklch_color)
|
|
7
|
+
raise_unless oklch_color, :oklch
|
|
8
|
+
|
|
9
|
+
l_oklch, c_oklch, h_oklch = oklch_color.coordinates.map { |coord| coord.to_f }
|
|
10
|
+
alpha = oklch_color.alpha
|
|
11
|
+
|
|
12
|
+
# Step 1: OKLCH to OKLAB
|
|
13
|
+
# (l_oklab, a_oklab, b_oklab)
|
|
14
|
+
l_oklab = l_oklch
|
|
15
|
+
# h_rad = (h_oklch * Math::PI) / AD(180) # h_oklch is AbcDecimal, Math::PI is Float. Coercion happens.
|
|
16
|
+
# More explicit for Math::PI:
|
|
17
|
+
h_rad = (h_oklch * AD(Math::PI.to_s)) / AD(180)
|
|
18
|
+
|
|
19
|
+
# Standard Math.cos/sin expect float. h_rad is AbcDecimal.
|
|
20
|
+
# .to_f is needed for conversion from AbcDecimal/BigDecimal to Float.
|
|
21
|
+
cos_h_rad = AD(Math.cos(h_rad.to_f))
|
|
22
|
+
sin_h_rad = AD(Math.sin(h_rad.to_f))
|
|
23
|
+
|
|
24
|
+
a_oklab = c_oklch * cos_h_rad
|
|
25
|
+
b_oklab = c_oklch * sin_h_rad
|
|
26
|
+
|
|
27
|
+
# Step 2: OKLAB to L'M'S' (cone responses, non-linear)
|
|
28
|
+
# (l_prime, m_prime, s_prime)
|
|
29
|
+
# These are the M_lms_prime_from_oklab matrix operations.
|
|
30
|
+
# The .to_f wrapper on the whole sum (as in OklabToLms.rb) is not strictly necessary
|
|
31
|
+
# if l_oklab, a_oklab, b_oklab are already AbcDecimal, as AbcDecimal ops return AbcDecimal.
|
|
32
|
+
l_prime = l_oklab + (AD("0.39633779217376785678") * a_oklab) + (AD("0.21580375806075880339") * b_oklab)
|
|
33
|
+
m_prime = l_oklab - (a_oklab * AD("-0.1055613423236563494")) + (b_oklab * AD("-0.063854174771705903402"))
|
|
34
|
+
s_prime = l_oklab - (a_oklab * AD("-0.089484182094965759684")) + (b_oklab * AD("-1.2914855378640917399"))
|
|
35
|
+
|
|
36
|
+
# Step 3: L'M'S' to LMS (cubing)
|
|
37
|
+
# (l_lms, m_lms, s_lms)
|
|
38
|
+
l_lms = l_prime**3
|
|
39
|
+
m_lms = m_prime**3
|
|
40
|
+
s_lms = s_prime**3
|
|
41
|
+
|
|
42
|
+
# Step 4: LMS to LRGB
|
|
43
|
+
# (r_lrgb, g_lrgb, b_lrgb)
|
|
44
|
+
# Using matrix M_lrgb_from_lms (OKLAB specific)
|
|
45
|
+
r_lrgb = (l_lms * AD("4.07674166134799")) + (m_lms * AD("-3.307711590408193")) + (s_lms * AD("0.230969928729428"))
|
|
46
|
+
g_lrgb = (l_lms * AD("-1.2684380040921763")) + (m_lms * AD("2.6097574006633715")) + (s_lms * AD("-0.3413193963102197"))
|
|
47
|
+
b_lrgb = (l_lms * AD("-0.004196086541837188")) + (m_lms * AD("-0.7034186144594493")) + (s_lms * AD("1.7076147009309444"))
|
|
48
|
+
|
|
49
|
+
# Clamp LRGB values to be non-negative (as done in LmsToLrgb.rb)
|
|
50
|
+
# Using the pattern [AbcDecimal, Integer].max which relies on AbcDecimal's <=> coercion.
|
|
51
|
+
# AD(0) is AbcDecimal zero.
|
|
52
|
+
zero_ad = AD(0)
|
|
53
|
+
r_lrgb_clamped = [r_lrgb, zero_ad].max
|
|
54
|
+
g_lrgb_clamped = [g_lrgb, zero_ad].max
|
|
55
|
+
b_lrgb_clamped = [b_lrgb, zero_ad].max
|
|
56
|
+
|
|
57
|
+
# Step 5: LRGB to XYZ
|
|
58
|
+
# (x_xyz, y_xyz, z_xyz)
|
|
59
|
+
# Using matrix M_xyz_from_lrgb (sRGB D65)
|
|
60
|
+
x_xyz = (r_lrgb_clamped * AD("0.4124564")) + (g_lrgb_clamped * AD("0.3575761")) + (b_lrgb_clamped * AD("0.1804375"))
|
|
61
|
+
y_xyz = (r_lrgb_clamped * AD("0.2126729")) + (g_lrgb_clamped * AD("0.7151522")) + (b_lrgb_clamped * AD("0.0721750"))
|
|
62
|
+
z_xyz = (r_lrgb_clamped * AD("0.0193339")) + (g_lrgb_clamped * AD("0.1191920")) + (b_lrgb_clamped * AD("0.9503041"))
|
|
63
|
+
|
|
64
|
+
Color.new(ColorSpace.find(:xyz), [x_xyz, y_xyz, z_xyz], alpha)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|