xrvg 0.0.3 → 0.0.4

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 (59) hide show
  1. data/CHANGES +21 -0
  2. data/README +3 -3
  3. data/Rakefile +4 -4
  4. data/examples/bezierbasic.rb +1 -0
  5. data/examples/bezierbasicvector.rb +1 -0
  6. data/examples/foreach.rb +1 -0
  7. data/examples/geodash.rb +1 -0
  8. data/examples/geodash2.rb +1 -0
  9. data/examples/hellocrown.rb +1 -0
  10. data/examples/hellocrown2.rb +1 -0
  11. data/examples/hellocrownrecurse.rb +1 -0
  12. data/examples/helloworldcompact.rb +1 -0
  13. data/examples/helloworldexpanded.rb +1 -0
  14. data/examples/multibezierbasic.rb +1 -0
  15. data/examples/palette_circle.rb +1 -0
  16. data/examples/randomdash.rb +1 -0
  17. data/examples/range_examples.rb +1 -0
  18. data/examples/range_examples2.rb +1 -0
  19. data/examples/sample.rb +1 -0
  20. data/examples/simpledash.rb +1 -0
  21. data/examples/uplets.rb +1 -0
  22. data/lib/bezier.rb +21 -55
  23. data/lib/bezierbuilders.rb +194 -0
  24. data/lib/beziermotifs.rb +114 -0
  25. data/lib/bezierspline.rb +20 -75
  26. data/lib/beziertools.rb +211 -0
  27. data/lib/color.rb +26 -7
  28. data/lib/fitting.rb +203 -0
  29. data/lib/frame.rb +2 -1
  30. data/lib/geometry2D.rb +6 -5
  31. data/lib/interbezier.rb +87 -0
  32. data/lib/interpolation.rb +6 -5
  33. data/lib/parametriclength.rb +87 -0
  34. data/lib/render.rb +4 -9
  35. data/lib/samplation.rb +71 -82
  36. data/lib/shape.rb +47 -25
  37. data/lib/style.rb +2 -1
  38. data/lib/trace.rb +2 -0
  39. data/lib/utils.rb +111 -17
  40. data/lib/xrvg.rb +16 -6
  41. data/test/test_attributable.rb +34 -2
  42. data/test/test_bezier.rb +93 -2
  43. data/test/test_bezierbuilders.rb +92 -0
  44. data/test/test_beziertools.rb +97 -0
  45. data/test/test_color.rb +65 -24
  46. data/test/test_fitting.rb +47 -0
  47. data/test/test_frame.rb +7 -2
  48. data/test/test_geometry2D.rb +26 -7
  49. data/test/test_interbezier.rb +29 -0
  50. data/test/test_interpolation.rb +16 -1
  51. data/test/test_parametric_length.rb +15 -0
  52. data/test/test_render.rb +54 -6
  53. data/test/test_shape.rb +103 -10
  54. data/test/test_trace.rb +13 -0
  55. data/test/test_utils.rb +114 -12
  56. data/test/test_xrvg.rb +3 -0
  57. metadata +16 -5
  58. data/lib/assertion.rb +0 -14
  59. data/lib/attributable.rb +0 -152
data/lib/color.rb CHANGED
@@ -8,8 +8,8 @@ require 'interpolation'
8
8
  require 'attributable'
9
9
  require 'utils'
10
10
  require 'shape'; # for gradient
11
- require 'matrix'
12
11
 
12
+ module XRVG
13
13
  #
14
14
  # Color class
15
15
  #
@@ -35,6 +35,10 @@ class Color
35
35
  #
36
36
  # r, g, b, a must be between 0.0 and 1.0
37
37
  def initialize( r, g, b, a)
38
+ r = Range.O.trim( r )
39
+ g = Range.O.trim( g )
40
+ b = Range.O.trim( b )
41
+ a = Range.O.trim( a )
38
42
  @items = [r,g,b,a]
39
43
  end
40
44
 
@@ -96,31 +100,31 @@ class Color
96
100
  # set the red composant
97
101
  # Color[0.1,0.2,0.3,0.4].r = 0.5 => Color[0.5,0.2,0.3,0.4]
98
102
  def r=(n)
99
- self[0]= n
103
+ @items[0]= n
100
104
  end
101
105
 
102
106
  # set the green composant
103
107
  # Color[0.1,0.2,0.3,0.4].g = 0.5 => Color[0.1,0.5,0.3,0.4]
104
108
  def g=(n)
105
- self[1] = n
109
+ @items[1] = n
106
110
  end
107
111
 
108
112
  # set the blue composant
109
113
  # Color[0.1,0.2,0.3,0.4].b = 0.5 => Color[0.1,0.2,0.5,0.4]
110
114
  def b=(n)
111
- self[2] = n
115
+ @items[2] = n
112
116
  end
113
117
 
114
118
  # set the opacity composant
115
119
  # Color[0.1,0.2,0.3,0.4].a = 0.5 => Color[0.1,0.2,0.3,0.5]
116
120
  def a=(n)
117
- self[3] = n
121
+ @items[3] = n
118
122
  end
119
123
 
120
124
  # return an array containing colors on 255 integer format
121
125
  # Color[0.0,1.0,0.0,1.0].format255 => [0,255,0,255]
122
126
  def format255()
123
- return self.map {|v| (v * 255.0).to_i}
127
+ return @items.map {|v| (v * 255.0).to_i}
124
128
  end
125
129
 
126
130
  # return a random color vector, with 1.0 opacity !!
@@ -165,13 +169,23 @@ class Color
165
169
  end
166
170
 
167
171
  # build a color vector from hsv parametrization (convert from hsv to rgb) h, s, v being between 0.0 and 1.0
172
+ #
168
173
  # taken from wikipedia[http://en.wikipedia.org/wiki/HSV_color_space]
174
+ #
175
+ # error on algo with h = 1.0 => hi == 6 mustbe taken into account
169
176
  def Color.hsv( h, s, v, a)
177
+ h = Range.O.trim( h )
178
+ s = Range.O.trim( s )
179
+ v = Range.O.trim( v )
180
+ a = Range.O.trim( a )
170
181
  if s == 0.0
171
182
  return Color[v, v, v, a]
172
183
  end
173
184
  h *= 360.0
174
185
  hi = (h/60.0).floor
186
+ if hi == 6
187
+ hi = 5
188
+ end
175
189
  f = (h/60.0) - hi
176
190
  p = v * ( 1 - s )
177
191
  q = v * ( 1 - f * s )
@@ -220,6 +234,10 @@ class Color
220
234
  # taken from [[http://en.wikipedia.org/wiki/HSV_color_space]]
221
235
  # h, s, l must be between 0.0 and 1.0
222
236
  def Color.hsl( h, s, l, a)
237
+ h = Range.O.trim( h )
238
+ s = Range.O.trim( s )
239
+ l = Range.O.trim( l )
240
+ a = Range.O.trim( a )
223
241
  h *= 360.0
224
242
  if l < 0.5
225
243
  q = l * (1.0 + s)
@@ -292,7 +310,7 @@ end
292
310
 
293
311
  class Gradient < Palette #:nodoc:
294
312
  def defsvg()
295
- Kernel::raise("Gradient::defsvg must be redefined in subclasses")
313
+ raise NotImplementedError.new("#{self.class.name}#defsvg is an abstract method.")
296
314
  end
297
315
  end
298
316
 
@@ -332,3 +350,4 @@ class CircularGradient < Gradient #:nodoc:
332
350
  "%r%" => circle.radius} )
333
351
  end
334
352
  end
353
+ end
data/lib/fitting.rb ADDED
@@ -0,0 +1,203 @@
1
+ #
2
+ # Fitting file. See +Fitting+
3
+ #
4
+
5
+ require 'matrix'; # for matrix inversion
6
+ require 'bezier.rb'; # for error computation
7
+
8
+ module XRVG
9
+ #
10
+ # = Fitting computation class
11
+ # == Intro
12
+ # Used to compute cubic curve fitting on a list of points (that is sampling inverse operation). Only 2D.
13
+ # == Example
14
+ # Compute the most fitting single piece bezier curve given list of points
15
+ # bezier = Fitting.compute( points )
16
+ # Compute multipieces bezier curve given list of points
17
+ # bezier = Fitting.adaptative_compute( points )
18
+ class Fitting
19
+
20
+ # compute first parameter t estimated values from length between consecutive points
21
+ def Fitting.initparameters( pointlist, parameters=nil ) #:nodoc:
22
+ lengths = [0.0]
23
+ pointlist.pairs do |p1, p2|
24
+ lengths.push( lengths[-1] + (p1-p2).r )
25
+ end
26
+ tlength = lengths[-1]
27
+ if not parameters
28
+ if not tlength == 0.0
29
+ parameters = lengths.map {|length| length / tlength}
30
+ else
31
+ parameters = [0.0] * pointlist.length
32
+ end
33
+ end
34
+ return [parameters, tlength]
35
+ end
36
+
37
+ # compute control points from polynomial bezier representation
38
+ #
39
+ # a, b, c, d are such as
40
+ # piece( t ) = at3 + bt2 + ct + d
41
+ def Fitting.bezierpiece( a, b, c, d )
42
+ p0 = d
43
+ p1 = p0 + c / 3.0
44
+ p2 = p1 + c / 3.0 + b / 3.0
45
+ p3 = p0 + c + b + a
46
+ return [p0, p1, p2, p3]
47
+ end
48
+
49
+
50
+ # Base method
51
+ #
52
+ # Given a pointlist, compute the closest matching cubic bezier curve
53
+ #
54
+ # Result is in the form [p1, pc1, pc2, p2], with [p1, pc1, pc2, p2] V2D
55
+ #
56
+ # maxerror is normalized with curve length. In case of good match (that is pointlist can be modelized by cubic bezier curve),
57
+ # result error will be under maxerror. If not, result may be above maxerror. In that case, computation is stopped because error
58
+ # no longer decrease, or because iteration is too long.
59
+ def Fitting.compute( pointlist, maxerror=0.01, maxiter=100 )
60
+ parameters, tlength = Fitting.initparameters( pointlist )
61
+ perror = 1.0
62
+ niter = 0
63
+ while true
64
+ bezier, coeffs = Fitting.iterate( pointlist, parameters )
65
+ error = Fitting.error( bezier, pointlist, parameters ) / tlength
66
+ parameters = Fitting.renormalize( bezier, coeffs, pointlist, parameters )
67
+ if (error < maxerror || (error-perror).abs < 0.00001 || niter > maxiter )
68
+ break
69
+ end
70
+ niter += 1
71
+ end
72
+ return bezier
73
+ end
74
+
75
+ # adaptative computation with automatic splitting if error not low enough, or if convergence is not fast enough
76
+ #
77
+ #
78
+ def Fitting.adaptative_compute( pointlist, maxerror=0.0001, maxiter=10, tlength=nil )
79
+ parameters, tlengthtmp = Fitting.initparameters( pointlist )
80
+ if parameters == [0] * pointlist.length
81
+ return [Bezier.single(:vector, pointlist[0], V2D::O, pointlist[0], V2D::O),0.0]
82
+ end
83
+ tlength ||= tlengthtmp
84
+ niter = 0
85
+ bezier = nil
86
+ while true
87
+ bezier, coeffs = Fitting.iterate( pointlist, parameters )
88
+ error = Fitting.error( bezier, pointlist, parameters ) / tlength
89
+ parameters = Fitting.renormalize( bezier, coeffs, pointlist, parameters )
90
+
91
+ # pointlist.length > 8 because matching with a bezier needs at least 4 points
92
+ if (niter > maxiter and error > maxerror and pointlist.length > 8)
93
+ pointlists = [pointlist[0..pointlist.length/2 - 1], pointlist[pointlist.length/2 - 1 ..-1]]
94
+ beziers = []
95
+ errors = []
96
+ pointlists.each do |subpointlist|
97
+ subbezier, suberror = Fitting.adaptative_compute( subpointlist, maxerror, maxiter, tlength )
98
+ beziers << subbezier
99
+ errors << suberror
100
+ end
101
+ bezier = beziers.sum
102
+ error = errors.max
103
+ break
104
+ elsif (error < maxerror || niter > maxiter)
105
+ break
106
+ end
107
+ perror = error
108
+ niter += 1
109
+ end
110
+ return [bezier, error]
111
+ end
112
+
113
+ # algo comes from http://www.tinaja.com/glib/bezdist.pdf
114
+ def Fitting.renormalize( bezier, coeffs, pointlist, parameters )
115
+ a3, a2, a1, a0 = coeffs
116
+ dxdu = Proc.new {|u| 3.0*a3.x*u**2 + 2.0*a2.x*u + a1.x}
117
+ dydu = Proc.new {|u| 3.0*a3.y*u**2 + 2.0*a2.y*u + a1.y}
118
+ container = V2D[]
119
+ z = Proc.new {|u,p4| p = bezier.point( u, container, :parameter ); (p.x - p4.x) * dxdu.call( u ) + (p.y - p4.y) * dydu.call( u )}
120
+ newparameters = []
121
+ [pointlist, parameters].forzip do |point, parameter|
122
+ u1 = parameter
123
+ if parameter < 0.99
124
+ u2 = parameter + 0.01
125
+ else
126
+ u2 = parameter - 0.01
127
+ end
128
+ z1 = z.call(u1,point)
129
+ z2 = z.call(u2,point)
130
+ if z1 == z2
131
+ u2 += 0.01
132
+ z2 = z.call(u2,point)
133
+ end
134
+ if z1 == z2
135
+ u2 -= 0.01
136
+ z2 = z.call(u2,point)
137
+ end
138
+ newparameters << (z2 * u1 - z1 * u2)/(z2-z1)
139
+ end
140
+ return newparameters
141
+ end
142
+
143
+ # error is max error between points in pointlist and points sampled from bezier with parameters
144
+ def Fitting.error( bezier, pointlist, parameters )
145
+ maxerror = 0.0
146
+ container = V2D[]
147
+ [pointlist, parameters].forzip do |point, parameter|
148
+ Trace("point #{point.inspect} parameter #{parameter}")
149
+ error = (point - bezier.point( parameter, container, :parameter )).r
150
+ if error > maxerror
151
+ maxerror = error
152
+ end
153
+ end
154
+ Trace("Fitting.error #{maxerror}")
155
+ return maxerror
156
+ end
157
+
158
+ # iterate method compute new bezier parameters from pointlist and previous bezier parameters
159
+ #
160
+ # Algo comes from http://www.tinaja.com/glib/bezdist.pdf
161
+ #
162
+ # TODO : optimized
163
+ def Fitting.iterate( pointlist, parameters )
164
+ p0 = pointlist[0]
165
+ p1 = pointlist[-1]
166
+
167
+ sumt0 = parameters.map{ |t| t**0.0 }.sum
168
+ sumt1 = parameters.map{ |t| t**1.0 }.sum
169
+ sumt2 = parameters.map{ |t| t**2.0 }.sum
170
+ sumt3 = parameters.map{ |t| t**3.0 }.sum
171
+ sumt4 = parameters.map{ |t| t**4.0 }.sum
172
+ sumt5 = parameters.map{ |t| t**5.0 }.sum
173
+ sumt6 = parameters.map{ |t| t**6.0 }.sum
174
+
175
+ psumt1 = [pointlist, parameters].forzip.foreach(2).map {|point, t| point * (t**1.0) }.inject(V2D::O){|sum, item| sum + item}
176
+ psumt2 = [pointlist, parameters].forzip.foreach(2).map {|point, t| point * (t**2.0) }.inject(V2D::O){|sum, item| sum + item}
177
+ psumt3 = [pointlist, parameters].forzip.foreach(2).map {|point, t| point * (t**3.0) }.inject(V2D::O){|sum, item| sum + item}
178
+
179
+ coeff11 = sumt6 - 2 * sumt4 + sumt2
180
+ coeff12 = sumt5 - sumt4 - sumt3 + sumt2
181
+
182
+ coeff21 = coeff12
183
+ coeff22 = sumt4 - 2 * sumt3 + sumt2
184
+
185
+ result1 = (p0 - p1) * (sumt4 - sumt2) - p0 * (sumt3 - sumt1) + psumt3 - psumt1
186
+ result2 = (p0 - p1) * (sumt3 - sumt2) - p0 * (sumt2 - sumt1) + psumt2 - psumt1
187
+
188
+ matrix = Matrix[ [coeff11, coeff12], [coeff21, coeff22] ]
189
+ matrixinv = matrix.inverse
190
+ ax, bx = (matrixinv * Vector[result1.x, result2.x])[0..-1]
191
+ ay, by = (matrixinv * Vector[result1.y, result2.y])[0..-1]
192
+
193
+ a = V2D[ax, ay]
194
+ b = V2D[bx, by]
195
+ d = p0
196
+ c = p1- (a + b + p0)
197
+
198
+ piece = Fitting.bezierpiece( a, b, c, d )
199
+ return [Bezier.raw( *piece ), [a, b, c, d] ]
200
+ end
201
+ end
202
+
203
+ end # end XRVG
data/lib/frame.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # See +Frame+
4
4
  require 'attributable'
5
5
 
6
+ module XRVG
6
7
  #
7
8
  # Frame class
8
9
  # = Intro
@@ -29,4 +30,4 @@ class Frame
29
30
  return false
30
31
  end
31
32
  end
32
-
33
+ end
data/lib/geometry2D.rb CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  require 'Attributable'
7
7
 
8
+ module XRVG
8
9
  #
9
10
  # 2D vector
10
11
  #
@@ -98,9 +99,9 @@ class V2D
98
99
 
99
100
  # method necessary to make V2D Ranges
100
101
  #
101
- # simply call succ on each coord
102
+ # simply add 1.0 to each coord
102
103
  def succ()
103
- return V2D[ self.x.succ, self.y.succ ]
104
+ return V2D[ self.x + 1.0, self.y + 1.0 ]
104
105
  end
105
106
 
106
107
  # compute length of 2D vector (r notation to be compatible with V2D)
@@ -169,8 +170,8 @@ class V2D
169
170
  return V2D[ -self.y, self.x ]
170
171
  end
171
172
 
172
- # compute the symetric of vector considered as point, with center of symetrie being "other" considered as point
173
- # V2D[1.0,2.0].sym( V2D[0.0,0.0] ) => V2D[-1.0,-2.0]
173
+ # compute the symetric of vector "other" considering self as symetry center
174
+ # V2D[1.0,2.0].sym( V2D[0.0,0.0] ) => V2D[2.0,4.0]
174
175
  def sym( other )
175
176
  return self * 2.0 - other
176
177
  end
@@ -230,4 +231,4 @@ class V2D
230
231
  return [xmax - xmin, ymax - ymin]
231
232
  end
232
233
  end
233
-
234
+ end
@@ -0,0 +1,87 @@
1
+ require 'bezier'
2
+
3
+ module XRVG
4
+ class InterBezier
5
+ include Attributable
6
+ attribute :bezierlist
7
+
8
+ include Interpolation
9
+
10
+ def initialize( *args )
11
+ super( *args )
12
+ self.init_interpolation_structures
13
+ end
14
+
15
+ def init_interpolation_structures
16
+ beziers = []
17
+ indexes = []
18
+ @bezierlist.foreach do |index, bezier|
19
+ beziers.push( bezier )
20
+ indexes.push( index )
21
+ end
22
+
23
+ lengthH = {}
24
+ alllengths = []
25
+ beziers.each do |bezier|
26
+ lengths = bezier.piecelengths
27
+ # Trace("bezier lengths #{lengths.inspect}")
28
+ lengthH[ bezier ] = lengths
29
+ alllengths += lengths
30
+ end
31
+ alllengths = Float.sort_float_list( alllengths )
32
+ # Trace("alllengths #{alllengths.inspect}")
33
+
34
+ newbezierlist = []
35
+ beziers.each do |bezier|
36
+ newpieces = []
37
+ initlengths = lengthH[ bezier ]
38
+ alllengths.pairs do |l1, l2|
39
+ newpieces += bezier.subbezier( l1, l2 ).pieces
40
+ end
41
+ newbezier = Bezier[ :pieces, newpieces ]
42
+ newbezierlist << newbezier
43
+ end
44
+
45
+ # Trace("newbezierlist #{newbezierlist.inspect}")
46
+ beziers = newbezierlist
47
+ bezierpointlists = beziers.map {|bezier| bezier.pointlist(:vector) }
48
+ # Trace("bezierpointlists #{bezierpointlists.map {|list| list.length}.inspect}")
49
+ pointsequencelist = bezierpointlists.forzip
50
+ @interpolatorlist = []
51
+ pointsequencelist.foreach(beziers.size) do |pointsequence|
52
+ interlist = [indexes, pointsequence].forzip
53
+ @interpolatorlist.push( Interpolator.new( :samplelist, interlist ) )
54
+ end
55
+ end
56
+
57
+ def interpolate( abs, container=nil )
58
+ pieces = []
59
+ @interpolatorlist.foreach(4) do |interpiece|
60
+ piece = interpiece.map {|inter| inter.interpolate( abs )}
61
+ pieces.push( [:vector] + piece )
62
+ end
63
+ return Bezier.multi( pieces )
64
+ end
65
+
66
+ include Samplable
67
+ alias apply_sample interpolate
68
+
69
+ end
70
+
71
+ class GradientBezier < InterBezier
72
+
73
+ # TODO : does not work !!
74
+ def samples( nsamples, &block )
75
+ return super( nsamples + 1, &block )
76
+ end
77
+
78
+ def apply_samples( samples )
79
+ samples = super( samples )
80
+ result = []
81
+ samples.pairs do |bezier1, bezier2|
82
+ result.push( ClosureBezier.build( :bezierlist, [bezier1, bezier2.reverse]) )
83
+ end
84
+ return result
85
+ end
86
+ end
87
+ end # XRVG
data/lib/interpolation.rb CHANGED
@@ -7,7 +7,9 @@
7
7
 
8
8
  require 'utils'
9
9
  require 'attributable'
10
+ require 'samplation'
10
11
 
12
+ module XRVG
11
13
  # Interpolation module
12
14
  # = Intro
13
15
  # Defines an interpolation service from a samplelist that must be a list [value1, index1, value2, index2, ..., valueN, indexN],
@@ -24,7 +26,7 @@ module Interpolation
24
26
  # for example, Palette redefines samplelist as
25
27
  # alias samplelist colorlist
26
28
  def samplelist()
27
- Kernel::raise("Interpolation::samplelist method must be redefined in subclasses")
29
+ raise NotImplementedError.new("#{self.class.name}#samplelist is an abstract method.")
28
30
  end
29
31
 
30
32
  # computing method
@@ -59,6 +61,8 @@ class Interpolator
59
61
  include Attributable
60
62
  attribute :samplelist
61
63
  include Interpolation
64
+ include Samplable
65
+ alias apply_sample interpolate
62
66
  end
63
67
 
64
68
  class QuadRange
@@ -70,10 +74,6 @@ class QuadRange
70
74
  @range = range
71
75
  end
72
76
 
73
- def limit
74
- return @limit
75
- end
76
-
77
77
  def range( index )
78
78
  if @limit
79
79
  if index < @limit
@@ -156,3 +156,4 @@ class InterpolatorQuad < Interpolator
156
156
  return result
157
157
  end
158
158
  end
159
+ end