Saturday, May 25, 2013

Fractal Engine

The last time I posted a sample of code it was hugely popular - so I'm doing it again. This time, it is the backbone of the FractalCanvas program - the Fractal Engine. The code is a little bit more complex than anything I have posted thus far, so cling to the red comments which explain what is going on!
Note: this code is not yet optimized - by storing some values instead of calculating them multiple times and by algebraically simplifying some operations I anticipate this code running far more quicklky and effeciently than it does.


//fractalCanvasEngine provides tools to generate and manipulate fractals
function fractalCanvasEngine() {
//transpose is a function to scale and move the points in a fractal so that the first point is on (0,0) and the last point is on (1, 0)
this.transpose = function(sketchPoints){
  var transposedSketchPoints = [], angle, length, pointAngle, pointLength, i;
  
  //calculates the length of the line connecting the first and last points
  length = Math.sqrt(Math.pow(sketchPoints[0][1] - sketchPoints[sketchPoints.length - 1][1], 2) + Math.pow(sketchPoints[0][2] - sketchPoints[sketchPoints.length - 1][2], 2));
  //calculates the angle of the line connecting the first and last points
angle = Math.asin((sketchPoints[0][2] - sketchPoints[sketchPoints.length - 1][2]) / length);
  //for each point in the fractal apply the rotation and scaling
for(i = 0; i < sketchPoints.length; i++){
   pointLength = Math.sqrt(Math.pow(sketchPoints[0][1] - sketchPoints[i][1], 2) + Math.pow(sketchPoints[0][2] - sketchPoints[i][2], 2));
   pointAngle = Math.asin((sketchPoints[0][2] - sketchPoints[i][2]) / pointLength) - angle;
   
   //Add the newly transformed point to the output array
   transposedSketchPoints.push([
    sketchPoints[i][0],
    Math.cos(pointAngle) * pointLength / length || 0,
    Math.sin(pointAngle) * pointLength * -1 / length || 0
   ]);
  }
  return transposedSketchPoints;
 }

 //scale takes a fractal and rescales it to fit in a 1 by 1 window
 this.scale = function(points){
  var minX = points[0][1] || 0,
  maxX = points[0][1] || 0,
  minY = points[0][2] || 0,
  maxY = points[0][2] || 0,
  scaleFactor, point;
  
  //find the size of the fractal (height and width)
  for(point in points){
   if(points[point][1] < minX) minX = points[point][1];
   if(points[point][1] > maxX) maxX = points[point][1];
   
   if(points[point][2] < minY) minY = points[point][2];
   if(points[point][2] > maxY) maxY = points[point][2];
  }
  
  //calculate how much the fractal needs to be scaled
  scaleFactor = Math.min(1 / (maxX - minX), 1 / (maxY - minY));
  
  //apply the scaling to each of the points
  for(point in points){
   points[point][1] -= minX;
   points[point][1] *= scaleFactor;
   
   points[point][2] -= minY;
   points[point][2] *= scaleFactor;
  }
  
  return points;
 }

 //the heart of the fractal engine - a recursive function that generates the fractal
 this.getLevel = function(remainingLevels, points)
 {
  //continue recursive operation, there are more levels to calculate
  if(remainingLevels > 0){
   var subLevel = this.getLevel(remainingLevels - 1, points),
   prevPoint,
   curPoint,
   rotFactor,
   scaleFactor,
   returnLevel = new Array(),
   subRot,
   subScale,
   i,
   inverse;
   
   for(i = 1; i < points.length; i++){
    prevPoint = points[i-1];
    curPoint = points[i];
    
    if(curPoint[0] === 1 || curPoint[0] === 2){
     //this is where the actual fractal is generated... its fairly complex. In short, it converts the fractal to polar, applies the transformations (including flipping it if necessary) and then converts it back to rectangular. I am not going to try to explain where/how all of that happens.
     scaleFactor = Math.sqrt(
      Math.pow((curPoint[2]-prevPoint[2]), 2)
      + Math.pow((curPoint[1]-prevPoint[1]), 2)
     );
     rotFactor = Math.asin((curPoint[2]-prevPoint[2])/scaleFactor);
     
     inverse = (curPoint[0] === 1 ? 1 : -1);
     
     for(var j = 0; j < subLevel.length; j++){
      subScale = Math.sqrt(Math.pow(subLevel[j][2], 2) + Math.pow(subLevel[j][1], 2));
      
      subRot = Math.asin((subLevel[j][2]/subScale) || 0);
      if(subLevel[j][1] < 0) subRot += (Math.PI/2 - subRot)*2;
      
      returnLevel.push(Array(subLevel[j][0],
       ((subScale)*scaleFactor)*Math.cos(rotFactor+inverse*subRot)+prevPoint[1],
       ((subScale)*scaleFactor)*Math.sin(rotFactor+inverse*subRot)+prevPoint[2]));
     }
    }
    else returnLevel.push(Array(curPoint[0], curPoint[1], curPoint[2])); //the type of this segment is not one that requires it to be replaced by a fractal
   }
   return returnLevel;
  }
  else return [[1, 0, 0], [1, 1, 0]]; //level 0 is just a line
 }
}
var fractalCanvasEngine = new fractalCanvasEngine();

No comments:

Post a Comment