color 1.8 → 2.0.1

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 -5
  2. data/CHANGELOG.md +314 -0
  3. data/CODE_OF_CONDUCT.md +128 -0
  4. data/CONTRIBUTING.md +84 -0
  5. data/CONTRIBUTORS.md +11 -0
  6. data/LICENCE.md +50 -0
  7. data/Manifest.txt +12 -23
  8. data/README.md +54 -0
  9. data/Rakefile +72 -61
  10. data/SECURITY.md +39 -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 +211 -161
  16. data/lib/color/rgb.rb +527 -551
  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 +204 -142
  21. data/licences/dco.txt +34 -0
  22. data/test/fixtures/cielab.json +444 -0
  23. data/test/minitest_helper.rb +20 -4
  24. data/test/test_cmyk.rb +49 -72
  25. data/test/test_color.rb +58 -112
  26. data/test/test_grayscale.rb +35 -57
  27. data/test/test_hsl.rb +71 -77
  28. data/test/test_rgb.rb +195 -267
  29. data/test/test_yiq.rb +12 -30
  30. metadata +104 -121
  31. data/.autotest +0 -5
  32. data/.coveralls.yml +0 -2
  33. data/.gemtest +0 -0
  34. data/.hoerc +0 -2
  35. data/.minitest.rb +0 -2
  36. data/.travis.yml +0 -41
  37. data/Code-of-Conduct.rdoc +0 -41
  38. data/Contributing.rdoc +0 -62
  39. data/Gemfile +0 -9
  40. data/History.rdoc +0 -194
  41. data/Licence.rdoc +0 -27
  42. data/README.rdoc +0 -52
  43. data/lib/color/css.rb +0 -7
  44. data/lib/color/palette/adobecolor.rb +0 -260
  45. data/lib/color/palette/gimp.rb +0 -104
  46. data/lib/color/palette/monocontrast.rb +0 -164
  47. data/lib/color/palette.rb +0 -4
  48. data/lib/color/rgb/contrast.rb +0 -57
  49. data/lib/color/rgb/metallic.rb +0 -28
  50. data/test/test_adobecolor.rb +0 -405
  51. data/test/test_css.rb +0 -19
  52. data/test/test_gimp.rb +0 -87
  53. data/test/test_monocontrast.rb +0 -130
data/lib/color.rb CHANGED
@@ -1,113 +1,222 @@
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 optional 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) cannot be reliably converted to relative color
17
+ # spaces (like RGB) without color profiles. When necessary for conversions, \Color
18
+ # provides \D65 and \D50 reference white values in Color::XYZ.
19
+ #
20
+ # Color 2.0 is a major release, dropping support for all versions of Ruby prior to 3.2 as
21
+ # well as removing or renaming a number of features. The main breaking changes are:
22
+ #
23
+ # - Color classes are immutable Data objects; they are no longer mutable.
24
+ # - RGB named colors are no longer loaded on gem startup, but must be required explicitly
25
+ # (this is _not_ done via `autoload` because there are more than 100 named colors with
26
+ # spelling variations) with `require "color/rgb/colors"`.
27
+ # - Color palettes have been removed.
28
+ # - `Color::CSS` and `Color::CSS#[]` have been removed.
5
29
  module Color
6
- COLOR_VERSION = '1.8'
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.
30
+ ##
31
+ # The maximum "resolution" for color math; if any value is less than or equal to this
32
+ # value, it is treated as zero.
33
+ EPSILON = 1e-5
34
+
35
+ ##
36
+ # The tolerance for comparing the components of two colors. In general, colors are
37
+ # considered equal if all of their components are within this tolerance value of each
38
+ # other.
39
+ TOLERANCE = 1e-4
40
+
41
+ # :stopdoc:
42
+ CIELAB = Data.define(:l, :a, :b)
43
+ CMYK = Data.define(:c, :m, :y, :k)
44
+ Grayscale = Data.define(:g)
45
+ HSL = Data.define(:h, :s, :l)
46
+ RGB = Data.define(:r, :g, :b, :names)
47
+ XYZ = Data.define(:x, :y, :z)
48
+ YIQ = Data.define(:y, :i, :q)
49
+ # :startdoc:
50
+
51
+ ##
52
+ # It is useful to know the number of components in some cases. Since most colors are
53
+ # defined with three components, we define a constant value here. Color classes that
54
+ # require more or less should override this.
26
55
  #
27
- # If the +other+ colour cannot be coerced to the current colour class, a
28
- # +NoMethodError+ exception will be raised.
56
+ # We _could_ define this as `members.count`, but this would require a special case
57
+ # for Color::RGB _regardless_ because there's an additional member for RGB colors
58
+ # (names).
59
+ def components = 3 # :nodoc:
60
+
61
+ ##
62
+ # Compares the `other` color to this one. The `other` color will be coerced to the same
63
+ # type as the current color. Such converted color comparisons will always be more
64
+ # approximate than non-converted comparisons.
29
65
  #
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.
66
+ # All values are compared as floating-point values, so two colors will be reported
67
+ # equivalent if all component values are within +TOLERANCE+ of each other.
33
68
  def ==(other)
34
- Color.equivalent?(self, other)
69
+ other.is_a?(Color) && to_internal.zip(coerce(other).to_internal).all? { near?(_1, _2) }
35
70
  end
36
71
 
37
- # The primary name for the colour.
38
- def name
39
- names.first
40
- end
72
+ ##
73
+ # Apply the provided block to each color component in turn, returning a new color
74
+ # instance.
75
+ def map(&block) = self.class.from_internal(*to_internal.map(&block))
41
76
 
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
77
+ ##
78
+ # Apply the provided block to the color component pairs in turn, returning a new color
79
+ # instance.
80
+ def map_with(other, &block) = self.class.from_internal(*zip(other).map(&block))
52
81
 
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
82
+ ##
83
+ # Zip the color component pairs together.
84
+ def zip(other) = to_internal.zip(coerce(other).to_internal)
58
85
 
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))
86
+ ##
87
+ # Multiplies each component value by the scaling factor or factors, returning a new
88
+ # color object with the scaled values.
89
+ #
90
+ # If a single scaling factor is provided, it is applied to all components:
91
+ #
92
+ # ```ruby
93
+ # rgb = Color::RGB::Wheat # => RGB [#f5deb3]
94
+ # rgb.scale(0.75) # => RGB [#b8a786]
95
+ # ```
96
+ #
97
+ # If more than one scaling factor is provided, there must be exactly one factor for each
98
+ # color component of the color object or an `ArgumentError` will be raised.
99
+ #
100
+ # ```ruby
101
+ # rgb = Color::RGB::Wheat # => RGB [#f5deb3]
102
+ # # 0xf5 * 0 == 0x00, 0xde * 0.5 == 0x6f, 0xb3 * 2 == 0x166 (clamped to 0xff)
103
+ # rgb.scale(0, 0.5, 2) # => RGB [#006fff]
104
+ #
105
+ # rgb.scale(1, 2) # => Invalid scaling factors [1, 2] for Color::RGB (ArgumentError)
106
+ # ```
107
+ def scale(*factors)
108
+ if factors.size == 1
109
+ factor = factors.first
110
+ map { _1 * factor }
111
+ elsif factors.size != components
112
+ raise ArgumentError, "Invalid scaling factors #{factors.inspect} for #{self.class}"
113
+ else
114
+ new_components = to_internal.zip(factors).map { _1 * _2 }
115
+ self.class.from_internal(*new_components)
116
+ end
63
117
  end
64
118
 
65
- # Returns +true+ if the value is within COLOR_EPSILON of one.
66
- def near_one?(value)
67
- near_zero?(value - 1.0)
119
+ ##
120
+ def css_value(value, format = nil) # :nodoc:
121
+ if value.nil?
122
+ "none"
123
+ elsif near_zero?(value)
124
+ "0"
125
+ else
126
+ suffix =
127
+ case format
128
+ in :percent
129
+ "%"
130
+ in :degrees
131
+ "deg"
132
+ else
133
+ ""
134
+ end
135
+
136
+ "%3.2f%s" % [value, suffix]
137
+ end
68
138
  end
69
139
 
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
140
+ private
141
+
142
+ ##
143
+ def from_internal(...) = self.class.from_internal(...)
144
+
145
+ ##
146
+ # Returns `true` if the value is less than EPSILON.
147
+ def near_zero?(value) = (value.abs <= Color::EPSILON) # :nodoc:
148
+
149
+ ##
150
+ # Returns `true` if the value is within EPSILON of zero or less than zero.
151
+ def near_zero_or_less?(value) = (value < 0.0 or near_zero?(value)) # :nodoc:
152
+
153
+ ##
154
+ # Returns +true+ if the value is within EPSILON of one.
155
+ def near_one?(value) = near_zero?(value - 1.0) # :nodoc:
156
+
157
+ ##
158
+ # Returns +true+ if the value is within EPSILON of one or more than one.
159
+ def near_one_or_more?(value) = (value > 1.0 or near_one?(value)) # :nodoc:
75
160
 
161
+ ##
76
162
  # 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
163
+ def near?(x, y) = (x - y).abs <= Color::TOLERANCE # :nodoc:
80
164
 
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
- return false unless a.kind_of?(Color) && b.kind_of?(Color)
86
- a.to_a.zip(a.coerce(b).to_a).all? { |(x, y)| near?(x, y) }
165
+ ##
166
+ def to_degrees(radians) # :nodoc:
167
+ if radians < 0
168
+ (Math::PI + radians % -Math::PI) * (180 / Math::PI) + 180
169
+ else
170
+ (radians % Math::PI) * (180 / Math::PI)
171
+ end
87
172
  end
88
173
 
89
- # Coerces, if possible, the second given colour object to the first
90
- # given colour object type. This will probably involve colour
91
- # conversion and therefore a loss of fidelity.
92
- def coerce(a, b)
93
- a.coerce(b)
174
+ ##
175
+ def to_radians(degrees) # :nodoc:
176
+ degrees = ((degrees % 360) + 360) % 360
177
+ if degrees >= 180
178
+ Math::PI * (degrees - 360) / 180.0
179
+ else
180
+ Math::PI * degrees / 180.0
181
+ end
94
182
  end
95
183
 
184
+ ##
96
185
  # Normalizes the value to the range (0.0) .. (1.0).
97
- def normalize(value)
98
- if near_zero_or_less? value
99
- 0.0
100
- elsif near_one_or_more? value
101
- 1.0
186
+ module_function def normalize(value, range = 0.0..1.0) # :nodoc:
187
+ value = value.clamp(range)
188
+ if near?(value, range.begin)
189
+ range.begin
190
+ elsif near?(value, range.end)
191
+ range.end
102
192
  else
103
193
  value
104
194
  end
105
195
  end
106
- alias normalize_fractional normalize
107
196
 
197
+ ##
198
+ # Translates a value from range `from` to range `to`. Both ranges must be closed.
199
+ # As 0.0 .. 1.0 is a common internal range, it is the default for `from`.
200
+ #
201
+ # This is based on the formula:
202
+ #
203
+ # [a, b] ← from ← [from.begin, from.end]
204
+ # [c, d] ← to ← [to.begin, to.end]
205
+ #
206
+ # y = (((x - a) * (d - c)) / (b - a)) + c
207
+ #
208
+ # The value is clamped to the values of `to`.
209
+ module_function def translate_range(x, to:, from: 0.0..1.0) # :nodoc:
210
+ a, b = [from.begin, from.end]
211
+ c, d = [to.begin, to.end]
212
+ y = (((x - a) * (d - c)) / (b - a)) + c
213
+ y.clamp(to)
214
+ end
215
+
216
+ ##
108
217
  # Normalizes the value to the specified range.
109
- def normalize_to_range(value, range)
110
- range = (range.end..range.begin) if (range.end < range.begin)
218
+ def normalize_to_range(value, range) # :nodoc:
219
+ range = (range.end..range.begin) if range.end < range.begin
111
220
 
112
221
  if value <= range.begin
113
222
  range.begin
@@ -118,68 +227,21 @@ class << Color
118
227
  end
119
228
  end
120
229
 
230
+ ##
121
231
  # Normalize the value to the range (0) .. (255).
122
- def normalize_byte(value)
123
- normalize_to_range(value, 0..255).to_i
124
- end
125
- alias normalize_8bit normalize_byte
232
+ def normalize_byte(value) = normalize_to_range(value, 0..255).to_i # :nodoc:
126
233
 
234
+ ##
127
235
  # Normalize the value to the range (0) .. (65535).
128
- def normalize_word(value)
129
- normalize_to_range(value, 0..65535).to_i
130
- end
131
- alias normalize_16bit normalize_word
236
+ def normalize_word(value) = normalize_to_range(value, 0..65535).to_i # :nodoc:
132
237
  end
133
238
 
134
- require 'color/rgb'
135
- require 'color/cmyk'
136
- require 'color/grayscale'
137
- require 'color/hsl'
138
- require 'color/yiq'
139
- require 'color/css'
140
-
141
- class << Color
142
- def const_missing(name) #:nodoc:
143
- case name
144
- when "VERSION", :VERSION, "COLOR_TOOLS_VERSION", :COLOR_TOOLS_VERSION
145
- warn "Color::#{name} has been deprecated. Use Color::COLOR_VERSION instead."
146
- Color::COLOR_VERSION
147
- else
148
- if Color::RGB.const_defined?(name)
149
- warn "Color::#{name} has been deprecated. Use Color::RGB::#{name} instead."
150
- Color::RGB.const_get(name)
151
- else
152
- super
153
- end
154
- end
155
- end
239
+ require "color/cmyk"
240
+ require "color/grayscale"
241
+ require "color/hsl"
242
+ require "color/cielab"
243
+ require "color/rgb"
244
+ require "color/xyz"
245
+ require "color/yiq"
156
246
 
157
- # Provides a thin veneer over the Color module to make it seem like this
158
- # is Color 0.1.0 (a class) and not Color 1.4 (a module). This
159
- # "constructor" will be removed in the future.
160
- #
161
- # mode = :hsl:: +values+ must be an array of [ hue deg, sat %, lum % ].
162
- # A Color::HSL object will be created.
163
- # mode = :rgb:: +values+ will either be an HTML-style colour string or
164
- # an array of [ red, green, blue ] (range 0 .. 255). A
165
- # Color::RGB object will be created.
166
- # mode = :cmyk:: +values+ must be an array of [ cyan %, magenta %, yellow
167
- # %, black % ]. A Color::CMYK object will be created.
168
- def new(values, mode = :rgb)
169
- warn "Color.new has been deprecated. Use Color::#{mode.to_s.upcase}.new instead."
170
- color = case mode
171
- when :hsl
172
- Color::HSL.new(*values)
173
- when :rgb
174
- values = [ values ].flatten
175
- if values.size == 1
176
- Color::RGB.from_html(*values)
177
- else
178
- Color::RGB.new(*values)
179
- end
180
- when :cmyk
181
- Color::CMYK.new(*values)
182
- end
183
- color.to_hsl
184
- end
185
- end
247
+ require "color/version"
data/licences/dco.txt ADDED
@@ -0,0 +1,34 @@
1
+ Developer Certificate of Origin
2
+ Version 1.1
3
+
4
+ Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
5
+
6
+ Everyone is permitted to copy and distribute verbatim copies of this
7
+ license document, but changing it is not allowed.
8
+
9
+
10
+ Developer's Certificate of Origin 1.1
11
+
12
+ By making a contribution to this project, I certify that:
13
+
14
+ (a) The contribution was created in whole or in part by me and I
15
+ have the right to submit it under the open source license
16
+ indicated in the file; or
17
+
18
+ (b) The contribution is based upon previous work that, to the best
19
+ of my knowledge, is covered under an appropriate open source
20
+ license and I have the right under that license to submit that
21
+ work with modifications, whether created in whole or in part
22
+ by me, under the same open source license (unless I am
23
+ permitted to submit under a different license), as indicated
24
+ in the file; or
25
+
26
+ (c) The contribution was provided directly to me by some other
27
+ person who certified (a), (b) or (c) and I have not modified
28
+ it.
29
+
30
+ (d) I understand and agree that this project and the contribution
31
+ are public and that a record of the contribution (including all
32
+ personal information I submit with it, including my sign-off) is
33
+ maintained indefinitely and may be redistributed consistent with
34
+ this project or the open source license(s) involved.