xrvg 0.0.6 → 0.0.7
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/CHANGES +12 -1
- data/Rakefile +30 -8
- data/lib/samplation.rb +10 -2
- data/lib/trace.rb +1 -0
- data/lib/utils.rb +24 -1
- data/test/test_color.rb +19 -13
- data/test/test_utils.rb +10 -0
- metadata +2 -17
- data/lib/bezier.rb +0 -536
- data/lib/bezierbuilders.rb +0 -210
- data/lib/beziermotifs.rb +0 -121
- data/lib/bezierspline.rb +0 -235
- data/lib/beziertools.rb +0 -245
- data/lib/color.rb +0 -401
- data/lib/fitting.rb +0 -203
- data/lib/frame.rb +0 -33
- data/lib/geovariety.rb +0 -128
- data/lib/interbezier.rb +0 -87
- data/lib/render.rb +0 -266
- data/lib/shape.rb +0 -421
- data/lib/spiral.rb +0 -72
- data/lib/style.rb +0 -76
- data/lib/xrvg.rb +0 -46
data/lib/fitting.rb
DELETED
@@ -1,203 +0,0 @@
|
|
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
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# frame.rb file
|
2
|
-
#
|
3
|
-
# See +Frame+
|
4
|
-
require 'attributable'
|
5
|
-
|
6
|
-
module XRVG
|
7
|
-
#
|
8
|
-
# Frame class
|
9
|
-
# = Intro
|
10
|
-
# Defines a local geometry. Used by +Curve+ interface.
|
11
|
-
# = Attributes
|
12
|
-
# attribute :center
|
13
|
-
# attribute :vector
|
14
|
-
# attribute :rotation
|
15
|
-
# attribute :scale
|
16
|
-
class Frame
|
17
|
-
include Attributable
|
18
|
-
attribute :center
|
19
|
-
attribute :vector
|
20
|
-
attribute :rotation
|
21
|
-
attribute :scale
|
22
|
-
|
23
|
-
def ==(other)
|
24
|
-
if self.center == other.center and
|
25
|
-
self.vector == other.vector and
|
26
|
-
self.rotation == other.rotation and
|
27
|
-
self.scale == other.scale
|
28
|
-
return true
|
29
|
-
end
|
30
|
-
return false
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
data/lib/geovariety.rb
DELETED
@@ -1,128 +0,0 @@
|
|
1
|
-
# File for GeoVariety
|
2
|
-
# See (also):
|
3
|
-
# - InterBezier
|
4
|
-
# - Offsetvariety
|
5
|
-
# - FuseauVariety
|
6
|
-
|
7
|
-
require 'interbezier'
|
8
|
-
|
9
|
-
module XRVG
|
10
|
-
|
11
|
-
# = GeoVariety abstract module
|
12
|
-
# == Principle
|
13
|
-
# Base module to define geometrical spaces or canvas different from simple euclidean one to draw curves on.
|
14
|
-
# It provides three different services:
|
15
|
-
# - point computation
|
16
|
-
# - geodesic computation
|
17
|
-
# - arbitrary bezier computation, this one by computing sampling of euclidean curve on the variety, and then fitting point
|
18
|
-
# sequence with FitBezierBuilder
|
19
|
-
module GeoVariety
|
20
|
-
|
21
|
-
# must be overriden
|
22
|
-
def point( point )
|
23
|
-
raise NotImplementedError.new("#{self.class.name}#point is an abstract method.")
|
24
|
-
end
|
25
|
-
|
26
|
-
# must be overriden
|
27
|
-
def line( x1, x2, y )
|
28
|
-
raise NotImplementedError.new("#{self.class.name}#line is an abstract method.")
|
29
|
-
end
|
30
|
-
|
31
|
-
# see GeoVariety module description for algorithm
|
32
|
-
def bezier( pointrange, bezier )
|
33
|
-
bezier = bezier.similar( pointrange )
|
34
|
-
points = bezier.samples( 20 )
|
35
|
-
points = points.map {|point| self.point( point )}
|
36
|
-
return FitBezierBuilder[ :points, points ]
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# = InterBezier GeoVariety implementation
|
41
|
-
# == Principle
|
42
|
-
# InterBezier defines a surface by the set of every possible curve sample from one interpolated curve to the other.
|
43
|
-
# Geodesic corresponds then to one interpolated result, and point to a point of this curve
|
44
|
-
class InterBezier
|
45
|
-
include GeoVariety
|
46
|
-
|
47
|
-
# Compute the geodesic curve by doing self.sample with y coord, and then compute point of this curve with length "x"
|
48
|
-
def point( point )
|
49
|
-
curve = self.sample( point.y )
|
50
|
-
return curve.point( point.x )
|
51
|
-
end
|
52
|
-
|
53
|
-
# Compute the geodesic subcurve with y coord between x1 and x2
|
54
|
-
def line( x1, x2, y )
|
55
|
-
# Trace("interbezier line x1 #{x1} x2 #{x2} y #{y}")
|
56
|
-
curve = self.sample( y )
|
57
|
-
result = curve.apply_split( x1, x2 )
|
58
|
-
# Trace("interbezier line result #{result.inspect}")
|
59
|
-
return result
|
60
|
-
end
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
# = OffsetVariety implementation
|
65
|
-
# == Principle
|
66
|
-
# Geovariety is defined by the set of offset curves from -ampl to +ampl
|
67
|
-
# == Extension
|
68
|
-
# Parameter could be a :samplable parameter : in that case, ampl will vary
|
69
|
-
#
|
70
|
-
# Another extension would be to parametrize range straightforwardly
|
71
|
-
#
|
72
|
-
# Finally, the two previous remarks must be synthetized :-)
|
73
|
-
class OffsetVariety
|
74
|
-
include Attributable
|
75
|
-
attribute :support
|
76
|
-
attribute :ampl, nil, Float
|
77
|
-
|
78
|
-
include GeoVariety
|
79
|
-
|
80
|
-
# builder: init static offset range with (-self.ampl..self.ampl)
|
81
|
-
def initialize( *args )
|
82
|
-
super( *args )
|
83
|
-
@range = (-self.ampl..self.ampl)
|
84
|
-
end
|
85
|
-
|
86
|
-
# point computed by computing offset curve with ampl y coord mapped onto offset range, and then sampling the curve with x coord
|
87
|
-
def point( point )
|
88
|
-
curve = Offset[ :support, @support, :ampl, @range.sample( point.y ) ]
|
89
|
-
return curve.point( point.x )
|
90
|
-
end
|
91
|
-
|
92
|
-
# subgeodesic computed by computing offset curve with ampl y coord
|
93
|
-
def line( x1, x2, y )
|
94
|
-
curve = Offset[ :support, @support, :ampl, @range.sample( y ) ]
|
95
|
-
return curve.apply_split( x1, x2 )
|
96
|
-
end
|
97
|
-
|
98
|
-
end
|
99
|
-
|
100
|
-
# = FuseauVariety implementation
|
101
|
-
# == Principle
|
102
|
-
# Same as OffsetVariety, with Fuseau shape, that is with linearly varying ampl range
|
103
|
-
class FuseauVariety
|
104
|
-
include Attributable
|
105
|
-
attribute :support
|
106
|
-
attribute :ampl, nil, Float
|
107
|
-
|
108
|
-
include GeoVariety
|
109
|
-
|
110
|
-
def initialize( *args )
|
111
|
-
super( *args )
|
112
|
-
@range = (-self.ampl..self.ampl)
|
113
|
-
end
|
114
|
-
|
115
|
-
def point( point )
|
116
|
-
curve = Offset[ :support, @support, :ampl, (0.0..@range.sample( point.y ))]
|
117
|
-
return curve.point( point.x )
|
118
|
-
end
|
119
|
-
|
120
|
-
def line( x1, x2, y )
|
121
|
-
curve = Offset[ :support, @support, :ampl, (0.0..@range.sample( y ))]
|
122
|
-
return curve.apply_split( x1, x2 )
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
end # XRVG
|
127
|
-
|
128
|
-
# see geovariety_test to see tests
|
data/lib/interbezier.rb
DELETED
@@ -1,87 +0,0 @@
|
|
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.length}")
|
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/render.rb
DELETED
@@ -1,266 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Render file. See
|
3
|
-
# - +Render+ for render "abstraction"
|
4
|
-
# - +SVGRender+ for effective SVG render
|
5
|
-
|
6
|
-
require 'color'
|
7
|
-
require 'style'
|
8
|
-
|
9
|
-
module XRVG
|
10
|
-
# Render abstract class
|
11
|
-
#
|
12
|
-
# Is pretty useless for the moment
|
13
|
-
class Render
|
14
|
-
include Attributable
|
15
|
-
attr_accessor :width
|
16
|
-
attr_accessor :height
|
17
|
-
end
|
18
|
-
|
19
|
-
# SVG Render class
|
20
|
-
#
|
21
|
-
# In charge of generating a svg output file from different object passed to it
|
22
|
-
# = Use
|
23
|
-
# Canonical use of the class
|
24
|
-
# render = SVGRender[ :filename, "essai.svg" ]
|
25
|
-
# render.add( Circle[] )
|
26
|
-
# render.end
|
27
|
-
# = Improvements
|
28
|
-
# Allows also the "with" syntax
|
29
|
-
# = Attributes
|
30
|
-
# attribute :filename, "", String
|
31
|
-
# attribute :imagesize, "2cm", String
|
32
|
-
# attribute :background, Color.white, [Color, String]
|
33
|
-
class SVGRender < Render
|
34
|
-
attribute :filename, "", String
|
35
|
-
attribute :imagesize, "2cm", String
|
36
|
-
attribute :background, "white", [Color, String]
|
37
|
-
attr_reader :viewbox
|
38
|
-
|
39
|
-
# SVGRender builder
|
40
|
-
#
|
41
|
-
# Allows to pass a block, to avoid using .end
|
42
|
-
# SVGRender.[] do |render|
|
43
|
-
# render.add( Circle[] )
|
44
|
-
# end
|
45
|
-
def SVGRender.[](*args,&block)
|
46
|
-
result = self.new( *args )
|
47
|
-
if block
|
48
|
-
yield result
|
49
|
-
result.end
|
50
|
-
end
|
51
|
-
return result
|
52
|
-
end
|
53
|
-
|
54
|
-
|
55
|
-
def initialize ( *args, &block ) #:nodoc:
|
56
|
-
super( *args )
|
57
|
-
@layers = {}
|
58
|
-
@defs = ""
|
59
|
-
@ngradients = 0
|
60
|
-
if @filename.length == 0
|
61
|
-
@filename = $0.split(".")[0..-2].join(".") + ".svg"
|
62
|
-
Trace("filename is #{filename}")
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def layers=( backtofront ) #:nodoc:
|
67
|
-
@sortlayers = backtofront
|
68
|
-
@sortlayers.each do |key|
|
69
|
-
@layers[ key ] = ""
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def add_content (string, layer) #:nodoc:
|
74
|
-
if not @layers.key? layer
|
75
|
-
@layers[ layer ] = ""
|
76
|
-
end
|
77
|
-
@layers[ layer ] += string
|
78
|
-
end
|
79
|
-
|
80
|
-
def svg_template #:nodoc:
|
81
|
-
return '<?xml version="1.0" standalone="no"?>
|
82
|
-
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
83
|
-
<svg width="%SIZE%" height="%SIZE%" %VIEWBOX% version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
84
|
-
%DEFS%
|
85
|
-
%BACKGROUND%
|
86
|
-
%CONTENT%
|
87
|
-
</svg>'
|
88
|
-
end
|
89
|
-
|
90
|
-
def add_def (object) #:nodoc:
|
91
|
-
@defs += object
|
92
|
-
end
|
93
|
-
|
94
|
-
def add_gradient( gradient ) #:nodoc:
|
95
|
-
id = "gradient#{@ngradients}"
|
96
|
-
add_def( gradient.svgdef.subreplace( {"%ID%" => id} ) )
|
97
|
-
@ngradients += 1
|
98
|
-
return id
|
99
|
-
end
|
100
|
-
|
101
|
-
# render fundamental method
|
102
|
-
#
|
103
|
-
# used to render an object, with a particular style, on an optional layer.
|
104
|
-
# render.add( Circle[], Style[ :fill, Color.black ], 1 )
|
105
|
-
# If style is not provided, render asked to object its default_style, if any
|
106
|
-
def add(object, style=nil, layer=0, type=:object)
|
107
|
-
add_content( render( object, style ), layer)
|
108
|
-
refresh_viewbox( object )
|
109
|
-
end
|
110
|
-
|
111
|
-
def render (object, style=nil) #:nodoc:
|
112
|
-
owidth, oheight = object.size
|
113
|
-
|
114
|
-
res = 0.0000001
|
115
|
-
if owidth < res and oheight < res
|
116
|
-
return ""
|
117
|
-
end
|
118
|
-
|
119
|
-
if not style
|
120
|
-
style = object.default_style
|
121
|
-
end
|
122
|
-
|
123
|
-
result = "<g #{style.svgline}>\n"
|
124
|
-
result += object.svg + "\n"
|
125
|
-
result += "</g>\n"
|
126
|
-
|
127
|
-
# puts "result #{result}"
|
128
|
-
|
129
|
-
if style.fill.is_a? Gradient
|
130
|
-
gradientID = add_gradient( style.fill )
|
131
|
-
result = result.subreplace( {"%fillgradient%" => "url(##{gradientID})"} )
|
132
|
-
end
|
133
|
-
|
134
|
-
if style.stroke.is_a? Gradient
|
135
|
-
gradientID = add_gradient( style.stroke )
|
136
|
-
result = result.subreplace( {"%strokegradient%" => "url(##{gradientID})"} )
|
137
|
-
end
|
138
|
-
return result
|
139
|
-
end
|
140
|
-
|
141
|
-
def viewbox #:nodoc:
|
142
|
-
return @viewbox
|
143
|
-
end
|
144
|
-
|
145
|
-
def size #:nodoc:
|
146
|
-
xmin, ymin, xmax, ymax = viewbox
|
147
|
-
return [xmax - xmin, ymax - ymin]
|
148
|
-
end
|
149
|
-
|
150
|
-
def refresh_viewbox (object) #:nodoc:
|
151
|
-
newviewbox = object.viewbox
|
152
|
-
if newviewbox.length > 0
|
153
|
-
if @viewbox == nil
|
154
|
-
@viewbox = newviewbox
|
155
|
-
else
|
156
|
-
newxmin, newymin, newxmax, newymax = newviewbox
|
157
|
-
xmin, ymin, xmax, ymax = viewbox
|
158
|
-
|
159
|
-
if newxmin < xmin
|
160
|
-
xmin = newxmin
|
161
|
-
end
|
162
|
-
if newymin < ymin
|
163
|
-
ymin = newymin
|
164
|
-
end
|
165
|
-
if newxmax > xmax
|
166
|
-
xmax = newxmax
|
167
|
-
end
|
168
|
-
if newymax > ymax
|
169
|
-
ymax = newymax
|
170
|
-
end
|
171
|
-
|
172
|
-
@viewbox = [xmin, ymin, xmax, ymax]
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def get_background_svg #:nodoc:
|
178
|
-
xmin, ymin, width, height = get_carre_viewbox( get_final_viewbox() )
|
179
|
-
template = '<rect x="%x%" y="%y%" width="%width%" height="%height%" fill="%fill%"/>'
|
180
|
-
bg = self.background
|
181
|
-
if bg.respond_to? :svg
|
182
|
-
bg = bg.svg
|
183
|
-
end
|
184
|
-
return template.subreplace( {"%x%" => xmin,
|
185
|
-
"%y%" => ymin,
|
186
|
-
"%width%" => width,
|
187
|
-
"%height%" => height,
|
188
|
-
"%fill%" => bg} )
|
189
|
-
end
|
190
|
-
|
191
|
-
def get_final_viewbox #:nodoc:
|
192
|
-
marginfactor = 0.2
|
193
|
-
xmin, ymin, xmax, ymax = viewbox()
|
194
|
-
width, height = size()
|
195
|
-
|
196
|
-
xcenter = (xmin + xmax)/2.0
|
197
|
-
ycenter = (ymin + ymax)/2.0
|
198
|
-
|
199
|
-
width *= 1.0 + marginfactor
|
200
|
-
height *= 1.0 + marginfactor
|
201
|
-
|
202
|
-
if width == 0.0
|
203
|
-
width = 1.0
|
204
|
-
end
|
205
|
-
if height == 0.0
|
206
|
-
height = 1.0
|
207
|
-
end
|
208
|
-
|
209
|
-
xmin = xcenter - width / 2.0
|
210
|
-
ymin = ycenter - height / 2.0
|
211
|
-
|
212
|
-
return xmin, ymin, width, height
|
213
|
-
end
|
214
|
-
|
215
|
-
def get_viewbox_svg #:nodoc:
|
216
|
-
return viewbox_svg( get_final_viewbox() )
|
217
|
-
end
|
218
|
-
|
219
|
-
def get_carre_viewbox( viewbox ) #:nodoc:
|
220
|
-
xmin, ymin, width, height = viewbox
|
221
|
-
xcenter = xmin + width / 2.0
|
222
|
-
ycenter = ymin + height / 2.0
|
223
|
-
maxsize = width < height ? height : width
|
224
|
-
return [xcenter - maxsize/2.0, ycenter - maxsize/2.0, maxsize, maxsize]
|
225
|
-
end
|
226
|
-
|
227
|
-
def viewbox_svg( viewbox ) #:nodoc:
|
228
|
-
xmin, ymin, width, height = viewbox
|
229
|
-
return "viewBox=\"#{xmin} #{ymin} #{width} #{height}\""
|
230
|
-
end
|
231
|
-
|
232
|
-
def content #:nodoc:
|
233
|
-
keys = @sortlayers ? @sortlayers : @layers.keys.sort
|
234
|
-
return keys.inject("") {|result,key| result += @layers[key]}
|
235
|
-
end
|
236
|
-
|
237
|
-
def svgdef #:nodoc:
|
238
|
-
return "<defs>\n#{@defs}\n</defs>\n"
|
239
|
-
end
|
240
|
-
|
241
|
-
def end () #:nodoc:
|
242
|
-
svgcontent = content()
|
243
|
-
svgviewbox = get_viewbox_svg()
|
244
|
-
svgbackground = get_background_svg()
|
245
|
-
|
246
|
-
content = svg_template().subreplace( {"%VIEWBOX%" => svgviewbox,
|
247
|
-
"%SIZE%" => @imagesize,
|
248
|
-
"%DEFS%" => svgdef,
|
249
|
-
"%BACKGROUND%" => svgbackground,
|
250
|
-
"%CONTENT%" => svgcontent})
|
251
|
-
|
252
|
-
File.open(filename(), "w") do |f|
|
253
|
-
f << content
|
254
|
-
end
|
255
|
-
|
256
|
-
puts "render #{filename()} OK"; # necessary for Emacs to get output name !!!!
|
257
|
-
end
|
258
|
-
|
259
|
-
def raster () #:nodoc:
|
260
|
-
# bg = background.format255
|
261
|
-
|
262
|
-
# Kernel.system( "ruby", "svg2png.rb", filename(), "2.0" )
|
263
|
-
# Kernel.system( "i_view32", filename().subreplace( ".svg" => ".png" ), "/fs" )
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|