colir 0.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.
@@ -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