Wednesday, March 2, 2011

SVG Fractal with Scala 3 : The Koch snowflakes

Result
Source Code
Following is a straight forward implementation pop up my mind.  I first, build a list of "angles" which defines direction from current point to the next point.  When it goes horizontally to the right, angle is 0, 90 for vertically up, 180 for horizontally left, 270 for vertically down and so on.  Start point is the peak of the triangle.  First 2 cases go like this.
  • Level 0 => List ( 300, 180,  60)

  • Level 1 => List ( 300, 0, 240, 300, 180, 240, 120, 180, 60, 120, 0, 60)


To get the list for level n form level n-1, we can do the following.
(level n-1 list).flatMap(x => List(x,x+60,x+300,x)).map(_%360)
Rest of the things I need to is to generate a list of (x,y) points, translate it into a string and put the string in  <pack><path d="M ... z"/></pack> statements.


class Koch (width: Double, level:Int) extends Fractal(width,(width*3*1.732/4+1).toInt) { 
 
 val scale = (width-20)/(2*math.pow(3,level))
 val level_0 = List(300,180,60)
 def recursive(current:List[Int], currlevel:Int): List[Int] = {
  if(currlevel == level)
  {
   current
  } else {
   recursive(current.flatMap(x => List(x,x+60,x+300,x)).map(_%360), currlevel+1)
  }
 }
 
 def angles2points(alist:List[Int]): List[(Double,Double)] = 
  (width/2,20.0) :: iter((width/2,20.0), alist, List())
  
 def iter(org:(Double,Double), alist:List[Int], plist:List[(Double,Double)])
 : List[(Double,Double)] = org match {
  case (x,y) => alist match {
   case a::as => 
    val next = a match {
     case 0 => (x+2*scale, y)
     case 60 => (x+scale, y-1.732*scale)
     case 120 => (x-scale, y-1.732*scale)
     case 180 => (x-2*scale, y)
     case 240 => (x-scale, y+1.732*scale)
     case 300 => (x+scale, y+1.732*scale)
    }
    iter(next, as, plist ++ List((next)))
   case _ => plist
  }
 }
 
 def points2string(plist:List[(Double,Double)]) : String = plist match {
  case (x,y)::ps => x.toString + "," + y.toString + " " + points2string(ps)
  case _ => ""
 }
 override def body = 
  <pack>
   <path stroke="blue" fill ="none"
    d= {"M"+ points2string(angles2points(recursive(level_0,0))) + "z"} />
  </pack>>
}

My concern with this code, from the beginning, was I might be using too much memories.  No wonder, I got OutOfMemoryError for level 6.

scala> val k = new Koch(400,6)
k: Koch = Koch@28fd3fba

scala> k.draw("koch.svg")     
java.lang.OutOfMemoryError: Java heap space
at scala.collection.mutable.ListBuffer.$plus$eq(ListBuffer.scala:119)
at scala.collection.mutable.ListBuffer.$plus$eq(ListBuffer.scala:42)
at scala.collection.generic.Growable$$anonfun$$plus$plus$eq$1.apply(Growable.scala:48)
at scala.collection.generic.Growable$$anonfun$$plus$plus$eq$1.apply(Growable.scala:48)
at scala.collection.LinearSeqOptimized$class.foreach(LinearSeqOptimized.scala:61)
at scala.collection.immutable.List.foreach(List.scala:45)
at scala.collection.generic.Growable$class.$plus$plus$eq(Growable.scala:48)
at scala.collection.immutable.List.$colon$colon$colon(List.scala:78)
at scala.collection.immutable.List.$plus$plus(List.scala:139)
at Koch.iter(:39)
at Koch.iter(:39)
at Koch.iter(:39)
...
scala> 

I guess I can write a point to file one by one as it being generated instead of preparing all the points in a list before writing them to the file all at once.  That way, I think, I can reduce the memory space and potentially generate any level of snowflakes.  But, doing that in obvious way, manually, normal way, whatever, does not sound so exciting.  I wonder if I could use lazy evaluation to achieve that, somehow.
That might be a next topic.