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)
}
}
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).
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
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>