color 1.7.1 → 2.0.0.pre.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 (53) hide show
  1. checksums.yaml +5 -13
  2. data/CHANGELOG.md +298 -0
  3. data/CODE_OF_CONDUCT.md +128 -0
  4. data/CONTRIBUTING.md +70 -0
  5. data/CONTRIBUTORS.md +10 -0
  6. data/LICENCE.md +27 -0
  7. data/Manifest.txt +11 -21
  8. data/README.md +54 -0
  9. data/Rakefile +74 -53
  10. data/SECURITY.md +34 -0
  11. data/lib/color/cielab.rb +348 -0
  12. data/lib/color/cmyk.rb +279 -213
  13. data/lib/color/grayscale.rb +128 -160
  14. data/lib/color/hsl.rb +205 -173
  15. data/lib/color/rgb/colors.rb +177 -163
  16. data/lib/color/rgb.rb +534 -537
  17. data/lib/color/version.rb +5 -0
  18. data/lib/color/xyz.rb +214 -0
  19. data/lib/color/yiq.rb +91 -46
  20. data/lib/color.rb +208 -141
  21. data/test/fixtures/cielab.json +444 -0
  22. data/test/minitest_helper.rb +20 -4
  23. data/test/test_cmyk.rb +49 -71
  24. data/test/test_color.rb +58 -106
  25. data/test/test_grayscale.rb +35 -56
  26. data/test/test_hsl.rb +72 -76
  27. data/test/test_rgb.rb +195 -267
  28. data/test/test_yiq.rb +12 -30
  29. metadata +165 -150
  30. checksums.yaml.gz.sig +0 -0
  31. data/.autotest +0 -5
  32. data/.gemtest +0 -0
  33. data/.hoerc +0 -2
  34. data/.minitest.rb +0 -2
  35. data/.travis.yml +0 -35
  36. data/Contributing.rdoc +0 -60
  37. data/Gemfile +0 -9
  38. data/History.rdoc +0 -172
  39. data/Licence.rdoc +0 -27
  40. data/README.rdoc +0 -50
  41. data/lib/color/css.rb +0 -7
  42. data/lib/color/palette/adobecolor.rb +0 -260
  43. data/lib/color/palette/gimp.rb +0 -104
  44. data/lib/color/palette/monocontrast.rb +0 -164
  45. data/lib/color/palette.rb +0 -4
  46. data/lib/color/rgb/contrast.rb +0 -57
  47. data/lib/color/rgb/metallic.rb +0 -28
  48. data/test/test_adobecolor.rb +0 -405
  49. data/test/test_css.rb +0 -19
  50. data/test/test_gimp.rb +0 -87
  51. data/test/test_monocontrast.rb +0 -130
  52. data.tar.gz.sig +0 -0
  53. metadata.gz.sig +0 -0
data/lib/color.rb CHANGED
@@ -1,112 +1,226 @@
1
- # :title: Color -- Colour Management with Ruby
2
- # :main: README.rdoc
3
-
4
- # = Colour Management with Ruby
1
+ # frozen_string_literal: true
2
+
3
+ # # \Color -- \Color Math in Ruby
4
+ #
5
+ # - **code**: [github.com/halostatue/color](https://github.com/halostatue/color)
6
+ # - **issues**: [github.com/halostatue/color/issues](https://github.com/halostatue/color/issues)
7
+ # - **changelog**: [CHANGELOG](rdoc-ref:CHANGELOG.md)
8
+ #
9
+ # \Color is a Ruby library to provide RGB, CMYK, HSL, and other color space manipulation
10
+ # support to applications that require it. It provides 152 named RGB colors that are
11
+ # commonly supported in HTML, SVG, and X11 applications.
12
+ #
13
+ # The \Color library performs purely mathematical manipulation of the colors based on
14
+ # color theory without reference to device color profiles (such as sRGB or Adobe RGB). For
15
+ # most purposes, when working with RGB and HSL color spaces, this won't matter. Absolute
16
+ # color spaces (like CIE LAB and CIE XYZ), however, cannot be reliably converted to
17
+ # relative color spaces (like RGB) without color profiles. When necessary for conversions,
18
+ # \Color provides \D65 and \D50 reference white values in Color::XYZ.
19
+ #
20
+ # \Color 2.0 is a major release to the \Color library, dropping support for all versions of
21
+ # Ruby prior to 3.2.
22
+ #
23
+ # > **NOTE**: This is a pre-release version of \Color 2.0.
24
+ # > The main goals for a 2.0 release have been met: modernizing the codebase, but
25
+ # > consideration will be given to improving color transformation robustness and accuracy
26
+ # > with Matrix operations and Rational numbers instead of floating point decimal values.
27
+ #
28
+ # In \Color 2.0, color objects are immutable (derived from Data) and do not expose the
29
+ # `new` class method, instead using only `from_*` class methods. There is always
30
+ # a `from_values` class method which represents the native color channel values (which may
31
+ # not match the internal representation). This method _may_ have a counterpart that is
32
+ # recommended for readability.
5
33
  module Color
6
- COLOR_VERSION = '1.7.1'
7
-
8
- class RGB; end
9
- class CMYK; end
10
- class HSL; end
11
- class GrayScale; end
12
- class YIQ; end
13
-
14
- # The maximum "resolution" for colour math; if any value is less than or
15
- # equal to this value, it is treated as zero.
16
- COLOR_EPSILON = 1e-5
17
- # The tolerance for comparing the components of two colours. In general,
18
- # colours are considered equal if all of their components are within this
19
- # tolerance value of each other.
20
- COLOR_TOLERANCE = 1e-4
21
-
22
- # Compares the +other+ colour to this one. The +other+ colour will be
23
- # coerced to the same type as the current colour. Such converted colour
24
- # comparisons will always be more approximate than non-converted
25
- # comparisons.
34
+ ##
35
+ # The maximum "resolution" for color math; if any value is less than or equal to this
36
+ # value, it is treated as zero.
37
+ EPSILON = 1e-5
38
+
39
+ ##
40
+ # The tolerance for comparing the components of two colors. In general, colors are
41
+ # considered equal if all of their components are within this tolerance value of each
42
+ # other.
43
+ TOLERANCE = 1e-4
44
+
45
+ # :stopdoc:
46
+ CIELAB = Data.define(:l, :a, :b)
47
+ CMYK = Data.define(:c, :m, :y, :k)
48
+ Grayscale = Data.define(:g)
49
+ HSL = Data.define(:h, :s, :l)
50
+ RGB = Data.define(:r, :g, :b, :names)
51
+ XYZ = Data.define(:x, :y, :z)
52
+ YIQ = Data.define(:y, :i, :q)
53
+ # :startdoc:
54
+
55
+ ##
56
+ # It is useful to know the number of components in some cases. Since most colors are
57
+ # defined with three components, we define a constant value here. Color classes that
58
+ # require more or less should override this.
26
59
  #
27
- # If the +other+ colour cannot be coerced to the current colour class, a
28
- # +NoMethodError+ exception will be raised.
60
+ # We _could_ define this as `members.count`, but this would require a special case
61
+ # for Color::RGB _regardless_ because there's an additional member for RGB colors
62
+ # (names).
63
+ def components = 3 # :nodoc:
64
+
65
+ ##
66
+ # Compares the `other` color to this one. The `other` color will be coerced to the same
67
+ # type as the current color. Such converted color comparisons will always be more
68
+ # approximate than non-converted comparisons.
29
69
  #
30
- # All values are compared as floating-point values, so two colours will be
31
- # reported equivalent if all component values are within COLOR_TOLERANCE
32
- # of each other.
70
+ # All values are compared as floating-point values, so two colors will be reported
71
+ # equivalent if all component values are within +TOLERANCE+ of each other.
33
72
  def ==(other)
34
- Color.equivalent?(self, other)
73
+ other.is_a?(Color) && to_internal.zip(coerce(other).to_internal).all? { near?(_1, _2) }
35
74
  end
36
75
 
37
- # The primary name for the colour.
38
- def name
39
- names.first
40
- end
76
+ ##
77
+ # Apply the provided block to each color component in turn, returning a new color
78
+ # instance.
79
+ def map(&block) = self.class.from_internal(*to_internal.map(&block))
41
80
 
42
- # All names for the colour.
43
- def names
44
- self.names = nil unless defined? @names
45
- @names
46
- end
47
- def names=(n) # :nodoc:
48
- @names = Array(n).flatten.compact.map(&:to_s).map(&:downcase).sort.uniq
49
- end
50
- alias_method :name=, :names=
51
- end
81
+ ##
82
+ # Apply the provided block to the color component pairs in turn, returning a new color
83
+ # instance.
84
+ def map_with(other, &block) = self.class.from_internal(*zip(other).map(&block))
52
85
 
53
- class << Color
54
- # Returns +true+ if the value is less than COLOR_EPSILON.
55
- def near_zero?(value)
56
- (value.abs <= Color::COLOR_EPSILON)
57
- end
86
+ ##
87
+ # Zip the color component pairs together.
88
+ def zip(other) = to_internal.zip(coerce(other).to_internal)
58
89
 
59
- # Returns +true+ if the value is within COLOR_EPSILON of zero or less than
60
- # zero.
61
- def near_zero_or_less?(value)
62
- (value < 0.0 or near_zero?(value))
90
+ ##
91
+ # Multiplies each component value by the scaling factor or factors, returning a new
92
+ # color object with the scaled values.
93
+ #
94
+ # If a single scaling factor is provided, it is applied to all components:
95
+ #
96
+ # ```ruby
97
+ # rgb = Color::RGB::Wheat # => RGB [#f5deb3]
98
+ # rgb.scale(0.75) # => RGB [#b8a786]
99
+ # ```
100
+ #
101
+ # If more than one scaling factor is provided, there must be exactly one factor for each
102
+ # color component of the color object or an `ArgumentError` will be raised.
103
+ #
104
+ # ```ruby
105
+ # rgb = Color::RGB::Wheat # => RGB [#f5deb3]
106
+ # # 0xf5 * 0 == 0x00, 0xde * 0.5 == 0x6f, 0xb3 * 2 == 0x166 (clamped to 0xff)
107
+ # rgb.scale(0, 0.5, 2) # => RGB [#006fff]
108
+ #
109
+ # rgb.scale(1, 2) # => Invalid scaling factors [1, 2] for Color::RGB (ArgumentError)
110
+ # ```
111
+ def scale(*factors)
112
+ if factors.size == 1
113
+ factor = factors.first
114
+ map { _1 * factor }
115
+ elsif factors.size != components
116
+ raise ArgumentError, "Invalid scaling factors #{factors.inspect} for #{self.class}"
117
+ else
118
+ new_components = to_internal.zip(factors).map { _1 * _2 }
119
+ self.class.from_internal(*new_components)
120
+ end
63
121
  end
64
122
 
65
- # Returns +true+ if the value is within COLOR_EPSILON of one.
66
- def near_one?(value)
67
- near_zero?(value - 1.0)
123
+ ##
124
+ def css_value(value, format = nil) # :nodoc:
125
+ if value.nil?
126
+ "none"
127
+ elsif near_zero?(value)
128
+ "0"
129
+ else
130
+ suffix =
131
+ case format
132
+ in :percent
133
+ "%"
134
+ in :degrees
135
+ "deg"
136
+ else
137
+ ""
138
+ end
139
+
140
+ "%3.2f%s" % [value, suffix]
141
+ end
68
142
  end
69
143
 
70
- # Returns +true+ if the value is within COLOR_EPSILON of one or more than
71
- # one.
72
- def near_one_or_more?(value)
73
- (value > 1.0 or near_one?(value))
74
- end
144
+ private
145
+
146
+ ##
147
+ def from_internal(...) = self.class.from_internal(...)
148
+
149
+ ##
150
+ # Returns `true` if the value is less than EPSILON.
151
+ def near_zero?(value) = (value.abs <= Color::EPSILON) # :nodoc:
152
+
153
+ ##
154
+ # Returns `true` if the value is within EPSILON of zero or less than zero.
155
+ def near_zero_or_less?(value) = (value < 0.0 or near_zero?(value)) # :nodoc:
156
+
157
+ ##
158
+ # Returns +true+ if the value is within EPSILON of one.
159
+ def near_one?(value) = near_zero?(value - 1.0) # :nodoc:
160
+
161
+ ##
162
+ # Returns +true+ if the value is within EPSILON of one or more than one.
163
+ def near_one_or_more?(value) = (value > 1.0 or near_one?(value)) # :nodoc:
75
164
 
165
+ ##
76
166
  # Returns +true+ if the two values provided are near each other.
77
- def near?(x, y)
78
- (x - y).abs <= Color::COLOR_TOLERANCE
79
- end
167
+ def near?(x, y) = (x - y).abs <= Color::TOLERANCE # :nodoc:
80
168
 
81
- # Returns +true+ if the two colours are roughly equivalent. If colour
82
- # conversions are required, this all conversions will be implemented
83
- # using the default conversion mechanism.
84
- def equivalent?(a, b)
85
- a.to_a.zip(a.coerce(b).to_a).all? { |(x, y)| near?(x, y) }
169
+ ##
170
+ def to_degrees(radians) # :nodoc:
171
+ if radians < 0
172
+ (Math::PI + radians % -Math::PI) * (180 / Math::PI) + 180
173
+ else
174
+ (radians % Math::PI) * (180 / Math::PI)
175
+ end
86
176
  end
87
177
 
88
- # Coerces, if possible, the second given colour object to the first
89
- # given colour object type. This will probably involve colour
90
- # conversion and therefore a loss of fidelity.
91
- def coerce(a, b)
92
- a.coerce(b)
178
+ ##
179
+ def to_radians(degrees) # :nodoc:
180
+ degrees = ((degrees % 360) + 360) % 360
181
+ if degrees >= 180
182
+ Math::PI * (degrees - 360) / 180.0
183
+ else
184
+ Math::PI * degrees / 180.0
185
+ end
93
186
  end
94
187
 
188
+ ##
95
189
  # Normalizes the value to the range (0.0) .. (1.0).
96
- def normalize(value)
97
- if near_zero_or_less? value
98
- 0.0
99
- elsif near_one_or_more? value
100
- 1.0
190
+ module_function def normalize(value, range = 0.0..1.0) # :nodoc:
191
+ value = value.clamp(range)
192
+ if near?(value, range.begin)
193
+ range.begin
194
+ elsif near?(value, range.end)
195
+ range.end
101
196
  else
102
197
  value
103
198
  end
104
199
  end
105
- alias normalize_fractional normalize
106
200
 
201
+ ##
202
+ # Translates a value from range `from` to range `to`. Both ranges must be closed.
203
+ # As 0.0 .. 1.0 is a common internal range, it is the default for `from`.
204
+ #
205
+ # This is based on the formula:
206
+ #
207
+ # [a, b] ← from ← [from.begin, from.end]
208
+ # [c, d] ← to ← [to.begin, to.end]
209
+ #
210
+ # y = (((x - a) * (d - c)) / (b - a)) + c
211
+ #
212
+ # The value is clamped to the values of `to`.
213
+ module_function def translate_range(x, to:, from: 0.0..1.0) # :nodoc:
214
+ a, b = [from.begin, from.end]
215
+ c, d = [to.begin, to.end]
216
+ y = (((x - a) * (d - c)) / (b - a)) + c
217
+ y.clamp(to)
218
+ end
219
+
220
+ ##
107
221
  # Normalizes the value to the specified range.
108
- def normalize_to_range(value, range)
109
- range = (range.end..range.begin) if (range.end < range.begin)
222
+ def normalize_to_range(value, range) # :nodoc:
223
+ range = (range.end..range.begin) if range.end < range.begin
110
224
 
111
225
  if value <= range.begin
112
226
  range.begin
@@ -117,68 +231,21 @@ class << Color
117
231
  end
118
232
  end
119
233
 
234
+ ##
120
235
  # Normalize the value to the range (0) .. (255).
121
- def normalize_byte(value)
122
- normalize_to_range(value, 0..255).to_i
123
- end
124
- alias normalize_8bit normalize_byte
236
+ def normalize_byte(value) = normalize_to_range(value, 0..255).to_i # :nodoc:
125
237
 
238
+ ##
126
239
  # Normalize the value to the range (0) .. (65535).
127
- def normalize_word(value)
128
- normalize_to_range(value, 0..65535).to_i
129
- end
130
- alias normalize_16bit normalize_word
240
+ def normalize_word(value) = normalize_to_range(value, 0..65535).to_i # :nodoc:
131
241
  end
132
242
 
133
- require 'color/rgb'
134
- require 'color/cmyk'
135
- require 'color/grayscale'
136
- require 'color/hsl'
137
- require 'color/yiq'
138
- require 'color/css'
139
-
140
- class << Color
141
- def const_missing(name) #:nodoc:
142
- case name
143
- when "VERSION", :VERSION, "COLOR_TOOLS_VERSION", :COLOR_TOOLS_VERSION
144
- warn "Color::#{name} has been deprecated. Use Color::COLOR_VERSION instead."
145
- Color::COLOR_VERSION
146
- else
147
- if Color::RGB.const_defined?(name)
148
- warn "Color::#{name} has been deprecated. Use Color::RGB::#{name} instead."
149
- Color::RGB.const_get(name)
150
- else
151
- super
152
- end
153
- end
154
- end
243
+ require "color/cmyk"
244
+ require "color/grayscale"
245
+ require "color/hsl"
246
+ require "color/cielab"
247
+ require "color/rgb"
248
+ require "color/xyz"
249
+ require "color/yiq"
155
250
 
156
- # Provides a thin veneer over the Color module to make it seem like this
157
- # is Color 0.1.0 (a class) and not Color 1.4 (a module). This
158
- # "constructor" will be removed in the future.
159
- #
160
- # mode = :hsl:: +values+ must be an array of [ hue deg, sat %, lum % ].
161
- # A Color::HSL object will be created.
162
- # mode = :rgb:: +values+ will either be an HTML-style colour string or
163
- # an array of [ red, green, blue ] (range 0 .. 255). A
164
- # Color::RGB object will be created.
165
- # mode = :cmyk:: +values+ must be an array of [ cyan %, magenta %, yellow
166
- # %, black % ]. A Color::CMYK object will be created.
167
- def new(values, mode = :rgb)
168
- warn "Color.new has been deprecated. Use Color::#{mode.to_s.upcase}.new instead."
169
- color = case mode
170
- when :hsl
171
- Color::HSL.new(*values)
172
- when :rgb
173
- values = [ values ].flatten
174
- if values.size == 1
175
- Color::RGB.from_html(*values)
176
- else
177
- Color::RGB.new(*values)
178
- end
179
- when :cmyk
180
- Color::CMYK.new(*values)
181
- end
182
- color.to_hsl
183
- end
184
- end
251
+ require "color/version"