Fractals

Principle

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.

Circle

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

Arc

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

Further

The previous examples are trivial, and can be extended in numerous ways: