abachrome 0.1.5 → 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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/abachrome.gemspec +1 -0
  3. data/devenv.nix +1 -1
  4. data/lib/abachrome/abc_decimal.rb +42 -35
  5. data/lib/abachrome/color.rb +61 -10
  6. data/lib/abachrome/color_mixins/blend.rb +7 -5
  7. data/lib/abachrome/color_mixins/harmonies.rb +187 -0
  8. data/lib/abachrome/color_mixins/lighten.rb +8 -6
  9. data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
  10. data/lib/abachrome/color_mixins/to_colorspace.rb +10 -8
  11. data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
  12. data/lib/abachrome/color_mixins/to_lms.rb +106 -0
  13. data/lib/abachrome/color_mixins/to_lrgb.rb +14 -12
  14. data/lib/abachrome/color_mixins/to_oklab.rb +16 -14
  15. data/lib/abachrome/color_mixins/to_oklch.rb +12 -10
  16. data/lib/abachrome/color_mixins/to_srgb.rb +17 -15
  17. data/lib/abachrome/color_mixins/to_xyz.rb +106 -0
  18. data/lib/abachrome/color_mixins/wcag.rb +126 -0
  19. data/lib/abachrome/color_models/cmyk.rb +160 -0
  20. data/lib/abachrome/color_models/hsv.rb +5 -3
  21. data/lib/abachrome/color_models/lms.rb +3 -1
  22. data/lib/abachrome/color_models/oklab.rb +3 -1
  23. data/lib/abachrome/color_models/oklch.rb +6 -4
  24. data/lib/abachrome/color_models/rgb.rb +4 -2
  25. data/lib/abachrome/color_models/xyz.rb +11 -1
  26. data/lib/abachrome/color_models/yiq.rb +37 -0
  27. data/lib/abachrome/color_space.rb +28 -14
  28. data/lib/abachrome/converter.rb +10 -8
  29. data/lib/abachrome/converters/base.rb +13 -11
  30. data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
  31. data/lib/abachrome/converters/lms_to_lrgb.rb +5 -3
  32. data/lib/abachrome/converters/lms_to_oklab.rb +28 -0
  33. data/lib/abachrome/converters/lms_to_srgb.rb +6 -4
  34. data/lib/abachrome/converters/lms_to_xyz.rb +5 -3
  35. data/lib/abachrome/converters/lrgb_to_lms.rb +29 -1
  36. data/lib/abachrome/converters/lrgb_to_oklab.rb +5 -3
  37. data/lib/abachrome/converters/lrgb_to_srgb.rb +6 -4
  38. data/lib/abachrome/converters/lrgb_to_xyz.rb +5 -3
  39. data/lib/abachrome/converters/oklab_to_lms.rb +9 -7
  40. data/lib/abachrome/converters/oklab_to_lrgb.rb +7 -7
  41. data/lib/abachrome/converters/oklab_to_oklch.rb +4 -2
  42. data/lib/abachrome/converters/oklab_to_srgb.rb +4 -2
  43. data/lib/abachrome/converters/oklab_to_xyz.rb +29 -0
  44. data/lib/abachrome/converters/oklch_to_lms.rb +28 -0
  45. data/lib/abachrome/converters/oklch_to_lrgb.rb +5 -3
  46. data/lib/abachrome/converters/oklch_to_oklab.rb +5 -3
  47. data/lib/abachrome/converters/oklch_to_srgb.rb +6 -4
  48. data/lib/abachrome/converters/oklch_to_xyz.rb +6 -4
  49. data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
  50. data/lib/abachrome/converters/srgb_to_lms.rb +29 -0
  51. data/lib/abachrome/converters/srgb_to_lrgb.rb +5 -3
  52. data/lib/abachrome/converters/srgb_to_oklab.rb +4 -2
  53. data/lib/abachrome/converters/srgb_to_oklch.rb +5 -3
  54. data/lib/abachrome/converters/srgb_to_xyz.rb +30 -0
  55. data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
  56. data/lib/abachrome/converters/xyz_to_lms.rb +5 -3
  57. data/lib/abachrome/converters/xyz_to_lrgb.rb +33 -0
  58. data/lib/abachrome/converters/xyz_to_oklab.rb +5 -3
  59. data/lib/abachrome/converters/xyz_to_srgb.rb +30 -0
  60. data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
  61. data/lib/abachrome/floatify.rb +282 -0
  62. data/lib/abachrome/gamut/base.rb +3 -3
  63. data/lib/abachrome/gamut/p3.rb +3 -3
  64. data/lib/abachrome/gamut/rec2020.rb +2 -2
  65. data/lib/abachrome/gamut/srgb.rb +4 -2
  66. data/lib/abachrome/illuminants/base.rb +2 -2
  67. data/lib/abachrome/illuminants/d50.rb +2 -2
  68. data/lib/abachrome/illuminants/d55.rb +2 -2
  69. data/lib/abachrome/illuminants/d65.rb +2 -2
  70. data/lib/abachrome/illuminants/d75.rb +2 -2
  71. data/lib/abachrome/named/css.rb +149 -149
  72. data/lib/abachrome/named/tailwind.rb +265 -265
  73. data/lib/abachrome/outputs/css.rb +2 -2
  74. data/lib/abachrome/palette.rb +26 -25
  75. data/lib/abachrome/palette_mixins/interpolate.rb +3 -1
  76. data/lib/abachrome/palette_mixins/resample.rb +2 -2
  77. data/lib/abachrome/palette_mixins/stretch_luminance.rb +2 -2
  78. data/lib/abachrome/parsers/css.rb +86 -71
  79. data/lib/abachrome/parsers/hex.rb +2 -2
  80. data/lib/abachrome/parsers/tailwind.rb +8 -8
  81. data/lib/abachrome/spectral.rb +277 -0
  82. data/lib/abachrome/to_abcd.rb +4 -4
  83. data/lib/abachrome/version.rb +2 -2
  84. data/lib/abachrome.rb +66 -10
  85. metadata +41 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9600763490f935450197f31388435f33c7d82c6dbcf854a8f2d01d8c5a301367
4
- data.tar.gz: 0e5fb3f804f9dd1332e37311f1e63e17b50aa6e204b67944c5338e18879b0ee8
3
+ metadata.gz: adfda90185a392dcd8859acac1f53b7ac4d9ace7a083b1b643baeeea49de76b8
4
+ data.tar.gz: b7b66f5e35f5f7f16b1704fced9de7cb85b2088b98afaf6c9aa0f31ad8e4fdb2
5
5
  SHA512:
6
- metadata.gz: 660d4f26b871b165eeb5bbf17c380ebe1ec8ba950746b409d6e44a26dab04c3ae95e3e8952ff9e60dccaef6d7cc5d2386bd8e24730214cabbbab2593b1bbe128
7
- data.tar.gz: 101a8a42a9509aedd238b96e5327441b4d4f79efc33715d857b88f8614f4d72ec1fe0c6af41f629f0c53b8db888ec7cc448ec603e6a0c4d379ff7a0b609af23f
6
+ metadata.gz: adaebc43ce61184e91852927c83f371234fdde03aeff491570b35146d0f899bb7919336c0f5908c23d80b8233c1a9327a7d8fe5a54da83eac1b32a3d04c7d26e
7
+ data.tar.gz: 4088bd45b14da9b2d7c475a07cd9ee5797df50abe03701f48f64f7c684edaac8b24736a53b0d49aac5fca88d4e45b21535c86fbfd927e7d2095d11b515e57ed8
data/abachrome.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
31
 
32
32
  # Runtime dependencies
33
+ spec.add_dependency "bigdecimal", "~> 3.1"
33
34
  spec.add_dependency "dry-inflector", "~> 1.0"
34
35
 
35
36
  end
data/devenv.nix CHANGED
@@ -5,7 +5,7 @@
5
5
  env.GREET = "devenv";
6
6
 
7
7
  # https://devenv.sh/packages/
8
- packages = [ pkgs.git];
8
+ packages = [ pkgs.git pkgs.ncurses];
9
9
 
10
10
  languages.ruby.enable = true;
11
11
  languages.ruby.version = "3.3.2";
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::AbcDecimal - High-precision decimal arithmetic for color calculations
2
4
  #
3
5
  # This class provides a wrapper around Ruby's BigDecimal to ensure consistent precision
@@ -19,6 +21,7 @@ require "forwardable"
19
21
  module Abachrome
20
22
  class AbcDecimal
21
23
  extend Forwardable
24
+
22
25
  DEFAULT_PRECISION = (ENV["ABC_DECIMAL_PRECISION"] || "24").to_i
23
26
 
24
27
  attr_accessor :value, :precision
@@ -26,7 +29,7 @@ module Abachrome
26
29
  def_delegators :@value, :to_i, :zero?, :nonzero?, :finite?
27
30
 
28
31
  # Initializes a new AbcDecimal object with the specified value and precision.
29
- #
32
+ #
30
33
  # @param value [AbcDecimal, BigDecimal, Rational, #to_s] The numeric value to represent.
31
34
  # If an AbcDecimal is provided, its internal value is used.
32
35
  # If a BigDecimal or Rational is provided, it's used directly.
@@ -49,11 +52,11 @@ module Abachrome
49
52
  end
50
53
 
51
54
  # Returns a string representation of the decimal value.
52
- #
55
+ #
53
56
  # This method converts the internal value to a String, using a fixed-point
54
57
  # notation format. If the internal value is a Rational, it's first converted
55
58
  # to a BigDecimal with the configured precision before string conversion.
56
- #
59
+ #
57
60
  # @return [String] The decimal value as a string in fixed-point notation
58
61
  def to_s
59
62
  if @value.is_a?(Rational)
@@ -64,14 +67,14 @@ module Abachrome
64
67
  end
65
68
 
66
69
  # Converts the decimal value to a floating-point number.
67
- #
70
+ #
68
71
  # @return [Float] the floating-point representation of the AbcDecimal value
69
72
  def to_f
70
73
  @value.to_f
71
74
  end
72
75
 
73
76
  # Creates a new AbcDecimal from a string representation of a number.
74
- #
77
+ #
75
78
  # @param str [String] The string representation of a number to convert to an AbcDecimal
76
79
  # @param precision [Integer] The precision to use for the decimal value (number of significant digits after the decimal point). Defaults to DEFAULT_PRECISION
77
80
  # @return [AbcDecimal] A new AbcDecimal instance initialized with the given string value and precision
@@ -80,7 +83,7 @@ module Abachrome
80
83
  end
81
84
 
82
85
  # Creates a new AbcDecimal from a Rational number.
83
- #
86
+ #
84
87
  # @param rational [Rational] The rational number to convert to an AbcDecimal
85
88
  # @param precision [Integer] The precision to use for the decimal representation, defaults to DEFAULT_PRECISION
86
89
  # @return [AbcDecimal] A new AbcDecimal instance with the value of the given rational number
@@ -89,7 +92,7 @@ module Abachrome
89
92
  end
90
93
 
91
94
  # Creates a new AbcDecimal instance from a float value.
92
- #
95
+ #
93
96
  # @param float [Float] The floating point number to convert to an AbcDecimal
94
97
  # @param precision [Integer] The precision to use for the decimal representation (default: DEFAULT_PRECISION)
95
98
  # @return [AbcDecimal] A new AbcDecimal instance representing the given float value
@@ -98,7 +101,7 @@ module Abachrome
98
101
  end
99
102
 
100
103
  # Creates a new AbcDecimal from an integer value.
101
- #
104
+ #
102
105
  # @param integer [Integer] The integer value to convert to an AbcDecimal
103
106
  # @param precision [Integer] The precision to use for the decimal, defaults to DEFAULT_PRECISION
104
107
  # @return [AbcDecimal] A new AbcDecimal instance with the specified integer value and precision
@@ -119,7 +122,7 @@ module Abachrome
119
122
  end
120
123
 
121
124
  # Subtracts another numeric value from this AbcDecimal.
122
- #
125
+ #
123
126
  # @param other [AbcDecimal, Numeric] The value to subtract from this AbcDecimal.
124
127
  # @return [AbcDecimal] A new AbcDecimal representing the result of the subtraction.
125
128
  def -(other)
@@ -128,14 +131,14 @@ module Abachrome
128
131
  end
129
132
 
130
133
  # Multiplies this AbcDecimal by another value.
131
- #
134
+ #
132
135
  # @param other [Object] The value to multiply by. If not an AbcDecimal, it will be converted to one.
133
136
  # @return [AbcDecimal] A new AbcDecimal instance representing the product of this decimal and the other value.
134
137
  # @example
135
138
  # dec1 = AbcDecimal.new(5)
136
139
  # dec2 = AbcDecimal.new(2)
137
140
  # result = dec1 * dec2 # => AbcDecimal representing 10
138
- #
141
+ #
139
142
  # # With a non-AbcDecimal value
140
143
  # result = dec1 * 3 # => AbcDecimal representing 15
141
144
  def *(other)
@@ -144,7 +147,7 @@ module Abachrome
144
147
  end
145
148
 
146
149
  # Divides this decimal by another value.
147
- #
150
+ #
148
151
  # @param other [Numeric, AbcDecimal] The divisor, which can be an AbcDecimal instance or any numeric value
149
152
  # @return [AbcDecimal] A new AbcDecimal representing the result of the division
150
153
  # @example
@@ -157,7 +160,7 @@ module Abachrome
157
160
  end
158
161
 
159
162
  # Performs modulo operation with another value.
160
- #
163
+ #
161
164
  # @param other [Numeric, AbcDecimal] The divisor for the modulo operation
162
165
  # @return [AbcDecimal] A new AbcDecimal containing the remainder after division
163
166
  def %(other)
@@ -166,7 +169,7 @@ module Abachrome
166
169
  end
167
170
 
168
171
  # Constrains the value to be between the specified minimum and maximum values.
169
- #
172
+ #
170
173
  # @param min [Numeric, AbcDecimal] The minimum value to clamp to
171
174
  # @param max [Numeric, AbcDecimal] The maximum value to clamp to
172
175
  # @return [AbcDecimal] A new AbcDecimal within the specified range
@@ -174,14 +177,14 @@ module Abachrome
174
177
  # AbcDecimal(5).clamp(0, 10) # => 5
175
178
  # AbcDecimal(15).clamp(0, 10) # => 10
176
179
  # AbcDecimal(-5).clamp(0, 10) # => 0
177
- def clamp(min,max)
178
- @value.clamp(AbcDecimal(min),AbcDecimal(max))
180
+ def clamp(min, max)
181
+ @value.clamp(AbcDecimal(min), AbcDecimal(max))
179
182
  end
180
183
 
181
184
  # Raises self to the power of another value.
182
185
  # This method handles different input types, including Rational values and
183
186
  # other AbcDecimal instances.
184
- #
187
+ #
185
188
  # @param other [Numeric, Rational, AbcDecimal] The exponent to raise this value to
186
189
  # @return [AbcDecimal] A new AbcDecimal representing self raised to the power of other
187
190
  def **(other)
@@ -194,7 +197,7 @@ module Abachrome
194
197
  end
195
198
 
196
199
  # Allows for mixed arithmetic operations between AbcDecimal and other numeric types.
197
- #
200
+ #
198
201
  # @param other [Numeric] The other number to be coerced into an AbcDecimal object
199
202
  # @return [Array<AbcDecimal>] A two-element array containing the coerced value and self,
200
203
  # allowing Ruby to perform arithmetic operations with mixed types
@@ -205,7 +208,7 @@ module Abachrome
205
208
  # Returns a string representation of the decimal value for inspection purposes.
206
209
  # This method returns a formatted string that includes the class name and
207
210
  # the string representation of the decimal value itself.
208
- #
211
+ #
209
212
  # @return [String] A string in the format "ClassName('value')"
210
213
  def inspect
211
214
  "#{self.class}('#{self}')"
@@ -213,7 +216,7 @@ module Abachrome
213
216
 
214
217
  # Compares this decimal value with another value for equality.
215
218
  # Attempts to convert the other value to an AbcDecimal if it isn't one already.
216
- #
219
+ #
217
220
  # @param other [Object] The value to compare against this AbcDecimal
218
221
  # @return [Boolean] True if the values are equal, false otherwise
219
222
  def ==(other)
@@ -222,7 +225,7 @@ module Abachrome
222
225
 
223
226
  # Compares this AbcDecimal instance with another AbcDecimal or a value that can be
224
227
  # converted to an AbcDecimal.
225
- #
228
+ #
226
229
  # @param other [Object] The value to compare with this AbcDecimal.
227
230
  # If not an AbcDecimal, it will be converted using AbcDecimal().
228
231
  # @return [Integer, nil] Returns -1 if self is less than other,
@@ -234,7 +237,7 @@ module Abachrome
234
237
  end
235
238
 
236
239
  # Compares this decimal with another value.
237
- #
240
+ #
238
241
  # @param other [Object] The value to compare with. Can be an AbcDecimal or any value
239
242
  # convertible to AbcDecimal
240
243
  # @return [Boolean] true if this decimal is greater than the other value, false otherwise
@@ -243,7 +246,7 @@ module Abachrome
243
246
  end
244
247
 
245
248
  # Compares this decimal value with another value.
246
- #
249
+ #
247
250
  # @param other [Object] The value to compare against. If not an AbcDecimal,
248
251
  # it will be converted to one.
249
252
  # @return [Boolean] true if this decimal is greater than or equal to the other value,
@@ -253,7 +256,7 @@ module Abachrome
253
256
  end
254
257
 
255
258
  # Compares this decimal with another value.
256
- #
259
+ #
257
260
  # @param other [Object] The value to compare with. Will be coerced to AbcDecimal if not already an instance.
258
261
  # @return [Boolean] true if this decimal is less than the other value, false otherwise.
259
262
  # @example
@@ -266,7 +269,7 @@ module Abachrome
266
269
  end
267
270
 
268
271
  # Compares this AbcDecimal with another value.
269
- #
272
+ #
270
273
  # @param other [AbcDecimal, Numeric] The value to compare with. If not an AbcDecimal,
271
274
  # it will be converted to one.
272
275
  # @return [Boolean] true if this AbcDecimal is less than or equal to the other value,
@@ -277,7 +280,7 @@ module Abachrome
277
280
 
278
281
  # @overload round(*args)
279
282
  # Rounds this decimal to a specified precision.
280
- #
283
+ #
281
284
  # @param args [Array] Arguments to be passed to BigDecimal#round. Can include
282
285
  # the number of decimal places to round to and the rounding mode.
283
286
  # @return [AbcDecimal] A new AbcDecimal instance with the rounded value
@@ -292,9 +295,9 @@ module Abachrome
292
295
  end
293
296
 
294
297
  # Returns the absolute value (magnitude) of the decimal number.
295
- #
298
+ #
296
299
  # Wraps BigDecimal#abs to ensure return values are properly converted to AbcDecimal.
297
- #
300
+ #
298
301
  # @param args [Array] Optional arguments to pass to BigDecimal#abs
299
302
  # @return [AbcDecimal] The absolute value of the decimal number
300
303
  def abs(*args)
@@ -304,23 +307,27 @@ module Abachrome
304
307
  # Returns the square root of the AbcDecimal value.
305
308
  # Calculates the square root by using Ruby's built-in Math.sqrt function
306
309
  # and converting the result back to an AbcDecimal.
307
- #
310
+ #
308
311
  # @return [AbcDecimal] A new AbcDecimal representing the square root of the value
309
312
  def sqrt
310
313
  AbcDecimal(Math.sqrt(@value))
311
314
  end
312
315
 
313
316
  # Returns true if the internal value is negative, false otherwise.
314
- #
317
+ #
315
318
  # @return [Boolean] true if the value is negative, false otherwise
316
319
  def negative?
317
320
  @value.negative?
318
321
  end
319
322
 
323
+ def positive?
324
+ @value > 0
325
+ end
326
+
320
327
  # Calculates the arctangent of y/x using the signs of the arguments to determine the quadrant.
321
328
  # Unlike the standard Math.atan2, this method accepts AbcDecimal objects or any values
322
329
  # that can be converted to AbcDecimal.
323
- #
330
+ #
324
331
  # @param y [AbcDecimal, Numeric] The y coordinate
325
332
  # @param x [AbcDecimal, Numeric] The x coordinate
326
333
  # @return [AbcDecimal] The angle in radians between the positive x-axis and the ray to the point (x,y)
@@ -333,10 +340,10 @@ module Abachrome
333
340
  end
334
341
 
335
342
  # Creates a new AbcDecimal instance.
336
- #
343
+ #
337
344
  # This is a convenience method that allows creating AbcDecimal objects
338
345
  # without explicitly referencing the Abachrome namespace.
339
- #
346
+ #
340
347
  # @param args [Array] Arguments to pass to the AbcDecimal constructor
341
348
  # @return [Abachrome::AbcDecimal] A new AbcDecimal instance
342
349
  def AbcDecimal(*args)
@@ -344,7 +351,7 @@ def AbcDecimal(*args)
344
351
  end
345
352
 
346
353
  # Creates a new AbcDecimal instance.
347
- #
354
+ #
348
355
  # @param args [Array] Arguments to pass to AbcDecimal.new
349
356
  # @return [Abachrome::AbcDecimal] A new AbcDecimal instance
350
357
  # @example
@@ -354,4 +361,4 @@ def AD(*args)
354
361
  Abachrome::AbcDecimal.new(*args)
355
362
  end
356
363
 
357
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
364
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::Color - Core color representation class
2
4
  #
3
5
  # This is the central color class that represents colors across multiple color spaces
@@ -27,7 +29,7 @@ module Abachrome
27
29
  attr_reader :color_space, :coordinates, :alpha
28
30
 
29
31
  # Initializes a new Color object with the specified color space, coordinates, and alpha value.
30
- #
32
+ #
31
33
  # @param color_space [ColorSpace] The color space for this color instance
32
34
  # @param coordinates [Array<Numeric, String>] The color coordinates in the specified color space
33
35
  # @param alpha [Numeric, String] The alpha (opacity) value, between 0.0 and 1.0 (default: 1.0)
@@ -51,7 +53,7 @@ module Abachrome
51
53
  end
52
54
 
53
55
  # Creates a new Color instance from RGB values
54
- #
56
+ #
55
57
  # @param r [Numeric] The red component value (typically 0-1)
56
58
  # @param g [Numeric] The green component value (typically 0-1)
57
59
  # @param b [Numeric] The blue component value (typically 0-1)
@@ -63,7 +65,7 @@ module Abachrome
63
65
  end
64
66
 
65
67
  # Creates a new Color instance from LRGB values
66
- #
68
+ #
67
69
  # @param r [Numeric] The red component value (typically 0-1)
68
70
  # @param g [Numeric] The green component value (typically 0-1)
69
71
  # @param b [Numeric] The blue component value (typically 0-1)
@@ -75,7 +77,7 @@ module Abachrome
75
77
  end
76
78
 
77
79
  # Creates a new Color object with OKLAB values.
78
- #
80
+ #
79
81
  # @param l [Float] The lightness component (L) of the OKLAB color space
80
82
  # @param a [Float] The green-red component (a) of the OKLAB color space
81
83
  # @param b [Float] The blue-yellow component (b) of the OKLAB color space
@@ -87,7 +89,7 @@ module Abachrome
87
89
  end
88
90
 
89
91
  # Creates a new color instance in the OKLCH color space.
90
- #
92
+ #
91
93
  # @param l [Numeric] The lightness component (L), typically in range 0..1
92
94
  # @param c [Numeric] The chroma component (C), typically starting from 0 with no upper bound
93
95
  # @param h [Numeric] The hue component (H) in degrees, typically in range 0..360
@@ -98,11 +100,60 @@ module Abachrome
98
100
  new(space, [l, c, h], alpha)
99
101
  end
100
102
 
103
+ # Creates a new Color instance from YIQ values
104
+ #
105
+ # @param y [Numeric] The luma (brightness) component, typically in range 0 to 1
106
+ # @param i [Numeric] The in-phase component (orange-blue), typically in range -0.5957 to 0.5957
107
+ # @param q [Numeric] The quadrature component (purple-green), typically in range -0.5226 to 0.5226
108
+ # @param alpha [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
109
+ # @return [Abachrome::Color] A new Color instance in the YIQ color space
110
+ def self.from_yiq(y, i, q, alpha = 1.0)
111
+ space = ColorSpace.find(:yiq)
112
+ new(space, [y, i, q], alpha)
113
+ end
114
+
115
+ # Creates a new Color instance from CMYK values
116
+ #
117
+ # @param c [Numeric] The cyan component, typically in range 0 to 1
118
+ # @param m [Numeric] The magenta component, typically in range 0 to 1
119
+ # @param y [Numeric] The yellow component, typically in range 0 to 1
120
+ # @param k [Numeric] The key/black component, typically in range 0 to 1
121
+ # @param alpha [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
122
+ # @return [Abachrome::Color] A new Color instance in the CMYK color space
123
+ def self.from_cmyk(c, m, y, k, alpha = 1.0)
124
+ space = ColorSpace.find(:cmyk)
125
+ new(space, [c, m, y, k], alpha)
126
+ end
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
+
101
152
  # Compares this color instance with another for equality.
102
- #
153
+ #
103
154
  # Two colors are considered equal if they have the same color space,
104
155
  # coordinates, and alpha value.
105
- #
156
+ #
106
157
  # @param other [Object] The object to compare with
107
158
  # @return [Boolean] true if the colors are equal, false otherwise
108
159
  def ==(other)
@@ -114,7 +165,7 @@ module Abachrome
114
165
  end
115
166
 
116
167
  # Checks if this color is equal to another color object.
117
- #
168
+ #
118
169
  # @param other [Object] The object to compare with
119
170
  # @return [Boolean] true if the two colors are equal, false otherwise
120
171
  # @see ==
@@ -126,7 +177,7 @@ module Abachrome
126
177
  # based on its color space, coordinates, and alpha value.
127
178
  # The method first converts these components to strings,
128
179
  # then computes a hash of the resulting array.
129
- #
180
+ #
130
181
  # @return [Integer] a hash code that can be used for equality comparison
131
182
  # and as a hash key in Hash objects
132
183
  def hash
@@ -168,4 +219,4 @@ module Abachrome
168
219
  end
169
220
  end
170
221
 
171
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
222
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorMixins::Blend - Color blending and mixing functionality
2
4
  #
3
5
  # This mixin provides methods for blending and mixing colors together in various color spaces.
@@ -58,7 +60,7 @@ module Abachrome
58
60
  # Blends this color with another color by the specified amount.
59
61
  # This is a destructive version of the blend method, modifying the current
60
62
  # color in place.
61
- #
63
+ #
62
64
  # @param other [Abachrome::Color] The color to blend with
63
65
  # @param amount [Float] The blend amount, between 0.0 and 1.0, where 0.0 is
64
66
  # this color and 1.0 is the other color (default: 0.5)
@@ -72,7 +74,7 @@ module Abachrome
72
74
  end
73
75
 
74
76
  # Alias for the blend method that mixes two colors together.
75
- #
77
+ #
76
78
  # @param other [Abachrome::Color] The color to mix with
77
79
  # @param amount [Float] The amount to mix, between 0.0 and 1.0, where 0.0 returns the original color and 1.0 returns the other color (default: 0.5)
78
80
  # @return [Abachrome::Color] A new color resulting from the mix of the two colors
@@ -81,10 +83,10 @@ module Abachrome
81
83
  end
82
84
 
83
85
  # Mix the current color with another color.
84
- #
86
+ #
85
87
  # This method is an alias for blend!. It combines the current color with
86
88
  # the provided color at the specified amount.
87
- #
89
+ #
88
90
  # @param other [Abachrome::Color] The color to mix with the current color
89
91
  # @param amount [Numeric] The amount of the other color to mix in, from 0 to 1 (default: 0.5)
90
92
  # @return [self] Returns the modified color object
@@ -95,4 +97,4 @@ module Abachrome
95
97
  end
96
98
  end
97
99
 
98
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
100
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -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.