xrvg 0.0.7 → 0.0.8

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.
data/lib/color.rb ADDED
@@ -0,0 +1,567 @@
1
+ # Color functionalities file. See
2
+ # - +Color+
3
+ # - +Palette+
4
+ # - +Gradient+
5
+
6
+ require 'utils'
7
+ require 'interpolation'
8
+ require 'shape'; # for gradient
9
+
10
+ module XRVG
11
+ #
12
+ # Color class
13
+ #
14
+ # = Basics
15
+ # Class Color consists in a 4D vector of (0.0..1.0) values, for red, blue, green, and opacity
16
+ # = Utilities
17
+ # Conversion from hsv and hsl color spaces available (see this link[http://en.wikipedia.org/wiki/HSV_color_space])
18
+ # = Future
19
+ # - Must use this library[https://rubyforge.org/projects/color/], to avoid effort duplication
20
+ # - Must add relative color operations as Nodebox wants to
21
+ # - Must optimize 4D vector operations (as C extension ?)
22
+ class Color
23
+
24
+ # Color builder
25
+ #
26
+ # only allows to build 4D vector, with composants between 0.0 and 1.0
27
+ def Color.[]( *args )
28
+ # TODO : check args number
29
+ Color.new( *args )
30
+ end
31
+
32
+ # builder
33
+ #
34
+ # r, g, b, a must be between 0.0 and 1.0
35
+ def initialize( r, g, b, a)
36
+ # cannot trim because otherwise color interpolation cannot be right !!
37
+ # r = Range.O.trim( r )
38
+ # g = Range.O.trim( g )
39
+ # b = Range.O.trim( b )
40
+ # a = Range.O.trim( a )
41
+ @items = [r,g,b,a]
42
+ end
43
+
44
+ # delegation componant indexation method
45
+ def [](index)
46
+ return @items[index]
47
+ end
48
+
49
+ # define addition operation, for interpolation
50
+ def +( other )
51
+ return Color[ self.r + other.r,
52
+ self.g + other.g,
53
+ self.b + other.b,
54
+ self.a + other.a ]
55
+ end
56
+
57
+ # define scalar multiplication, for interpolation
58
+ def *( scalar )
59
+ return Color[ self.r * scalar,
60
+ self.g * scalar,
61
+ self.b * scalar,
62
+ self.a * scalar ]
63
+ end
64
+
65
+ # return [r,g,b,a]
66
+ def rgba
67
+ return @items
68
+ end
69
+
70
+ # return hsva components
71
+ def hsva
72
+ return (Color.rgb2hsv(self.r, self.g, self.b) + [self.a])
73
+ end
74
+
75
+ # return hsla components
76
+ def hsla
77
+ return (Color.rgb2hsl(self.r, self.g, self.b) + [self.a])
78
+ end
79
+
80
+ # equality operator
81
+ def ==( other )
82
+ return (self.rgba == other.rgba)
83
+ end
84
+
85
+ # return the red composant
86
+ # Color[0.1,0.2,0.3,0.4].r => 0.1
87
+ # Color[0.1,0.2,0.3,0.4].r( 0.3 ) => Color[0.3,0.2,0.3,0.4]
88
+ def r(val=nil)
89
+ if not val
90
+ return self[0]
91
+ else
92
+ return Color[ val, self.g, self.b, self.a ]
93
+ end
94
+ end
95
+
96
+ # return the green composant
97
+ # Color[0.1,0.2,0.3,0.4].r => 0.2
98
+ def g(val=nil)
99
+ if not val
100
+ return self[1]
101
+ else
102
+ return Color[ self.r, val, self.b, self.a ]
103
+ end
104
+ end
105
+
106
+ # return the blue composant
107
+ # Color[0.1,0.2,0.3,0.4].r => 0.3
108
+ def b(val=nil)
109
+ if not val
110
+ return self[2]
111
+ else
112
+ return Color[ self.r, self.g, val, self.a ]
113
+ end
114
+ end
115
+
116
+ # return the opacity composant
117
+ # Color[0.1,0.2,0.3,0.4].r => 0.4
118
+ def a(val=nil)
119
+ if not val
120
+ return self[3]
121
+ else
122
+ return Color[ self.r, self.g, self.b, val ]
123
+ end
124
+ end
125
+
126
+ # get or set hue of color
127
+ # Color.red.hue => 0.0
128
+ # Color.red.hue( 0.1 ) => Color.hsva( 0.1, 1.0, 1.0, 1.0 )
129
+ def hue(newhue=nil)
130
+ if not newhue
131
+ return self.hsva[0]
132
+ else
133
+ hsva = self.hsva
134
+ hsva[0] = newhue
135
+ return Color.hsva( *hsva )
136
+ end
137
+ end
138
+
139
+ # set saturation of color
140
+ # Color.red.saturation => 1.0
141
+ # Color.red.saturation( 0.1 ) => Color.hsva( 0.0, 0.1, 1.0, 1.0 )
142
+ def saturation(newsat=nil)
143
+ if not newsat
144
+ return self.hsva[1]
145
+ else
146
+ hsva = self.hsva
147
+ hsva[1] = newsat
148
+ return Color.hsva( *hsva )
149
+ end
150
+ end
151
+
152
+ # set value (from hsv) of color
153
+ # Color.red.value => 1.0
154
+ # Color.red.value( 0.1 ) => Color.hsva( 0.0, 1.0, 0.1, 1.0 )
155
+ def value(newval=nil)
156
+ if not newval
157
+ return self.hsva[2]
158
+ else
159
+ hsva = self.hsva
160
+ hsva[2] = newval
161
+ return Color.hsva( *hsva )
162
+ end
163
+ end
164
+
165
+ # set lightness (from hsl) of color
166
+ # Color.white.lightness => 1.0
167
+ # Color.white.lightness( 0.1 ) => Color.hsla( 1.0, 1.0, 0.1, 1.0 )
168
+ def lightness(newlight=nil)
169
+ if not newlight
170
+ return self.hsla[2]
171
+ else
172
+ hsla = self.hsla
173
+ hsla[2] = newlight
174
+ return Color.hsla( *hsla )
175
+ end
176
+ end
177
+
178
+ # set the red composant
179
+ # Color[0.1,0.2,0.3,0.4].r = 0.5 => Color[0.5,0.2,0.3,0.4]
180
+ def r=(n)
181
+ @items[0]= n
182
+ end
183
+
184
+ # set the green composant
185
+ # Color[0.1,0.2,0.3,0.4].g = 0.5 => Color[0.1,0.5,0.3,0.4]
186
+ def g=(n)
187
+ @items[1] = n
188
+ end
189
+
190
+ # set the blue composant
191
+ # Color[0.1,0.2,0.3,0.4].b = 0.5 => Color[0.1,0.2,0.5,0.4]
192
+ def b=(n)
193
+ @items[2] = n
194
+ end
195
+
196
+ # set the opacity composant
197
+ # Color[0.1,0.2,0.3,0.4].a = 0.5 => Color[0.1,0.2,0.3,0.5]
198
+ def a=(n)
199
+ @items[3] = n
200
+ end
201
+
202
+ # compute complementary color
203
+ # Color.red.complement => Color.green
204
+ def complement
205
+ newvalues = self.rgba[0..-2].map {|v| Range.O.complement( v )}
206
+ newvalues += [self.a]
207
+ return Color[ *newvalues ]
208
+ end
209
+
210
+ # return an array containing colors on 255 integer format
211
+ # Color[0.0,1.0,0.0,1.0].format255 => [0,255,0,255]
212
+ def format255()
213
+ return @items.map {|v| (v * 255.0).to_i}
214
+ end
215
+
216
+ # return a random color vector, with 1.0 opacity !!
217
+ # Color.rand => Color[0.2345, 0.987623, 0.4123, 1.0]
218
+ def Color.rand( opacity=1.0 )
219
+ return Color[Kernel::rand,Kernel::rand,Kernel::rand,opacity]
220
+ end
221
+
222
+ # return a black color vector
223
+ def Color.black(opacity=1.0)
224
+ return Color[0.0, 0.0, 0.0, opacity]
225
+ end
226
+
227
+ # return a blue color vector
228
+ def Color.blue(opacity=1.0)
229
+ return Color[0.0, 0.0, 1.0, opacity]
230
+ end
231
+
232
+ # return a red color vector
233
+ def Color.red(opacity=1.0)
234
+ return Color[1.0, 0.0, 0.0, opacity]
235
+ end
236
+
237
+ # return a yellow color vector
238
+ def Color.yellow(opacity=1.0)
239
+ return Color[1.0, 1.0, 0.0, opacity]
240
+ end
241
+
242
+ # return a orange color vector
243
+ def Color.orange(opacity=1.0)
244
+ return Color[1.0, 0.5, 0.0, opacity]
245
+ end
246
+
247
+ # return a green color vector
248
+ def Color.green(opacity=1.0)
249
+ return Color[0.0, 1.0, 0.0, opacity]
250
+ end
251
+
252
+ # return a white color vector
253
+ def Color.white(opacity=1.0)
254
+ return Color[1.0, 1.0, 1.0, opacity]
255
+ end
256
+
257
+ # return a grey color vector
258
+ def Color.grey(light,opacity=1.0)
259
+ return Color[light, light, light, opacity]
260
+ end
261
+
262
+
263
+ # build a color vector from hsv parametrization (convert from hsv to rgb) h, s, v being between 0.0 and 1.0
264
+ #
265
+ # taken from wikipedia[http://en.wikipedia.org/wiki/HSV_color_space]
266
+ #
267
+ # error on algo with h = 1.0 => hi == 6 must be taken into account
268
+ def Color.hsva( h, s, v, a)
269
+ h = Range.O.trim( h )
270
+ s = Range.O.trim( s )
271
+ v = Range.O.trim( v )
272
+ a = Range.O.trim( a )
273
+ values = Color.hsv(h,s,v) + [a]
274
+ return Color[*values]
275
+ end
276
+
277
+ def Color.hsv( h, s, v)
278
+ if s == 0.0
279
+ return [v, v, v]
280
+ end
281
+ h *= 360.0
282
+ hi = (h/60.0).floor
283
+ if hi == 6
284
+ hi = 5
285
+ end
286
+ f = (h/60.0) - hi
287
+ p = v * ( 1 - s )
288
+ q = v * ( 1 - f * s )
289
+ t = v * ( 1 - ( 1 - f ) * s )
290
+ if hi == 0
291
+ return [ v, t, p]
292
+ end
293
+ if hi == 1
294
+ return [ q, v, p]
295
+ end
296
+ if hi == 2
297
+ return [ p, v, t]
298
+ end
299
+ if hi == 3
300
+ return [ p, q, v]
301
+ end
302
+ if hi == 4
303
+ return [ t, p, v]
304
+ end
305
+ if hi == 5
306
+ return [ v, p, q]
307
+ end
308
+ end
309
+
310
+
311
+ def Color.getHSLcomponent( tC, p, q ) #:nodoc:
312
+ while tC < 0.0
313
+ tC = tC + 1.0
314
+ end
315
+ while tC > 1.0
316
+ tC = tC - 1.0
317
+ end
318
+
319
+ if tC < (1.0 / 6.0)
320
+ tC = p + ( (q-p) * 6.0 * tC )
321
+ elsif tC >=(1.0 / 6.0) and tC < 0.5
322
+ tC = q
323
+ elsif tC >= 0.5 and tC < (2.0 / 3.0)
324
+ tC = p + ( (q-p) * 6.0 * ((2.0 / 3.0) - tC) )
325
+ else
326
+ tC = p
327
+ end
328
+ return tC
329
+ end
330
+
331
+ # build a color vector from hsl parametrization (convert from hsl to rgb) h, s, l being between 0.0 and 1.0
332
+ # taken from [[http://en.wikipedia.org/wiki/HSV_color_space]]
333
+ # h, s, l must be between 0.0 and 1.0
334
+ def Color.hsla( h, s, l, a)
335
+ Trace("Color hsla")
336
+ h = Range.O.trim( h )
337
+ s = Range.O.trim( s )
338
+ l = Range.O.trim( l )
339
+ a = Range.O.trim( a )
340
+ values = Color.hsl( h, s, l ) + [a]
341
+ return Color[*values]
342
+ end
343
+
344
+ def Color.hsl( h, s, l)
345
+ h *= 360.0
346
+ if l < 0.5
347
+ q = l * (1.0 + s)
348
+ else
349
+ q = l+ s - (l * s)
350
+ end
351
+ p = 2 * l - q
352
+ hk = h / 360.0
353
+ tR = hk + 1.0 / 3.0
354
+ tG = hk
355
+ tB = hk - 1.0 / 3.0
356
+
357
+ tR = self.getHSLcomponent( tR, p, q )
358
+ tG = self.getHSLcomponent( tG, p, q )
359
+ tB = self.getHSLcomponent( tB, p, q )
360
+ return [tR, tG, tB]
361
+ end
362
+
363
+ # from http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV
364
+ def Color.rgb2h(r,g,b)
365
+ result = 0.0
366
+ range = [r,g,b].range
367
+ if range.begin == range.end
368
+ result = 0.0
369
+ elsif range.end == r
370
+ result = (60.0 * (g - b) / range.size + 0.0)
371
+ elsif range.end == g
372
+ result = (60.0 * (b - r) / range.size + 120.0)
373
+ else
374
+ result = (60.0 * (r - g) / range.size + 240.0)
375
+ end
376
+ return (result % 360.0) / 360.0
377
+ end
378
+
379
+ def Color.rgb2sl(r,g,b)
380
+ range = [r,g,b].range
381
+ l = range.middle
382
+ s = 0.0
383
+ if range.begin == range.end
384
+ s = 0.0
385
+ elsif l <= 0.5
386
+ s = range.size / (2.0 * l)
387
+ else
388
+ s = range.size / (2.0 - 2.0 * l)
389
+ end
390
+ return [s,l]
391
+ end
392
+
393
+ def Color.rgb2sv(r,g,b)
394
+ range = [r,g,b].range
395
+ v = range.end
396
+ if v == 0.0
397
+ s = 0.0
398
+ else
399
+ s = (1.0 - (range.begin/range.end))
400
+ end
401
+ return [s,v]
402
+ end
403
+
404
+ def Color.rgb2hsl(r,g,b)
405
+ h = Color.rgb2h(r,g,b)
406
+ s, l = Color.rgb2sl(r,g,b)
407
+ return [h,s,l]
408
+ end
409
+
410
+ def Color.rgb2hsv(r,g,b)
411
+ h = Color.rgb2h(r,g,b)
412
+ s, v = Color.rgb2sv(r,g,b)
413
+ return [h,s,v]
414
+ end
415
+
416
+ # get svg description of a color
417
+ def svg
418
+ values = self[0..2].map do |v|
419
+ v = Range.O.trim( v )
420
+ (255.0 * v).to_i
421
+ end
422
+ return "rgb(#{values.join(",")})"
423
+ end
424
+
425
+ end
426
+
427
+ # class Palette
428
+ # = Intro
429
+ # Palette defines color palettes, as interpolation between color points. As such, use Interpolation module, so uses for the moment only linear interpolation.
430
+ # But once built with interpolation, palette provides a continuous color "interval", and so is Samplable !
431
+ # = Use
432
+ # palette = Palette[ :colorlist, [ 0.0, Color.blue, 0.5, Color.orange, 1.0, Color.yellow ] ]
433
+ # palette.rand( 10 ) # => return 10 random colors in palette
434
+ # palette.color( 0.5 ) # => Color.orange
435
+ class Palette
436
+ include Attributable
437
+ attribute :colorlist, nil, Array
438
+ attribute :interpoltype, :linear
439
+ attribute :interpoldomain, :rgb # can also be :hsv or :hsl
440
+
441
+ include Samplable
442
+ include Interpolation
443
+
444
+ def initialize( *args )
445
+ super( *args )
446
+ build_interpolators
447
+ end
448
+
449
+ # build an interpolator by color componant
450
+ def build_interpolators()
451
+ vlists = [[],[],[],[]]
452
+ self.colorlist.foreach do |index, color|
453
+ values = []
454
+ if self.interpoldomain == :rgb
455
+ values = color.rgba
456
+ elsif self.interpoldomain == :hsv
457
+ values = color.hsva
458
+ elsif self.interpoldomain == :hsl
459
+ values = color.hsla
460
+ else
461
+ raise RuntimeError.new("#{self.interpoldomain} is an unknown color scheme.")
462
+ end
463
+ vlists[0] += [index, values[0] ]
464
+ vlists[1] += [index, values[1] ]
465
+ vlists[2] += [index, values[2] ]
466
+ vlists[3] += [index, values[3] ]
467
+ end
468
+ @interpolators = vlists.map {|samplelist| Interpolator[ :samplelist, samplelist, :interpoltype, self.interpoltype]}
469
+ end
470
+
471
+ # interpolators accessor (for debugging)
472
+ def interpolators()
473
+ return @interpolators
474
+ end
475
+
476
+ # overloading to reset interpolators if interpoltype changes
477
+ def interpoltype=(value)
478
+ @interpoltype = value
479
+ if @interpolators
480
+ self.build_interpolators
481
+ end
482
+ end
483
+
484
+ # method overloading to delegate computation to componant interpolators
485
+ def interpolate( dindex )
486
+ vs = self.interpolators.map {|inter| inter.interpolate( dindex )}
487
+ if self.interpoldomain == :rgb
488
+ return Color[ *vs ]
489
+ elsif self.interpoldomain == :hsv
490
+ return Color.hsva( *vs )
491
+ else
492
+ return Color.hsla( *vs )
493
+ end
494
+ end
495
+
496
+ # compute color given float pourcentage.
497
+ # Palette[ :colorlist, [ 0.0, Color.black, 1.0, Color.white ] ].sample( 0.5 ) => Color[0.5,0.5,0.5,1.O]
498
+ # "sample" method as defined in Samplable module
499
+ def color(dindex)
500
+ result = self.interpolate(dindex)
501
+ return result
502
+ end
503
+
504
+ # return a new palette by reversing the current one
505
+ def reverse()
506
+ newcolorlist = []
507
+ self.colorlist.reverse.foreach do |color, index|
508
+ newcolorlist += [(0.0..1.0).complement( index ), color]
509
+ end
510
+ return Palette[ :colorlist, newcolorlist ]
511
+ end
512
+
513
+ def apply_sample( abs ) #:nodoc:
514
+ Trace("Palette#apply_sample abs #{abs}")
515
+ return self.color( abs )
516
+ end
517
+
518
+
519
+ # alias apply_sample color
520
+ # alias apply_split ? => TODO
521
+ alias samplelist colorlist
522
+ alias colors samples
523
+ end
524
+
525
+ class Gradient < Palette #:nodoc:
526
+ def defsvg()
527
+ raise NotImplementedError.new("#{self.class.name}#defsvg is an abstract method.")
528
+ end
529
+ end
530
+
531
+
532
+ class LinearGradient < Gradient #:nodoc:
533
+
534
+ def svgdef
535
+ template = '<linearGradient id="%ID%" x1="0%" y1="0%" x2="0%" y2="100%">%stops%</linearGradient>'
536
+ stoptemplate = '<stop offset="%offset%" stop-color="%color%" stop-opacity="%opacity%"/>'
537
+
538
+ stops = "\n"
539
+ self.colorlist.foreach do |index, color|
540
+ stops += stoptemplate.subreplace( {"%offset%" => index, "%color%" => color.svg, "%opacity%" => color.a} )
541
+ stops += "\n"
542
+ end
543
+
544
+ return template.subreplace( {"%stops%" => stops} )
545
+ end
546
+ end
547
+
548
+ class CircularGradient < Gradient #:nodoc:
549
+ attribute :circle, nil, Circle
550
+
551
+ def svgdef
552
+ template = '<radialGradient id="%ID%" gradientUnits="userSpaceOnUse" cx="%cx%" cy="%cy%" r="%r%">%stops%</radialGradient>'
553
+ stoptemplate = '<stop offset="%offset%" stop-color="%color%" stop-opacity="%opacity%"/>'
554
+
555
+ stops = "\n"
556
+ self.colorlist.foreach do |index, color|
557
+ stops += stoptemplate.subreplace( {"%offset%" => index, "%color%" => color.svg, "%opacity%" => color.a} )
558
+ stops += "\n"
559
+ end
560
+
561
+ return template.subreplace( {"%stops%" => stops,
562
+ "%cx%" => circle.center.x,
563
+ "%cy%" => circle.center.y,
564
+ "%r%" => circle.radius} )
565
+ end
566
+ end
567
+ end