2020 July 15th
The first program that I wrote solo and self-motivated was a creative project. I wanted to draw a tree. I placed some squares on a grid and assigned them various colours. The squares that were in the middle third of the image were painted various shades of brown for the trunk and the squares in the top half were painted various shades of green for leaves. It wasn't overly complicated, but it also did not need to be.
Although a lot has changed since I first wrote that tree program in Visual Basic, it was retrospectively very impactful and one of my early inflection points. Creative coding is expressive, fun, and meaningful. I want to revisit "drawing a tree" with a handful of different techniques (spoilers: it was just as fun as the first time).
Formal Language Theory defines a language's Grammar as a ruleset for forming a string of characters from that language's alphabet. Grammars are effectively descriptions of allowed structures and shapes within the language without prescribing meaning (semantics) to the created strings.
Grammars are represented as transformations from symbols to other symbols. Symbols that can be transformed into other symbols are non-terminal symbols and are represented with capital letters. Terminal symbols are represented as lowercase letters and cannot be transformed further. In an ambiguous language there are multiple ways to transform non-terminal strings, resulting in different final strings.
Consider the grammar example:
Initial | Result | |
---|---|---|
→ | ||
→ | ||
→ |
Because it's an ambiguous grammar with multiple rules for the non-terminal E symbol, there are different possible outcomes. Consider one example derivation from the starting string:
Current value | Rule | Result |
---|---|---|
1 | ||
2 | ||
2 | ||
3 |
One especially interesting type of grammar is the Lindenmayer system. Aristid Lindenmayer described plant development and behaviour of plant cells with his L-Systems. Plants and trees are examples of well-known L-Systems in two dimensions.
We can use L-Systems to model the growth of a tree (and how to draw it)! One such grammar for drawing a fractal plant can be described as follows.
Initial | Result | |
---|---|---|
→ |
Each of the symbols in the language has a meaning for when we draw our tree. The X is a non-terminal symbol, but the other symbols are constants described in the following table.
Symbols | Action |
---|---|
Move forward | |
Turn left | |
Turn right | |
Store current position and angle | |
Return to last stored position and angle |
Examining the first derivation of the starting string fX
, we can see that applying would end up with f[-fX]+fX
. Ignoring the non-terminal X
symbols, this translates to:
We can now create a function to derive n
iterations (or steps) of our grammar. Luckily, our grammar is not ambiguous and we can just recursively apply the same single transformation rule over and over.
1function grammarParser(input: string, iterations: number) {2while (iterations > 0) {3iterations--;45input = input.replaceAll("X", "[-fX]+fX");6}7return input;8}9
Iteration # | Result |
---|---|
We now have our drawing instructions. But how do we actually represent this as a tree?
Turtle graphics describe a simple digital drawing technique that is based around a paintbrush's (referred to as a Turtle) movements relative to its current position. Imagine drawing a line without lifting the pen or... a turtle walking across your canvas. So far, we have described our tree language's grammar (syntax), and now our turtle will draw the tree based on the meaning (semantics) of the language's alphabet.
A turtle will be represented by its current position and angle. The turtle also needs to remember the previous positions and angles as requested by the derived instruction set.
1interface Location {2x: number;3y: number;4angle: number; // in radians5length: number; // length of the next segment6}78interface Turtle {9location: Location;10history: Location[]; // to be treated as a stack11}12
Now our turtle needs to interpret each instruction and perform the required paint stroke. I have chosen to use the HTML5 Canvas API to draw to the page. The majority of the code is telling the turtle what to do when encountering each symbol from our grammar.
1function turtleWalk(instructions: string) {2const draw = document.getElementById('canvas').getContext('2d');34const turtle: Turtle = {5// start the turtle at the bottom-middle position pointing up6location: {7x: draw.canvas.width / 2,8y: draw.canvas.height,9angle: Math.PI / 2,10length: draw.canvas.height / 3,11},12history: [],13}1415for (const instr of instructions) {16draw.beginPath();17draw.moveTo(turtle.location.x, turtle.location.y);1819if (instruction === 'f') {20// move the turtle based on its angle21turtle.location.y -= turtle.location.length * Math.sin(turtle.location.angle);22turtle.location.x += turtle.location.length * Math.cos(turtle.location.angle);23turtle.location.length /= 2;24// draw our line!25draw.lineTo(turtle.location.x, turtle.location.y);26} else if (instruction === '+') {27turtle.location.angle -= Math.PI / 4;28} else if (instruction === '-') {29turtle.location.angle += Math.PI / 4;30} else if (instruction === '[') {31// store our current location in the history32turtle.history.push({...turtle.location});33} else if (instruction === ']') {34// restore our most recent location35turtle.location = turtle.history.pop();36}3738draw.closePath();39draw.stroke();40}41}42
If we run that code we can produce a tree! I have applied some colours to the canvas, but you should see a result very similar to below.
Finally, I built an interactive version of our turtle and grammar! Feel free to experiment with your own grammar rules and angle delta!
I hope that you had fun learning a little bit about Turtles and Grammars. The cross-domain usage of Grammars has always fascinated me and I love that it extends into the realm of graphics. Plus, I had a ton of fun revisiting one my earliest inspirations to program.
Thank you for reading. Thank you for exploring ideas with me.