One of the main recipes to create fractals is to replace a basic shape by a set of equivalent subshapes, and reitering this process on these subshapes.
As we have seen in one of the first tutorials, we can experiment this definition with circles in a simple way:
|
|
require 'xrvg' include XRVG render = SVGRender[:imagesize, "3cm" ] style = Style[ :fill, Color.blue ] Circle[].samples( 6 ) do |point| render.add( Circle[:center, point, :radius, 0.333 ], style) end render.end |
|
|
require 'xrvg' include XRVG render = SVGRender[:imagesize, "3cm" ] style = Style[ :fill, Color.blue ] Circle[].samples( 6 ) do |point| Circle[:center, point, :radius, 0.3333 ].samples( 6 ) do |point| render.add( Circle[:center, point, :radius, 0.1111 ], style ) end end render.end |
|
|
require 'xrvg' include XRVG render = SVGRender[:imagesize, "3cm" ] style = Style[ :fill, Color.blue ] Circle[].samples( 6 ) do |point| Circle[:center, point, :radius, 0.3333 ].samples( 6 ) do |point| Circle[:center, point, :radius, 0.1111 ].samples( 6 ) do |point| render.add( Circle[:center, point, :radius, 0.037 ], style ) end end end render.end |
We can soon notice that an interesting pattern emerges, which does not rely on shapes used to build it, but more on the numerical patterns implied in the process. We also can notice that notation above is not quite extendable, so we are going to refactor the previous code a bit.
We can define two methods:
By doing this, we can also debug the fact that we use same first and last point during circle sampling.
Here is the result, with one additional recursion level:
|
|
require 'xrvg' include XRVG def subcircles( circle, nsamples, radiusfactor ) return circle.samples( nsamples )[1..-1].map do |point| Circle[:center, point, :radius, circle.radius * radiusfactor ] end end def circlerecurse( circles, niter, nsamples, radiusfactor ) if niter == 0 return circles else subcircles = [] circles.each do |circle| subcircles += subcircles( circle, nsamples, radiusfactor ) end return circlerecurse( subcircles, niter-1, nsamples, radiusfactor ) end end render = SVGRender[:imagesize, "3cm" ] style = Style[ :fill, Color.blue ] circlerecurse( [Circle[]], 4, 6, 1.0/3.0 ).each do |circle| render.add( circle, style ) end render.end |
And one more (check for difference between the two codes :-)
|
|
require 'xrvg' include XRVG def subcircles( circle, nsamples, radiusfactor ) return circle.samples( nsamples )[1..-1].map do |point| Circle[:center, point, :radius, circle.radius * radiusfactor ] end end def circlerecurse( circles, niter, nsamples, radiusfactor ) if niter == 0 return circles else subcircles = [] circles.each do |circle| subcircles += subcircles( circle, nsamples, radiusfactor ) end return circlerecurse( subcircles, niter-1, nsamples, radiusfactor ) end end render = SVGRender[:imagesize, "3cm" ] style = Style[ :fill, Color.blue ] circlerecurse( [Circle[]], 5, 6, 1.0/3.0 ).each do |circle| render.add( circle, style ) end render.end |
In the last example, the figure seems to dilute itself in the image, while pattern still remains. You can touch here the notion of a fractal attractor: the fractal attractor is the mathematical object you obtain by iterating the previous process an infinite number of times. While there are more and more points in the set, the surface to be drawn converges to 0.
As a final point, let's use the previous example, and draw every level of recursion:
|
|
require 'xrvg' include XRVG def subcircles( circle, nsamples, radiusfactor ) return circle.samples( nsamples )[1..-1].map do |point| Circle[:center, point, :radius, circle.radius * radiusfactor ] end end def circlerecurse( circles, niter, nsamples, radiusfactor ) if niter == 0 return circles else subcircles = [] circles.each do |circle| subcircles += subcircles( circle, nsamples, radiusfactor ) end return circlerecurse( subcircles, niter-1, nsamples, radiusfactor ) end end render = SVGRender[:imagesize, "3cm" ] style = Style[ :fill, Color.blue( 0.2 ) ] 5.times do |time| circlerecurse( [Circle[]], time, 6, 1.0/3.0 ).each do |circle| render.add( circle, style ) end end render.end |
The very same process can now be extended to other geometrical shapes, as arc bezier curves for example. Hereafter the recursive shape pattern is first described:
|
|
require 'xrvg' include XRVG render = SVGRender[:imagesize, "3cm" ] style = Style[ :stroke, Color.blue, :strokewidth, 0.01 ] arc = ArcBezier[ :support, [V2D::O, V2D::X] ] samples = [0.1,0.3,0.4,0.6,0.7,0.9] subarcs = arc.samples( samples ).foreach(2).map do |points| ArcBezier[ :support, points ] end render.add( arc, style ) subarcs.each {|arc| render.add( arc, style ) } render.end |
We can then use the same refactoring process as above, to get:
|
|
require 'xrvg' include XRVG def subarcs( arc, samples ) return arc.samples( samples ).foreach(2).map do |points| ArcBezier[:support, points ] end end def arcrecurse( arcs, niter, samples ) if niter == 0 return arcs else subarcs = [] arcs.each do |arc| subarcs += subarcs( arc, samples ) end return arcrecurse( subarcs, niter-1, samples ) end end render = SVGRender[:imagesize, "3cm" ] style = Style[ :stroke, Color.blue, :strokewidth, 0.01 ] samples = [0.1,0.3,0.4,0.6,0.7,0.9] roots = [ArcBezier[ :support, [V2D::O, V2D::X]]] arcrecurse( roots, 4, samples ).each do |arc| render.add( arc, style ) end render.end |
As for circle, and more interesting, displaying each recursion level with a slightly different color (and raw management of strokewidth) gives:
|
|
require 'xrvg' include XRVG def subarcs( arc, samples ) return arc.samples( samples ).foreach(2).map do |points| ArcBezier[:support, points ] end end def arcrecurse( arcs, niter, samples ) if niter <= 0 return arcs else subarcs = [] arcs.each do |arc| subarcs += subarcs( arc, samples ) end return arcrecurse( subarcs, niter-1, samples ) end end render = SVGRender[:imagesize, "3cm" ] style = Style[ :stroke, Color.blue, :strokewidth, 0.01 ] samples = [0.1,0.3,0.4,0.6,0.7,0.9] roots = [ArcBezier[ :support, [V2D::O, V2D::X]]] palette = Palette[ :colorlist, [ 0.0, Color.black, 1.0, Color.blue]] niter = 6 SyncS[(0.0..1.0),(0.01..0.001),palette].samples( niter ) do |time,width,color| time = (niter * time).to_i style.stroke = color style.strokewidth = width arcrecurse( roots, time, samples ).each do |arc| render.add( arc, style ) end end render.end |
The previous examples are trivial, and can be extended in numerous ways: