xrvg 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.
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