xrvg 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/render.rb ADDED
@@ -0,0 +1,269 @@
1
+ #
2
+ # Render file. See
3
+ # - +Render+ for render "abstraction"
4
+ # - +SVGRender+ for effective SVG render
5
+
6
+ require 'color'
7
+ require 'attributable'
8
+ require 'style'
9
+
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, Color.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
+ end
69
+
70
+ def add_content (string, layer) #:nodoc:
71
+ if not @layers.key? layer
72
+ @layers[ layer ] = ""
73
+ end
74
+ @layers[ layer ] += string
75
+ end
76
+
77
+ def svg_template #:nodoc:
78
+ return '<?xml version="1.0" standalone="no"?>
79
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
80
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
81
+ <svg width="%SIZE%" height="%SIZE%" %VIEWBOX% version="1.1"
82
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
83
+ %DEFS%
84
+ %BACKGROUND%
85
+ %CONTENT%
86
+ </svg>'
87
+ end
88
+
89
+ def add_def (object) #:nodoc:
90
+ @defs += object
91
+ end
92
+
93
+ def add_gradient( gradient ) #:nodoc:
94
+ id = "gradient#{@ngradients}"
95
+ add_def( gradient.svgdef.subreplace( {"%ID%" => id} ) )
96
+ @ngradients += 1
97
+ return id
98
+ end
99
+
100
+ # render fundamental method
101
+ #
102
+ # used to render an object, with a particular style, on an optional layer.
103
+ # render.add( Circle[], Style[ :fill, Color.black ], 1 )
104
+ # If style is not provided, render asked to object its default_style, if any
105
+ def add(object, style=nil, layer=0, type=:object)
106
+ add_content( render( object, style ), layer)
107
+ refresh_viewbox( object )
108
+ end
109
+
110
+ def adds (objects) #:nodoc:
111
+ objects.each { |object| add( object )}
112
+ end
113
+
114
+ def render (object, style=nil) #:nodoc:
115
+ owidth, oheight = object.size
116
+
117
+ res = 0.0000001
118
+ if owidth < res and oheight < res
119
+ return ""
120
+ end
121
+
122
+ if not style
123
+ style = object.default_style
124
+ end
125
+
126
+ result = "<g #{style.svgline}>\n"
127
+ result += object.svg + "\n"
128
+ result += "</g>\n"
129
+
130
+ # puts "result #{result}"
131
+
132
+ if style.fill.is_a? Gradient
133
+ gradientID = add_gradient( style.fill )
134
+ result = result.subreplace( {"%fillgradient%" => "url(##{gradientID})"} )
135
+ end
136
+
137
+ if style.stroke.is_a? Gradient
138
+ gradientID = add_gradient( style.stroke )
139
+ result = result.subreplace( {"%strokegradient%" => "url(##{gradientID})"} )
140
+ end
141
+ return result
142
+ end
143
+
144
+ def viewbox #:nodoc:
145
+ return @viewbox
146
+ end
147
+
148
+ def size #:nodoc:
149
+ xmin, ymin, xmax, ymax = viewbox
150
+ return [xmax - xmin, ymax - ymin]
151
+ end
152
+
153
+ def refresh_viewbox (object) #:nodoc:
154
+ newviewbox = object.viewbox
155
+ if newviewbox.length > 0
156
+ if @viewbox == nil
157
+ @viewbox = newviewbox
158
+ else
159
+ newxmin, newymin, newxmax, newymax = newviewbox
160
+ xmin, ymin, xmax, ymax = viewbox
161
+
162
+ if newxmin < xmin
163
+ xmin = newxmin
164
+ end
165
+ if newymin < ymin
166
+ ymin = newymin
167
+ end
168
+ if newxmax > xmax
169
+ xmax = newxmax
170
+ end
171
+ if newymax > ymax
172
+ ymax = newymax
173
+ end
174
+
175
+ @viewbox = [xmin, ymin, xmax, ymax]
176
+ end
177
+ end
178
+ end
179
+
180
+ def get_background_svg #:nodoc:
181
+ xmin, ymin, width, height = get_carre_viewbox( get_final_viewbox() )
182
+ template = '<rect x="%x%" y="%y%" width="%width%" height="%height%" fill="%fill%"/>'
183
+ bg = self.background
184
+ if bg.is_a? Color
185
+ bg = bg.svg
186
+ end
187
+ return template.subreplace( {"%x%" => xmin,
188
+ "%y%" => ymin,
189
+ "%width%" => width,
190
+ "%height%" => height,
191
+ "%fill%" => bg} )
192
+ end
193
+
194
+ def get_final_viewbox #:nodoc:
195
+ marginfactor = 0.2
196
+ xmin, ymin, xmax, ymax = viewbox()
197
+ width, height = size()
198
+
199
+ xcenter = (xmin + xmax)/2.0
200
+ ycenter = (ymin + ymax)/2.0
201
+
202
+ width *= 1.0 + marginfactor
203
+ height *= 1.0 + marginfactor
204
+
205
+ if width == 0.0
206
+ width = 1.0
207
+ end
208
+ if height == 0.0
209
+ height = 1.0
210
+ end
211
+
212
+ xmin = xcenter - width / 2.0
213
+ ymin = ycenter - height / 2.0
214
+
215
+ return xmin, ymin, width, height
216
+ end
217
+
218
+ def get_viewbox_svg #:nodoc:
219
+ return viewbox_svg( get_final_viewbox() )
220
+ end
221
+
222
+ def get_carre_viewbox( viewbox ) #:nodoc:
223
+ xmin, ymin, width, height = viewbox
224
+ xcenter = xmin + width / 2.0
225
+ ycenter = ymin + height / 2.0
226
+ maxsize = width < height ? height : width
227
+ return [xcenter - maxsize/2.0, ycenter - maxsize/2.0, maxsize, maxsize]
228
+ end
229
+
230
+ def viewbox_svg( viewbox ) #:nodoc:
231
+ xmin, ymin, width, height = viewbox
232
+ return "viewBox=\"#{xmin} #{ymin} #{width} #{height}\""
233
+ end
234
+
235
+ def content #:nodoc:
236
+ keys = @sortlayers ? @sortlayers : @layers.keys.sort
237
+ return keys.inject("") {|result,key| result += @layers[key]}
238
+ end
239
+
240
+ def svgdef #:nodoc:
241
+ return "<defs>\n#{@defs}\n</defs>\n"
242
+ end
243
+
244
+ def end () #:nodoc:
245
+ svgcontent = content()
246
+ svgviewbox = get_viewbox_svg()
247
+ svgbackground = get_background_svg()
248
+
249
+ content = svg_template().subreplace( {"%VIEWBOX%" => svgviewbox,
250
+ "%SIZE%" => @imagesize,
251
+ "%DEFS%" => svgdef,
252
+ "%BACKGROUND%" => svgbackground,
253
+ "%CONTENT%" => svgcontent})
254
+
255
+ File.open(filename(), "w") do |f|
256
+ f << content
257
+ end
258
+
259
+ puts "render #{filename()} OK"; # necessary for Emacs to get output name !!!!
260
+ end
261
+
262
+ def raster () #:nodoc:
263
+ # bg = background.format255
264
+
265
+ # Kernel.system( "ruby", "svg2png.rb", filename(), "2.0" )
266
+ # Kernel.system( "i_view32", filename().subreplace( ".svg" => ".png" ), "/fs" )
267
+ end
268
+
269
+ end
data/lib/samplation.rb ADDED
@@ -0,0 +1,416 @@
1
+ #
2
+ # Define float series functional processing
3
+ # The base module is FloatFunctor, used by Samplable and Splittable
4
+ #
5
+
6
+ #
7
+ # Base module to define float lists processing and computation.
8
+ # = Design principle
9
+ # Composite design pattern, to be able to chain functors
10
+ # = Concepts
11
+ # on each functor object :
12
+ # - "trigger" method triggers float processing, that is
13
+ # * if arg is integer, call "generate" to get values
14
+ # * else (array float values), just keep them
15
+ # - pass inputs to "process" method, which wraps "compute" method to be able to call block and iterate
16
+ # - "compute" method first call "modify" which is the recursive processing function
17
+ # - then call "apply" which is specific processing method of the functor
18
+ # - "apply" then dispatchs processing according to the processing "type", ie "sample" or "split"
19
+ # - by default, FloatFunctor provides "transforms" and "transform" to be used by "type" processing
20
+ module FloatFunctor
21
+ # -------------------------------------------------------------
22
+ # essential recursivity chaining method
23
+ # -------------------------------------------------------------
24
+
25
+ # building recursivity method
26
+ #
27
+ # is private ?
28
+ def addfilter( newfilter )
29
+ # Trace("Sampler addfilter method self #{self.inspect}")
30
+ if not @subfilter
31
+ @subfilter = newfilter
32
+ else
33
+ # Trace("Sampler addfilter recurse on subfilter #{@subfilter.inspect}")
34
+ @subfilter.addfilter( newfilter )
35
+ end
36
+ return self
37
+ end
38
+
39
+ # -------------------------------------------------------------
40
+ # generic generator method
41
+ # -------------------------------------------------------------
42
+
43
+ # must not be overloaded
44
+ def trigger( nsamples, type, &block )
45
+ if nsamples.is_a? Integer
46
+ indata = self.generate( nsamples )
47
+ else
48
+ indata = nsamples
49
+ end
50
+ return self.process( indata, type, &block )
51
+ end
52
+
53
+ # hook for Array
54
+ def compute( indata, type )
55
+ return self.apply( self.modify( indata ), type )
56
+ end
57
+
58
+ # hook for rand()
59
+ def process( indata, type, &block )
60
+ outdata = self.compute( indata, type )
61
+ if not block
62
+ # Trace("Samplable#trigger object #{self.inspect} indata #{indata.inspect} outdata #{outdata.inspect}")
63
+ return outdata
64
+ else
65
+ outdata.foreach(nil,&block)
66
+ end
67
+ end
68
+
69
+ # recursive method to compose modifications.
70
+ #
71
+ # must not be overloaded
72
+ def modify( inputs )
73
+ # Trace("Samplable#modify object #{self.inspect} inputs #{inputs.inspect}")
74
+ if @subfilter
75
+ inputs = @subfilter.modify( inputs )
76
+ end
77
+ result = self.transforms( inputs )
78
+ # Trace("Samplable#filter object #{self.inspect} inputs #{inputs.inspect} result #{result.inspect}")
79
+ return result
80
+ end
81
+
82
+ # to be overloaded if needed
83
+ def transforms( inputs )
84
+ return inputs.map {|abs| self.transform( abs )}
85
+ end
86
+
87
+ # to be overloaded
88
+ def transform( abs )
89
+ return abs
90
+ end
91
+
92
+ # default generator method
93
+ def generate( nsamples )
94
+ # Trace("Samplable#generate object #{self.inspect} nsamples #{nsamples}")
95
+ return (0.0..1.0).generate( nsamples )
96
+ end
97
+
98
+ #
99
+ # apply interface
100
+ #
101
+ # it is a registration interface
102
+ #
103
+ # must not be overloaded
104
+ # must be refactored with some meta hooks
105
+ def applyhash
106
+ result = {}
107
+ result[:sample] = :apply_samples
108
+ result[:split] = :apply_splits
109
+ return result
110
+ end
111
+
112
+ # must not be overloaded (apart from Samplable containers)
113
+ def apply( data, type )
114
+ @applyhash = self.applyhash
115
+ if not @applyhash.key? type
116
+ Kernel::Raise("FloatFunctor::apply no regsitration for type #{type} and object #{object.inspect}")
117
+ else
118
+ return self.send(@applyhash[type], data )
119
+ end
120
+ end
121
+
122
+ # -------------------------------------------------------------
123
+ # filters management
124
+ # -------------------------------------------------------------
125
+
126
+ # geometric filter
127
+ def geo( speed )
128
+ return self.addfilter( Filter.with {|x| 1.0 - Math.exp(-speed * x)} )
129
+ end
130
+
131
+ # random filter
132
+ def random()
133
+ return self.addfilter( RandomFilter.new )
134
+ end
135
+
136
+ # sorting filter
137
+ def ssort()
138
+ return self.addfilter( SortFilter.new )
139
+ end
140
+
141
+ # shortcut method to build a sampler from self and a block
142
+ def filter(samplemethod=:sample,&block)
143
+ return Filter.new( self, samplemethod, &block )
144
+ end
145
+
146
+
147
+ # -------------------------------------------------------------
148
+ # old methods to be refactored
149
+ # -------------------------------------------------------------
150
+
151
+ # deprecated
152
+ #
153
+ # ratios sum must be equal to 1.0
154
+ def multisamples( nsamples, ratios )
155
+ ratiosum = ratios.sum
156
+ samplesum = nsamples.sum
157
+ ratios = ratios.map {|ratio| ratio / ratiosum}
158
+
159
+ rratios = ratios
160
+ index = 0
161
+ ratios.each do |ratio|
162
+ rratios[index] = ratio / nsamples[index]
163
+ index += 1
164
+ end
165
+
166
+ sum = 0.0
167
+ samples = [0.0]
168
+ periodindex = 0
169
+ while sum <= 1.0
170
+ sum += rratios[ periodindex ]
171
+ if sum > 1.0
172
+ break
173
+ end
174
+ samples += [sum]
175
+ periodindex += 1
176
+ if periodindex >= rratios.length
177
+ periodindex = 0.0
178
+ end
179
+ end
180
+ return self.samples( samples )
181
+ end
182
+
183
+ # deprecated
184
+ #
185
+ # TODO : must add gauss parameters
186
+ def randgauss()
187
+ begin
188
+ x1 = 2.0 * Kernel::rand - 1.0
189
+ x2 = 2.0 * Kernel::rand - 1.0
190
+ w = x1 * x1 + x2 * x2
191
+ end while w >= 1.0
192
+ w = Math.sqrt( ( -2.0 * Math.log( w ) ) / w )
193
+ return 1.0/2.0 * ( 1.0 + x1 * w )
194
+ end
195
+
196
+ end
197
+
198
+ # -------------------------------------------------------------
199
+ # Samplable interface
200
+ # -------------------------------------------------------------
201
+
202
+ #
203
+ # Samplable module, based on FloatFunctor
204
+ # = Concept
205
+ # Basically allows advanced item computations from an continuous "Interval" object.
206
+ # Is used by :
207
+ # - Range for computing float values
208
+ # - Curve for computing points
209
+ # - Palette for computing colors
210
+ # - ...
211
+ module Samplable
212
+ include FloatFunctor
213
+
214
+ # -------------------------------------------------------------
215
+ # sampling generator methods
216
+ # -------------------------------------------------------------
217
+
218
+ # fundamental method of the module
219
+ #
220
+ # call the FloatFunctor trigger method, with type :sample
221
+ #
222
+ # (0.0..1.0).samples( 3 ) => [0.0, 0.5, 1.0]
223
+ def samples( nsamples, &block )
224
+ return self.trigger( nsamples, :sample, &block )
225
+ end
226
+
227
+ # shortcut method to call float processing for one input
228
+ #
229
+ # is basically .samples([abs]).pop
230
+ def sample( abs )
231
+ return self.apply( self.modify( [abs] ), :sample ).pop
232
+ end
233
+
234
+ # alias for sample( 0.5 )
235
+ def mean()
236
+ return self.sample( 0.5 )
237
+ end
238
+
239
+ # alias for mean
240
+ alias middle mean
241
+
242
+ # alias for .random.samples
243
+ def rand(nsamples=1,&block)
244
+ inputs = []
245
+ nsamples.times {|v| inputs.push( Kernel::rand )}
246
+ result = self.process( inputs, :sample, &block )
247
+ return nsamples == 1 ? result[0] : result
248
+ end
249
+
250
+ # to be overloaded if needed
251
+ #
252
+ # called by FloatFunctor apply method
253
+ def apply_samples( inputs )
254
+ return inputs.map {|abs| self.apply_sample( abs ) }
255
+ end
256
+
257
+ # to be overloaded if needed
258
+ def apply_sample( abs )
259
+ return abs
260
+ end
261
+
262
+ # apply_register( :sample, :apply_samples )
263
+
264
+ end
265
+
266
+ # -------------------------------------------------------------
267
+ # Splittable interface
268
+ # -------------------------------------------------------------
269
+
270
+ #
271
+ # Splittable module, based on FloatFunctor
272
+ # = Concept
273
+ # Basically allows advanced subdivision computations from an continuous "Interval" object.
274
+ # = Example
275
+ # (1.0..2.0).splits(2) => [(1.0..1.5), (1.5..2.0)]
276
+ module Splittable
277
+ include FloatFunctor
278
+
279
+ # generator method for split interface.
280
+ # Must not be overriden
281
+ #
282
+ # Basically call FloatFunctor trigger method with at least 2 data !!
283
+ def splits( nsamples, &block )
284
+ if nsamples.is_a? Integer
285
+ nsamples += 1
286
+ if nsamples < 2
287
+ Kernel::Raise("Samplable#split method needs at least two data, instead of #{nsamples}")
288
+ end
289
+ else
290
+ if nsamples.size < 2
291
+ Kernel::Raise("Samplable#split method needs at least two data, instead of #{nsamples.inspect}")
292
+ end
293
+ end
294
+ return self.trigger( nsamples, :split, &block )
295
+ end
296
+
297
+ # must not be overriden
298
+ def split( abs1, abs2 )
299
+ return self.apply( self.filter( [abs1,abs2] ), :split ).pop
300
+ end
301
+
302
+ # to be overloaded if needed
303
+ def apply_splits( inputs )
304
+ return inputs.pairs.map {|t1,t2| self.apply_split( t1, t2 )}
305
+ end
306
+
307
+ # to be overloaded if needed
308
+ def apply_split( t1, t2 )
309
+ return self.class.new( t1, t2 )
310
+ end
311
+
312
+ # apply_register( :split, :apply_splits )
313
+ end
314
+
315
+
316
+ # Filter class
317
+ # = Intro
318
+ # Filter class allows to call generically a method with arg between 0.0..0.1 on a given object, for use in a +FloatFunctor+ context.
319
+ # = Example
320
+ # this allow to do for example
321
+ # [bezier1.filter(:point), bezier1.filter(:tangent), palette].samples( 10 ) do |point, tangent, color|
322
+ class Filter
323
+
324
+ include Samplable
325
+
326
+ # identity functor
327
+ def Filter.identity
328
+ return Filter.new(nil,nil) {|x| x}
329
+ end
330
+
331
+ # to define a filter on "object" with method "samplemethod", or with a block (exclusive)
332
+ #
333
+ # from these two alternatives, returns a proc to call for float processing
334
+ def initialize(object=nil, samplemethod=:sample, &block)
335
+ if object
336
+ # Trace("Filter::initialize object #{object.inspect} method #{samplemethod.inspect} ")
337
+ @proc = object.method( samplemethod )
338
+ else
339
+ @proc = Proc.new( &block )
340
+ end
341
+ end
342
+
343
+ # Samplable redefinition : call previous proc built from initialize to process float "abs"
344
+ def transform( abs )
345
+ result = @proc.call( abs )
346
+ # Trace("Filter::transform abs #{abs} result #{result.inspect}")
347
+ return result
348
+ end
349
+
350
+ # shortcut method aliasing Filter.new( nil, nil, &block)
351
+ #
352
+ # must change the name, because "with" seems to refer to LISP "with" design pattern, and this is not the case here
353
+ def Filter.with( &block )
354
+ return Filter.new( nil, nil, &block)
355
+ end
356
+
357
+ end
358
+
359
+ # RandomFilter class, to resample randomly
360
+ class RandomFilter < Filter
361
+ def initialize(*args) #:nodoc:
362
+ end
363
+
364
+ def transform(abs) #:nodoc:
365
+ return (0.0..1.0).rand
366
+ end
367
+
368
+ def transforms( inputs ) #:nodoc:
369
+ result = inputs.map { |v| self.transform( v ) }
370
+ result[0] = 0.0
371
+ result[-1] = 1.0
372
+ return result
373
+ end
374
+ end
375
+
376
+ # SortFilter, to sort inputs (in particular from Random)
377
+ class SortFilter < Filter
378
+ def initialize(*args) #:nodoc:
379
+ end
380
+
381
+ def transforms( inputs ) #:nodoc:
382
+ return inputs.sort
383
+ end
384
+
385
+ end
386
+
387
+ # Experimental class to add discrete processing in float processing chains
388
+ # Roller["white","black"].samples(3) => ["white","black","white"]
389
+ class Roller
390
+ include Samplable
391
+
392
+ def initialize( *args ) #:nodoc:
393
+ @index = 0
394
+ @items = args
395
+ end
396
+
397
+ def Roller.[](*args) #:nodoc:
398
+ return self.new(*args)
399
+ end
400
+
401
+ def transform( abs ) #:nodoc:
402
+ result = @items[@index]
403
+ @index += 1
404
+ if @index >= @items.size
405
+ @index = 0
406
+ end
407
+ return result
408
+ end
409
+
410
+ # TODO : must be generalized
411
+ def rand() #:nodoc:
412
+ index = (0.0..@items.size).rand.to_i
413
+ return @items[index]
414
+ end
415
+
416
+ end