color 1.4.2 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,7 @@
1
- require 'color'
2
-
3
1
  # This namespace contains some CSS colour names.
4
2
  module Color::CSS
5
3
  # Returns the RGB colour for name or +nil+ if the name is not valid.
6
4
  def self.[](name)
7
- @colors[name.to_s.downcase.to_sym]
8
- end
9
-
10
- @colors = {}
11
- Color::RGB.constants.each do |const|
12
- next if const == "PDF_FORMAT_STR"
13
- next if const == "Metallic"
14
- @colors[const.downcase.to_sym] ||= Color::RGB.const_get(const)
5
+ Color::RGB.by_name(name) { nil }
15
6
  end
16
7
  end
@@ -1,45 +1,40 @@
1
1
  # A colour object representing shades of grey. Used primarily in PDF
2
2
  # document creation.
3
3
  class Color::GrayScale
4
+ include Color
5
+
4
6
  # The format of a DeviceGrey colour for PDF. In color-tools 2.0 this will
5
7
  # be removed from this package and added back as a modification by the
6
8
  # PDF::Writer package.
7
9
  PDF_FORMAT_STR = "%.3f %s"
8
10
 
9
- # Creates a greyscale colour object from fractional values 0..1.
10
- #
11
- # Color::GreyScale.from_fraction(0.5)
12
- def self.from_fraction(g = 0)
13
- color = Color::GrayScale.new
14
- color.g = g
15
- color
16
- end
11
+ class << self
12
+ # Creates a greyscale colour object from fractional values 0..1.
13
+ #
14
+ # Color::GreyScale.from_fraction(0.5)
15
+ def from_fraction(g = 0, &block)
16
+ new(g, 1.0, &block)
17
+ end
17
18
 
18
- # Creates a greyscale colour object from percentages 0..100.
19
- #
20
- # Color::GrayScale.from_percent(50)
21
- def self.from_percent(g = 0)
22
- Color::GrayScale.new(g)
19
+ # Creates a greyscale colour object from percentages 0..100.
20
+ #
21
+ # Color::GrayScale.from_percent(50)
22
+ def from_percent(g = 0, &block)
23
+ new(g, &block)
24
+ end
23
25
  end
24
26
 
25
27
  # Creates a greyscale colour object from percentages 0..100.
26
28
  #
27
29
  # Color::GrayScale.new(50)
28
- def initialize(g = 0)
29
- @g = g / 100.0
30
+ def initialize(g = 0, radix = 100.0, &block) # :yields self:
31
+ @g = Color.normalize(g / radix)
32
+ block.call if block
30
33
  end
31
34
 
32
- # Compares the other colour to this one. The other colour will be
33
- # converted to GreyScale before comparison, so the comparison between a
34
- # GreyScale colour and a non-GreyScale colour will be approximate and
35
- # based on the other colour's #to_greyscale conversion. If there is no
36
- # #to_greyscale conversion, this will raise an exception. This will report
37
- # that two GreyScale values are equivalent if they are within
38
- # COLOR_TOLERANCE of each other.
39
- def ==(other)
40
- other = other.to_grayscale
41
- other.kind_of?(Color::GrayScale) and
42
- ((@g - other.g).abs <= Color::COLOR_TOLERANCE)
35
+ # Coerces the other Color object to grayscale.
36
+ def coerce(other)
37
+ other.to_grayscale
43
38
  end
44
39
 
45
40
  # Present the colour as a DeviceGrey fill colour string for PDF. This will
@@ -170,10 +165,7 @@ class Color::GrayScale
170
165
  # The addition is done using the grayscale accessor methods to ensure a
171
166
  # valid colour in the result.
172
167
  def +(other)
173
- other = other.to_grayscale
174
- ng = self.dup
175
- ng.g += other.g
176
- ng
168
+ self.class.from_fraction(g + other.to_grayscale.g)
177
169
  end
178
170
 
179
171
  # Subtracts another colour to the current colour. The other colour will be
@@ -183,15 +175,22 @@ class Color::GrayScale
183
175
  # The subtraction is done using the grayscale accessor methods to ensure a
184
176
  # valid colour in the result.
185
177
  def -(other)
186
- other = other.to_grayscale
187
- ng = self.dup
188
- ng.g -= other.g
189
- ng
178
+ self + (-other)
190
179
  end
191
180
 
192
181
  def inspect
193
182
  "Gray [%.2f%%]" % [ gray ]
194
183
  end
184
+
185
+ def to_a
186
+ [ g ]
187
+ end
188
+
189
+ def -@
190
+ gs = self.dup
191
+ gs.instance_variable_set(:@g, -g)
192
+ gs
193
+ end
195
194
  end
196
195
 
197
196
  # A synonym for Color::GrayScale.
@@ -1,39 +1,30 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+
1
3
  # An HSL colour object. Internally, the hue (#h), saturation (#s), and
2
4
  # luminosity/lightness (#l) values are dealt with as fractional values in
3
5
  # the range 0..1.
4
6
  class Color::HSL
7
+ include Color
8
+
5
9
  class << self
6
10
  # Creates an HSL colour object from fractional values 0..1.
7
- def from_fraction(h = 0.0, s = 0.0, l = 0.0)
8
- colour = Color::HSL.new
9
- colour.h = h
10
- colour.s = s
11
- colour.l = l
12
- colour
11
+ def from_fraction(h = 0.0, s = 0.0, l = 0.0, &block)
12
+ new(h, s, l, 1.0, 1.0, &block)
13
13
  end
14
14
  end
15
15
 
16
- # Compares the other colour to this one. The other colour will be
17
- # converted to HSL before comparison, so the comparison between a HSL
18
- # colour and a non-HSL colour will be approximate and based on the other
19
- # colour's #to_hsl conversion. If there is no #to_hsl conversion, this
20
- # will raise an exception. This will report that two HSL values are
21
- # equivalent if all component values are within Color::COLOR_TOLERANCE of
22
- # each other.
23
- def ==(other)
24
- other = other.to_hsl
25
- other.kind_of?(Color::HSL) and
26
- ((@h - other.h).abs <= Color::COLOR_TOLERANCE) and
27
- ((@s - other.s).abs <= Color::COLOR_TOLERANCE) and
28
- ((@l - other.l).abs <= Color::COLOR_TOLERANCE)
16
+ # Coerces the other Color object into HSL.
17
+ def coerce(other)
18
+ other.to_hsl
29
19
  end
30
20
 
31
21
  # Creates an HSL colour object from the standard values of degrees and
32
22
  # percentages (e.g., 145 deg, 30%, 50%).
33
- def initialize(h = 0, s = 0, l = 0)
34
- @h = h / 360.0
35
- @s = s / 100.0
36
- @l = l / 100.0
23
+ def initialize(h = 0, s = 0, l = 0, radix1 = 360.0, radix2 = 100.0, &block) # :yields self:
24
+ @h = Color.normalize(h / radix1)
25
+ @s = Color.normalize(s / radix2)
26
+ @l = Color.normalize(l / radix2)
27
+ block.call if block
37
28
  end
38
29
 
39
30
  # Present the colour as an HTML/CSS colour string.
@@ -67,46 +58,30 @@ class Color::HSL
67
58
  "hsla(%3.2f, %3.2f%%, %3.2f%%, %3.2f)" % [ hue, saturation, luminosity, 1 ]
68
59
  end
69
60
 
70
- # Converting to HSL as adapted from Foley and Van-Dam from
71
- # http://www.bobpowell.net/RGBHSB.htm.
61
+ # Converting from HSL to RGB. As with all colour conversions, this is
62
+ # approximate at best. The code here is adapted from fvd and van Dam,
63
+ # originally found at [1] (implemented similarly at [2]).
72
64
  #
73
- # NOTE:
74
- # * If the colour's luminosity is near zero, the colour is always black.
75
- # * If the colour's luminosity is near one, the colour is always white.
76
- # * If the colour's saturation is near zero, the colour is always a shade
77
- # of grey and is based only on the luminosity of the colour.
65
+ # This simplifies the calculations with the following assumptions:
66
+ # - Luminance values <= 0 always translate to Color::RGB::Black.
67
+ # - Luminance values >= 1 always translate to Color::RGB::White.
68
+ # - Saturation values <= 0 always translate to a shade of gray using
69
+ # luminance as a percentage of gray.
78
70
  #
79
- def to_rgb(ignored = nil)
80
- return Color::RGB.new if Color.near_zero_or_less?(@l)
81
- return Color::RGB.new(0xff, 0xff, 0xff) if Color.near_one_or_more?(@l)
82
- return Color::RGB.from_fraction(@l, @l, @l) if Color.near_zero?(@s)
83
-
84
- # Is the value less than 0.5?
85
- if Color.near_zero_or_less?(@l - 0.5)
86
- tmp2 = @l * (1.0 + @s.to_f)
71
+ # [1] http://bobpowell.net/RGBHSB.aspx
72
+ # [2] http://support.microsoft.com/kb/29240
73
+ def to_rgb(*)
74
+ if Color.near_zero_or_less?(l)
75
+ Color::RGB::Black
76
+ elsif Color.near_one_or_more?(l)
77
+ Color::RGB::White
78
+ elsif Color.near_zero?(s)
79
+ Color::RGB.from_grayscale_fraction(l)
87
80
  else
88
- tmp2 = @l + @s - (@l * @s.to_f)
81
+ # Only needed for Ruby 1.8. For Ruby 1.9+, we can do:
82
+ # Color::RGB.new(*compute_fvd_rgb, 1.0)
83
+ Color::RGB.new(*(compute_fvd_rgb + [ 1.0 ]))
89
84
  end
90
- tmp1 = 2.0 * @l - tmp2
91
-
92
- tmp3 = [ @h + (1.0 / 3.0), @h, @h - (1.0 / 3.0) ]
93
-
94
- rgb = tmp3.map { |hue|
95
- hue += 1.0 if Color.near_zero_or_less?(hue)
96
- hue -= 1.0 if Color.near_one_or_more?(hue)
97
-
98
- if Color.near_zero_or_less?((6.0 * hue) - 1.0)
99
- tmp1 + ((tmp2 - tmp1) * hue * 6.0)
100
- elsif Color.near_zero_or_less?((2.0 * hue) - 1.0)
101
- tmp2
102
- elsif Color.near_zero_or_less?((3.0 * hue) - 2.0)
103
- tmp1 + (tmp2 - tmp1) * ((2 / 3.0) - hue) * 6.0
104
- else
105
- tmp1
106
- end
107
- }
108
-
109
- Color::RGB.from_fraction(*rgb)
110
85
  end
111
86
 
112
87
  # Converts to RGB then YIQ.
@@ -197,13 +172,69 @@ class Color::HSL
197
172
  # Mix the mask colour (which will be converted to an HSL colour) with the
198
173
  # current colour at the stated mix percentage as a decimal value.
199
174
  #
200
- # NOTE:: This differs from Color::RGB#mix_with.
175
+ # NOTE:: This differs from Color::RGB#mix_with.
201
176
  def mix_with(color, mix_percent = 0.5)
202
- color = color.to_hsl
203
- _h = ((color.h - self.h) * mix_percent) + self.h
204
- _s = ((color.s - self.s) * mix_percent) + self.s
205
- _l = ((color.l - self.l) * mix_percent) + self.l
177
+ v = to_a.zip(coerce(color).to_a).map { |(x, y)|
178
+ ((y - x) * mix_percent) + x
179
+ }
180
+ self.class.from_fraction(*v)
181
+ end
182
+
183
+ def to_a
184
+ [ h, s, l ]
185
+ end
206
186
 
207
- self.class.from_fraction(_h, _s, _l)
187
+ private
188
+
189
+ # This algorithm calculates based on a mixture of the saturation and
190
+ # luminance, and then takes the RGB values from the hue + 1/3, hue, and
191
+ # hue - 1/3 positions in a circular representation of colour divided into
192
+ # four parts (confusing, I know, but it's the way that it works). See
193
+ # #hue_to_rgb for more information.
194
+ def compute_fvd_rgb
195
+ t1, t2 = fvd_mix_sat_lum
196
+ [ h + (1 / 3.0), h, h - (1 / 3.0) ].map { |v|
197
+ hue_to_rgb(rotate_hue(v), t1, t2)
198
+ }
199
+ end
200
+
201
+ # Mix saturation and luminance for use in hue_to_rgb. The base value is
202
+ # different depending on whether luminance is <= 50% or > 50%.
203
+ def fvd_mix_sat_lum
204
+ t = if Color.near_zero_or_less?(l - 0.5)
205
+ l * (1.0 + s.to_f)
206
+ else
207
+ l + s - (l * s.to_f)
208
+ end
209
+ [ 2.0 * l - t, t ]
210
+ end
211
+
212
+ # In HSL, hues are referenced as degrees in a colour circle. The flow
213
+ # itself is endless; therefore, we can rotate around. The only thing our
214
+ # implementation restricts is that you should not be > 1.0.
215
+ def rotate_hue(h)
216
+ h += 1.0 if Color.near_zero_or_less?(h)
217
+ h -= 1.0 if Color.near_one_or_more?(h)
218
+ h
219
+ end
220
+
221
+ # We calculate the interaction of the saturation/luminance mix (calculated
222
+ # earlier) based on the position of the hue in the circular colour space
223
+ # divided into quadrants. Our hue range is [0, 1), not [0, 360º).
224
+ #
225
+ # - The first quadrant covers the first 60º [0, 60º].
226
+ # - The second quadrant covers the next 120º (60º, 180º].
227
+ # - The third quadrant covers the next 60º (180º, 240º].
228
+ # - The fourth quadrant covers the final 120º (240º, 360º).
229
+ def hue_to_rgb(h, t1, t2)
230
+ if Color.near_zero_or_less?((6.0 * h) - 1.0)
231
+ t1 + ((t2 - t1) * h * 6.0)
232
+ elsif Color.near_zero_or_less?((2.0 * h) - 1.0)
233
+ t2
234
+ elsif Color.near_zero_or_less?((3.0 * h) - 2.0)
235
+ t1 + (t2 - t1) * ((2 / 3.0) - h) * 6.0
236
+ else
237
+ t1
238
+ end
208
239
  end
209
240
  end
@@ -36,19 +36,17 @@ class Color::Palette::MonoContrast
36
36
  # the palette based on the base colours. The default value for this is 125
37
37
  # / 255.0. If this value is set to +nil+, it will be restored to the
38
38
  # default.
39
- attr_accessor :minimum_brightness_diff
40
- remove_method :minimum_brightness_diff= ;
39
+ attr_reader :minimum_brightness_diff
41
40
  def minimum_brightness_diff=(bd) #:nodoc:
42
- if bd.nil?
43
- @minimum_brightness_diff = DEFAULT_MINIMUM_BRIGHTNESS_DIFF
44
- elsif bd > 1.0
45
- @minimum_brightness_diff = 1.0
46
- elsif bd < 0.0
47
- @minimum_brightness_diff = 0.0
48
- else
49
- @minimum_brightness_diff = bd
50
- end
51
-
41
+ @minimum_brightness_diff = if bd.nil?
42
+ DEFAULT_MINIMUM_BRIGHTNESS_DIFF
43
+ elsif bd > 1.0
44
+ 1.0
45
+ elsif bd < 0.0
46
+ 0.0
47
+ else
48
+ bd
49
+ end
52
50
  regenerate(@background[0], @foreground[0])
53
51
  end
54
52
 
@@ -57,18 +55,17 @@ class Color::Palette::MonoContrast
57
55
  # The minimum colour difference between the background and the foreground,
58
56
  # and must be between 0..3. Setting this value will regenerate the palette
59
57
  # based on the base colours. The default value for this is 500 / 255.0.
60
- attr_accessor :minimum_color_diff
61
- remove_method :minimum_color_diff= ;
62
- def minimum_color_diff=(cd) #:noco:
63
- if cd.nil?
64
- @minimum_color_diff = DEFAULT_MINIMUM_COLOR_DIFF
65
- elsif cd > 3.0
66
- @minimum_color_diff = 3.0
67
- elsif cd < 0.0
68
- @minimum_color_diff = 0.0
69
- else
70
- @minimum_color_diff = cd
71
- end
58
+ attr_reader :minimum_color_diff
59
+ def minimum_color_diff=(cd) #:nodoc:
60
+ @minimum_color_diff = if cd.nil?
61
+ DEFAULT_MINIMUM_COLOR_DIFF
62
+ elsif cd > 3.0
63
+ 3.0
64
+ elsif cd < 0.0
65
+ 0.0
66
+ else
67
+ cd
68
+ end
72
69
  regenerate(@background[0], @foreground[0])
73
70
  end
74
71
 
@@ -76,7 +73,6 @@ class Color::Palette::MonoContrast
76
73
  def initialize(background, foreground = nil)
77
74
  @minimum_brightness_diff = DEFAULT_MINIMUM_BRIGHTNESS_DIFF
78
75
  @minimum_color_diff = DEFAULT_MINIMUM_COLOR_DIFF
79
-
80
76
  regenerate(background, foreground)
81
77
  end
82
78
 
@@ -148,7 +144,7 @@ class Color::Palette::MonoContrast
148
144
 
149
145
  # Returns the absolute difference between the brightness levels of two
150
146
  # colours. This will be a decimal value between 0 and 1. W3C accessibility
151
- # guidelines for colour contrast[http://www.w3.org/TR/AERT#color-contrast]
147
+ # guidelines for {colour contrast}[http://www.w3.org/TR/AERT#color-contrast]
152
148
  # suggest that this value be at least approximately 0.49 (125 / 255.0) for
153
149
  # proper contrast.
154
150
  def brightness_diff(c1, c2)
@@ -156,8 +152,8 @@ class Color::Palette::MonoContrast
156
152
  end
157
153
 
158
154
  # Returns the contrast between to colours, a decimal value between 0 and
159
- # 3. W3C accessibility guidelines for colour
160
- # contrast[http://www.w3.org/TR/AERT#color-contrast] suggest that this
155
+ # 3. W3C accessibility guidelines for {colour
156
+ # contrast}[http://www.w3.org/TR/AERT#color-contrast] suggest that this
161
157
  # value be at least approximately 1.96 (500 / 255.0) for proper contrast.
162
158
  def color_diff(c1, c2)
163
159
  r = (c1.r - c2.r).abs
@@ -1,5 +1,7 @@
1
1
  # An RGB colour object.
2
2
  class Color::RGB
3
+ include Color
4
+
3
5
  # The format of a DeviceRGB colour for PDF. In color-tools 2.0 this will
4
6
  # be removed from this package and added back as a modification by the
5
7
  # PDF::Writer package.
@@ -9,20 +11,22 @@ class Color::RGB
9
11
  # Creates an RGB colour object from percentages 0..100.
10
12
  #
11
13
  # Color::RGB.from_percentage(10, 20 30)
12
- def from_percentage(r = 0, g = 0, b = 0)
13
- from_fraction(r / 100.0, g / 100.0, b / 100.0)
14
+ def from_percentage(r = 0, g = 0, b = 0, &block)
15
+ new(r, g, b, 100.0, &block)
14
16
  end
15
17
 
16
18
  # Creates an RGB colour object from fractional values 0..1.
17
19
  #
18
20
  # Color::RGB.from_fraction(.3, .2, .1)
19
- def from_fraction(r = 0.0, g = 0.0, b = 0.0)
20
- colour = Color::RGB.new
21
- colour.r = r
22
- colour.g = g
23
- colour.b = b
24
- colour
21
+ def from_fraction(r = 0.0, g = 0.0, b = 0.0, &block)
22
+ new(r, g, b, 1.0, &block)
23
+ end
24
+
25
+ # Creates an RGB colour object from a grayscale fractional value 0..1.
26
+ def from_grayscale_fraction(l = 0.0, &block)
27
+ new(l, l, l, 1.0, &block)
25
28
  end
29
+ alias_method :from_greyscale_fraction, :from_grayscale_fraction
26
30
 
27
31
  # Creates an RGB colour object from an HTML colour descriptor (e.g.,
28
32
  # <tt>"fed"</tt> or <tt>"#cabbed;"</tt>.
@@ -31,44 +35,86 @@ class Color::RGB
31
35
  # Color::RGB.from_html("#fed")
32
36
  # Color::RGB.from_html("#cabbed")
33
37
  # Color::RGB.from_html("cabbed")
34
- def from_html(html_colour)
35
- html_colour = html_colour.gsub(%r{[#;]}, '')
36
- case html_colour.size
38
+ def from_html(html_colour, &block)
39
+ # When we can move to 1.9+ only, this will be \h
40
+ h = html_colour.scan(/[0-9a-f]/i)
41
+ case h.size
37
42
  when 3
38
- colours = html_colour.scan(%r{[0-9A-Fa-f]}).map { |el| (el * 2).to_i(16) }
43
+ new(*h.map { |v| (v * 2).to_i(16) }, &block)
39
44
  when 6
40
- colours = html_colour.scan(%r<[0-9A-Fa-f]{2}>).map { |el| el.to_i(16) }
45
+ new(*h.each_slice(2).map { |v| v.join.to_i(16) }, &block)
41
46
  else
42
- raise ArgumentError
47
+ raise ArgumentError, "Not a supported HTML colour type."
43
48
  end
49
+ end
44
50
 
45
- Color::RGB.new(*colours)
51
+ # Find or create a colour by an HTML hex code. This differs from the
52
+ # #from_html method in that if the colour code matches a named colour,
53
+ # the existing colour will be returned.
54
+ #
55
+ # Color::RGB.by_hex('ff0000').name # => 'red'
56
+ # Color::RGB.by_hex('ff0001').name # => nil
57
+ #
58
+ # If a block is provided, the value that is returned by the block will
59
+ # be returned instead of the exception caused by an error in providing a
60
+ # correct hex format.
61
+ def by_hex(hex, &block)
62
+ __by_hex.fetch(html_hexify(hex)) { from_html(hex) }
63
+ rescue
64
+ if block
65
+ block.call
66
+ else
67
+ raise
68
+ end
69
+ end
70
+
71
+ # Return a colour as identified by the colour name.
72
+ def by_name(name, &block)
73
+ __by_name.fetch(name.to_s.downcase, &block)
74
+ end
75
+
76
+ # Return a colour as identified by the colour name, or by hex.
77
+ def by_css(name_or_hex, &block)
78
+ by_name(name_or_hex) { by_hex(name_or_hex, &block) }
79
+ end
80
+
81
+ # Extract named or hex colours from the provided text.
82
+ def extract_colors(text, mode = :both)
83
+ text = text.downcase
84
+ regex = case mode
85
+ when :name
86
+ Regexp.union(__by_name.keys)
87
+ when :hex
88
+ Regexp.union(__by_hex.keys)
89
+ when :both
90
+ Regexp.union(__by_hex.keys + __by_name.keys)
91
+ end
92
+
93
+ text.scan(regex).map { |match|
94
+ case mode
95
+ when :name
96
+ by_name(match)
97
+ when :hex
98
+ by_hex(match)
99
+ when :both
100
+ by_css(match)
101
+ end
102
+ }
46
103
  end
47
104
  end
48
105
 
49
- # Compares the other colour to this one. The other colour will be
50
- # converted to RGB before comparison, so the comparison between a RGB
51
- # colour and a non-RGB colour will be approximate and based on the other
52
- # colour's default #to_rgb conversion. If there is no #to_rgb conversion,
53
- # this will raise an exception. This will report that two RGB colours are
54
- # equivalent if all component values are within COLOR_TOLERANCE of each
55
- # other.
56
- def ==(other)
57
- other = other.to_rgb
58
- other.kind_of?(Color::RGB) and
59
- ((@r - other.r).abs <= Color::COLOR_TOLERANCE) and
60
- ((@g - other.g).abs <= Color::COLOR_TOLERANCE) and
61
- ((@b - other.b).abs <= Color::COLOR_TOLERANCE)
106
+ # Coerces the other Color object into RGB.
107
+ def coerce(other)
108
+ other.to_rgb
62
109
  end
63
110
 
64
111
  # Creates an RGB colour object from the standard range 0..255.
65
112
  #
66
113
  # Color::RGB.new(32, 64, 128)
67
114
  # Color::RGB.new(0x20, 0x40, 0x80)
68
- def initialize(r = 0, g = 0, b = 0)
69
- @r = r / 255.0
70
- @g = g / 255.0
71
- @b = b / 255.0
115
+ def initialize(r = 0, g = 0, b = 0, radix = 255.0, &block) # :yields self:
116
+ @r, @g, @b = [ r, g, b ].map { |v| Color.normalize(v / radix) }
117
+ block.call(self) if block
72
118
  end
73
119
 
74
120
  # Present the colour as a DeviceRGB fill colour string for PDF. This will
@@ -83,8 +129,8 @@ class Color::RGB
83
129
  PDF_FORMAT_STR % [ @r, @g, @b, "RG" ]
84
130
  end
85
131
 
86
- # Present the colour as an HTML/CSS colour string.
87
- def html
132
+ # Present the colour as an RGB hex triplet.
133
+ def hex
88
134
  r = (@r * 255).round
89
135
  r = 255 if r > 255
90
136
 
@@ -94,7 +140,12 @@ class Color::RGB
94
140
  b = (@b * 255).round
95
141
  b = 255 if b > 255
96
142
 
97
- "#%02x%02x%02x" % [ r, g, b ]
143
+ "%02x%02x%02x" % [ r, g, b ]
144
+ end
145
+
146
+ # Present the colour as an HTML/CSS colour string.
147
+ def html
148
+ "##{hex}"
98
149
  end
99
150
 
100
151
  # Present the colour as an RGB HTML/CSS colour string (e.g., "rgb(0%, 50%,
@@ -267,11 +318,7 @@ class Color::RGB
267
318
  # Color::RGB::DarkBlue.adjust_brightness(10)
268
319
  # Color::RGB::DarkBlue.adjust_brightness(-10)
269
320
  def adjust_brightness(percent)
270
- percent /= 100.0
271
- percent += 1.0
272
- percent = [ percent, 2.0 ].min
273
- percent = [ 0.0, percent ].max
274
-
321
+ percent = normalize_percent(percent)
275
322
  hsl = to_hsl
276
323
  hsl.l *= percent
277
324
  hsl.to_rgb
@@ -284,11 +331,7 @@ class Color::RGB
284
331
  # Color::RGB::DarkBlue.adjust_saturation(10)
285
332
  # Color::RGB::DarkBlue.adjust_saturation(-10)
286
333
  def adjust_saturation(percent)
287
- percent /= 100.0
288
- percent += 1.0
289
- percent = [ percent, 2.0 ].min
290
- percent = [ 0.0, percent ].max
291
-
334
+ percent = normalize_percent(percent)
292
335
  hsl = to_hsl
293
336
  hsl.s *= percent
294
337
  hsl.to_rgb
@@ -301,11 +344,7 @@ class Color::RGB
301
344
  # Color::RGB::DarkBlue.adjust_hue(10)
302
345
  # Color::RGB::DarkBlue.adjust_hue(-10)
303
346
  def adjust_hue(percent)
304
- percent /= 100.0
305
- percent += 1.0
306
- percent = [ percent, 2.0 ].min
307
- percent = [ 0.0, percent ].max
308
-
347
+ percent = normalize_percent(percent)
309
348
  hsl = to_hsl
310
349
  hsl.h *= percent
311
350
  hsl.to_rgb
@@ -399,14 +438,7 @@ class Color::RGB
399
438
  # The addition is done using the RGB Accessor methods to ensure a valid
400
439
  # colour in the result.
401
440
  def +(other)
402
- other = other.to_rgb
403
- rgb = self.dup
404
-
405
- rgb.r += other.r
406
- rgb.g += other.g
407
- rgb.b += other.b
408
-
409
- rgb
441
+ self.class.from_fraction(r + other.r, g + other.g, b + other.b)
410
442
  end
411
443
 
412
444
  # Subtracts another colour to the current colour. The other colour will be
@@ -416,14 +448,7 @@ class Color::RGB
416
448
  # The subtraction is done using the RGB Accessor methods to ensure a valid
417
449
  # colour in the result.
418
450
  def -(other)
419
- other = other.to_rgb
420
- rgb = self.dup
421
-
422
- rgb.r -= other.r
423
- rgb.g -= other.g
424
- rgb.b -= other.b
425
-
426
- rgb
451
+ self + (-other)
427
452
  end
428
453
 
429
454
  # Retrieve the maxmum RGB value from the current colour as a GrayScale
@@ -436,6 +461,66 @@ class Color::RGB
436
461
  def inspect
437
462
  "RGB [#{html}]"
438
463
  end
464
+
465
+ def to_a
466
+ [ r, g, b ]
467
+ end
468
+
469
+ # Numerically negate the color. This results in a color that is only
470
+ # usable for subtraction.
471
+ def -@
472
+ rgb = self.dup
473
+ rgb.instance_variable_set(:@r, -rgb.r)
474
+ rgb.instance_variable_set(:@g, -rgb.g)
475
+ rgb.instance_variable_set(:@b, -rgb.b)
476
+ rgb
477
+ end
478
+
479
+ private
480
+ def normalize_percent(percent)
481
+ percent /= 100.0
482
+ percent += 1.0
483
+ percent = [ percent, 2.0 ].min
484
+ percent = [ 0.0, percent ].max
485
+ percent
486
+ end
487
+ end
488
+
489
+ class << Color::RGB
490
+ private
491
+ def __named_color(mod, rgb, *names)
492
+ if names.any? { |n| mod.const_defined? n }
493
+ raise ArgumentError, "#{names.join(', ')} already defined in #{mod}"
494
+ end
495
+
496
+ names.each { |n| mod.const_set(n, rgb) }
497
+
498
+ rgb.names = names
499
+ rgb.names.each { |n| __by_name[n] = rgb }
500
+ __by_hex[rgb.hex] = rgb
501
+ rgb.freeze
502
+ end
503
+
504
+ def __by_hex
505
+ @__by_hex ||= {}
506
+ end
507
+
508
+ def __by_name
509
+ @__by_name ||= {}
510
+ end
511
+
512
+ def html_hexify(hex)
513
+ # When we can move to 1.9+ only, this will be \h
514
+ h = hex.to_s.downcase.scan(/[0-9a-f]/)
515
+ case h.size
516
+ when 3
517
+ h.map { |v| (v * 2) }.join
518
+ when 6
519
+ h.join
520
+ else
521
+ raise ArgumentError, "Not a supported HTML colour type."
522
+ end
523
+ end
439
524
  end
440
525
 
441
- require 'color/rgb-colors'
526
+ require 'color/rgb/colors'