Fractal Tree in P5.js

88 views Asked by At

I've been trying to create a simple fractal tree program using P5.js, for displaying on a website, but I seem to be getting unexpected behavior. Code and picture attached.

function setup() {
  createCanvas(1920, 1080);
}

function draw() {
  background(10);
  x = 1920/2
  y = 1080/2
  fractalDraw(x, y, 5, 0)
}

function fractalDraw(nodeX, nodeY, numNodes, sideFlag){
  offset = 10 * numNodes
  leftNodeX = nodeX - offset
  rightNodeX = nodeX + offset
  topNodeY = nodeY - offset
  botNodeY = nodeY + offset
  
  if(sideFlag === -1){                                 //Leftside draw
    line(nodeX, nodeY, leftNodeX, topNodeY)
    stroke(255,255,255)
    line(nodeX, nodeY, leftNodeX, botNodeY)
    stroke(255,255,255);
  }
  else if(sideFlag === 1){                            //Rightside draw
    line(nodeX, nodeY, rightNodeX, topNodeY)
    stroke(255,255,255);
    line(nodeX, nodeY, rightNodeX, botNodeY)
    stroke(255,255,255);
  }
  else{                                              //Starting draw
    line(nodeX, nodeY, leftNodeX, topNodeY)
    stroke(255,255,255)
    line(nodeX, nodeY, leftNodeX, botNodeY)
    stroke(255,255,255);
    line(nodeX, nodeY, rightNodeX, topNodeY)
    stroke(255,255,255);
    line(nodeX, nodeY, rightNodeX, botNodeY)
    stroke(255,255,255);
  }
  
  if(numNodes === 1){                              //Recursion Base Case
    return 1
  }
  else{                                            //Recursive calls
    fractalDraw(leftNodeX, topNodeY, numNodes-1, -1)
    fractalDraw(leftNodeX, botNodeY, numNodes-1, -1)
    fractalDraw(rightNodeX, topNodeY, numNodes-1, 1)
    fractalDraw(rightNodeX, botNodeY, numNodes-1, 1)
  }
}

Fractals not fractaling

I'm using recursive calls, and it seems only my first recursive call is running, then the entire draw loop restarts in a different starting position than intended once the first (and only the first) recursive call finishes. Also I had some weird behavior when using small number of branch layers (3 or less).

1

There are 1 answers

3
trincot On BEST ANSWER

The main issue is that you don't define your variables with var, let or const, and so all your variables are implicitly declared as global variables, which is the cause of havoc. The values of leftNodeX and similar variables are modified by the recursive calls, and so when these recursive calls return, these variables no longer have their intended values, and the next recursive call will get arguments that no longer make sense.

There are also a few other issues:

  • The offset should not diminish with a constant. If you start with numNodes equal to 5, then at different recursion depths, you'll have an offset of 50, then 40, 30, 20, ... This is not ideal. This sequences should be a geometric sequence, i.e. the offset should reduce by a factor, not by a constant. To achieve that, it will be easier to just pass the offset as argument instead of the number of nodes.

  • The very first line is not drawn, because the stroke was not set. Be aware that the line is drawn when the call line() is made, and you only need to call stroke once: it is a configuration for any of the next calls to line (and other drawing functions). So you could move that stroke() call in your setup.

  • To support other screen sizes, don't hard-code the canvas size, but use the browser's information (together with CSS) about the screen size (or alternatively, the window's size).

  • p5 will continue to call draw forever. As you have no animation, there is no need for that: one call is enough. You can indicate this by adding a call to p5's noLoop

  • Should I use semicolons in JavaScript?

Correction:

function setup() {
  // Use browser information to set size (see also CSS settings for body)
  createCanvas(screen.width, screen.height);
  // Call stroke before drawing
  stroke(255,255,255);
}

function draw() {
  background(10);
  // Define variables with const 
  const x = width >> 1; // Use integer division
  const y = height >> 1;
  // Pass the initial offset instead of the number of nodes
  fractalDraw(x, y, width >> 2, 0);
  noLoop(); // Avoid repeating calls to draw()
}

function fractalDraw(nodeX, nodeY, offset, sideFlag){
  // Define variables with const (here was the most dramatic bug) 
  const leftNodeX = nodeX - offset;
  const rightNodeX = nodeX + offset;
  const topNodeY = nodeY - offset;
  const botNodeY = nodeY + offset;
  
  if(sideFlag === -1){
    line(nodeX, nodeY, leftNodeX, topNodeY);
    line(nodeX, nodeY, leftNodeX, botNodeY);
  }
  else if(sideFlag === 1){
    line(nodeX, nodeY, rightNodeX, topNodeY);
    line(nodeX, nodeY, rightNodeX, botNodeY);
  }
  else{
    line(nodeX, nodeY, leftNodeX, topNodeY);
    line(nodeX, nodeY, leftNodeX, botNodeY);
    line(nodeX, nodeY, rightNodeX, topNodeY);
    line(nodeX, nodeY, rightNodeX, botNodeY);
  }
  
  if(offset <= 1){
    return 1;
  }
  else{
    // The offset should decrease by a factor, not by a constant
    offset >>= 1; // For example: integer divide by 2
    fractalDraw(leftNodeX, topNodeY, offset, -1);
    fractalDraw(leftNodeX, botNodeY, offset, -1);
    fractalDraw(rightNodeX, topNodeY, offset, 1);
    fractalDraw(rightNodeX, botNodeY, offset, 1);
  }
}
html, body { margin: 0; height: 100%; overflow: hidden; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>

There is also an issue with the "tree" shape your code draws. I assume that lines should all be connected, but in case the flag is -1 or 1 (so all cases except the initial call), two out of four of the recursive calls will pass coordinates that might not have been used by a previous line call, and so you sometimes get disconnected lines.

I can only guess which shape you were going for, but here is an alternative you may want to consider:

function setup() {
  createCanvas(screen.width, screen.height);
  stroke(255,255,255);
}

function draw() {
  background(10);
  const x = width >> 1
  const y = height >> 1;
  fractalDraw(x, y, x, y, width >> 2);
  noLoop();
}

function fractalDraw(fromX, fromY, nodeX, nodeY, offset){
  line(fromX, fromY, nodeX, nodeY);
  if (offset < 1) {
    return;
  }
  const leftNodeX = nodeX - offset;
  const rightNodeX = nodeX + offset;
  const topNodeY = nodeY - offset;
  const botNodeY = nodeY + offset;

  offset >>= 1;
  fractalDraw(nodeX, nodeY, leftNodeX, topNodeY, offset);
  fractalDraw(nodeX, nodeY, leftNodeX, botNodeY, offset);
  fractalDraw(nodeX, nodeY, rightNodeX, topNodeY, offset);
  fractalDraw(nodeX, nodeY, rightNodeX, botNodeY, offset);
}
html, body { margin: 0; height: 100%; overflow: hidden; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>