processingp5.jsfractalsprocedural-generationl-systems

How do I find beginning/end vertices of branches on an L-Systems tree? (p5.js)


I'm trying to get the x, y coordinates of branch endpoints on a simple L-Systems tree. The idea is to create a p5.Vector(x, y) and push it to an array.

Right now, I'm able to draw ellipses marking the desired points by setting their origin to (0, -len), but I have a problem. When I try to push (0, -len) as a new p5.Vector(x, y) to an array, every single point has an x coordinate of 0, albeit with the correct y coordinate.

I know that it has something to do with translating the coordinate back to (width/2, height), but I'm just not able to figure out the correct calculation. I've even tried tan(angle) * (y1 - y2) but it's not quite right. TIA!

var axiom = 'F';
var sentence = axiom;
var len = 300;

var count = 0;
var flowerArr = [];

var rules = [];
rules[0] = {
    a: 'F',
    b: 'G[+F][-F]GF'
};

rules[1] = {
    a: 'G',
    b: 'GG'
};


function setup() {
    createCanvas(window.innerWidth, window.innerHeight);
    stroke(10);
    smooth();
    turtle();
}


function turtle() {
    background(255);
    strokeWeight(1);
    angle = radians(Math.random() * (25 - 15) + 15);
    resetMatrix();
    translate(width / 2, height);

    for (var i = 0; i < sentence.length; i++) {
        var current = sentence.charAt(i);
        var randomSeed = 2;
        if (current == 'F' || current == 'G') {
            ellipse(0, -len, 5);
            line(0, 0, 0, -len);
            translate(0, -len);
        } else if (current == '+') {
            let positiveRotation = angle * Math.random() * randomSeed;
            rotate(positiveRotation);
        } else if (current == '-') {
            let negativeRotation = -angle * Math.random() * randomSeed;
            rotate(negativeRotation);
        } else if (current == '[') {
            push();
        } else if (current == ']') {
            pop();
            count++;
        }
    }
    if (i >= sentence.length) {
        finished = true;
        console.log("done", count);
    }
}

function generateStems(iterations) {
    for (i = iterations - 1; i > 0 ; i--) {
        branch();
    }
}

function branch() {
    len *= Math.random() * (.52 - .45) + .45;
    var nextSentence = '';
    for (var i = 0; i < sentence.length; i++) {
        var current = sentence.charAt(i);
        var found = false;
        for (var j = 0; j < rules.length; j++) {
            if (current == rules[j].a) {
                found = true;
                nextSentence += rules[j].b;
                break;
            }
        }
        if (!found) {
            nextSentence += current;
        }
    }
    sentence = nextSentence;
    turtle();
}

function draw() {
    generateStems(4);
    noLoop();
}

Tree with branch end points marked with ellipses


Solution

  • As far as I know, at the moment, p5.js support for vector/matrix operations and coordinate space conversion isn't quite there yet.

    In theory you could manually keep track of every single transformation (translate/rotate) and manually compute it to get the transformed positions, howeve, in practice this may be error prone and cumbersome.

    In Processing you could rely on PMatrix's mult(PVector) method to transform a point from one coordinate system to another, but not in p5.js at the moment. Functions like screenX()/screenY() simplify this even further.

    Here's a basic example (note the usage of P3D):

    PVector v1 = new PVector();
    float len = 100;
    
    void setup(){
      size(300,300,P3D);
      noFill();
      strokeWeight(3);
    }
    
    void draw(){
      background(255);
      // isolate coordinate system
      pushMatrix();
      // apply a set of transformations
      translate(width / 2, height);
      translate(0,-len);
      rotate(radians(45));
      // draw a blue rectangle from the corner to illustrate this transformed position
      stroke(0,0,192);
      rect(0,0,30,30);
      // further transform
      translate(90,0);
      // draw a rect rectangle
      stroke(192,0,0);
      rect(0,0,30,30);
      // use screenX/screenY to calculate the transformed coordinates
      v1.set(screenX(0,0,0),screenY(0,0,0)); 
      popMatrix();
    
      // simply draw a (green) circle on top at the same transformed coordinates, without being in that local coordinate system 
      stroke(0,192,0);
      ellipse(v1.x, v1.y, 30, 30);
    }
    

    At the moment, for practical reasons, if computing the transformed locations is a must, I would recommend porting your code to Processing.

    Update Based on your comment it is easier to use the L-System to introduce a new rule for the flower.

    Let's say * represents a flower, you could modify your rule to include it for example as the last instruction: b: 'G[+F][-F]GF' becomes b: 'G[+F][-F]GF*'

    then it's just a matter of handling that symbol as you traverse the current sentence:

    var axiom = 'F';
    var sentence = axiom;
    var len = 300;
    
    var count = 0;
    var flowerArr = [];
    
    var rules = [];
    rules[0] = {
        a: 'F',
        b: 'G[+F][-F]GF*'
    };
    
    rules[1] = {
        a: 'G',
        b: 'GG'
    };
    
    
    function setup() {
        createCanvas(630, 630);
        stroke(10);
        noFill();
        smooth();
        turtle();
    }
    
    
    function turtle() {
        background(255);
        strokeWeight(1);
        angle = radians(Math.random() * (25 - 15) + 15);
        resetMatrix();
        translate(width / 2, height);
        for (var i = 0; i < sentence.length; i++) {
            var current = sentence.charAt(i);
            var randomSeed = 2;
            if (current == 'F' || current == 'G') {
                ellipse(0, -len, 5);
                line(0, 0, 0, -len);
                translate(0, -len);
            // flower rule
            } else if (current == '*') {
                flower(6,len * 0.618033);
            } else if (current == '+') {
                let positiveRotation = angle * Math.random() * randomSeed;
                rotate(positiveRotation);
            } else if (current == '-') {
                let negativeRotation = -angle * Math.random() * randomSeed;
                rotate(negativeRotation);
            } else if (current == '[') {
                push();
            } else if (current == ']') {
                pop();
                count++;
            }
        }
        if (i >= sentence.length) {
            finished = true;
            // console.log("done", count);
        }
    }
    
    function flower(sides, sideLength){
      beginShape();
      let angleIncrement = TWO_PI / sides;
      for(let i = 0 ; i <= sides; i++){
        vertex(cos(angleIncrement * i) * sideLength,
               sin(angleIncrement * i) * sideLength);
      }
      endShape();
    }
    
    function generateStems(iterations) {
        for (i = iterations - 1; i > 0 ; i--) {
            branch();
        }
    }
    
    function branch() {
        len *= Math.random() * (.52 - .45) + .45;
        var nextSentence = '';
        for (var i = 0; i < sentence.length; i++) {
            var current = sentence.charAt(i);
            var found = false;
            for (var j = 0; j < rules.length; j++) {
              
                if (current == rules[j].a) {
                  
                    found = true;
                    nextSentence += rules[j].b;
                    break;
                }
            }
            if (!found) {
                nextSentence += current;
            }
        }
        sentence = nextSentence;
        turtle();
    }
    
    function draw() {
        generateStems(5);
        noLoop();
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.0.0/p5.min.js"></script>

    l-system tree with flower

    As further explorations here are a couple of fun JS implementations of L-system to play with: