abachrome 0.1.6 → 0.2.0
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/lib/abachrome/abc_decimal.rb +4 -0
- data/lib/abachrome/color.rb +24 -0
- data/lib/abachrome/color_mixins/harmonies.rb +187 -0
- data/lib/abachrome/color_mixins/to_lms.rb +106 -0
- data/lib/abachrome/color_mixins/to_xyz.rb +106 -0
- data/lib/abachrome/color_mixins/wcag.rb +126 -0
- data/lib/abachrome/color_models/cmyk.rb +2 -1
- data/lib/abachrome/color_models/xyz.rb +8 -0
- data/lib/abachrome/converters/lms_to_oklab.rb +28 -0
- data/lib/abachrome/converters/lrgb_to_lms.rb +26 -0
- data/lib/abachrome/converters/oklab_to_xyz.rb +29 -0
- data/lib/abachrome/converters/oklch_to_lms.rb +28 -0
- data/lib/abachrome/converters/srgb_to_lms.rb +29 -0
- data/lib/abachrome/converters/srgb_to_xyz.rb +30 -0
- data/lib/abachrome/converters/xyz_to_lrgb.rb +33 -0
- data/lib/abachrome/converters/xyz_to_srgb.rb +30 -0
- data/lib/abachrome/floatify.rb +282 -0
- data/lib/abachrome/version.rb +1 -1
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: adfda90185a392dcd8859acac1f53b7ac4d9ace7a083b1b643baeeea49de76b8
|
|
4
|
+
data.tar.gz: b7b66f5e35f5f7f16b1704fced9de7cb85b2088b98afaf6c9aa0f31ad8e4fdb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: adaebc43ce61184e91852927c83f371234fdde03aeff491570b35146d0f899bb7919336c0f5908c23d80b8233c1a9327a7d8fe5a54da83eac1b32a3d04c7d26e
|
|
7
|
+
data.tar.gz: 4088bd45b14da9b2d7c475a07cd9ee5797df50abe03701f48f64f7c684edaac8b24736a53b0d49aac5fca88d4e45b21535c86fbfd927e7d2095d11b515e57ed8
|
|
@@ -320,6 +320,10 @@ module Abachrome
|
|
|
320
320
|
@value.negative?
|
|
321
321
|
end
|
|
322
322
|
|
|
323
|
+
def positive?
|
|
324
|
+
@value > 0
|
|
325
|
+
end
|
|
326
|
+
|
|
323
327
|
# Calculates the arctangent of y/x using the signs of the arguments to determine the quadrant.
|
|
324
328
|
# Unlike the standard Math.atan2, this method accepts AbcDecimal objects or any values
|
|
325
329
|
# that can be converted to AbcDecimal.
|
data/lib/abachrome/color.rb
CHANGED
|
@@ -125,6 +125,30 @@ module Abachrome
|
|
|
125
125
|
new(space, [c, m, y, k], alpha)
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
# Creates a new Color instance from XYZ values
|
|
129
|
+
#
|
|
130
|
+
# @param x [Numeric] The X tristimulus value representing the CIE RGB red primary
|
|
131
|
+
# @param y [Numeric] The Y tristimulus value representing luminance
|
|
132
|
+
# @param z [Numeric] The Z tristimulus value representing the CIE RGB blue primary
|
|
133
|
+
# @param alpha [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
|
134
|
+
# @return [Abachrome::Color] A new Color instance in the XYZ color space
|
|
135
|
+
def self.from_xyz(x, y, z, alpha = 1.0)
|
|
136
|
+
space = ColorSpace.find(:xyz)
|
|
137
|
+
new(space, [x, y, z], alpha)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Creates a new Color instance from LMS values
|
|
141
|
+
#
|
|
142
|
+
# @param l [Numeric] The long wavelength (L) cone response component
|
|
143
|
+
# @param m [Numeric] The medium wavelength (M) cone response component
|
|
144
|
+
# @param s [Numeric] The short wavelength (S) cone response component
|
|
145
|
+
# @param alpha [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
|
146
|
+
# @return [Abachrome::Color] A new Color instance in the LMS color space
|
|
147
|
+
def self.from_lms(l, m, s, alpha = 1.0)
|
|
148
|
+
space = ColorSpace.find(:lms)
|
|
149
|
+
new(space, [l, m, s], alpha)
|
|
150
|
+
end
|
|
151
|
+
|
|
128
152
|
# Compares this color instance with another for equality.
|
|
129
153
|
#
|
|
130
154
|
# Two colors are considered equal if they have the same color space,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::ColorMixins::Harmonies - Color Harmony Generation Module
|
|
4
|
+
#
|
|
5
|
+
# This module provides methods for generating color harmonies based on color theory.
|
|
6
|
+
# Color harmonies are sets of colors that work well together according to established
|
|
7
|
+
# design principles. The module uses OKLCH color space for hue manipulation, which
|
|
8
|
+
# provides perceptually uniform results.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Analogous harmony: Colors adjacent on the color wheel (±30°)
|
|
12
|
+
# - Complementary harmony: Colors opposite on the color wheel (180°)
|
|
13
|
+
# - Triadic harmony: Three colors evenly spaced around the color wheel (120° intervals)
|
|
14
|
+
# - Tetradic/Square harmony: Four colors evenly spaced (90° intervals)
|
|
15
|
+
# - Split-complementary harmony: Base color plus two colors adjacent to its complement (150°, 210°)
|
|
16
|
+
# - Option to generate harmonies in different color spaces (HSL or OKLCH)
|
|
17
|
+
#
|
|
18
|
+
# All harmonies preserve the lightness and chroma of the base color while rotating
|
|
19
|
+
# the hue to create harmonious color combinations.
|
|
20
|
+
#
|
|
21
|
+
# References:
|
|
22
|
+
# - Color Theory: https://en.wikipedia.org/wiki/Color_theory
|
|
23
|
+
# - Color Harmony: https://en.wikipedia.org/wiki/Harmony_(color)
|
|
24
|
+
|
|
25
|
+
module Abachrome
|
|
26
|
+
module ColorMixins
|
|
27
|
+
module Harmonies
|
|
28
|
+
# Generates an analogous color harmony.
|
|
29
|
+
# Analogous colors are adjacent to each other on the color wheel,
|
|
30
|
+
# typically within ±30 degrees. This creates a harmonious, cohesive palette.
|
|
31
|
+
#
|
|
32
|
+
# @param angle [Numeric] The hue angle offset in degrees (default: 30)
|
|
33
|
+
# @param space [Symbol] The color space to use for hue manipulation (:hsl or :oklch, default: :oklch)
|
|
34
|
+
# @return [Array<Abachrome::Color>] An array of three colors: [color at -angle, original, color at +angle]
|
|
35
|
+
def analogous(angle: 30, space: :oklch)
|
|
36
|
+
original_space = color_space
|
|
37
|
+
|
|
38
|
+
# Convert to the specified space for hue manipulation
|
|
39
|
+
color_in_space = space == color_space.id ? self : to_color_space(space)
|
|
40
|
+
l, c, h = color_in_space.coordinates
|
|
41
|
+
|
|
42
|
+
# Generate analogous colors by rotating hue
|
|
43
|
+
color_minus = create_harmony_color(l, c, h - angle, space)
|
|
44
|
+
color_plus = create_harmony_color(l, c, h + angle, space)
|
|
45
|
+
|
|
46
|
+
# Convert back to original color space
|
|
47
|
+
[
|
|
48
|
+
color_minus.to_color_space(original_space.id),
|
|
49
|
+
self,
|
|
50
|
+
color_plus.to_color_space(original_space.id)
|
|
51
|
+
]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Generates a complementary color harmony.
|
|
55
|
+
# Complementary colors are opposite each other on the color wheel (180° apart).
|
|
56
|
+
# They create maximum contrast and vibrant combinations.
|
|
57
|
+
#
|
|
58
|
+
# @param space [Symbol] The color space to use for hue manipulation (:hsl or :oklch, default: :oklch)
|
|
59
|
+
# @return [Array<Abachrome::Color>] An array of two colors: [original, complement]
|
|
60
|
+
def complementary(space: :oklch)
|
|
61
|
+
original_space = color_space
|
|
62
|
+
|
|
63
|
+
# Convert to the specified space for hue manipulation
|
|
64
|
+
color_in_space = space == color_space.id ? self : to_color_space(space)
|
|
65
|
+
l, c, h = color_in_space.coordinates
|
|
66
|
+
|
|
67
|
+
# Generate complementary color by rotating hue 180°
|
|
68
|
+
complement = create_harmony_color(l, c, h + 180, space)
|
|
69
|
+
|
|
70
|
+
# Convert back to original color space
|
|
71
|
+
[
|
|
72
|
+
self,
|
|
73
|
+
complement.to_color_space(original_space.id)
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Generates a triadic color harmony.
|
|
78
|
+
# Triadic colors are evenly spaced around the color wheel at 120° intervals.
|
|
79
|
+
# They create vibrant, balanced palettes with good contrast.
|
|
80
|
+
#
|
|
81
|
+
# @param space [Symbol] The color space to use for hue manipulation (:hsl or :oklch, default: :oklch)
|
|
82
|
+
# @return [Array<Abachrome::Color>] An array of three colors evenly spaced around the color wheel
|
|
83
|
+
def triadic(space: :oklch)
|
|
84
|
+
original_space = color_space
|
|
85
|
+
|
|
86
|
+
# Convert to the specified space for hue manipulation
|
|
87
|
+
color_in_space = space == color_space.id ? self : to_color_space(space)
|
|
88
|
+
l, c, h = color_in_space.coordinates
|
|
89
|
+
|
|
90
|
+
# Generate triadic colors by rotating hue 120° and 240°
|
|
91
|
+
color_1 = create_harmony_color(l, c, h + 120, space)
|
|
92
|
+
color_2 = create_harmony_color(l, c, h + 240, space)
|
|
93
|
+
|
|
94
|
+
# Convert back to original color space
|
|
95
|
+
[
|
|
96
|
+
self,
|
|
97
|
+
color_1.to_color_space(original_space.id),
|
|
98
|
+
color_2.to_color_space(original_space.id)
|
|
99
|
+
]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Generates a tetradic (square) color harmony.
|
|
103
|
+
# Tetradic colors are evenly spaced around the color wheel at 90° intervals.
|
|
104
|
+
# They create rich, varied palettes with multiple complementary pairs.
|
|
105
|
+
#
|
|
106
|
+
# @param space [Symbol] The color space to use for hue manipulation (:hsl or :oklch, default: :oklch)
|
|
107
|
+
# @return [Array<Abachrome::Color>] An array of four colors evenly spaced around the color wheel
|
|
108
|
+
def tetradic(space: :oklch)
|
|
109
|
+
original_space = color_space
|
|
110
|
+
|
|
111
|
+
# Convert to the specified space for hue manipulation
|
|
112
|
+
color_in_space = space == color_space.id ? self : to_color_space(space)
|
|
113
|
+
l, c, h = color_in_space.coordinates
|
|
114
|
+
|
|
115
|
+
# Generate tetradic colors by rotating hue 90°, 180°, and 270°
|
|
116
|
+
color_1 = create_harmony_color(l, c, h + 90, space)
|
|
117
|
+
color_2 = create_harmony_color(l, c, h + 180, space)
|
|
118
|
+
color_3 = create_harmony_color(l, c, h + 270, space)
|
|
119
|
+
|
|
120
|
+
# Convert back to original color space
|
|
121
|
+
[
|
|
122
|
+
self,
|
|
123
|
+
color_1.to_color_space(original_space.id),
|
|
124
|
+
color_2.to_color_space(original_space.id),
|
|
125
|
+
color_3.to_color_space(original_space.id)
|
|
126
|
+
]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Generates a split-complementary color harmony.
|
|
130
|
+
# Split-complementary uses the base color plus two colors adjacent to its complement
|
|
131
|
+
# (at 150° and 210° from the base). This creates strong visual contrast while being
|
|
132
|
+
# more subtle than a pure complementary scheme.
|
|
133
|
+
#
|
|
134
|
+
# @param space [Symbol] The color space to use for hue manipulation (:hsl or :oklch, default: :oklch)
|
|
135
|
+
# @return [Array<Abachrome::Color>] An array of three colors: [original, complement-30°, complement+30°]
|
|
136
|
+
def split_complementary(space: :oklch)
|
|
137
|
+
original_space = color_space
|
|
138
|
+
|
|
139
|
+
# Convert to the specified space for hue manipulation
|
|
140
|
+
color_in_space = space == color_space.id ? self : to_color_space(space)
|
|
141
|
+
l, c, h = color_in_space.coordinates
|
|
142
|
+
|
|
143
|
+
# Generate split-complementary colors at 150° and 210° (complement ± 30°)
|
|
144
|
+
color_1 = create_harmony_color(l, c, h + 150, space)
|
|
145
|
+
color_2 = create_harmony_color(l, c, h + 210, space)
|
|
146
|
+
|
|
147
|
+
# Convert back to original color space
|
|
148
|
+
[
|
|
149
|
+
self,
|
|
150
|
+
color_1.to_color_space(original_space.id),
|
|
151
|
+
color_2.to_color_space(original_space.id)
|
|
152
|
+
]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# Helper method to create a harmony color with normalized hue
|
|
158
|
+
#
|
|
159
|
+
# @param l [AbcDecimal] The lightness value
|
|
160
|
+
# @param c [AbcDecimal] The chroma value
|
|
161
|
+
# @param h [Numeric] The hue angle in degrees (will be normalized to 0-360)
|
|
162
|
+
# @param space [Symbol] The color space (:hsl or :oklch)
|
|
163
|
+
# @return [Abachrome::Color] A new color in the specified color space
|
|
164
|
+
def create_harmony_color(l, c, h, space)
|
|
165
|
+
# Normalize hue to 0-360 range
|
|
166
|
+
normalized_hue = h % 360
|
|
167
|
+
|
|
168
|
+
case space
|
|
169
|
+
when :oklch
|
|
170
|
+
Abachrome::Color.from_oklch(l, c, normalized_hue, alpha)
|
|
171
|
+
when :hsl
|
|
172
|
+
# For HSL, assuming coordinates are [h, s, l]
|
|
173
|
+
# Note: HSL hue is first coordinate
|
|
174
|
+
Abachrome::Color.new(
|
|
175
|
+
Abachrome::ColorSpace.find(:hsl),
|
|
176
|
+
[normalized_hue, c, l],
|
|
177
|
+
alpha
|
|
178
|
+
)
|
|
179
|
+
else
|
|
180
|
+
raise ArgumentError, "Unsupported color space for harmonies: #{space}. Use :oklch or :hsl"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::ColorMixins::ToLms - LMS color space conversion functionality
|
|
4
|
+
#
|
|
5
|
+
# This mixin provides methods for converting colors to the LMS color space, which represents
|
|
6
|
+
# the response of the three types of cone cells in the human eye (Long, Medium, Short wavelength
|
|
7
|
+
# sensitivity). LMS serves as an intermediate color space in the OKLAB transformation pipeline
|
|
8
|
+
# and provides a foundation for perceptually uniform color representations.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Convert colors to LMS with automatic converter lookup
|
|
12
|
+
# - Both non-destructive (to_lms) and destructive (to_lms!) conversion methods
|
|
13
|
+
# - Direct access to LMS components (long, medium, short)
|
|
14
|
+
# - Utility methods for LMS array and value extraction
|
|
15
|
+
# - Optimized to return the same object when no conversion is needed
|
|
16
|
+
# - High-precision decimal arithmetic for accurate color science calculations
|
|
17
|
+
#
|
|
18
|
+
# The LMS color space uses three components: L (long wavelength), M (medium wavelength),
|
|
19
|
+
# and S (short wavelength), representing cone cell responses that bridge the gap between
|
|
20
|
+
# linear RGB and perceptually uniform color spaces like OKLAB.
|
|
21
|
+
|
|
22
|
+
require_relative "../converter"
|
|
23
|
+
|
|
24
|
+
module Abachrome
|
|
25
|
+
module ColorMixins
|
|
26
|
+
module ToLms
|
|
27
|
+
# Converts the current color to the LMS color space.
|
|
28
|
+
#
|
|
29
|
+
# If the color is already in LMS, it returns the color unchanged.
|
|
30
|
+
# Otherwise, it uses the Converter to transform the color to LMS.
|
|
31
|
+
#
|
|
32
|
+
# @return [Abachrome::Color] A new Color object in the LMS color space
|
|
33
|
+
def to_lms
|
|
34
|
+
return self if color_space.name == :lms
|
|
35
|
+
|
|
36
|
+
Converter.convert(self, :lms)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Converts the color to the LMS color space in place.
|
|
40
|
+
# This method transforms the current color into LMS space,
|
|
41
|
+
# modifying the original object by updating its color space
|
|
42
|
+
# and coordinates if not already in LMS.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# color = Abachrome::Color.from_hex("#ff5500")
|
|
46
|
+
# color.to_lms! # Color now uses LMS color space
|
|
47
|
+
#
|
|
48
|
+
# @return [Abachrome::Color] self, with updated color space and coordinates
|
|
49
|
+
def to_lms!
|
|
50
|
+
unless color_space.name == :lms
|
|
51
|
+
lms_color = to_lms
|
|
52
|
+
@color_space = lms_color.color_space
|
|
53
|
+
@coordinates = lms_color.coordinates
|
|
54
|
+
end
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the L component (long wavelength) from the LMS color space.
|
|
59
|
+
#
|
|
60
|
+
# The L component represents the response of long-wavelength sensitive cone cells
|
|
61
|
+
# in the human eye, which are most sensitive to red light.
|
|
62
|
+
#
|
|
63
|
+
# @return [AbcDecimal] The L (long wavelength) value from the LMS color space
|
|
64
|
+
def long
|
|
65
|
+
to_lms.coordinates[0]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the M component (medium wavelength) from the LMS color space.
|
|
69
|
+
#
|
|
70
|
+
# The M component represents the response of medium-wavelength sensitive cone cells
|
|
71
|
+
# in the human eye, which are most sensitive to green light.
|
|
72
|
+
#
|
|
73
|
+
# @return [AbcDecimal] The M (medium wavelength) value from the LMS color space
|
|
74
|
+
def medium
|
|
75
|
+
to_lms.coordinates[1]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the S component (short wavelength) from the LMS color space.
|
|
79
|
+
#
|
|
80
|
+
# The S component represents the response of short-wavelength sensitive cone cells
|
|
81
|
+
# in the human eye, which are most sensitive to blue light.
|
|
82
|
+
#
|
|
83
|
+
# @return [AbcDecimal] The S (short wavelength) value from the LMS color space
|
|
84
|
+
def short
|
|
85
|
+
to_lms.coordinates[2]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the LMS color space coordinates for this color.
|
|
89
|
+
#
|
|
90
|
+
# @return [Array<AbcDecimal>] An array of LMS coordinates [L, M, S] representing the color in LMS color space
|
|
91
|
+
def lms_values
|
|
92
|
+
to_lms.coordinates
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns an array representation of the color's coordinates in the LMS color space.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<AbcDecimal>] An array containing the coordinates of the color
|
|
98
|
+
# in the LMS color space in the order [L, M, S]
|
|
99
|
+
def lms_array
|
|
100
|
+
to_lms.coordinates
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::ColorMixins::ToXyz - XYZ color space conversion functionality
|
|
4
|
+
#
|
|
5
|
+
# This mixin provides methods for converting colors to the XYZ color space, which is the
|
|
6
|
+
# CIE 1931 color space that forms the basis for most other color space definitions and
|
|
7
|
+
# serves as a device-independent reference color space. XYZ represents colors using
|
|
8
|
+
# tristimulus values that correspond to the response of the human visual system to light.
|
|
9
|
+
#
|
|
10
|
+
# Key features:
|
|
11
|
+
# - Convert colors to XYZ with automatic converter lookup
|
|
12
|
+
# - Both non-destructive (to_xyz) and destructive (to_xyz!) conversion methods
|
|
13
|
+
# - Direct access to XYZ components (x, y, z)
|
|
14
|
+
# - Utility methods for XYZ array and value extraction
|
|
15
|
+
# - Optimized to return the same object when no conversion is needed
|
|
16
|
+
# - High-precision decimal arithmetic for accurate color science calculations
|
|
17
|
+
#
|
|
18
|
+
# The XYZ color space uses three components: X, Y, and Z tristimulus values, providing
|
|
19
|
+
# a device-independent representation of colors that serves as the foundation for defining
|
|
20
|
+
# other color spaces, making it essential for accurate color transformations.
|
|
21
|
+
|
|
22
|
+
require_relative "../converter"
|
|
23
|
+
|
|
24
|
+
module Abachrome
|
|
25
|
+
module ColorMixins
|
|
26
|
+
module ToXyz
|
|
27
|
+
# Converts the current color to the XYZ color space.
|
|
28
|
+
#
|
|
29
|
+
# If the color is already in XYZ, it returns the color unchanged.
|
|
30
|
+
# Otherwise, it uses the Converter to transform the color to XYZ.
|
|
31
|
+
#
|
|
32
|
+
# @return [Abachrome::Color] A new Color object in the XYZ color space
|
|
33
|
+
def to_xyz
|
|
34
|
+
return self if color_space.name == :xyz
|
|
35
|
+
|
|
36
|
+
Converter.convert(self, :xyz)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Converts the color to the XYZ color space in place.
|
|
40
|
+
# This method transforms the current color into XYZ space,
|
|
41
|
+
# modifying the original object by updating its color space
|
|
42
|
+
# and coordinates if not already in XYZ.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# color = Abachrome::Color.from_hex("#ff5500")
|
|
46
|
+
# color.to_xyz! # Color now uses XYZ color space
|
|
47
|
+
#
|
|
48
|
+
# @return [Abachrome::Color] self, with updated color space and coordinates
|
|
49
|
+
def to_xyz!
|
|
50
|
+
unless color_space.name == :xyz
|
|
51
|
+
xyz_color = to_xyz
|
|
52
|
+
@color_space = xyz_color.color_space
|
|
53
|
+
@coordinates = xyz_color.coordinates
|
|
54
|
+
end
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the X component from the XYZ color space.
|
|
59
|
+
#
|
|
60
|
+
# The X component represents the mix of cone response curves chosen to be
|
|
61
|
+
# nonnegative and represents a scale of the CIE RGB red primary.
|
|
62
|
+
#
|
|
63
|
+
# @return [AbcDecimal] The X tristimulus value from the XYZ color space
|
|
64
|
+
def x
|
|
65
|
+
to_xyz.coordinates[0]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the Y component from the XYZ color space.
|
|
69
|
+
#
|
|
70
|
+
# The Y component represents luminance, which closely matches human perception
|
|
71
|
+
# of brightness. It corresponds to the CIE RGB green primary.
|
|
72
|
+
#
|
|
73
|
+
# @return [AbcDecimal] The Y tristimulus value from the XYZ color space
|
|
74
|
+
def y
|
|
75
|
+
to_xyz.coordinates[1]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the Z component from the XYZ color space.
|
|
79
|
+
#
|
|
80
|
+
# The Z component represents the CIE RGB blue primary and is roughly equal
|
|
81
|
+
# to blue stimulation.
|
|
82
|
+
#
|
|
83
|
+
# @return [AbcDecimal] The Z tristimulus value from the XYZ color space
|
|
84
|
+
def z
|
|
85
|
+
to_xyz.coordinates[2]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the XYZ color space coordinates for this color.
|
|
89
|
+
#
|
|
90
|
+
# @return [Array<AbcDecimal>] An array of XYZ coordinates [X, Y, Z] representing the color in XYZ color space
|
|
91
|
+
def xyz_values
|
|
92
|
+
to_xyz.coordinates
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns an array representation of the color's coordinates in the XYZ color space.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<AbcDecimal>] An array containing the coordinates of the color
|
|
98
|
+
# in the XYZ color space in the order [X, Y, Z]
|
|
99
|
+
def xyz_array
|
|
100
|
+
to_xyz.coordinates
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::ColorMixins::Wcag - WCAG Accessibility Module
|
|
4
|
+
#
|
|
5
|
+
# This module provides methods for calculating color contrast ratios and checking
|
|
6
|
+
# WCAG 2.0/2.1 compliance for accessibility. It implements the relative luminance
|
|
7
|
+
# calculation with proper sRGB linearization according to WCAG specifications.
|
|
8
|
+
#
|
|
9
|
+
# Key features:
|
|
10
|
+
# - Calculate relative luminance using WCAG formula: Y = 0.2126R + 0.7152G + 0.0722B
|
|
11
|
+
# - Compute contrast ratio between two colors
|
|
12
|
+
# - Check WCAG 2.0 Level AA and AAA compliance for normal and large text
|
|
13
|
+
# - Check WCAG 2.1 non-text contrast compliance (3:1 minimum)
|
|
14
|
+
# - Predicate methods for easy accessibility checks
|
|
15
|
+
#
|
|
16
|
+
# References:
|
|
17
|
+
# - WCAG 2.0: https://www.w3.org/TR/WCAG20/
|
|
18
|
+
# - WCAG 2.1: https://www.w3.org/TR/WCAG21/
|
|
19
|
+
# - Relative luminance: https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
|
20
|
+
# - Contrast ratio: https://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
|
21
|
+
|
|
22
|
+
module Abachrome
|
|
23
|
+
module ColorMixins
|
|
24
|
+
module Wcag
|
|
25
|
+
# Calculates the relative luminance of the color according to WCAG specifications.
|
|
26
|
+
# Uses the formula: Y = 0.2126R + 0.7152G + 0.0722B
|
|
27
|
+
# where R, G, B are linearized sRGB values.
|
|
28
|
+
#
|
|
29
|
+
# The linearization follows the sRGB specification:
|
|
30
|
+
# - For values <= 0.03928: linear_value = value / 12.92
|
|
31
|
+
# - For values > 0.03928: linear_value = ((value + 0.055) / 1.055) ^ 2.4
|
|
32
|
+
#
|
|
33
|
+
# @return [AbcDecimal] The relative luminance value between 0.0 (darkest) and 1.0 (lightest)
|
|
34
|
+
def relative_luminance
|
|
35
|
+
rgb = to_srgb
|
|
36
|
+
r, g, b = rgb.coordinates
|
|
37
|
+
|
|
38
|
+
# Linearize sRGB values
|
|
39
|
+
r_linear = linearize_srgb_component(r)
|
|
40
|
+
g_linear = linearize_srgb_component(g)
|
|
41
|
+
b_linear = linearize_srgb_component(b)
|
|
42
|
+
|
|
43
|
+
# Apply WCAG luminance coefficients (Rec. 709)
|
|
44
|
+
AbcDecimal("0.2126") * r_linear +
|
|
45
|
+
AbcDecimal("0.7152") * g_linear +
|
|
46
|
+
AbcDecimal("0.0722") * b_linear
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Calculates the contrast ratio between this color and another color.
|
|
50
|
+
# The contrast ratio is calculated according to WCAG:
|
|
51
|
+
# (L1 + 0.05) / (L2 + 0.05)
|
|
52
|
+
# where L1 is the relative luminance of the lighter color and
|
|
53
|
+
# L2 is the relative luminance of the darker color.
|
|
54
|
+
#
|
|
55
|
+
# @param other [Abachrome::Color] The color to compare against
|
|
56
|
+
# @return [AbcDecimal] The contrast ratio, ranging from 1:1 (no contrast) to 21:1 (maximum contrast)
|
|
57
|
+
def contrast_ratio(other)
|
|
58
|
+
l1 = relative_luminance
|
|
59
|
+
l2 = other.relative_luminance
|
|
60
|
+
|
|
61
|
+
# Ensure L1 is the lighter color
|
|
62
|
+
l1, l2 = l2, l1 if l1 < l2
|
|
63
|
+
|
|
64
|
+
(l1 + AbcDecimal("0.05")) / (l2 + AbcDecimal("0.05"))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Checks if the contrast ratio with another color meets WCAG 2.0 Level AA standards.
|
|
68
|
+
# AA requirements:
|
|
69
|
+
# - Normal text (< 18pt or < 14pt bold): minimum 4.5:1 contrast ratio
|
|
70
|
+
# - Large text (≥ 18pt or ≥ 14pt bold): minimum 3:1 contrast ratio
|
|
71
|
+
#
|
|
72
|
+
# @param other [Abachrome::Color] The color to compare against
|
|
73
|
+
# @param large_text [Boolean] Whether the text is considered large (default: false)
|
|
74
|
+
# @return [Boolean] true if the contrast meets AA standards
|
|
75
|
+
def meets_wcag_aa?(other, large_text: false)
|
|
76
|
+
ratio = contrast_ratio(other)
|
|
77
|
+
minimum = large_text ? AbcDecimal("3.0") : AbcDecimal("4.5")
|
|
78
|
+
ratio >= minimum
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Checks if the contrast ratio with another color meets WCAG 2.0 Level AAA standards.
|
|
82
|
+
# AAA requirements:
|
|
83
|
+
# - Normal text (< 18pt or < 14pt bold): minimum 7:1 contrast ratio
|
|
84
|
+
# - Large text (≥ 18pt or ≥ 14pt bold): minimum 4.5:1 contrast ratio
|
|
85
|
+
#
|
|
86
|
+
# @param other [Abachrome::Color] The color to compare against
|
|
87
|
+
# @param large_text [Boolean] Whether the text is considered large (default: false)
|
|
88
|
+
# @return [Boolean] true if the contrast meets AAA standards
|
|
89
|
+
def meets_wcag_aaa?(other, large_text: false)
|
|
90
|
+
ratio = contrast_ratio(other)
|
|
91
|
+
minimum = large_text ? AbcDecimal("4.5") : AbcDecimal("7.0")
|
|
92
|
+
ratio >= minimum
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Checks if the contrast ratio with another color meets WCAG 2.1 non-text contrast requirements.
|
|
96
|
+
# Non-text contrast applies to:
|
|
97
|
+
# - Graphical objects (icons, graphs, infographics)
|
|
98
|
+
# - User interface components (buttons, form inputs, focus indicators)
|
|
99
|
+
# Requirement: minimum 3:1 contrast ratio
|
|
100
|
+
#
|
|
101
|
+
# @param other [Abachrome::Color] The color to compare against
|
|
102
|
+
# @return [Boolean] true if the contrast meets the 3:1 minimum for non-text content
|
|
103
|
+
def meets_wcag_non_text?(other)
|
|
104
|
+
ratio = contrast_ratio(other)
|
|
105
|
+
ratio >= AbcDecimal("3.0")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Linearizes a single sRGB component value according to the sRGB specification.
|
|
111
|
+
# This is required before applying the luminance coefficients.
|
|
112
|
+
#
|
|
113
|
+
# @param component [AbcDecimal] The sRGB component value (0.0 to 1.0)
|
|
114
|
+
# @return [AbcDecimal] The linearized component value
|
|
115
|
+
def linearize_srgb_component(component)
|
|
116
|
+
if component <= AbcDecimal("0.03928")
|
|
117
|
+
component / AbcDecimal("12.92")
|
|
118
|
+
else
|
|
119
|
+
((component + AbcDecimal("0.055")) / AbcDecimal("1.055"))**AbcDecimal("2.4")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class LmsToOklab < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from LMS color space to OKLAB color space.
|
|
7
|
+
#
|
|
8
|
+
# This method converts LMS to LRGB first, then to OKLAB. The LMS color space
|
|
9
|
+
# represents the response of the three types of cone cells in the human eye.
|
|
10
|
+
#
|
|
11
|
+
# @param lms_color [Abachrome::Color] The color in LMS color space
|
|
12
|
+
# @raise [ArgumentError] If the input color is not in LMS color space
|
|
13
|
+
# @return [Abachrome::Color] The resulting color in OKLAB color space with
|
|
14
|
+
# the same alpha as the input color
|
|
15
|
+
def self.convert(lms_color)
|
|
16
|
+
raise_unless lms_color, :lms
|
|
17
|
+
|
|
18
|
+
# Convert LMS to LRGB first
|
|
19
|
+
lrgb_color = Converters::LmsToLrgb.convert(lms_color)
|
|
20
|
+
|
|
21
|
+
# Then convert LRGB to OKLAB
|
|
22
|
+
Converters::LrgbToOklab.convert(lrgb_color)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class LrgbToLms < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from linear RGB color space to LMS color space.
|
|
7
|
+
#
|
|
8
|
+
# This method converts linear RGB to XYZ first, then to LMS cone response space.
|
|
9
|
+
# The LMS color space represents the response of the three types of cone cells
|
|
10
|
+
# in the human eye (Long, Medium, Short wavelength sensitivity).
|
|
11
|
+
#
|
|
12
|
+
# @param lrgb_color [Abachrome::Color] The color in linear RGB color space
|
|
13
|
+
# @raise [ArgumentError] If the input color is not in linear RGB 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(lrgb_color)
|
|
17
|
+
raise_unless lrgb_color, :lrgb
|
|
18
|
+
|
|
19
|
+
# Convert linear RGB to XYZ first
|
|
20
|
+
xyz_color = Converters::LrgbToXyz.convert(lrgb_color)
|
|
21
|
+
|
|
22
|
+
# Then convert XYZ to LMS
|
|
23
|
+
Converters::XyzToLms.convert(xyz_color)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
3
29
|
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class OklabToXyz < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from OKLAB color space to XYZ color space.
|
|
7
|
+
#
|
|
8
|
+
# This method converts OKLAB to LMS first, then to XYZ. The OKLAB color space
|
|
9
|
+
# is a perceptually uniform color space, and XYZ is the CIE 1931 color space
|
|
10
|
+
# that forms the basis for most other color space definitions.
|
|
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 XYZ 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
|
+
# Convert OKLAB to LMS first
|
|
20
|
+
lms_color = Converters::OklabToLms.convert(oklab_color)
|
|
21
|
+
|
|
22
|
+
# Then convert LMS to XYZ
|
|
23
|
+
Converters::LmsToXyz.convert(lms_color)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class OklchToLms < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from OKLCH color space to LMS color space.
|
|
7
|
+
#
|
|
8
|
+
# This method converts OKLCH to OKLAB first, then to LMS. The OKLCH color space
|
|
9
|
+
# is a cylindrical representation of OKLAB using lightness, chroma, and hue.
|
|
10
|
+
#
|
|
11
|
+
# @param oklch_color [Abachrome::Color] The color in OKLCH color space
|
|
12
|
+
# @raise [ArgumentError] If the input color is not in OKLCH color space
|
|
13
|
+
# @return [Abachrome::Color] The resulting color in LMS color space with
|
|
14
|
+
# the same alpha as the input color
|
|
15
|
+
def self.convert(oklch_color)
|
|
16
|
+
raise_unless oklch_color, :oklch
|
|
17
|
+
|
|
18
|
+
# Convert OKLCH to OKLAB first
|
|
19
|
+
oklab_color = Converters::OklchToOklab.convert(oklch_color)
|
|
20
|
+
|
|
21
|
+
# Then convert OKLAB to LMS
|
|
22
|
+
Converters::OklabToLms.convert(oklab_color)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class SrgbToLms < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from sRGB color space to LMS color space.
|
|
7
|
+
#
|
|
8
|
+
# This method converts sRGB to linear RGB first, then to LMS cone response space.
|
|
9
|
+
# The LMS color space represents the response of the three types of cone cells
|
|
10
|
+
# in the human eye (Long, Medium, Short wavelength sensitivity).
|
|
11
|
+
#
|
|
12
|
+
# @param srgb_color [Abachrome::Color] The color in sRGB color space
|
|
13
|
+
# @raise [ArgumentError] If the input color is not in sRGB 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(srgb_color)
|
|
17
|
+
raise_unless srgb_color, :srgb
|
|
18
|
+
|
|
19
|
+
# Convert sRGB to linear RGB first
|
|
20
|
+
lrgb_color = Converters::SrgbToLrgb.convert(srgb_color)
|
|
21
|
+
|
|
22
|
+
# Then convert linear RGB to LMS
|
|
23
|
+
Converters::LrgbToLms.convert(lrgb_color)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class SrgbToXyz < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from sRGB color space to XYZ color space.
|
|
7
|
+
#
|
|
8
|
+
# This method first converts sRGB to linear RGB by removing gamma correction,
|
|
9
|
+
# then applies the linear RGB to XYZ transformation matrix. The XYZ color space
|
|
10
|
+
# is the CIE 1931 color space that forms the basis for most other color space
|
|
11
|
+
# definitions and serves as a device-independent reference.
|
|
12
|
+
#
|
|
13
|
+
# @param srgb_color [Abachrome::Color] The color in sRGB color space
|
|
14
|
+
# @raise [ArgumentError] If the input color is not in sRGB 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(srgb_color)
|
|
18
|
+
raise_unless srgb_color, :srgb
|
|
19
|
+
|
|
20
|
+
# Convert sRGB to linear RGB first
|
|
21
|
+
lrgb_color = Converters::SrgbToLrgb.convert(srgb_color)
|
|
22
|
+
|
|
23
|
+
# Then convert linear RGB to XYZ
|
|
24
|
+
Converters::LrgbToXyz.convert(lrgb_color)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 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 XyzToLrgb < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from XYZ color space to linear RGB color space.
|
|
7
|
+
#
|
|
8
|
+
# This method implements the XYZ to linear RGB transformation using the inverse
|
|
9
|
+
# of the standard transformation matrix for the sRGB color space with D65 white point.
|
|
10
|
+
# The XYZ color space is the CIE 1931 color space that serves as a device-independent
|
|
11
|
+
# reference for color definitions.
|
|
12
|
+
#
|
|
13
|
+
# @param xyz_color [Abachrome::Color] The color in XYZ color space
|
|
14
|
+
# @raise [ArgumentError] If the input color is not in XYZ color space
|
|
15
|
+
# @return [Abachrome::Color] The resulting color in linear RGB color space with
|
|
16
|
+
# the same alpha as the input color
|
|
17
|
+
def self.convert(xyz_color)
|
|
18
|
+
raise_unless xyz_color, :xyz
|
|
19
|
+
|
|
20
|
+
x, y, z = xyz_color.coordinates.map { |_| AbcDecimal(_) }
|
|
21
|
+
|
|
22
|
+
# XYZ to Linear RGB transformation matrix (inverse of sRGB/D65)
|
|
23
|
+
r = (x * AD("3.2404542")) + (y * AD("-1.5371385")) + (z * AD("-0.4985314"))
|
|
24
|
+
g = (x * AD("-0.9692660")) + (y * AD("1.8760108")) + (z * AD("0.0415560"))
|
|
25
|
+
b = (x * AD("0.0556434")) + (y * AD("-0.2040259")) + (z * AD("1.0572252"))
|
|
26
|
+
|
|
27
|
+
Color.new(ColorSpace.find(:lrgb), [r, g, b], xyz_color.alpha)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module Converters
|
|
5
|
+
class XyzToSrgb < Abachrome::Converters::Base
|
|
6
|
+
# Converts a color from XYZ color space to sRGB color space.
|
|
7
|
+
#
|
|
8
|
+
# This method first converts XYZ to linear RGB using the inverse transformation
|
|
9
|
+
# matrix, then applies gamma correction to convert to sRGB. The XYZ color space
|
|
10
|
+
# is the CIE 1931 color space that forms the basis for most other color space
|
|
11
|
+
# definitions and serves as a device-independent reference.
|
|
12
|
+
#
|
|
13
|
+
# @param xyz_color [Abachrome::Color] The color in XYZ color space
|
|
14
|
+
# @raise [ArgumentError] If the input color is not in XYZ color space
|
|
15
|
+
# @return [Abachrome::Color] The resulting color in sRGB color space with
|
|
16
|
+
# the same alpha as the input color
|
|
17
|
+
def self.convert(xyz_color)
|
|
18
|
+
raise_unless xyz_color, :xyz
|
|
19
|
+
|
|
20
|
+
# Convert XYZ to linear RGB first
|
|
21
|
+
lrgb_color = Converters::XyzToLrgb.convert(xyz_color)
|
|
22
|
+
|
|
23
|
+
# Then convert linear RGB to sRGB
|
|
24
|
+
Converters::LrgbToSrgb.convert(lrgb_color)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Floatify - Float-based arithmetic for color calculations
|
|
4
|
+
#
|
|
5
|
+
# This module monkey-patches AbcDecimal to use Float instead of BigDecimal.
|
|
6
|
+
# Require this file to make AbcDecimal use floats throughout the codebase.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# require 'abachrome/floatify'
|
|
10
|
+
#
|
|
11
|
+
# This makes AbcDecimal a drop-in replacement that uses Float arithmetic
|
|
12
|
+
# for performance-critical applications where BigDecimal precision is not required.
|
|
13
|
+
|
|
14
|
+
require_relative "abc_decimal"
|
|
15
|
+
|
|
16
|
+
module Abachrome
|
|
17
|
+
class AbcDecimal
|
|
18
|
+
# Remove the precision-related constant and attribute
|
|
19
|
+
remove_const(:DEFAULT_PRECISION) if defined?(DEFAULT_PRECISION)
|
|
20
|
+
|
|
21
|
+
attr_accessor :value
|
|
22
|
+
|
|
23
|
+
# Initializes a new AbcDecimal object with the specified value.
|
|
24
|
+
# Precision parameter is ignored when floatify is loaded.
|
|
25
|
+
#
|
|
26
|
+
# @param value [AbcDecimal, Rational, #to_f] The numeric value to represent.
|
|
27
|
+
# If an AbcDecimal is provided, its internal value is used.
|
|
28
|
+
# Otherwise, the value is converted to a float.
|
|
29
|
+
# @param _precision [Integer] Ignored - included for API compatibility
|
|
30
|
+
# @return [AbcDecimal] A new AbcDecimal instance.
|
|
31
|
+
def initialize(value, _precision = nil)
|
|
32
|
+
@value = case value
|
|
33
|
+
when AbcDecimal
|
|
34
|
+
value.value
|
|
35
|
+
else
|
|
36
|
+
value.to_f
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns a string representation of the float value.
|
|
41
|
+
#
|
|
42
|
+
# @return [String] The float value as a string
|
|
43
|
+
def to_s
|
|
44
|
+
@value.to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Converts the value to a floating-point number.
|
|
48
|
+
#
|
|
49
|
+
# @return [Float] the floating-point representation of the AbcDecimal value
|
|
50
|
+
def to_f
|
|
51
|
+
@value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Creates a new AbcDecimal from a string representation of a number.
|
|
55
|
+
#
|
|
56
|
+
# @param str [String] The string representation of a number to convert to an AbcDecimal
|
|
57
|
+
# @param _precision [Integer] Ignored - included for API compatibility
|
|
58
|
+
# @return [AbcDecimal] A new AbcDecimal instance initialized with the given string value
|
|
59
|
+
def self.from_string(str, _precision = nil)
|
|
60
|
+
new(str)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Creates a new AbcDecimal from a Rational number.
|
|
64
|
+
#
|
|
65
|
+
# @param rational [Rational] The rational number to convert to an AbcDecimal
|
|
66
|
+
# @param _precision [Integer] Ignored - included for API compatibility
|
|
67
|
+
# @return [AbcDecimal] A new AbcDecimal instance with the value of the given rational number
|
|
68
|
+
def self.from_rational(rational, _precision = nil)
|
|
69
|
+
new(rational)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Creates a new AbcDecimal instance from a float value.
|
|
73
|
+
#
|
|
74
|
+
# @param float [Float] The floating point number to convert to an AbcDecimal
|
|
75
|
+
# @param _precision [Integer] Ignored - included for API compatibility
|
|
76
|
+
# @return [AbcDecimal] A new AbcDecimal instance representing the given float value
|
|
77
|
+
def self.from_float(float, _precision = nil)
|
|
78
|
+
new(float)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Creates a new AbcDecimal from an integer value.
|
|
82
|
+
#
|
|
83
|
+
# @param integer [Integer] The integer value to convert to an AbcDecimal
|
|
84
|
+
# @param _precision [Integer] Ignored - included for API compatibility
|
|
85
|
+
# @return [AbcDecimal] A new AbcDecimal instance with the specified integer value
|
|
86
|
+
def self.from_integer(integer, _precision = nil)
|
|
87
|
+
new(integer)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Addition operation
|
|
91
|
+
#
|
|
92
|
+
# Adds another value to this float.
|
|
93
|
+
#
|
|
94
|
+
# @param other [AbcDecimal, Numeric] The value to add. If not an AbcDecimal,
|
|
95
|
+
# it will be converted to one.
|
|
96
|
+
# @return [AbcDecimal] A new AbcDecimal instance with the sum of the two values
|
|
97
|
+
def +(other)
|
|
98
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
|
99
|
+
self.class.new(@value + other_value)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Subtracts another numeric value from this AbcDecimal.
|
|
103
|
+
#
|
|
104
|
+
# @param other [AbcDecimal, Numeric] The value to subtract from this AbcDecimal.
|
|
105
|
+
# @return [AbcDecimal] A new AbcDecimal representing the result of the subtraction.
|
|
106
|
+
def -(other)
|
|
107
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
|
108
|
+
self.class.new(@value - other_value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Multiplies this AbcDecimal by another value.
|
|
112
|
+
#
|
|
113
|
+
# @param other [Object] The value to multiply by. If not an AbcDecimal, it will be converted to one.
|
|
114
|
+
# @return [AbcDecimal] A new AbcDecimal instance representing the product of this float and the other value.
|
|
115
|
+
def *(other)
|
|
116
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
|
117
|
+
self.class.new(@value * other_value)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Divides this float by another value.
|
|
121
|
+
#
|
|
122
|
+
# @param other [Numeric, AbcDecimal] The divisor, which can be an AbcDecimal instance or any numeric value
|
|
123
|
+
# @return [AbcDecimal] A new AbcDecimal representing the result of the division
|
|
124
|
+
def /(other)
|
|
125
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
|
126
|
+
self.class.new(@value / other_value)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Performs modulo operation with another value.
|
|
130
|
+
#
|
|
131
|
+
# @param other [Numeric, AbcDecimal] The divisor for the modulo operation
|
|
132
|
+
# @return [AbcDecimal] A new AbcDecimal containing the remainder after division
|
|
133
|
+
def %(other)
|
|
134
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
|
135
|
+
self.class.new(@value % other_value)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Constrains the value to be between the specified minimum and maximum values.
|
|
139
|
+
#
|
|
140
|
+
# @param min [Numeric, AbcDecimal] The minimum value to clamp to
|
|
141
|
+
# @param max [Numeric, AbcDecimal] The maximum value to clamp to
|
|
142
|
+
# @return [Float] A float within the specified range
|
|
143
|
+
def clamp(min, max)
|
|
144
|
+
@value.clamp(AbcDecimal(min).value, AbcDecimal(max).value)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Raises self to the power of another value.
|
|
148
|
+
#
|
|
149
|
+
# @param other [Numeric, AbcDecimal] The exponent to raise this value to
|
|
150
|
+
# @return [AbcDecimal] A new AbcDecimal representing self raised to the power of other
|
|
151
|
+
def **(other)
|
|
152
|
+
other_value = other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value
|
|
153
|
+
self.class.new(@value**other_value)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Allows for mixed arithmetic operations between AbcDecimal and other numeric types.
|
|
157
|
+
#
|
|
158
|
+
# @param other [Numeric] The other number to be coerced into an AbcDecimal object
|
|
159
|
+
# @return [Array<AbcDecimal>] A two-element array containing the coerced value and self,
|
|
160
|
+
# allowing Ruby to perform arithmetic operations with mixed types
|
|
161
|
+
def coerce(other)
|
|
162
|
+
[self.class.new(other), self]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Returns a string representation of the float value for inspection purposes.
|
|
166
|
+
#
|
|
167
|
+
# @return [String] A string in the format "ClassName('value')"
|
|
168
|
+
def inspect
|
|
169
|
+
"#{self.class}('#{self}')"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Compares this float value with another value for equality.
|
|
173
|
+
# Attempts to convert the other value to an AbcDecimal if it isn't one already.
|
|
174
|
+
#
|
|
175
|
+
# @param other [Object] The value to compare against this AbcDecimal
|
|
176
|
+
# @return [Boolean] True if the values are equal, false otherwise
|
|
177
|
+
def ==(other)
|
|
178
|
+
@value == (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Compares this AbcDecimal instance with another AbcDecimal or a value that can be
|
|
182
|
+
# converted to an AbcDecimal.
|
|
183
|
+
#
|
|
184
|
+
# @param other [Object] The value to compare with this AbcDecimal.
|
|
185
|
+
# If not an AbcDecimal, it will be converted using AbcDecimal().
|
|
186
|
+
# @return [Integer, nil] Returns -1 if self is less than other,
|
|
187
|
+
# 0 if they are equal,
|
|
188
|
+
# 1 if self is greater than other,
|
|
189
|
+
# or nil if the comparison is not possible.
|
|
190
|
+
def <=>(other)
|
|
191
|
+
@value <=> (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Compares this float with another value.
|
|
195
|
+
#
|
|
196
|
+
# @param other [Object] The value to compare with. Can be an AbcDecimal or any value
|
|
197
|
+
# convertible to AbcDecimal
|
|
198
|
+
# @return [Boolean] true if this float is greater than the other value, false otherwise
|
|
199
|
+
def >(other)
|
|
200
|
+
@value > (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Compares this float value with another value.
|
|
204
|
+
#
|
|
205
|
+
# @param other [Object] The value to compare against. If not an AbcDecimal,
|
|
206
|
+
# it will be converted to one.
|
|
207
|
+
# @return [Boolean] true if this float is greater than or equal to the other value,
|
|
208
|
+
# false otherwise.
|
|
209
|
+
def >=(other)
|
|
210
|
+
@value >= (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Compares this float with another value.
|
|
214
|
+
#
|
|
215
|
+
# @param other [Object] The value to compare with. Will be coerced to AbcDecimal if not already an instance.
|
|
216
|
+
# @return [Boolean] true if this float is less than the other value, false otherwise.
|
|
217
|
+
def <(other)
|
|
218
|
+
@value < (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Compares this AbcDecimal with another value.
|
|
222
|
+
#
|
|
223
|
+
# @param other [AbcDecimal, Numeric] The value to compare with. If not an AbcDecimal,
|
|
224
|
+
# it will be converted to one.
|
|
225
|
+
# @return [Boolean] true if this AbcDecimal is less than or equal to the other value,
|
|
226
|
+
# false otherwise.
|
|
227
|
+
def <=(other)
|
|
228
|
+
@value <= (other.is_a?(AbcDecimal) ? other.value : AbcDecimal(other).value)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Rounds this float to a specified precision.
|
|
232
|
+
#
|
|
233
|
+
# @param args [Array] Arguments to be passed to Float#round. Can include
|
|
234
|
+
# the number of decimal places to round to.
|
|
235
|
+
# @return [AbcDecimal] A new AbcDecimal instance with the rounded value
|
|
236
|
+
def round(*args)
|
|
237
|
+
AbcDecimal(@value.round(*args))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Returns the absolute value (magnitude) of the float number.
|
|
241
|
+
#
|
|
242
|
+
# @param _args [Array] Ignored - included for API compatibility
|
|
243
|
+
# @return [AbcDecimal] The absolute value of the float number
|
|
244
|
+
def abs(*_args)
|
|
245
|
+
AbcDecimal(@value.abs)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Returns the square root of the AbcDecimal value.
|
|
249
|
+
#
|
|
250
|
+
# @return [AbcDecimal] A new AbcDecimal representing the square root of the value
|
|
251
|
+
def sqrt
|
|
252
|
+
AbcDecimal(Math.sqrt(@value))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns true if the internal value is negative, false otherwise.
|
|
256
|
+
#
|
|
257
|
+
# @return [Boolean] true if the value is negative, false otherwise
|
|
258
|
+
def negative?
|
|
259
|
+
@value.negative?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Returns true if the internal value is positive, false otherwise.
|
|
263
|
+
#
|
|
264
|
+
# @return [Boolean] true if the value is positive, false otherwise
|
|
265
|
+
def positive?
|
|
266
|
+
@value > 0
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Calculates the arctangent of y/x using the signs of the arguments to determine the quadrant.
|
|
270
|
+
#
|
|
271
|
+
# @param y [AbcDecimal, Numeric] The y coordinate
|
|
272
|
+
# @param x [AbcDecimal, Numeric] The x coordinate
|
|
273
|
+
# @return [AbcDecimal] The angle in radians between the positive x-axis and the ray to the point (x,y)
|
|
274
|
+
def self.atan2(y, x)
|
|
275
|
+
y_value = y.is_a?(AbcDecimal) ? y.value : AbcDecimal(y).value
|
|
276
|
+
x_value = x.is_a?(AbcDecimal) ? x.value : AbcDecimal(x).value
|
|
277
|
+
new(Math.atan2(y_value, x_value))
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
data/lib/abachrome/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abachrome
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Durable Programming
|
|
@@ -66,14 +66,18 @@ files:
|
|
|
66
66
|
- lib/abachrome/abc_decimal.rb
|
|
67
67
|
- lib/abachrome/color.rb
|
|
68
68
|
- lib/abachrome/color_mixins/blend.rb
|
|
69
|
+
- lib/abachrome/color_mixins/harmonies.rb
|
|
69
70
|
- lib/abachrome/color_mixins/lighten.rb
|
|
70
71
|
- lib/abachrome/color_mixins/spectral_mix.rb
|
|
71
72
|
- lib/abachrome/color_mixins/to_colorspace.rb
|
|
72
73
|
- lib/abachrome/color_mixins/to_grayscale.rb
|
|
74
|
+
- lib/abachrome/color_mixins/to_lms.rb
|
|
73
75
|
- lib/abachrome/color_mixins/to_lrgb.rb
|
|
74
76
|
- lib/abachrome/color_mixins/to_oklab.rb
|
|
75
77
|
- lib/abachrome/color_mixins/to_oklch.rb
|
|
76
78
|
- lib/abachrome/color_mixins/to_srgb.rb
|
|
79
|
+
- lib/abachrome/color_mixins/to_xyz.rb
|
|
80
|
+
- lib/abachrome/color_mixins/wcag.rb
|
|
77
81
|
- lib/abachrome/color_models/cmyk.rb
|
|
78
82
|
- lib/abachrome/color_models/hsv.rb
|
|
79
83
|
- lib/abachrome/color_models/lms.rb
|
|
@@ -87,6 +91,7 @@ files:
|
|
|
87
91
|
- lib/abachrome/converters/base.rb
|
|
88
92
|
- lib/abachrome/converters/cmyk_to_srgb.rb
|
|
89
93
|
- lib/abachrome/converters/lms_to_lrgb.rb
|
|
94
|
+
- lib/abachrome/converters/lms_to_oklab.rb
|
|
90
95
|
- lib/abachrome/converters/lms_to_srgb.rb
|
|
91
96
|
- lib/abachrome/converters/lms_to_xyz.rb
|
|
92
97
|
- lib/abachrome/converters/lrgb_to_lms.rb
|
|
@@ -97,18 +102,25 @@ files:
|
|
|
97
102
|
- lib/abachrome/converters/oklab_to_lrgb.rb
|
|
98
103
|
- lib/abachrome/converters/oklab_to_oklch.rb
|
|
99
104
|
- lib/abachrome/converters/oklab_to_srgb.rb
|
|
105
|
+
- lib/abachrome/converters/oklab_to_xyz.rb
|
|
106
|
+
- lib/abachrome/converters/oklch_to_lms.rb
|
|
100
107
|
- lib/abachrome/converters/oklch_to_lrgb.rb
|
|
101
108
|
- lib/abachrome/converters/oklch_to_oklab.rb
|
|
102
109
|
- lib/abachrome/converters/oklch_to_srgb.rb
|
|
103
110
|
- lib/abachrome/converters/oklch_to_xyz.rb
|
|
104
111
|
- lib/abachrome/converters/srgb_to_cmyk.rb
|
|
112
|
+
- lib/abachrome/converters/srgb_to_lms.rb
|
|
105
113
|
- lib/abachrome/converters/srgb_to_lrgb.rb
|
|
106
114
|
- lib/abachrome/converters/srgb_to_oklab.rb
|
|
107
115
|
- lib/abachrome/converters/srgb_to_oklch.rb
|
|
116
|
+
- lib/abachrome/converters/srgb_to_xyz.rb
|
|
108
117
|
- lib/abachrome/converters/srgb_to_yiq.rb
|
|
109
118
|
- lib/abachrome/converters/xyz_to_lms.rb
|
|
119
|
+
- lib/abachrome/converters/xyz_to_lrgb.rb
|
|
110
120
|
- lib/abachrome/converters/xyz_to_oklab.rb
|
|
121
|
+
- lib/abachrome/converters/xyz_to_srgb.rb
|
|
111
122
|
- lib/abachrome/converters/yiq_to_srgb.rb
|
|
123
|
+
- lib/abachrome/floatify.rb
|
|
112
124
|
- lib/abachrome/gamut/base.rb
|
|
113
125
|
- lib/abachrome/gamut/p3.rb
|
|
114
126
|
- lib/abachrome/gamut/rec2020.rb
|