colir 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,188 @@
1
+ require 'bigdecimal'
2
+
3
+ class Colir
4
+ # The module provides methods for RGB to HSL and HSL to RGB conversions. I
5
+ # just ported it from a C implementation.
6
+ # @see http://goo.gl/WLwi6
7
+ # @since 0.0.1
8
+ # @private
9
+ module HSLRGB
10
+
11
+ # Provides helper methods for the RGB colours.
12
+ module RGB
13
+
14
+ # The possible values of an RGB colour.
15
+ RGB_RANGE = 0x000000..0xffffff
16
+
17
+ # The possible values of an RGB byte.
18
+ RGB_BYTE_RANGE = 0x00..0xff
19
+
20
+ # Performs a validation check for the +rgb+.
21
+ #
22
+ # @example Truthy
23
+ # RGB.valid_rgb?([255, 13, 0]) #=> true
24
+ #
25
+ # @example Falsey
26
+ # RGB.valid_rgb?([256, 13, -1]) #=> false
27
+ #
28
+ # @param [Array<Integer>] rgb The RGB colour to be checked
29
+ # @return [Boolean] true if the given +rgb+ colour lies within the
30
+ # `RGB_BYTE_RANGE`
31
+ def self.valid_rgb?(rgb)
32
+ rgb.all? { |b| valid_byte?(b) } && (rgb.length == 3)
33
+ end
34
+
35
+ # Converts +hex+ number to the RGB array.
36
+ #
37
+ # @example
38
+ # RGB.int_bytes(0x123456) #=> [18, 52, 86]
39
+ #
40
+ # @param [String] hex The hex number expressed without the preceding `0x`
41
+ # @return [Array<Integer>] the RGB array
42
+ def self.int_bytes(hex)
43
+ hex.to_s(16).rjust(6, '0').scan(/../).map { |b| b.to_i(16) }
44
+ end
45
+
46
+ def self.valid_byte?(byte)
47
+ RGB_BYTE_RANGE.cover?(byte) && byte.integer?
48
+ end
49
+ private_class_method :valid_byte?
50
+ end
51
+
52
+ # Provides helper methods for HSL colours.
53
+ module HSL
54
+
55
+ # The possible values for the hue.
56
+ H_RANGE = 0..360
57
+
58
+ # The possible values for the saturation.
59
+ S_RANGE = 0..1
60
+
61
+ # The possible values for the lightness.
62
+ L_RANGE = S_RANGE
63
+
64
+ # Performs a validation check for the +hsl+.
65
+ #
66
+ # @example Truthy
67
+ # RGB.valid_hsl?([180, 1, 0.55]) #=> true
68
+ #
69
+ # @example Falsey
70
+ # RGB.valid_hsl?([180, 1.1, 0.55]) #=> false
71
+ #
72
+ # @param [Array<Number>] hsl The HSL colour to be checked
73
+ # @return [Boolean] true if the given +hsl+ colour lies within the
74
+ # `H_RANGE`, `S_RANGE`, `L_RANGE`
75
+ def self.valid_hsl?(hsl)
76
+ valid_hue?(hsl[0]) && S_RANGE.cover?(hsl[1]) &&
77
+ L_RANGE.cover?(hsl[2]) && (hsl.length == 3)
78
+ end
79
+
80
+ def self.valid_hue?(hue)
81
+ H_RANGE.cover?(hue) && hue.integer?
82
+ end
83
+ private_class_method :valid_hue?
84
+ end
85
+
86
+ # The cached array of arrays, which contains order codes for the model from
87
+ # `::hsl_to_rgb`. The model is an array, where the needed for conversion
88
+ # values are calculated only once. The order codes allows to refer to the
89
+ # calculated model array values via indices. Hopefully, it saves some CPU
90
+ # cycles.
91
+ RGB_ORDER_CODES = [
92
+ [0, 1, 2], # [0, 60)
93
+ [1, 0, 2], # [60, 120)
94
+ [2, 0, 1], # [120, 180)
95
+ [2, 1, 0], # [180, 240)
96
+ [1, 2, 0], # [240, 300)
97
+ [0, 2, 1], # [300, 360)
98
+ ]
99
+
100
+ # The code returned for 0 or 360 degrees.
101
+ RGB_DEFAULT_CODE = [2, 2, 2]
102
+
103
+ # Converts an RGB colour to its HSL counterpart.
104
+ #
105
+ # @example
106
+ # rgb = 'c0c0c0'.scan(/../).map { |b| b.to_i(16) } #=> [192, 192, 192]
107
+ # HSLRGB.rgb_to_hsl(192, 192, 192) #=> [0, 0.0, 0.75]
108
+ #
109
+ # @param [Integer] red Possible values: 0..255
110
+ # @param [Integer] green Possible values: 0..255
111
+ # @param [Integer] blue Possible values: 0..255
112
+ # @return [Array<Integer,BigDecimal>] the converted HSL representation of
113
+ # an RGB colour
114
+ # @raise [RangeError] if one of the parameters doesn't lie within the
115
+ # accepted range
116
+ # @see http://en.wikipedia.org/wiki/HSL_color_space HSL color space
117
+ # @see ::hsl_to_rgb
118
+ def self.rgb_to_hsl(red, green, blue)
119
+ validate_rgb!([red, green, blue])
120
+
121
+ red, green, blue = [red, green, blue].map { |b| b / BigDecimal('255.0') }
122
+ min, max = [red, green, blue].minmax
123
+ chroma = max - min
124
+
125
+ lightness = (min + max) * BigDecimal('0.5')
126
+
127
+ hue = 0
128
+ saturation = BigDecimal('0.0')
129
+
130
+ if chroma.nonzero?
131
+ hue = case max
132
+ when red then (green - blue) / chroma % 6
133
+ when green then (blue - red) / chroma + 2
134
+ else (red - green) / chroma + 4
135
+ end
136
+ hue = (hue * 60).round
137
+ saturation = chroma / (1 - (2 * lightness - 1).abs)
138
+ end
139
+
140
+ [hue, saturation, lightness]
141
+ end
142
+
143
+ def self.validate_rgb!(rgb)
144
+ unless RGB.valid_rgb?(rgb)
145
+ raise RangeError, 'out of allowed RGB byte values (0-255)'
146
+ end
147
+ end
148
+ private_class_method :validate_rgb!
149
+
150
+ def self.validate_hsl!(hsl)
151
+ unless HSL.valid_hsl?(hsl)
152
+ raise RangeError, 'out of allowed HSL values (h:0-360 s:0-1 l:0-1)'
153
+ end
154
+ end
155
+ private_class_method :validate_hsl!
156
+
157
+ # Converts an HSL colour to its RGB counterpart. The algorithm is correct,
158
+ # but sometimes you'll notice little laxity (because of rounding problems).
159
+ #
160
+ # @example
161
+ # HSLRGB.hsl_to_rgb(180, 1, 0.55) #=> [26, 255, 255]
162
+ #
163
+ # @param [Integer] hue Possible values: 0..360
164
+ # @param [BigDecimal] saturation Possible values: 0..1
165
+ # @param [BigDecimal] lightness Possible values: 0..1
166
+ # @return [Array<Integer>] the converted RGB representation of a HSL colour
167
+ # @raise [RangeError] if one of the parameters doesn't lie within the
168
+ # accepted range
169
+ # @see ::rgb_to_hsl
170
+ def self.hsl_to_rgb(hue, saturation, lightness)
171
+ validate_hsl!([hue, saturation, lightness])
172
+
173
+ chroma = (1 - (2 * lightness - 1).abs) * saturation
174
+ a = 1 * (lightness - 0.5 * chroma)
175
+ b = chroma * (1 - ((hue / BigDecimal('60.0')).modulo(2) - 1).abs)
176
+
177
+ degrees = Array.new(7) { |idx| idx * 60 }.each
178
+ model = [chroma+a, a+b, a]
179
+
180
+ RGB_ORDER_CODES.find(->{RGB_DEFAULT_CODE}) {
181
+ (degrees.next...degrees.peek) === hue
182
+ }.map { |id|
183
+ (model[id] * 255).round
184
+ }
185
+ end
186
+
187
+ end
188
+ end
@@ -0,0 +1,407 @@
1
+ require_relative 'helper'
2
+
3
+ describe Colir do
4
+ describe "some shortcuts" do
5
+ it "has a shortcut for red colour" do
6
+ Colir.red.hexa.should == 0xff000000
7
+ end
8
+
9
+ it "has a shortcut for blanched almond colour" do
10
+ Colir.blanched_almond.hexa.should == 0xffebcd00
11
+ end
12
+
13
+ it "has a shortcut royal blue colour" do
14
+ Colir.royal_blue.hexa.should == 0x4169e100
15
+ end
16
+
17
+ it "can set the colour's transparency" do
18
+ Colir.snow(0.5).transparency.should == 0.5
19
+ end
20
+ end
21
+
22
+ describe "#intialize" do
23
+ it "accepts transparency parameter" do
24
+ Colir.new(0x012345, 0.77).transparency.should == 0.77
25
+ end
26
+
27
+ it "doesn't allow too low HEX number" do
28
+ should.raise(RangeError) {
29
+ Colir.new(-0x000001)
30
+ }.message.should =~ /out of allowed RGB values/
31
+ end
32
+
33
+ it "doesn't allow too high HEX number" do
34
+ should.raise(RangeError) {
35
+ Colir.new(0x1000000)
36
+ }.message.should =~ /out of allowed RGB values/
37
+ end
38
+ end
39
+
40
+ describe "#hex" do
41
+ it "returns the hex code of a colour (without transparency)" do
42
+ Colir.new(0x123456, 0.1).hex.should == 0x123456
43
+ end
44
+ end
45
+
46
+ describe "#hexa" do
47
+ it "returns the hex code of a colour (with transparency)" do
48
+ Colir.new(0x123456, 0.1).hexa.should == 0x1234560a
49
+ end
50
+ end
51
+
52
+ describe "#transparency" do
53
+ before do
54
+ @colir = Colir.new(0x123456)
55
+ end
56
+
57
+ it "defaults to no transparency" do
58
+ @colir.transparency.should == 0.0
59
+ end
60
+
61
+ it "can be set to a specific value" do
62
+ Colir.new(0x123456, 0.98).transparency.should == 0.98
63
+ end
64
+
65
+ it "must lie within the range of 0 to 1 (min)" do
66
+ should.raise(RangeError) {
67
+ Colir.new(0x123456, -0.01)
68
+ }.message.should =~ /out of allowed transparency values \(0-1\)/
69
+ end
70
+
71
+ it "must lie within the range of 0 to 1 (max)" do
72
+ should.raise(RangeError) {
73
+ Colir.new(0x123456, 1.01)
74
+ }.message.should =~ /out of allowed transparency values \(0-1\)/
75
+ end
76
+
77
+ it "returns a Float value" do
78
+ @colir.transparency.should.be kind_of(Float)
79
+ end
80
+ end
81
+
82
+ describe "#transparency=" do
83
+ before do
84
+ @colir = Colir.new(0x123456)
85
+ end
86
+
87
+ it "can be set" do
88
+ @colir.transparency = 0.89
89
+ @colir.transparency.should == 0.89
90
+ end
91
+
92
+ it "raises error on a bad value" do
93
+ should.raise(RangeError) {
94
+ @colir.transparency = 1.01
95
+ }.message.should =~ /out of allowed transparency values/
96
+ end
97
+ end
98
+
99
+ describe "#transparent!" do
100
+ it "changes the object's transparency to full transparency" do
101
+ colir = Colir.new(0x123456, 0.3).transparent!
102
+ colir.transparency.should == 1.0
103
+ end
104
+ end
105
+
106
+ describe "#opaque!" do
107
+ it "changes the object's transparency to no transparency" do
108
+ colir = Colir.new(0x123456, 0.3).opaque!
109
+ colir.transparency.should == 0.0
110
+ end
111
+ end
112
+
113
+ describe "#shade" do
114
+ before do
115
+ @colir = Colir.new(0x123456)
116
+ end
117
+
118
+ it "returns 0 colour shade for a colour without shades" do
119
+ @colir.shade.should == 0
120
+ end
121
+
122
+ it "returns -1 for a darker colour" do
123
+ @colir.darken.shade.should == -1
124
+ end
125
+
126
+ it "returns 1 for a lighter colour" do
127
+ @colir.lighten.shade.should == 1
128
+ end
129
+ end
130
+
131
+ describe "#reset_shade" do
132
+ before do
133
+ @colir = Colir.new(0x123456)
134
+ end
135
+
136
+ it "resets the colour to its initial state" do
137
+ before = @colir.shade
138
+ 3.times { @colir.darken }
139
+ @colir.shade.should.not == before
140
+ @colir.reset_shade
141
+ @colir.shade.should == before
142
+ end
143
+
144
+ it "restores the hex number" do
145
+ before = @colir.hex
146
+ 3.times { @colir.darken }
147
+ @colir.hex.should.not == before
148
+ @colir.reset_shade
149
+ @colir.hex.should == before
150
+ end
151
+ end
152
+
153
+ describe "#darker" do
154
+ before do
155
+ @colir = Colir.new(0x123456)
156
+ end
157
+
158
+ it "returns a new object" do
159
+ before_id = @colir.object_id
160
+ after_id = @colir.darker.object_id
161
+ after_id.should.not == before_id
162
+ end
163
+
164
+ it "doesn't modify the base colour" do
165
+ before = @colir.hex
166
+ @colir.darker
167
+ @colir.hex.should == before
168
+ end
169
+
170
+ it "returns a new colour, but a little bit darker" do
171
+ @colir.darker.shade.should == -1
172
+ end
173
+ end
174
+
175
+ describe "#lighter" do
176
+ before do
177
+ @colir = Colir.new(0x123456)
178
+ end
179
+
180
+ it "returns a new object" do
181
+ before_id = @colir.object_id
182
+ after_id = @colir.lighter.object_id
183
+ after_id.should.not == before_id
184
+ end
185
+
186
+ it "doesn't modify the base colour" do
187
+ before = @colir.hex
188
+ @colir.lighter
189
+ @colir.hex.should == before
190
+ end
191
+
192
+ it "returns a new colour, but a little bit darker" do
193
+ @colir.lighter.shade.should == 1
194
+ end
195
+ end
196
+
197
+ describe "#darken" do
198
+ before do
199
+ @colir = Colir.new(0x123456)
200
+ end
201
+
202
+ it "modifies the base colour" do
203
+ old_colour = @colir.hex
204
+ @colir.darken
205
+ @colir.hex.should.not == old_colour
206
+ end
207
+
208
+ it "makes the current colour darker" do
209
+ @colir.darken
210
+ @colir.hex.should == 0x0e2a45
211
+ end
212
+
213
+ it "makes the current colour even more dark" do
214
+ 3.times { @colir.darken }
215
+ @colir.hex.should == 0x071522
216
+ end
217
+
218
+ describe "lower limit" do
219
+ before do
220
+ 5.times { @colir.darken }
221
+ end
222
+
223
+ it "stops darkening the current colour when meets the limit of shades" do
224
+ old_hex = @colir.hex
225
+ @colir.darken.hex.should == old_hex
226
+ end
227
+
228
+ it "finally reaches black colour" do
229
+ @colir.hex.should == 0x000000
230
+ end
231
+ end
232
+ end
233
+
234
+ describe "#lighten" do
235
+ before do
236
+ @colir = Colir.new(0x123456)
237
+ end
238
+
239
+ it "modifies the base colour" do
240
+ old_colour = @colir.hex
241
+ @colir.lighten
242
+ @colir.hex.should.not == old_colour
243
+ end
244
+
245
+ it "makes the current colour lighter" do
246
+ @colir.lighten
247
+ @colir.hex.should == 0x205d99
248
+ end
249
+
250
+ it "makes the current colour even more light" do
251
+ 3.times { @colir.lighten }
252
+ @colir.hex.should == 0x79aee3
253
+ end
254
+
255
+ describe "upper limit" do
256
+ before do
257
+ 5.times { @colir.lighten }
258
+ end
259
+
260
+ it "stops lightening the current colour when meets the limit of shades" do
261
+ old_hex = @colir.hex
262
+ @colir.lighten.hex.should == old_hex
263
+ end
264
+
265
+ it "finally reaches white colour" do
266
+ @colir.hex.should == 0xffffff
267
+ end
268
+ end
269
+ end
270
+
271
+ describe "shading" do
272
+ before do
273
+ @colir = Colir.new(0x123456)
274
+ end
275
+
276
+ it "darkens properly" do
277
+ @colir.darken.hex.should == 0x0e2a45
278
+ @colir.darken.hex.should == 0x0b1f34
279
+ @colir.darken.hex.should == 0x071522
280
+ @colir.darken.hex.should == 0x040a11
281
+ @colir.darken.hex.should == 0
282
+ end
283
+
284
+ it "lightens properly" do
285
+ @colir.lighten.hex.should == 0x205d99
286
+ @colir.lighten.hex.should == 0x3685d5
287
+ @colir.lighten.hex.should == 0x79aee3
288
+ @colir.lighten.hex.should == 0xbcd6f1
289
+ @colir.lighten.hex.should == 0xffffff
290
+ end
291
+
292
+ it "darkens and lightens properly" do
293
+ before = @colir.hex
294
+ 2.times { @colir.lighten }
295
+ 5.times { @colir.darken }
296
+ 10.times { @colir.lighten }
297
+ 5.times { @colir.darken }
298
+ after = @colir.hex
299
+ before.should == after
300
+ end
301
+
302
+ describe "lower limit - 0x000000" do
303
+ describe "darkening" do
304
+ before do
305
+ @colir = Colir.new(0x000000)
306
+ end
307
+
308
+ it "doesn't change the hex value" do
309
+ before = @colir.hex
310
+ after = @colir.darken.hex
311
+ after.should == before
312
+ end
313
+
314
+ it "does change the shade level" do
315
+ @colir.darken.shade.should == -1
316
+ end
317
+
318
+ it "doesn't overflow the shade level" do
319
+ 6.times { @colir.darken }
320
+ @colir.shade.should == -5
321
+ end
322
+ end
323
+
324
+ describe "lightening" do
325
+ before do
326
+ @colir = Colir.new(0x000000)
327
+ end
328
+
329
+ it "does change the hex value" do
330
+ before = @colir.hex
331
+ after = @colir.lighten.hex
332
+ after.should.not == before
333
+ end
334
+
335
+ it "does change the shade level" do
336
+ @colir.lighten.shade.should == 1
337
+ end
338
+
339
+ it "doesn't overflow the shade level" do
340
+ 6.times { @colir.lighten }
341
+ @colir.shade.should == 5
342
+ end
343
+ end
344
+ end
345
+
346
+ describe "very close to lower limit" do
347
+ describe "darkening" do
348
+ before do
349
+ @colir = Colir.new(0x000001)
350
+ end
351
+
352
+ it "does change the hex value when it's time" do
353
+ before = @colir.hex
354
+ 3.times { @colir.darken }
355
+ after = @colir.hex
356
+ after.should.not == before
357
+ end
358
+
359
+ it "stops darkening when reaches the lower limit" do
360
+ 3.times { @colir.darken }
361
+ before = @colir.hex
362
+ after = @colir.darken.hex
363
+ after.should == before
364
+ end
365
+
366
+ it "keeps changing the shade level" do
367
+ 2.times { @colir.darken }
368
+ @colir.shade.should == -2
369
+
370
+ 2.times { @colir.darken }
371
+ @colir.shade.should == -4
372
+ end
373
+ end
374
+ end
375
+
376
+ describe "very close to upper limit" do
377
+ describe "lightening" do
378
+ before do
379
+ @colir = Colir.new(0xfffffe)
380
+ end
381
+
382
+ it "does change the hex value when it's time" do
383
+ before = @colir.hex
384
+ 3.times { @colir.lighten }
385
+ after = @colir.hex
386
+ after.should.not == before
387
+ end
388
+
389
+ it "stops lightening when reaches the upper limit" do
390
+ 3.times { @colir.lighten }
391
+ before = @colir.hex
392
+ after = @colir.lighten.hex
393
+ after.should == before
394
+ end
395
+
396
+ it "keeps changing the shade level" do
397
+ 2.times { @colir.lighten }
398
+ @colir.shade.should == 2
399
+
400
+ 2.times { @colir.lighten }
401
+ @colir.shade.should == 4
402
+ end
403
+ end
404
+ end
405
+
406
+ end
407
+ end