problem converting LSystems code from p5js to HTML Canvas javascript

60 views Asked by At

I'm trying to convert this Daniel Shiffman tutorial from p5js to html5 canvas without a library:


var angle;
var axiom = "F";
var sentence = axiom;
var len = 100;

var rules = [];
rules[0] = {
  a: "F",
  b: "FF+[+F-F-F]-[-F+F+F]"
}

function generate() {
  len *= 0.5;
  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;
  createP(sentence);
  turtle();

}

function turtle() {
  background(51);
  resetMatrix();
  translate(width / 2, height);
  stroke(255, 100);
  for (var i = 0; i < sentence.length; i++) {
    var current = sentence.charAt(i);

    if (current == "F") {
      line(0, 0, 0, -len);
      translate(0, -len);
    } else if (current == "+") {
      rotate(angle);
    } else if (current == "-") {
      rotate(-angle)
    } else if (current == "[") {
      push();
    } else if (current == "]") {
      pop();
    }
  }
}

function setup() {
  createCanvas(400, 400);
  angle = radians(25);
  background(51);
  createP(axiom);
  turtle();
  var button = createButton("generate");
  button.mousePressed(generate);
}

This is my code and there is a problem with it that I am not able to debug... When I run the following code, I get the start of this tree with an error message prompting me to change rotate(-this.angle) to call the context. But when I fix the issue, the tree disappears & the rectangle in the "buildFlower()" function appears... why is this happening? :/ :

broken lsystems tree

My code:

const init = () => {
  const html = document.getElementsByTagName("html").item(0),
    canvas = document.getElementsByTagName("canvas").item(0),
    c = canvas.getContext("2d");

  let flower;
  const gui = new dat.GUI();

  function line(x1, y1, x2, y2, color) {
    c.strokeStyle = color;
    c.beginPath();
    c.moveTo(x1, y1);
    c.lineTo(x2, y2);
    c.stroke();
  }

  class Flower {
    constructor(x, y, size) {
      this.x = x;
      this.y = y;
      this.size = size;

      this.angle = (Math.PI / 180) * 25;
      this.axiom = "F";
      this.sentence = this.axiom;
      this.len = 100;
      this.rules = [];
      this.rules[0] = {
        a: "F",
        b: "FF+[+F-F-F]-[-F+F+F]",
      };
    }

    generateLSystem() {
      this.len *= 0.5;
      let nextSentence = "";
      for (let i = 0; i < this.sentence.length; i++) {
        let current = this.sentence.charAt(i);
        let found = false;
        for (let j = 0; j < this.rules.length; j++) {
          if (current == this.rules[j].a) {
            found = true;
            nextSentence += this.rules[j].b;
            break;
          }
        }
        if (!found) {
          nextSentence += current;
        }
      }
      this.sentence = nextSentence;
      this.turtle();
    }

    turtle() {
      c.resetTransform();
      c.translate(100, getResolution().h);

      for (let i = 0; i < this.sentence.length; i++) {
        let current = [this.sentence.charAt(i)];

        if (current == "F") {
          line(0, 0, 0, -this.len);
          c.translate(0, -this.len);
        } else if (current == "+") {
          c.rotate(this.angle);
        } else if (current == "-") {
          // WHEN I CHANGE THE LINE BELOW TO (`c.rotate`) THE ENTIRE THING DISAPPEARS.
          rotate(-this.angle);
        } else if (current == "[") {
          current.push();
        } else if (current == "]") {
          current.pop();
        }
      }
    }

    buildFlower() {
      c.beginPath();
      c.rect(
        getResolution().w / 2 - this.size / 2,
        getResolution().h / 2 - this.size / 2,
        this.size,
        this.size
      );
      c.stroke();
    }
  }

  const resize = () => {
    canvas.width = w = window.innerWidth;
    canvas.height = h = window.innerHeight;
    console.log(`screen resolution: ${w}px × ${h}px`);
  };

  const getResolution = () => {
    return { w: canvas.width, h: canvas.height };
  };

  const setup = () => {
    c.clearRect(0, 0, getResolution().w, getResolution().h);
    flower = new Flower(getResolution().w, getResolution().h, 200);
    flower.generateLSystem();
    gui.add(flower, "size", 0, 200);
    gui.add(flower, "axiom");
  };

  setup();

  const draw = (t) => {
    c.fillStyle = "rgba(255, 255, 255, .5)";
    c.fillRect(0, 0, w, h);

    window.requestAnimationFrame(draw);
  };

  let w,
    h,
    last,
    i = 0,
    start = 0;

  window.removeEventListener("load", init);
  window.addEventListener("resize", resize);
  resize();
  window.requestAnimationFrame(draw);
};

window.addEventListener("load", init);

I've been trying for hrs and can't seem to debug :/

1

There are 1 answers

1
ggorlen On BEST ANSWER

General suggestion: work in small sprints and run the code frequently to verify that it behaves as expected. The code here seems like it was built up in just a few long sprints without much validation for each component along the way. This apparently led to a buildup of complexity, causing confusion and subtle bugs.

Try to avoid premature abstractions like excessive functions and classes until the code is working correctly. At that point, the correct abstractions and cut points will be apparent.

Also, minimize the problem space by only introducing "bells and whistles"-type peripheral functionality once the core logic is working. When the basic drawing is failing, functionality like a GUI, resizing, a RAF loop and so forth just get in the way of progress. Add those after the basic drawing works.


The main reason for the disappearing drawing on the canvas is that resize() is called after setup(). When you resize the canvas, it wipes it clean. Call setup() after your initial resize() call:

window.removeEventListener("load", init);
window.addEventListener("resize", resize);
resize(); // <-- wipes the screen
setup();  // <-- draw after resize
window.requestAnimationFrame(draw);

You see a partial drawing showing up because your crash prevents the resize() from executing. This should be a big debug hint: some code after the drawing must be wiping it out.


The translation from p5's push() and pop() to HTML5 canvas doesn't appear correct:

} else if (current == "[") {
  current.push();
} else if (current == "]") {
  current.pop();
}

current is an array, but we don't want to mess with it. We're supposed to be pushing and popping the canvas context, not an array. These calls are context.save() and context.restore() in HTML5:

} else if (current === "[") {
  c.save();
} else if (current === "]") {
  c.restore();
}

A strange design choice is:

let current = [this.sentence.charAt(i)];

if (current == "F") {

Here, you've created an array of one element, then used coercion to compare it to a string. Always use === and don't create a one-element array unnecessarily:

// use const instead of let and [i] instead of charAt(i)
const current = this.sentence[i];

if (current === "F") {

I could analyze the design further and do a full rewrite/code review, but I'll leave it at this just to get you moving again and not belabor the point:

const init = () => {
  const html = document.getElementsByTagName("html").item(0),
    canvas = document.getElementsByTagName("canvas").item(0),
    c = canvas.getContext("2d");

  let flower;
//  const gui = new dat.GUI();

  function line(x1, y1, x2, y2, color) {
    c.strokeStyle = color;
    c.beginPath();
    c.moveTo(x1, y1);
    c.lineTo(x2, y2);
    c.stroke();
  }

  class Flower {
    constructor(x, y, size) {
      this.x = x;
      this.y = y;
      this.size = size;

      this.angle = (Math.PI / 180) * 25;
      this.axiom = "F";
      this.sentence = this.axiom;
      this.len = 50;
      this.rules = [];
      this.rules[0] = {
        a: "F",
        b: "FF+[+F-F-F]-[-F+F+F]",
      };
    }

    generateLSystem() {
      this.len *= 0.5;
      let nextSentence = "";
      for (let i = 0; i < this.sentence.length; i++) {
        let current = this.sentence[i];
        let found = false;
        for (let j = 0; j < this.rules.length; j++) {
          if (current == this.rules[j].a) {
            found = true;
            nextSentence += this.rules[j].b;
            break;
          }
        }
        if (!found) {
          nextSentence += current;
        }
      }
      this.sentence = nextSentence;
      this.turtle();
    }

    turtle() {
      c.resetTransform();
      c.translate(100, getResolution().h);

      for (let i = 0; i < this.sentence.length; i++) {
        const current = this.sentence[i];

        if (current === "F") {
          line(0, 0, 0, -this.len);
          c.translate(0, -this.len);
        } else if (current === "+") {
          c.rotate(this.angle);
        } else if (current === "-") {
          c.rotate(-this.angle);
        } else if (current === "[") {
          c.save();
        } else if (current === "]") {
          c.restore();
        }
      }
    }

    buildFlower() {
      c.beginPath();
      c.rect(
        getResolution().w / 2 - this.size / 2,
        getResolution().h / 2 - this.size / 2,
        this.size,
        this.size
      );
      c.stroke();
    }
  }

  const resize = () => {
    canvas.width = w = window.innerWidth;
    canvas.height = h = window.innerHeight;
    console.log(`screen resolution: ${w}px × ${h}px`);
  };

  const getResolution = () => {
    return { w: canvas.width, h: canvas.height };
  };

  const setup = () => {
    c.clearRect(0, 0, getResolution().w, getResolution().h);
    flower = new Flower(getResolution().w, getResolution().h, 200);
    flower.generateLSystem();
    //gui.add(flower, "size", 0, 200);
    //gui.add(flower, "axiom");
  };

  const draw = (t) => {
    c.fillStyle = "rgba(255, 255, 255, .5)";
    c.fillRect(0, 0, w, h);

    window.requestAnimationFrame(draw);
  };

  let w,
    h,
    last,
    i = 0,
    start = 0;

  window.removeEventListener("load", init);
  window.addEventListener("resize", resize);
  resize();
  setup();
  window.requestAnimationFrame(draw);
};

window.addEventListener("load", init);
<canvas></canvas>